Files
openwork/apps/app/tests/composer-state-store.test.ts

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);
});
});