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([]);
+ });
+});