diff --git a/test/kept-window.test.ts b/test/kept-window.test.ts new file mode 100644 index 0000000..72633dd --- /dev/null +++ b/test/kept-window.test.ts @@ -0,0 +1,117 @@ +import type { SessionEntry } from "@earendil-works/pi-coding-agent"; +import { describe, expect, it } from "vitest"; +import { + filterSkillsNeedingReinject, + getKeptEntries, + skillsPresentInKeptWindow, +} from "../src/kept"; +import type { TrackedSkill } from "../src/state"; + +const ts = "2024-12-03T14:00:00.000Z"; + +function userMessage(id: string, content: string): SessionEntry { + return { + type: "message", + id, + parentId: null, + timestamp: ts, + message: { role: "user", content }, + } as SessionEntry; +} + +function assistantMessage(id: string, content: string): SessionEntry { + return { + type: "message", + id, + parentId: null, + timestamp: ts, + message: { role: "assistant", content: [{ type: "text", text: content }] }, + } as SessionEntry; +} + +function compactionEntry(id: string, firstKeptEntryId: string): SessionEntry { + return { + type: "compaction", + id, + parentId: null, + timestamp: ts, + summary: "summary", + firstKeptEntryId, + tokensBefore: 1000, + } as SessionEntry; +} + +const skillBlock = (name: string) => + `\nbody\n`; + +const trackedSkills: TrackedSkill[] = [ + { + name: "alpha", + filePath: "/skills/alpha/SKILL.md", + baseDir: "/skills/alpha", + firstSeenAt: 1, + lastSeenAt: 1, + sources: ["slash"], + }, + { + name: "beta", + filePath: "/skills/beta/SKILL.md", + baseDir: "/skills/beta", + firstSeenAt: 2, + lastSeenAt: 2, + sources: ["skill-block"], + }, +]; + +describe("getKeptEntries", () => { + const branch: SessionEntry[] = [ + userMessage("e1", "old"), + userMessage("e2", "kept start"), + assistantMessage("e3", "reply"), + compactionEntry("e4", "e2"), + userMessage("e5", "after compact"), + ]; + + it("slices from firstKeptEntryId through tail", () => { + expect(getKeptEntries(branch, "e2").map((entry) => entry.id)).toEqual(["e2", "e3", "e4", "e5"]); + }); + + it("returns empty when firstKeptEntryId is missing", () => { + expect(getKeptEntries(branch, "missing")).toEqual([]); + expect(getKeptEntries([], "e1")).toEqual([]); + }); +}); + +describe("skillsPresentInKeptWindow", () => { + it("detects skill blocks in kept user messages", () => { + const kept = [userMessage("k1", skillBlock("alpha")), assistantMessage("k2", "ignored")]; + expect(skillsPresentInKeptWindow(kept, ["alpha", "beta"])).toEqual(new Set(["alpha"])); + }); + + it("returns empty when no tracked skills or no blocks", () => { + expect(skillsPresentInKeptWindow([userMessage("k1", "plain")], ["alpha"])).toEqual(new Set()); + expect(skillsPresentInKeptWindow([userMessage("k1", skillBlock("alpha"))], [])).toEqual(new Set()); + expect(skillsPresentInKeptWindow([], ["alpha", "beta"])).toEqual(new Set()); + }); +}); + +describe("filterSkillsNeedingReinject", () => { + it("returns registered tracked skills absent from kept window", () => { + const keptPresent = new Set(["alpha"]); + const registered = new Set(["alpha", "beta"]); + expect(filterSkillsNeedingReinject(trackedSkills, keptPresent, registered)).toEqual(["beta"]); + }); + + it("excludes unregistered skills and preserves tracked order", () => { + const keptPresent = new Set(); + const registered = new Set(["alpha", "beta"]); + expect(filterSkillsNeedingReinject(trackedSkills, keptPresent, registered)).toEqual(["alpha", "beta"]); + expect(filterSkillsNeedingReinject(trackedSkills, keptPresent, new Set(["beta"]))).toEqual(["beta"]); + }); + + it("returns empty when all tracked skills are in kept window", () => { + const keptPresent = new Set(["alpha", "beta"]); + const registered = new Set(["alpha", "beta"]); + expect(filterSkillsNeedingReinject(trackedSkills, keptPresent, registered)).toEqual([]); + }); +});