源码快照 @ 2026-09-02 (source-only, 剔除非源码素材)
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user