131 lines
5.2 KiB
TypeScript
131 lines
5.2 KiB
TypeScript
import { beforeEach, describe, expect, test } from "bun:test";
|
|
|
|
import type { ComposerDraft } from "../src/app/types";
|
|
import {
|
|
composerDraftNeedsHydration,
|
|
getComposerQueuedDrafts,
|
|
getComposerRevertMessageId,
|
|
useComposerStateStore,
|
|
} from "../src/react-app/domains/session/surface/composer-state-store";
|
|
|
|
function reset() {
|
|
useComposerStateStore.setState({ sessions: {}, queuedDrafts: {}, history: {} });
|
|
}
|
|
|
|
function draft(text: string): ComposerDraft {
|
|
return {
|
|
mode: "prompt",
|
|
parts: [{ type: "text", text }],
|
|
attachments: [],
|
|
text,
|
|
resolvedText: text,
|
|
command: undefined,
|
|
};
|
|
}
|
|
|
|
function queuedTexts(sessionId: string) {
|
|
return getComposerQueuedDrafts(useComposerStateStore.getState(), sessionId).map((item) => item.draft.text);
|
|
}
|
|
|
|
describe("composer state store", () => {
|
|
beforeEach(reset);
|
|
|
|
test("scopes queued drafts by session", () => {
|
|
const { appendQueuedDraft } = useComposerStateStore.getState();
|
|
appendQueuedDraft("session-a", draft("queued in A"));
|
|
appendQueuedDraft("session-b", draft("queued in B"));
|
|
|
|
expect(queuedTexts("session-a")).toEqual(["queued in A"]);
|
|
expect(queuedTexts("session-b")).toEqual(["queued in B"]);
|
|
});
|
|
|
|
test("clearing composer input does not clear queued drafts", () => {
|
|
const { appendQueuedDraft, clearSession, setDraft } = useComposerStateStore.getState();
|
|
setDraft("session-a", "in-progress draft");
|
|
appendQueuedDraft("session-a", draft("queued follow-up"));
|
|
|
|
clearSession("session-a");
|
|
|
|
expect(queuedTexts("session-a")).toEqual(["queued follow-up"]);
|
|
});
|
|
|
|
test("remove and clear only affect the target session", () => {
|
|
const { appendQueuedDraft, clearQueuedDrafts, removeQueuedDraft } = useComposerStateStore.getState();
|
|
appendQueuedDraft("session-a", draft("first A"));
|
|
appendQueuedDraft("session-a", draft("second A"));
|
|
appendQueuedDraft("session-b", draft("only B"));
|
|
|
|
const firstId = getComposerQueuedDrafts(useComposerStateStore.getState(), "session-a")[0]?.id;
|
|
expect(firstId).toBeTruthy();
|
|
removeQueuedDraft("session-a", firstId ?? "");
|
|
expect(queuedTexts("session-a")).toEqual(["second A"]);
|
|
expect(queuedTexts("session-b")).toEqual(["only B"]);
|
|
|
|
clearQueuedDrafts("session-a");
|
|
expect(queuedTexts("session-a")).toEqual([]);
|
|
expect(queuedTexts("session-b")).toEqual(["only B"]);
|
|
});
|
|
|
|
test("reorders queued drafts and updates their text", () => {
|
|
const { appendQueuedDraft, reorderQueuedDrafts, updateQueuedDraft } = useComposerStateStore.getState();
|
|
appendQueuedDraft("session-a", draft("first"));
|
|
appendQueuedDraft("session-a", draft("second"));
|
|
appendQueuedDraft("session-a", draft("third"));
|
|
|
|
const items = getComposerQueuedDrafts(useComposerStateStore.getState(), "session-a");
|
|
const ids = items.map((item) => item.id);
|
|
reorderQueuedDrafts("session-a", [ids[2] ?? "", ids[0] ?? "", ids[1] ?? ""]);
|
|
expect(queuedTexts("session-a")).toEqual(["third", "first", "second"]);
|
|
|
|
const secondId = getComposerQueuedDrafts(useComposerStateStore.getState(), "session-a")[1]?.id;
|
|
expect(secondId).toBeTruthy();
|
|
updateQueuedDraft("session-a", secondId ?? "", draft("first edited"));
|
|
expect(queuedTexts("session-a")).toEqual(["third", "first edited", "second"]);
|
|
});
|
|
|
|
test("carries an edit boundary until clear, replacement, or session switch", () => {
|
|
const { clearRevertTarget, replaceDraft, setDraft } = useComposerStateStore.getState();
|
|
replaceDraft("session-a", "original prompt", "message-a");
|
|
|
|
expect(getComposerRevertMessageId(useComposerStateStore.getState(), "session-a")).toBe("message-a");
|
|
setDraft("session-a", "edited prompt");
|
|
expect(getComposerRevertMessageId(useComposerStateStore.getState(), "session-a")).toBe("message-a");
|
|
|
|
setDraft("session-a", "");
|
|
expect(getComposerRevertMessageId(useComposerStateStore.getState(), "session-a")).toBeNull();
|
|
|
|
replaceDraft("session-a", "original prompt", "message-a");
|
|
replaceDraft("session-a", "normal replacement");
|
|
expect(getComposerRevertMessageId(useComposerStateStore.getState(), "session-a")).toBeNull();
|
|
|
|
replaceDraft("session-a", "original prompt", "message-a");
|
|
clearRevertTarget("session-a");
|
|
expect(getComposerRevertMessageId(useComposerStateStore.getState(), "session-a")).toBeNull();
|
|
expect(useComposerStateStore.getState().sessions["session-a"]?.draft).toBe("original prompt");
|
|
});
|
|
|
|
test("retains live attachment state only inside the same claimed draft scope", () => {
|
|
const currentText = "Review this[attachment att-private]";
|
|
const storedText = "Review this";
|
|
|
|
expect(composerDraftNeedsHydration({
|
|
claimedScopeKey: "alice-org-a|workspace|session",
|
|
nextScopeKey: "alice-org-a|workspace|session",
|
|
currentText,
|
|
storedText,
|
|
})).toBe(false);
|
|
expect(composerDraftNeedsHydration({
|
|
claimedScopeKey: "alice-org-a|workspace|session",
|
|
nextScopeKey: "bob-org-a|workspace|session",
|
|
currentText,
|
|
storedText,
|
|
})).toBe(true);
|
|
expect(composerDraftNeedsHydration({
|
|
claimedScopeKey: "alice-org-a|workspace|session",
|
|
nextScopeKey: "alice-org-b|workspace|session",
|
|
currentText: storedText,
|
|
storedText,
|
|
})).toBe(true);
|
|
});
|
|
});
|