diff --git a/test/reinject-mid-turn.test.ts b/test/reinject-mid-turn.test.ts new file mode 100644 index 0000000..2167f7e --- /dev/null +++ b/test/reinject-mid-turn.test.ts @@ -0,0 +1,116 @@ +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "fs"; +import { tmpdir } from "os"; +import { join } from "path"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { createCompactionRuntime } from "../src/compaction"; +import { DEFERRED_REINJECT_CUSTOM_TYPE, deliverDeferredReinjectSteer, tryConsumeDeferredReinject } from "../src/reinject"; +import { createDefaultSettings } from "../src/settings"; +import { createInitialState, trackSkill } from "../src/state"; + +const tempDirs: string[] = []; + +afterEach(() => { + for (const dir of tempDirs.splice(0)) { + rmSync(dir, { recursive: true, force: true }); + } +}); + +function tempSkillDir(name: string): { filePath: string; baseDir: string } { + const root = mkdtempSync(join(tmpdir(), "pi-skill-reinject-mid-turn-")); + tempDirs.push(root); + const baseDir = join(root, name); + mkdirSync(baseDir, { recursive: true }); + const filePath = join(baseDir, "SKILL.md"); + writeFileSync(filePath, `---\nname: ${name}\n---\n# body\n`, "utf8"); + return { filePath, baseDir }; +} + +describe("deliverDeferredReinjectSteer", () => { + it("sends steer message and clears pending when skills resolve", () => { + const { filePath, baseDir } = tempSkillDir("alpha"); + const runtime = createCompactionRuntime(); + const state = createInitialState(); + trackSkill(state, { name: "alpha", filePath, baseDir, source: "slash" }); + state.pendingReinject = ["alpha"]; + const sendMessage = vi.fn(); + const pi = { sendMessage } as never; + const registered = [{ name: "alpha", filePath, baseDir }]; + + const delivered = deliverDeferredReinjectSteer( + pi, + state, + createDefaultSettings(), + registered, + runtime, + "compact-1", + ); + + expect(delivered).toBe(true); + expect(state.pendingReinject).toEqual([]); + expect(runtime.deferredDeliveredForCompactionId).toBe("compact-1"); + expect(sendMessage).toHaveBeenCalledOnce(); + expect(sendMessage).toHaveBeenCalledWith( + expect.objectContaining({ + customType: DEFERRED_REINJECT_CUSTOM_TYPE, + display: true, + content: expect.stringContaining(' { + const runtime = createCompactionRuntime(); + const state = createInitialState(); + const sendMessage = vi.fn(); + const pi = { sendMessage } as never; + + expect( + deliverDeferredReinjectSteer(pi, state, createDefaultSettings(), [], runtime, "compact-1"), + ).toBe(false); + expect(sendMessage).not.toHaveBeenCalled(); + }); +}); + +describe("tryConsumeDeferredReinject after mid-turn steer", () => { + it("skips before_agent_start inject when steer already delivered for compaction", () => { + const { filePath, baseDir } = tempSkillDir("beta"); + const runtime = createCompactionRuntime(); + runtime.lastCompactionEntryId = "compact-2"; + runtime.deferredDeliveredForCompactionId = "compact-2"; + const state = createInitialState(); + trackSkill(state, { name: "beta", filePath, baseDir, source: "slash" }); + state.pendingReinject = ["beta"]; + + expect( + tryConsumeDeferredReinject( + state, + createDefaultSettings(), + [{ name: "beta", filePath, baseDir }], + undefined, + runtime, + ), + ).toBeUndefined(); + }); + + it("consumes pending on before_agent_start when steer did not run (idle path)", () => { + const { filePath, baseDir } = tempSkillDir("gamma"); + const runtime = createCompactionRuntime(); + runtime.lastCompactionEntryId = "compact-3"; + const state = createInitialState(); + trackSkill(state, { name: "gamma", filePath, baseDir, source: "slash" }); + state.pendingReinject = ["gamma"]; + + const result = tryConsumeDeferredReinject( + state, + createDefaultSettings(), + [{ name: "gamma", filePath, baseDir }], + undefined, + runtime, + ); + + expect(result?.message.customType).toBe(DEFERRED_REINJECT_CUSTOM_TYPE); + expect(state.pendingReinject).toEqual([]); + expect(runtime.deferredDeliveredForCompactionId).toBeNull(); + }); +});