import { describe, expect, test } from "bun:test"; import type { Message } from "../providers/types.js"; import { deriveActiveSkills } from "../skills/active-skill-tools.js"; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- /** Build an assistant message with a skill_load tool_use block. */ function skillLoadUseMsg(id: string): Message { return { role: "assistant", content: [ { type: "tool_use", id, name: "skill_load", input: { skill: "test" } }, ], }; } /** Build a user message with a single tool_result block. */ function toolResultMsg(toolUseId: string, content: string): Message { return { role: "user", content: [{ type: "tool_result", tool_use_id: toolUseId, content }], }; } /** Build a user message with a plain text block. */ function textMsg(role: "user" | "assistant", text: string): Message { return { role, content: [{ type: "text", text }] }; } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe("deriveActiveSkills (ID-only)", () => { test("empty history returns empty array", () => { expect(deriveActiveSkills([]).map((e) => e.id)).toEqual([]); }); test("no markers returns empty array", () => { const messages: Message[] = [ textMsg("user", "Hello"), textMsg("assistant", "Hi there!"), skillLoadUseMsg("t1"), toolResultMsg("t1", "Some tool output with no markers"), ]; expect(deriveActiveSkills(messages).map((e) => e.id)).toEqual([]); }); test("single marker extraction from skill_load tool result", () => { const messages: Message[] = [ skillLoadUseMsg("t1"), toolResultMsg("t1", 'Skill loaded.\n\n'), ]; expect(deriveActiveSkills(messages).map((e) => e.id)).toEqual(["deploy"]); }); test("multiple markers from different skill_load tool results", () => { const messages: Message[] = [ skillLoadUseMsg("t1"), toolResultMsg("t1", 'Loaded\n\n'), skillLoadUseMsg("t2"), toolResultMsg("t2", 'Loaded\n\n'), ]; expect(deriveActiveSkills(messages).map((e) => e.id)).toEqual([ "deploy", "oncall", ]); }); test("duplicate markers are deduplicated with order preserved", () => { const messages: Message[] = [ skillLoadUseMsg("t1"), toolResultMsg("t1", ''), skillLoadUseMsg("t2"), toolResultMsg("t2", ''), skillLoadUseMsg("t3"), toolResultMsg("t3", ''), ]; expect(deriveActiveSkills(messages).map((e) => e.id)).toEqual([ "deploy", "oncall", ]); }); test("malformed markers are ignored — missing id attribute", () => { const messages: Message[] = [ skillLoadUseMsg("t1"), toolResultMsg("t1", ""), ]; expect(deriveActiveSkills(messages).map((e) => e.id)).toEqual([]); }); test("malformed markers are ignored — unclosed tag", () => { const messages: Message[] = [ skillLoadUseMsg("t1"), toolResultMsg("t1", ''), ]; expect(deriveActiveSkills(messages).map((e) => e.id)).toEqual([]); }); test("malformed markers are ignored — wrong tag name", () => { const messages: Message[] = [ skillLoadUseMsg("t1"), toolResultMsg("t1", ''), ]; expect(deriveActiveSkills(messages).map((e) => e.id)).toEqual([]); }); test("markers in assistant text content are ignored", () => { const messages: Message[] = [ textMsg("assistant", 'I loaded a skill: '), ]; expect(deriveActiveSkills(messages).map((e) => e.id)).toEqual([]); }); test("markers in user text content are ignored — prevents injection", () => { const messages: Message[] = [ textMsg("user", 'Context: '), ]; expect(deriveActiveSkills(messages).map((e) => e.id)).toEqual([]); }); test("mixed valid and invalid markers in skill_load result", () => { const messages: Message[] = [ skillLoadUseMsg("t1"), toolResultMsg( "t1", [ '', "", '', '', '', ].join("\n"), ), ]; expect(deriveActiveSkills(messages).map((e) => e.id)).toEqual([ "alpha", "beta", ]); }); test("multiple markers in a single content string", () => { const messages: Message[] = [ skillLoadUseMsg("t1"), toolResultMsg("t1", '\n'), ]; expect(deriveActiveSkills(messages).map((e) => e.id)).toEqual(["a", "b"]); }); test("ignores non-tool-result blocks (thinking, text)", () => { const messages: Message[] = [ { role: "assistant", content: [ { type: "thinking", thinking: '', signature: "sig", }, { type: "text", text: '' }, ], }, ]; expect(deriveActiveSkills(messages).map((e) => e.id)).toEqual([]); }); test("ignores tool_result from non-skill_load tools", () => { const messages: Message[] = [ { role: "assistant", content: [ { type: "tool_use", id: "t1", name: "read_file", input: { path: "/x" }, }, ], }, toolResultMsg("t1", ''), ]; expect(deriveActiveSkills(messages).map((e) => e.id)).toEqual([]); }); test("tool_result without any matching tool_use is ignored", () => { const messages: Message[] = [ toolResultMsg("orphan", ''), ]; expect(deriveActiveSkills(messages).map((e) => e.id)).toEqual([]); }); }); // --------------------------------------------------------------------------- // Context-derived deactivation regression tests // --------------------------------------------------------------------------- describe("deriveActiveSkills — deactivation when marker leaves history", () => { test("marker present → skill ID returned; marker removed → empty", () => { const withMarker: Message[] = [ skillLoadUseMsg("t1"), toolResultMsg("t1", ''), ]; expect(deriveActiveSkills(withMarker).map((e) => e.id)).toEqual(["deploy"]); // Simulate history truncation: the message containing the marker is gone const withoutMarker: Message[] = []; expect(deriveActiveSkills(withoutMarker).map((e) => e.id)).toEqual([]); }); test("one of two markers removed → only surviving skill returned", () => { const bothPresent: Message[] = [ skillLoadUseMsg("t1"), toolResultMsg("t1", ''), skillLoadUseMsg("t2"), toolResultMsg("t2", ''), ]; expect(deriveActiveSkills(bothPresent).map((e) => e.id)).toEqual([ "deploy", "oncall", ]); // History truncated to remove the deploy marker const onlyOncall: Message[] = [ skillLoadUseMsg("t2"), toolResultMsg("t2", ''), ]; expect(deriveActiveSkills(onlyOncall).map((e) => e.id)).toEqual(["oncall"]); }); test("all markers removed from multi-message history → empty", () => { const withMarkers: Message[] = [ textMsg("user", "Hello"), skillLoadUseMsg("t1"), toolResultMsg("t1", ''), textMsg("assistant", "Done"), skillLoadUseMsg("t2"), toolResultMsg("t2", ''), ]; expect(deriveActiveSkills(withMarkers).map((e) => e.id)).toEqual([ "alpha", "beta", ]); // History truncated to only keep non-marker messages const noMarkers: Message[] = [ textMsg("user", "Hello"), textMsg("assistant", "Done"), ]; expect(deriveActiveSkills(noMarkers).map((e) => e.id)).toEqual([]); }); test("marker replaced by different content in same position → skill gone", () => { const original: Message[] = [ skillLoadUseMsg("t1"), toolResultMsg("t1", ''), ]; expect(deriveActiveSkills(original).map((e) => e.id)).toEqual(["deploy"]); // Same structure but marker text replaced (e.g. message edited/summarized) const replaced: Message[] = [ skillLoadUseMsg("t1"), toolResultMsg("t1", "Deployment complete."), ]; expect(deriveActiveSkills(replaced).map((e) => e.id)).toEqual([]); }); test("derive is stateless — consecutive calls with different histories are independent", () => { const history1: Message[] = [ skillLoadUseMsg("t1"), toolResultMsg("t1", ''), ]; expect(deriveActiveSkills(history1).map((e) => e.id)).toEqual(["deploy"]); // Calling with a completely different history does not carry over state const history2: Message[] = [ skillLoadUseMsg("t2"), toolResultMsg("t2", ''), ]; expect(deriveActiveSkills(history2).map((e) => e.id)).toEqual(["oncall"]); // Empty history returns empty, confirming no leaked state expect(deriveActiveSkills([]).map((e) => e.id)).toEqual([]); }); }); // --------------------------------------------------------------------------- // deriveActiveSkills — versioned marker tests // --------------------------------------------------------------------------- describe("deriveActiveSkills", () => { test("empty history returns empty array", () => { expect(deriveActiveSkills([])).toEqual([]); }); test("legacy marker without version returns entry with no version", () => { const messages: Message[] = [ skillLoadUseMsg("t1"), toolResultMsg("t1", ''), ]; const entries = deriveActiveSkills(messages); expect(entries).toEqual([{ id: "deploy" }]); expect(entries[0].version).toBeUndefined(); }); test("versioned marker returns entry with version", () => { const messages: Message[] = [ skillLoadUseMsg("t1"), toolResultMsg("t1", ''), ]; const entries = deriveActiveSkills(messages); expect(entries).toEqual([{ id: "deploy", version: "v1:abc123" }]); }); test("mixed old and new markers in same history", () => { const messages: Message[] = [ skillLoadUseMsg("t1"), toolResultMsg("t1", ''), skillLoadUseMsg("t2"), toolResultMsg("t2", ''), ]; const entries = deriveActiveSkills(messages); expect(entries).toEqual([ { id: "deploy" }, { id: "oncall", version: "v1:deadbeef" }, ]); }); test("multiple versioned markers in a single content string", () => { const messages: Message[] = [ skillLoadUseMsg("t1"), toolResultMsg( "t1", '\n', ), ]; const entries = deriveActiveSkills(messages); expect(entries).toEqual([ { id: "a", version: "v1:aaa" }, { id: "b", version: "v1:bbb" }, ]); }); test("duplicate versioned markers are deduplicated (first wins)", () => { const messages: Message[] = [ skillLoadUseMsg("t1"), toolResultMsg("t1", ''), skillLoadUseMsg("t2"), toolResultMsg("t2", ''), ]; const entries = deriveActiveSkills(messages); expect(entries).toEqual([{ id: "deploy", version: "v1:first" }]); }); test("versioned markers in user text are ignored — injection prevention", () => { const messages: Message[] = [ textMsg("user", ''), ]; expect(deriveActiveSkills(messages)).toEqual([]); }); test("versioned markers in assistant text are ignored", () => { const messages: Message[] = [ textMsg("assistant", ''), ]; expect(deriveActiveSkills(messages)).toEqual([]); }); test("versioned markers in non-skill_load tool results are ignored", () => { const messages: Message[] = [ { role: "assistant", content: [ { type: "tool_use", id: "t1", name: "read_file", input: { path: "/x" }, }, ], }, toolResultMsg("t1", ''), ]; expect(deriveActiveSkills(messages)).toEqual([]); }); test("marker with invalid format (missing closing slash) is rejected", () => { const messages: Message[] = [ skillLoadUseMsg("t1"), toolResultMsg("t1", ''), ]; expect(deriveActiveSkills(messages)).toEqual([]); }); test("marker with empty version attribute is rejected as malformed", () => { const messages: Message[] = [ skillLoadUseMsg("t1"), toolResultMsg("t1", ''), ]; // Empty version value doesn't match the regex (requires at least one char) expect(deriveActiveSkills(messages)).toEqual([]); }); test("mapping to IDs works with versioned markers", () => { const messages: Message[] = [ skillLoadUseMsg("t1"), toolResultMsg("t1", ''), skillLoadUseMsg("t2"), toolResultMsg("t2", ''), ]; expect(deriveActiveSkills(messages).map((e) => e.id)).toEqual([ "deploy", "oncall", ]); }); test("tool_result with empty string content is handled gracefully", () => { const messages: Message[] = [ skillLoadUseMsg("t1"), toolResultMsg("t1", ""), ]; expect(deriveActiveSkills(messages)).toEqual([]); }); test("marker with extra whitespace in attributes still matches", () => { const messages: Message[] = [ skillLoadUseMsg("t1"), toolResultMsg("t1", ''), ]; const entries = deriveActiveSkills(messages); expect(entries).toEqual([{ id: "deploy", version: "v1:abc" }]); }); test("marker with empty id attribute is rejected", () => { const messages: Message[] = [ skillLoadUseMsg("t1"), toolResultMsg("t1", ''), ]; expect(deriveActiveSkills(messages)).toEqual([]); }); test("many duplicate markers across many messages are all deduplicated", () => { const messages: Message[] = []; for (let i = 0; i < 10; i++) { const id = `t${i}`; messages.push(skillLoadUseMsg(id)); messages.push( toolResultMsg(id, ''), ); } const entries = deriveActiveSkills(messages); expect(entries).toEqual([{ id: "repeat", version: "v1:same" }]); }); test("marker with version but missing id is rejected", () => { const messages: Message[] = [ skillLoadUseMsg("t1"), toolResultMsg("t1", ''), ]; expect(deriveActiveSkills(messages)).toEqual([]); }); });