Files
paseo/packages/app/src/attachments/workspace-attachments-store.test.ts
T
Mohamed BoudraandGitHub 85acaceb16 Fork assistant turns into new drafts (#1788)
* feat(chat): fork assistant turns into new drafts

Builds chat-history attachments on the daemon and routes them into draft composers so users can continue an assistant turn in an existing workspace tab or New Workspace.

* fix(chat): clean up fork draft handling

* fix(chat): address fork draft review feedback

* fix(server): build fork context from bounded timeline

* fix(app): restore assistant turn footer in streams

* fix(app): preserve fork draft setup

* fix(app): tighten assistant fork footers

* fix(app): lock active fork draft handoff

* fix(app): preserve attachment-only draft retries

* fix(app): unify composer attachment scopes

* fix(app): align chat history attachment labels

* fix(app): centralize attachment pill content

* fix(app): trim fork context for workspace naming
2026-06-29 18:29:59 +02:00

196 lines
5.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { WorkspaceComposerAttachment } from "./types";
import {
appendWorkspaceAttachment,
buildDraftWorkspaceAttachmentScopeKey,
buildWorkspaceAttachmentScopeKey,
collectWorkspaceAttachmentsForScopes,
resetWorkspaceAttachmentsStore,
useWorkspaceAttachmentsStore,
} from "./workspace-attachments-store";
function reviewAttachment(body: string): WorkspaceComposerAttachment {
return {
kind: "review",
reviewDraftKey: `review:${body}`,
commentCount: 1,
attachment: {
type: "review",
mimeType: "application/paseo-review",
cwd: "/repo",
mode: "uncommitted",
baseRef: null,
comments: [
{
filePath: "src/example.ts",
side: "new",
lineNumber: 41,
body,
context: {
hunkHeader: "@@ -40,1 +40,1 @@",
targetLine: {
oldLineNumber: null,
newLineNumber: 41,
type: "add",
content: "const value = newValue;",
},
lines: [
{
oldLineNumber: null,
newLineNumber: 41,
type: "add",
content: "const value = newValue;",
},
],
},
},
],
},
};
}
function contextAttachment(id: string): WorkspaceComposerAttachment {
return {
kind: "github.pull_request_comment",
id,
title: "Comment · octocat",
text: "GitHub pull request comment\n\nLooks good.",
url: `https://github.com/getpaseo/paseo/pull/42#${id}`,
};
}
function chatHistoryAttachment(id: string, text = "Previous chat."): WorkspaceComposerAttachment {
return {
kind: "chat_history",
id,
attachment: {
type: "text",
mimeType: "text/plain",
contextKind: "chat_history",
title: "Chat history",
text,
},
source: {
serverId: "local",
agentId: "agent-1",
},
};
}
describe("workspace attachments store", () => {
it("scopes workspace attachments by server and workspace before cwd fallback", () => {
expect(
buildWorkspaceAttachmentScopeKey({
serverId: " local ",
workspaceId: " workspace-1 ",
cwd: "/repo",
}),
).toBe("workspace-attachments:server=local:workspace=workspace-1");
expect(
buildWorkspaceAttachmentScopeKey({
serverId: "local",
workspaceId: null,
cwd: "/repo/",
}),
).toBe("workspace-attachments:server=local:cwd=%2Frepo");
});
it("scopes draft attachments by draft id", () => {
expect(buildDraftWorkspaceAttachmentScopeKey("draft-1")).toBe(
"workspace-attachments:draft=draft-1",
);
});
it("publishes and clears attachments for a workspace scope", () => {
resetWorkspaceAttachmentsStore();
const scopeKey = buildWorkspaceAttachmentScopeKey({
serverId: "local",
workspaceId: "workspace-1",
cwd: "/repo",
});
const attachment = reviewAttachment("Please simplify this.");
useWorkspaceAttachmentsStore
.getState()
.setWorkspaceAttachments({ scopeKey, attachments: [attachment] });
expect(useWorkspaceAttachmentsStore.getState().attachmentsByScope[scopeKey]).toEqual([
attachment,
]);
useWorkspaceAttachmentsStore.getState().clearWorkspaceAttachments({ scopeKey });
expect(useWorkspaceAttachmentsStore.getState().attachmentsByScope[scopeKey]).toBeUndefined();
});
it("appends unique context attachments without dropping other workspace attachments", () => {
const review = reviewAttachment("Please simplify this.");
const context = contextAttachment("comment-1");
expect(appendWorkspaceAttachment([review], context)).toEqual([review, context]);
});
it("dedupes repeated context attachments by provider, source, and id", () => {
const original = contextAttachment("comment-1");
const replacement = {
...contextAttachment("comment-1"),
title: "Comment · octocat updated",
text: "Updated text",
};
expect(appendWorkspaceAttachment([original], replacement)).toEqual([replacement]);
});
it("dedupes repeated chat history attachments by id", () => {
const original = chatHistoryAttachment("chat_history:draft-1", "Original chat.");
const replacement = chatHistoryAttachment("chat_history:draft-1", "Updated chat.");
expect(appendWorkspaceAttachment([original], replacement)).toEqual([replacement]);
});
it("adds a workspace attachment against the current scope state", () => {
resetWorkspaceAttachmentsStore();
const scopeKey = buildWorkspaceAttachmentScopeKey({
serverId: "local",
workspaceId: "workspace-1",
cwd: "/repo",
});
const review = reviewAttachment("Please simplify this.");
const context = contextAttachment("comment-1");
const addWorkspaceAttachment = useWorkspaceAttachmentsStore.getState().addWorkspaceAttachment;
useWorkspaceAttachmentsStore
.getState()
.setWorkspaceAttachments({ scopeKey, attachments: [review] });
addWorkspaceAttachment({ scopeKey, attachment: context });
expect(useWorkspaceAttachmentsStore.getState().attachmentsByScope[scopeKey]).toEqual([
review,
context,
]);
});
it("collects attachments across requested scopes in scope order", () => {
const draftScopeKey = buildDraftWorkspaceAttachmentScopeKey("draft-1");
const workspaceScopeKey = buildWorkspaceAttachmentScopeKey({
serverId: "local",
workspaceId: "workspace-1",
cwd: "/repo",
});
const draftContext = chatHistoryAttachment("chat_history:draft-1");
const workspaceContext = contextAttachment("comment-1");
expect(
collectWorkspaceAttachmentsForScopes({
attachmentsByScope: {
[workspaceScopeKey]: [workspaceContext],
[draftScopeKey]: [draftContext],
},
scopeKeys: [` ${draftScopeKey} `, "", workspaceScopeKey],
}),
).toEqual([draftContext, workspaceContext]);
});
});