



























@@ -39,13 +39,43 @@ function requireString(value: string | undefined, label: string): string {
3939return value;
4040}
414142-function requireValue<T>(value: T | undefined, label: string): T {
43-if (value === undefined) {
42+function requireValue<T>(value: T | null | undefined, label: string): T {
43+if (value == null) {
4444throw new Error(`expected ${label}`);
4545}
4646return value;
4747}
484849+type TranscriptEntry = ReturnType<SessionManager["getEntries"]>[number];
50+51+function requireEntryByIdAndType<T extends TranscriptEntry["type"]>(
52+entries: readonly TranscriptEntry[],
53+id: string,
54+type: T,
55+label: string,
56+): Extract<TranscriptEntry, { type: T }> {
57+const entry = entries.find((candidate) => candidate.id === id);
58+if (!entry) {
59+throw new Error(`expected ${label}`);
60+}
61+if (entry.type !== type) {
62+throw new Error(`expected ${label} to be ${type}, got ${entry.type}`);
63+}
64+return entry as Extract<TranscriptEntry, { type: T }>;
65+}
66+67+function requireEntryByType<T extends TranscriptEntry["type"]>(
68+entries: readonly TranscriptEntry[],
69+type: T,
70+label: string,
71+): Extract<TranscriptEntry, { type: T }> {
72+const entry = entries.find((candidate) => candidate.type === type);
73+if (!entry) {
74+throw new Error(`expected ${label}`);
75+}
76+return entry as Extract<TranscriptEntry, { type: T }>;
77+}
78+4979function createCompactedSession(sessionDir: string): {
5080manager: SessionManager;
5181sessionFile: string;
@@ -91,10 +121,9 @@ describe("rotateTranscriptAfterCompaction", () => {
91121const successorFile = requireString(result.sessionFile, "successor session file");
9212293123const successor = SessionManager.open(successorFile);
94-expect(successor.getHeader()).toMatchObject({
95-parentSession: sessionFile,
96-cwd: dir,
97-});
124+const header = requireValue(successor.getHeader(), "successor header");
125+expect(header.parentSession).toBe(sessionFile);
126+expect(header.cwd).toBe(dir);
98127expect(successor.buildSessionContext().messages.length).toBeGreaterThan(0);
99128});
100129@@ -117,20 +146,19 @@ describe("rotateTranscriptAfterCompaction", () => {
117146expect(await fs.readFile(sessionFile, "utf8")).toBe(originalBytes);
118147119148const successor = SessionManager.open(successorFile);
120-expect(successor.getHeader()).toMatchObject({
121-id: successorSessionId,
122-parentSession: sessionFile,
123-cwd: dir,
124-});
149+const header = requireValue(successor.getHeader(), "successor header");
150+expect(header.id).toBe(successorSessionId);
151+expect(header.parentSession).toBe(sessionFile);
152+expect(header.cwd).toBe(dir);
125153expect(successor.getEntries().length).toBeLessThan(originalEntryCount);
126154expect(successor.getBranch()[0]?.type).toBe("model_change");
127-expect(successor.getBranch()).toContainEqual(
128-expect.objectContaining({
129-type: "custom",
130-customType: "test-extension",
131-data: { cursor: "before-compaction" },
132-}),
155+const customBranchEntry = requireEntryByType(
156+successor.getBranch(),
157+"custom",
158+"preserved custom branch entry",
133159);
160+expect(customBranchEntry.customType).toBe("test-extension");
161+expect(customBranchEntry.data).toStrictEqual({ cursor: "before-compaction" });
134162135163const context = successor.buildSessionContext();
136164const contextText = JSON.stringify(context.messages);
@@ -184,17 +212,12 @@ describe("rotateTranscriptAfterCompaction", () => {
184212expect(countEntryType("model_change")).toBe(1);
185213expect(countEntryType("thinking_level_change")).toBe(1);
186214expect(countEntryType("session_info")).toBe(1);
187-expect(entries.find((entry) => entry.type === "model_change")).toMatchObject({
188-provider: "openai",
189-modelId: "gpt-5.2",
190-});
191-expect(entries).toContainEqual(
192-expect.objectContaining({
193-type: "custom",
194-customType: "test-extension",
195-data: { cursor: "preserved" },
196-}),
197-);
215+const modelChange = requireEntryByType(entries, "model_change", "current model change");
216+expect(modelChange.provider).toBe("openai");
217+expect(modelChange.modelId).toBe("gpt-5.2");
218+const customEntry = requireEntryByType(entries, "custom", "preserved custom entry");
219+expect(customEntry.customType).toBe("test-extension");
220+expect(customEntry.data).toStrictEqual({ cursor: "preserved" });
198221199222const context = successor.buildSessionContext();
200223expect(context.thinkingLevel).toBe("high");
@@ -250,10 +273,8 @@ describe("rotateTranscriptAfterCompaction", () => {
250273sessionFile: requireString(manager.getSessionFile(), "source session file"),
251274});
252275253-expect(result).toMatchObject({
254-rotated: false,
255-reason: "no compaction entry",
256-});
276+expect(result.rotated).toBe(false);
277+expect(result.reason).toBe("no compaction entry");
257278});
258279259280it("uses a refreshed manager after manual boundary hardening", async () => {
@@ -297,9 +318,10 @@ describe("rotateTranscriptAfterCompaction", () => {
297318const successorCompaction = successor
298319.getEntries()
299320.find((entry) => entry.type === "compaction" && entry.id === compactionId);
300-expect(successorCompaction).toMatchObject({
301-firstKeptEntryId: compactionId,
302-});
321+if (!successorCompaction || successorCompaction.type !== "compaction") {
322+throw new Error("expected successor compaction entry");
323+}
324+expect(successorCompaction.firstKeptEntryId).toBe(compactionId);
303325});
304326305327it("preserves unsummarized sibling branches and branch summaries", async () => {
@@ -338,14 +360,23 @@ describe("rotateTranscriptAfterCompaction", () => {
338360requireString(result.sessionFile, "successor session file"),
339361);
340362const allEntries = successor.getEntries();
341-expect(allEntries.find((entry) => entry.id === branchSummaryId)).toMatchObject({
342-type: "branch_summary",
343-summary: "Summary of the abandoned branch.",
344-});
345-expect(allEntries.find((entry) => entry.id === siblingMsgId)).toMatchObject({
346-type: "message",
347-message: expect.objectContaining({ content: "do task B instead" }),
348-});
363+const branchSummary = requireEntryByIdAndType(
364+allEntries,
365+branchSummaryId,
366+"branch_summary",
367+"preserved branch summary",
368+);
369+expect(branchSummary.summary).toBe("Summary of the abandoned branch.");
370+const siblingMessage = requireEntryByIdAndType(
371+allEntries,
372+siblingMsgId,
373+"message",
374+"preserved sibling message",
375+);
376+if (!("content" in siblingMessage.message)) {
377+throw new Error("expected sibling message content");
378+}
379+expect(siblingMessage.message.content).toBe("do task B instead");
349380350381const activeContextText = JSON.stringify(successor.buildSessionContext().messages);
351382expect(activeContextText).toContain("Summary of main branch.");
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。