Clarify compaction test harness (#26777)
This commit is contained in:
parent
426d92e352
commit
fed716ada5
1 changed files with 38 additions and 48 deletions
|
|
@ -233,16 +233,22 @@ const env = Layer.mergeAll(
|
||||||
|
|
||||||
const it = testEffect(env)
|
const it = testEffect(env)
|
||||||
|
|
||||||
const processEnv = Layer.mergeAll(SessionNs.defaultLayer, CrossSpawnSpawner.defaultLayer)
|
const compactionEnv = Layer.mergeAll(SessionNs.defaultLayer, CrossSpawnSpawner.defaultLayer)
|
||||||
const itProcess = testEffect(processEnv)
|
const itCompaction = testEffect(compactionEnv)
|
||||||
|
|
||||||
function compactionProcessLayer(options?: {
|
type CompactionProcessOptions = {
|
||||||
result?: "continue" | "compact"
|
result?: "continue" | "compact"
|
||||||
llm?: Layer.Layer<LLM.Service>
|
llm?: Layer.Layer<LLM.Service>
|
||||||
plugin?: Layer.Layer<Plugin.Service>
|
plugin?: Layer.Layer<Plugin.Service>
|
||||||
provider?: ReturnType<typeof ProviderTest.fake>
|
provider?: ReturnType<typeof ProviderTest.fake>
|
||||||
config?: Layer.Layer<Config.Service>
|
config?: Layer.Layer<Config.Service>
|
||||||
}) {
|
}
|
||||||
|
|
||||||
|
function withCompaction(options?: CompactionProcessOptions) {
|
||||||
|
return Effect.provide(compactionProcessLayer(options))
|
||||||
|
}
|
||||||
|
|
||||||
|
function compactionProcessLayer(options?: CompactionProcessOptions) {
|
||||||
const bus = Bus.layer
|
const bus = Bus.layer
|
||||||
const status = SessionStatus.layer.pipe(Layer.provide(bus))
|
const status = SessionStatus.layer.pipe(Layer.provide(bus))
|
||||||
const processor = options?.llm
|
const processor = options?.llm
|
||||||
|
|
@ -857,7 +863,7 @@ describe("session.compaction.process", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
itProcess.instance(
|
itCompaction.instance(
|
||||||
"marks summary message as errored on compact result",
|
"marks summary message as errored on compact result",
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const ssn = yield* SessionNs.Service
|
const ssn = yield* SessionNs.Service
|
||||||
|
|
@ -882,7 +888,7 @@ describe("session.compaction.process", () => {
|
||||||
expect(summary.info.finish).toBe("error")
|
expect(summary.info.finish).toBe("error")
|
||||||
expect(JSON.stringify(summary.info.error)).toContain("Session too large to compact")
|
expect(JSON.stringify(summary.info.error)).toContain("Session too large to compact")
|
||||||
}
|
}
|
||||||
}).pipe(Effect.provide(compactionProcessLayer({ result: "compact" }))),
|
}).pipe(withCompaction({ result: "compact" })),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.instance(
|
it.instance(
|
||||||
|
|
@ -916,7 +922,7 @@ describe("session.compaction.process", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
itProcess.instance(
|
itCompaction.instance(
|
||||||
"persists tail_start_id for retained recent turns",
|
"persists tail_start_id for retained recent turns",
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const ssn = yield* SessionNs.Service
|
const ssn = yield* SessionNs.Service
|
||||||
|
|
@ -939,10 +945,10 @@ describe("session.compaction.process", () => {
|
||||||
const part = yield* readCompactionPart(session.id)
|
const part = yield* readCompactionPart(session.id)
|
||||||
expect(part?.type).toBe("compaction")
|
expect(part?.type).toBe("compaction")
|
||||||
expect(part?.tail_start_id).toBe(keep.id)
|
expect(part?.tail_start_id).toBe(keep.id)
|
||||||
}).pipe(Effect.provide(compactionProcessLayer({ config: cfg({ tail_turns: 2, preserve_recent_tokens: 10_000 }) }))),
|
}).pipe(withCompaction({ config: cfg({ tail_turns: 2, preserve_recent_tokens: 10_000 }) })),
|
||||||
)
|
)
|
||||||
|
|
||||||
itProcess.instance(
|
itCompaction.instance(
|
||||||
"shrinks retained tail to fit preserve token budget",
|
"shrinks retained tail to fit preserve token budget",
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const ssn = yield* SessionNs.Service
|
const ssn = yield* SessionNs.Service
|
||||||
|
|
@ -965,10 +971,10 @@ describe("session.compaction.process", () => {
|
||||||
const part = yield* readCompactionPart(session.id)
|
const part = yield* readCompactionPart(session.id)
|
||||||
expect(part?.type).toBe("compaction")
|
expect(part?.type).toBe("compaction")
|
||||||
expect(part?.tail_start_id).toBe(keep.id)
|
expect(part?.tail_start_id).toBe(keep.id)
|
||||||
}).pipe(Effect.provide(compactionProcessLayer({ config: cfg({ tail_turns: 2, preserve_recent_tokens: 100 }) }))),
|
}).pipe(withCompaction({ config: cfg({ tail_turns: 2, preserve_recent_tokens: 100 }) })),
|
||||||
)
|
)
|
||||||
|
|
||||||
itProcess.instance(
|
itCompaction.instance(
|
||||||
"falls back to full summary when even one recent turn exceeds preserve token budget",
|
"falls back to full summary when even one recent turn exceeds preserve token budget",
|
||||||
() => {
|
() => {
|
||||||
const stub = llm()
|
const stub = llm()
|
||||||
|
|
@ -990,16 +996,12 @@ describe("session.compaction.process", () => {
|
||||||
expect(part?.type).toBe("compaction")
|
expect(part?.type).toBe("compaction")
|
||||||
expect(part?.tail_start_id).toBeUndefined()
|
expect(part?.tail_start_id).toBeUndefined()
|
||||||
expect(captured).toContain("yyyy")
|
expect(captured).toContain("yyyy")
|
||||||
}).pipe(
|
}).pipe(withCompaction({ llm: stub.layer, config: cfg({ tail_turns: 1, preserve_recent_tokens: 20 }) }))
|
||||||
Effect.provide(
|
|
||||||
compactionProcessLayer({ llm: stub.layer, config: cfg({ tail_turns: 1, preserve_recent_tokens: 20 }) }),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{ git: true },
|
{ git: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
itProcess.instance(
|
itCompaction.instance(
|
||||||
"falls back to full summary when retained tail media exceeds preserve token budget",
|
"falls back to full summary when retained tail media exceeds preserve token budget",
|
||||||
() => {
|
() => {
|
||||||
const stub = llm()
|
const stub = llm()
|
||||||
|
|
@ -1031,16 +1033,12 @@ describe("session.compaction.process", () => {
|
||||||
expect(part?.tail_start_id).toBeUndefined()
|
expect(part?.tail_start_id).toBeUndefined()
|
||||||
expect(captured).toContain("recent image turn")
|
expect(captured).toContain("recent image turn")
|
||||||
expect(captured).toContain("Attached image/png: big.png")
|
expect(captured).toContain("Attached image/png: big.png")
|
||||||
}).pipe(
|
}).pipe(withCompaction({ llm: stub.layer, config: cfg({ tail_turns: 1, preserve_recent_tokens: 100 }) }))
|
||||||
Effect.provide(
|
|
||||||
compactionProcessLayer({ llm: stub.layer, config: cfg({ tail_turns: 1, preserve_recent_tokens: 100 }) }),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{ git: true },
|
{ git: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
itProcess.instance(
|
itCompaction.instance(
|
||||||
"retains a split turn suffix when a later message fits the preserve token budget",
|
"retains a split turn suffix when a later message fits the preserve token budget",
|
||||||
() => {
|
() => {
|
||||||
const stub = llm()
|
const stub = llm()
|
||||||
|
|
@ -1086,16 +1084,12 @@ describe("session.compaction.process", () => {
|
||||||
expect(filtered[1]?.info.role).toBe("assistant")
|
expect(filtered[1]?.info.role).toBe("assistant")
|
||||||
expect(filtered[1]?.info.role === "assistant" ? filtered[1].info.summary : false).toBe(true)
|
expect(filtered[1]?.info.role === "assistant" ? filtered[1].info.summary : false).toBe(true)
|
||||||
expect(filtered.map((msg) => msg.info.id)).not.toContain(large.id)
|
expect(filtered.map((msg) => msg.info.id)).not.toContain(large.id)
|
||||||
}).pipe(
|
}).pipe(withCompaction({ llm: stub.layer, config: cfg({ tail_turns: 1, preserve_recent_tokens: 100 }) }))
|
||||||
Effect.provide(
|
|
||||||
compactionProcessLayer({ llm: stub.layer, config: cfg({ tail_turns: 1, preserve_recent_tokens: 100 }) }),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{ git: true },
|
{ git: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
itProcess.instance(
|
itCompaction.instance(
|
||||||
"allows plugins to disable synthetic continue prompt",
|
"allows plugins to disable synthetic continue prompt",
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const ssn = yield* SessionNs.Service
|
const ssn = yield* SessionNs.Service
|
||||||
|
|
@ -1124,7 +1118,7 @@ describe("session.compaction.process", () => {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).toBe(false)
|
).toBe(false)
|
||||||
}).pipe(Effect.provide(compactionProcessLayer({ plugin: autocontinue(false) }))),
|
}).pipe(withCompaction({ plugin: autocontinue(false) })),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.instance(
|
it.instance(
|
||||||
|
|
@ -1192,7 +1186,7 @@ describe("session.compaction.process", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
itProcess.instance(
|
itCompaction.instance(
|
||||||
"stops quickly when aborted during retry backoff",
|
"stops quickly when aborted during retry backoff",
|
||||||
() => {
|
() => {
|
||||||
const stub = llm()
|
const stub = llm()
|
||||||
|
|
@ -1249,12 +1243,12 @@ describe("session.compaction.process", () => {
|
||||||
expect(Cause.hasInterrupts(exit.cause)).toBe(true)
|
expect(Cause.hasInterrupts(exit.cause)).toBe(true)
|
||||||
expect(Date.now() - start).toBeLessThan(250)
|
expect(Date.now() - start).toBeLessThan(250)
|
||||||
}
|
}
|
||||||
}).pipe(Effect.provide(compactionProcessLayer({ llm: stub.layer })))
|
}).pipe(withCompaction({ llm: stub.layer }))
|
||||||
},
|
},
|
||||||
{ git: true },
|
{ git: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
itProcess.instance(
|
itCompaction.instance(
|
||||||
"does not leave a summary assistant when aborted before processor setup",
|
"does not leave a summary assistant when aborted before processor setup",
|
||||||
() =>
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|
@ -1281,12 +1275,12 @@ describe("session.compaction.process", () => {
|
||||||
expect(Exit.isFailure(exit)).toBe(true)
|
expect(Exit.isFailure(exit)).toBe(true)
|
||||||
if (Exit.isFailure(exit)) expect(Cause.hasInterrupts(exit.cause)).toBe(true)
|
if (Exit.isFailure(exit)) expect(Cause.hasInterrupts(exit.cause)).toBe(true)
|
||||||
expect(all.some((msg) => msg.info.role === "assistant" && msg.info.summary)).toBe(false)
|
expect(all.some((msg) => msg.info.role === "assistant" && msg.info.summary)).toBe(false)
|
||||||
}).pipe(Effect.provide(compactionProcessLayer({ plugin: plugin(ready) })))
|
}).pipe(withCompaction({ plugin: plugin(ready) }))
|
||||||
}),
|
}),
|
||||||
{ git: true },
|
{ git: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
itProcess.instance(
|
itCompaction.instance(
|
||||||
"does not allow tool calls while generating the summary",
|
"does not allow tool calls while generating the summary",
|
||||||
() => {
|
() => {
|
||||||
const stub = llm()
|
const stub = llm()
|
||||||
|
|
@ -1350,12 +1344,12 @@ describe("session.compaction.process", () => {
|
||||||
|
|
||||||
expect(summary?.info.role).toBe("assistant")
|
expect(summary?.info.role).toBe("assistant")
|
||||||
expect(summary?.parts.some((part) => part.type === "tool")).toBe(false)
|
expect(summary?.parts.some((part) => part.type === "tool")).toBe(false)
|
||||||
}).pipe(Effect.provide(compactionProcessLayer({ llm: stub.layer })))
|
}).pipe(withCompaction({ llm: stub.layer }))
|
||||||
},
|
},
|
||||||
{ git: true },
|
{ git: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
itProcess.instance(
|
itCompaction.instance(
|
||||||
"summarizes only the head while keeping recent tail out of summary input",
|
"summarizes only the head while keeping recent tail out of summary input",
|
||||||
() => {
|
() => {
|
||||||
const stub = llm()
|
const stub = llm()
|
||||||
|
|
@ -1387,12 +1381,12 @@ describe("session.compaction.process", () => {
|
||||||
expect(captured).not.toContain("keep this turn")
|
expect(captured).not.toContain("keep this turn")
|
||||||
expect(captured).not.toContain("and this one too")
|
expect(captured).not.toContain("and this one too")
|
||||||
expect(captured).not.toContain("What did we do so far?")
|
expect(captured).not.toContain("What did we do so far?")
|
||||||
}).pipe(Effect.provide(compactionProcessLayer({ llm: stub.layer })))
|
}).pipe(withCompaction({ llm: stub.layer }))
|
||||||
},
|
},
|
||||||
{ git: true },
|
{ git: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
itProcess.instance(
|
itCompaction.instance(
|
||||||
"anchors repeated compactions with the previous summary",
|
"anchors repeated compactions with the previous summary",
|
||||||
() => {
|
() => {
|
||||||
const stub = llm()
|
const stub = llm()
|
||||||
|
|
@ -1429,12 +1423,12 @@ describe("session.compaction.process", () => {
|
||||||
expect(captured.match(/summary one/g)?.length).toBe(1)
|
expect(captured.match(/summary one/g)?.length).toBe(1)
|
||||||
expect(captured).toContain("## Constraints & Preferences")
|
expect(captured).toContain("## Constraints & Preferences")
|
||||||
expect(captured).toContain("## Progress")
|
expect(captured).toContain("## Progress")
|
||||||
}).pipe(Effect.provide(compactionProcessLayer({ llm: stub.layer })))
|
}).pipe(withCompaction({ llm: stub.layer }))
|
||||||
},
|
},
|
||||||
{ git: true },
|
{ git: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
itProcess.instance("keeps recent pre-compaction turns across repeated compactions", () => {
|
itCompaction.instance("keeps recent pre-compaction turns across repeated compactions", () => {
|
||||||
const stub = llm()
|
const stub = llm()
|
||||||
stub.push(reply("summary one"))
|
stub.push(reply("summary one"))
|
||||||
stub.push(reply("summary two"))
|
stub.push(reply("summary two"))
|
||||||
|
|
@ -1471,14 +1465,10 @@ describe("session.compaction.process", () => {
|
||||||
expect(
|
expect(
|
||||||
filtered.some((msg) => msg.info.role === "user" && msg.parts.some((part) => part.type === "compaction")),
|
filtered.some((msg) => msg.info.role === "user" && msg.parts.some((part) => part.type === "compaction")),
|
||||||
).toBe(true)
|
).toBe(true)
|
||||||
}).pipe(
|
}).pipe(withCompaction({ llm: stub.layer, config: cfg({ tail_turns: 2, preserve_recent_tokens: 10_000 }) }))
|
||||||
Effect.provide(
|
|
||||||
compactionProcessLayer({ llm: stub.layer, config: cfg({ tail_turns: 2, preserve_recent_tokens: 10_000 }) }),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
itProcess.instance(
|
itCompaction.instance(
|
||||||
"ignores previous summaries when sizing the retained tail",
|
"ignores previous summaries when sizing the retained tail",
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const ssn = yield* SessionNs.Service
|
const ssn = yield* SessionNs.Service
|
||||||
|
|
@ -1519,7 +1509,7 @@ describe("session.compaction.process", () => {
|
||||||
const part = yield* readCompactionPart(session.id)
|
const part = yield* readCompactionPart(session.id)
|
||||||
expect(part?.type).toBe("compaction")
|
expect(part?.type).toBe("compaction")
|
||||||
expect(part?.tail_start_id).toBe(keep.id)
|
expect(part?.tail_start_id).toBe(keep.id)
|
||||||
}).pipe(Effect.provide(compactionProcessLayer({ config: cfg({ tail_turns: 2, preserve_recent_tokens: 500 }) }))),
|
}).pipe(withCompaction({ config: cfg({ tail_turns: 2, preserve_recent_tokens: 500 }) })),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue