Skip to content

Commit 5120de6

Browse files
committed
🤖 tests: add ipc compactHistory integration test (Haiku)
1 parent b5f9d32 commit 5120de6

File tree

1 file changed

+123
-0
lines changed

1 file changed

+123
-0
lines changed

tests/ipc/compactHistory.test.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/**
2+
* compactHistory integration tests.
3+
*
4+
* Ensures compaction is a control-plane operation (not a slash-command string), and that
5+
* history is replaced only on successful compaction completion.
6+
*
7+
* Requirements:
8+
* - Uses the Haiku model for both normal messages and compaction
9+
* - Builds history by sending messages (replicates user behavior)
10+
*/
11+
12+
import { shouldRunIntegrationTests, validateApiKeys } from "./setup";
13+
import {
14+
createSharedRepo,
15+
cleanupSharedRepo,
16+
withSharedWorkspace,
17+
configureTestRetries,
18+
} from "./sendMessageTestHelpers";
19+
import { assertStreamSuccess, modelString, sendMessageWithModel } from "./helpers";
20+
import { KNOWN_MODELS } from "../../src/common/constants/knownModels";
21+
22+
// Skip all tests if TEST_INTEGRATION is not set
23+
const describeIntegration = shouldRunIntegrationTests() ? describe : describe.skip;
24+
25+
if (shouldRunIntegrationTests()) {
26+
validateApiKeys(["ANTHROPIC_API_KEY"]);
27+
}
28+
29+
beforeAll(createSharedRepo);
30+
afterAll(cleanupSharedRepo);
31+
32+
describeIntegration("compactHistory integration tests", () => {
33+
configureTestRetries(3);
34+
35+
test.concurrent(
36+
"should compact history using Haiku for both messages + compaction",
37+
async () => {
38+
await withSharedWorkspace("anthropic", async ({ env, workspaceId, collector }) => {
39+
const haiku = modelString("anthropic", KNOWN_MODELS.HAIKU.providerModelId);
40+
41+
// Build history via normal user interactions.
42+
collector.clear();
43+
44+
const message1 =
45+
"You are helping me plan a small refactor. Explain, in a few sentences, what the risks are when refactoring code without tests.";
46+
const result1 = await sendMessageWithModel(env, workspaceId, message1, haiku);
47+
expect(result1.success).toBe(true);
48+
const streamEnd1 = await collector.waitForEvent("stream-end", 20000);
49+
expect(streamEnd1).not.toBeNull();
50+
expect((streamEnd1 as { metadata: { model?: string } }).metadata.model).toBe(haiku);
51+
assertStreamSuccess(collector);
52+
53+
collector.clear();
54+
55+
const message2 =
56+
"Now list three concrete steps I should take to refactor safely. Include enough detail that it would be useful in a code review.";
57+
const result2 = await sendMessageWithModel(env, workspaceId, message2, haiku);
58+
expect(result2.success).toBe(true);
59+
const streamEnd2 = await collector.waitForEvent("stream-end", 20000);
60+
expect(streamEnd2).not.toBeNull();
61+
expect((streamEnd2 as { metadata: { model?: string } }).metadata.model).toBe(haiku);
62+
assertStreamSuccess(collector);
63+
64+
collector.clear();
65+
66+
// Trigger compaction explicitly via the control-plane API.
67+
const compactResult = await env.orpc.workspace.compactHistory({
68+
workspaceId,
69+
model: haiku,
70+
maxOutputTokens: 800,
71+
source: "user",
72+
interrupt: "none",
73+
sendMessageOptions: {
74+
model: haiku,
75+
thinkingLevel: "off",
76+
},
77+
});
78+
79+
expect(compactResult.success).toBe(true);
80+
if (!compactResult.success) {
81+
throw new Error(String(compactResult.error));
82+
}
83+
84+
// Ensure this stream is actually the compaction stream.
85+
const streamStart = await collector.waitForEvent("stream-start", 20000);
86+
expect(streamStart).not.toBeNull();
87+
const compactionMessageId = (streamStart as { messageId: string }).messageId;
88+
89+
const streamEnd = await collector.waitForEvent("stream-end", 30000);
90+
expect(streamEnd).not.toBeNull();
91+
expect((streamEnd as { messageId: string }).messageId).toBe(compactionMessageId);
92+
expect((streamEnd as { metadata: { model?: string } }).metadata.model).toBe(haiku);
93+
assertStreamSuccess(collector);
94+
95+
// The compaction handler emits a single summary message + delete event.
96+
const deleteEvent = collector.getEvents().find((e) => e.type === "delete");
97+
expect(deleteEvent).toBeDefined();
98+
99+
const summaryMessage = collector
100+
.getEvents()
101+
.find((e) => e.type === "message" && e.role === "assistant" && e.metadata?.compacted);
102+
expect(summaryMessage).toBeDefined();
103+
expect((summaryMessage as { metadata?: { model?: string } }).metadata?.model).toBe(haiku);
104+
105+
// Verify persisted history was replaced (user behavior: reload workspace).
106+
const replay = await env.orpc.workspace.getFullReplay({ workspaceId });
107+
const replayMessages = replay.filter((m) => m.type === "message");
108+
109+
// After compaction we should only have a single assistant summary message.
110+
expect(replayMessages).toHaveLength(1);
111+
expect(replayMessages[0].role).toBe("assistant");
112+
expect(replayMessages[0].metadata?.compacted).toBeDefined();
113+
expect(replayMessages[0].metadata?.model).toBe(haiku);
114+
115+
// Sanity check: original user prompt text should not be present after replacement.
116+
const replayText = JSON.stringify(replayMessages[0]);
117+
expect(replayText).not.toContain("refactoring code without tests");
118+
expect(replayText).not.toContain("three concrete steps");
119+
});
120+
},
121+
90000
122+
);
123+
});

0 commit comments

Comments
 (0)