mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
feat: enable permission-aware subagents
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* context.test.ts — Parent conversation context extraction for inherit_context spawns.
|
||||
*
|
||||
* buildParentContext shapes what a subagent sees from its parent; silent bugs
|
||||
* here would feed wrong context into spawns. Tests use realistic SessionEntry
|
||||
* shapes and a minimal ExtensionContext stub — no mocking beyond the one
|
||||
* getBranch() call the function actually reads.
|
||||
*/
|
||||
|
||||
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildParentContext, extractText } from "../src/context.js";
|
||||
|
||||
function makeCtx(entries: unknown[]): ExtensionContext {
|
||||
return { sessionManager: { getBranch: () => entries } } as unknown as ExtensionContext;
|
||||
}
|
||||
|
||||
function userMsg(content: string | unknown[]) {
|
||||
return { type: "message", message: { role: "user", content } };
|
||||
}
|
||||
|
||||
function assistantMsg(blocks: unknown[]) {
|
||||
return { type: "message", message: { role: "assistant", content: blocks } };
|
||||
}
|
||||
|
||||
describe("extractText", () => {
|
||||
it("joins multiple text blocks with newlines", () => {
|
||||
expect(extractText([{ type: "text", text: "a" }, { type: "text", text: "b" }])).toBe("a\nb");
|
||||
});
|
||||
|
||||
it("filters out non-text blocks (tool_use, etc.)", () => {
|
||||
expect(
|
||||
extractText([
|
||||
{ type: "text", text: "keep" },
|
||||
{ type: "tool_use", name: "x", input: {} },
|
||||
{ type: "text", text: "also keep" },
|
||||
]),
|
||||
).toBe("keep\nalso keep");
|
||||
});
|
||||
|
||||
it("treats a text block with missing text field as empty", () => {
|
||||
expect(extractText([{ type: "text" }, { type: "text", text: "x" }])).toBe("\nx");
|
||||
});
|
||||
|
||||
it("returns empty string for an empty content array", () => {
|
||||
expect(extractText([])).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildParentContext", () => {
|
||||
it("returns empty string for an empty branch", () => {
|
||||
expect(buildParentContext(makeCtx([]))).toBe("");
|
||||
});
|
||||
|
||||
it("returns empty string when no entries produce extractable content", () => {
|
||||
// toolResult is skipped, empty-summary compaction is skipped
|
||||
const out = buildParentContext(
|
||||
makeCtx([
|
||||
{ type: "message", message: { role: "tool_result", content: "..." } },
|
||||
{ type: "compaction", summary: "" },
|
||||
]),
|
||||
);
|
||||
expect(out).toBe("");
|
||||
});
|
||||
|
||||
it("wraps a user+assistant exchange with the parent-context header and task footer", () => {
|
||||
const out = buildParentContext(
|
||||
makeCtx([userMsg("hello"), assistantMsg([{ type: "text", text: "hi back" }])]),
|
||||
);
|
||||
expect(out).toContain("# Parent Conversation Context");
|
||||
expect(out).toContain("[User]: hello");
|
||||
expect(out).toContain("[Assistant]: hi back");
|
||||
expect(out).toMatch(/# Your Task \(below\)\n$/);
|
||||
// Entries are joined with a blank line, preserving conversation order
|
||||
expect(out).toContain("[User]: hello\n\n[Assistant]: hi back");
|
||||
});
|
||||
|
||||
it("accepts user messages whose content is content-blocks (not just a string)", () => {
|
||||
const out = buildParentContext(makeCtx([userMsg([{ type: "text", text: "from blocks" }])]));
|
||||
expect(out).toContain("[User]: from blocks");
|
||||
});
|
||||
|
||||
it("includes compaction summaries inline in their original position", () => {
|
||||
const out = buildParentContext(
|
||||
makeCtx([
|
||||
userMsg("orig question"),
|
||||
{ type: "compaction", summary: "we discussed X" },
|
||||
assistantMsg([{ type: "text", text: "follow-up" }]),
|
||||
]),
|
||||
);
|
||||
expect(out.indexOf("[User]: orig question"))
|
||||
.toBeLessThan(out.indexOf("[Summary]: we discussed X"));
|
||||
expect(out.indexOf("[Summary]: we discussed X"))
|
||||
.toBeLessThan(out.indexOf("[Assistant]: follow-up"));
|
||||
});
|
||||
|
||||
it("skips tool_result messages — they're too verbose for inherited context", () => {
|
||||
const out = buildParentContext(
|
||||
makeCtx([
|
||||
userMsg("real user"),
|
||||
{ type: "message", message: { role: "tool_result", content: "noisy tool output" } },
|
||||
assistantMsg([{ type: "text", text: "real assistant" }]),
|
||||
]),
|
||||
);
|
||||
expect(out).not.toContain("noisy tool output");
|
||||
expect(out).toContain("[User]: real user");
|
||||
expect(out).toContain("[Assistant]: real assistant");
|
||||
});
|
||||
|
||||
it("trims and skips whitespace-only messages", () => {
|
||||
const out = buildParentContext(
|
||||
makeCtx([userMsg(" \n "), assistantMsg([{ type: "text", text: "non-empty" }])]),
|
||||
);
|
||||
expect(out).not.toMatch(/\[User\]:/);
|
||||
expect(out).toContain("[Assistant]: non-empty");
|
||||
});
|
||||
|
||||
it("ignores assistant messages whose only content is non-text blocks", () => {
|
||||
// Assistant emitted only a tool_use — nothing extractable, so it shouldn't appear
|
||||
const out = buildParentContext(
|
||||
makeCtx([
|
||||
userMsg("question"),
|
||||
assistantMsg([{ type: "tool_use", name: "x", input: {} }]),
|
||||
]),
|
||||
);
|
||||
expect(out).toContain("[User]: question");
|
||||
expect(out).not.toContain("[Assistant]:");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user