repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/good-job/__tests__/harness.test.ts
Rawimport { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import {
fauxAssistantMessage,
fauxProvider,
fauxToolCall,
getCurrentTools,
} from "@earendil-works/pi-ai";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
createTestSession,
type TestSession,
withProcessEnv,
} from "../../../test/harness";
import {
counts,
goodJobDataDirectory,
layout,
loadRecords,
} from "../storage.js";
function writeUserSettings(t: TestSession, settings: unknown): void {
const dir = path.join(t.cwd, ".test-home/.pi/agent");
mkdirSync(dir, { recursive: true });
writeFileSync(path.join(dir, "settings.json"), JSON.stringify(settings));
}
function extensionPath(): string {
return path.resolve(import.meta.dirname, "../index.ts");
}
async function eventually<T>(
read: () => Promise<T>,
accept: (value: T) => boolean,
): Promise<T> {
const deadline = Date.now() + 3_000;
let value = await read();
while (!accept(value) && Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 20));
value = await read();
}
return value;
}
function learning(summary: string, slug = "shared-route-first") {
return fauxAssistantMessage(
fauxToolCall("submit_learning", {
slug,
summary,
evidence: ["Focused tests passed"],
behaviors: ["Inspected callers before editing"],
candidate_rule: "Fix shared routes before leaf callers.",
}),
{ stopReason: "toolUse" },
);
}
describe("good-job pi-test-harness", () => {
let t: TestSession | undefined;
afterEach(async () => {
try {
await t?.session.extensionRunner.emit({
type: "session_shutdown",
reason: "quit",
});
} finally {
t?.dispose();
t = undefined;
vi.restoreAllMocks();
}
});
it("registers only /gj with list-driven commands and no prompt or tools", async () => {
t = await createTestSession({ extensions: [extensionPath()] });
const extension = t.session.extensionRunner.extensions[0];
expect(extension.path).toBe(extensionPath());
expect([...extension.commands.keys()]).toEqual(["gj"]);
expect([...extension.tools.keys()]).toEqual([]);
expect(extension.handlers.get("resources_discover")).toBeUndefined();
expect(
t.session.resourceLoader
.getPrompts()
.prompts.find(({ name }) => name === "wtf"),
).toBeUndefined();
expect(extension.commands.get("gj").getArgumentCompletions("")).toEqual([
{ value: "list", label: "list" },
{ value: "status", label: "status" },
{ value: "open", label: "open" },
{ value: "learn", label: "learn" },
]);
});
it("records praise while passing the original message to the agent", async () => {
const originalDebug = process.env.PI_GOOD_JOB_DEBUG;
let extensionModule: typeof import("../index.ts") | undefined;
try {
process.env.PI_GOOD_JOB_DEBUG = "1";
vi.resetModules();
extensionModule = await import("../index.ts");
} finally {
if (originalDebug === undefined) delete process.env.PI_GOOD_JOB_DEBUG;
else process.env.PI_GOOD_JOB_DEBUG = originalDebug;
}
if (!extensionModule) throw new Error("could not load good-job extension");
t = await createTestSession({
extensionFactories: [extensionModule.default],
env: { PI_GOOD_JOB_DEBUG: "1" },
});
const runner = t.session.extensionRunner;
const registered = runner.extensions[0];
const input = registered.handlers.get("input")[0];
const base = runner.createContext();
const faux = fauxProvider({
provider: "good-job-test",
models: [{ id: "worker", maxTokens: 4_096 }],
});
faux.setResponses([learning("Fixed the shared route")]);
const stream = vi.spyOn(faux.provider, "streamSimple");
base.modelRegistry.registerProvider(faux.provider);
const model = base.modelRegistry.find("good-job-test", "worker");
if (!model) throw new Error("missing faux model");
const ctx = Object.create(base) as typeof base;
Object.defineProperty(ctx, "model", { value: model });
const manager = ctx.sessionManager as any;
manager.appendMessage({
role: "user",
content: "Fix the shared route",
timestamp: Date.now(),
});
manager.appendMessage(
fauxAssistantMessage("Implemented and tested the shared fix."),
);
const result = await withProcessEnv(t.env, () =>
input(
{
type: "input",
text: "good job! excellent scope control",
source: "interactive",
},
ctx,
),
);
expect(result).toEqual({ action: "continue" });
const root = goodJobDataDirectory(t.env, process.platform, t.env.HOME);
const loaded = await eventually(
() => loadRecords(layout(root)),
(value) => value.records.length === 1,
);
expect(loaded.errors).toEqual([]);
expect(loaded.records[0]).toMatchObject({
kind: "praise",
feedback: "excellent scope control",
learning: {
slug: "shared-route-first",
summary: "Fixed the shared route",
},
});
expect(loaded.records[0]?.id).toMatch(/^gj-[0-9a-hjkmnp-tv-z]{10}$/);
expect(loaded.records[0]?.source).toMatchObject({
type: "session",
cwd: ctx.cwd,
});
const debugRoot = path.join(
t.cwd,
".test-state",
"pi-ext",
"debug",
"good-job",
);
const events = readdirSync(debugRoot).flatMap((file) =>
readFileSync(path.join(debugRoot, file), "utf8")
.trim()
.split("\n")
.map((line) => JSON.parse(line)),
);
expect(events).toEqual(
expect.arrayContaining([
expect.objectContaining({
event: "feedback.capture",
kind: "praise",
}),
expect.objectContaining({
event: "learning.outcome",
status: "committed",
}),
]),
);
const sentMessages = stream.mock.calls[0]?.[1].messages ?? [];
expect(JSON.stringify(sentMessages)).toContain(
"Implemented and tested the shared fix.",
);
expect(JSON.stringify(sentMessages.at(-1))).toContain("kebab-case");
expect(getCurrentTools(sentMessages).map(({ name }) => name)).toEqual([
"submit_learning",
]);
await withProcessEnv(t.env, () =>
registered.commands.get("gj").handler("status", ctx as never),
);
expect(t.events.ui.at(-1)?.args[0]).toContain("1 learnings");
});
it("rejects analysis that does not submit structured learning", async () => {
t = await createTestSession({ extensions: [extensionPath()] });
const runner = t.session.extensionRunner;
const extension = runner.extensions[0];
const input = extension.handlers.get("input")[0];
const base = runner.createContext();
const faux = fauxProvider({
provider: "good-job-unstructured",
models: [{ id: "worker", maxTokens: 4_096 }],
});
faux.setResponses([fauxAssistantMessage('{"summary":"not submitted"}')]);
base.modelRegistry.registerProvider(faux.provider);
const model = base.modelRegistry.find("good-job-unstructured", "worker");
if (!model) throw new Error("missing unstructured faux model");
const ctx = Object.create(base) as typeof base;
Object.defineProperty(ctx, "model", { value: model });
await withProcessEnv(t.env, () =>
input({ type: "input", text: "gj", source: "interactive" }, ctx),
);
const paths = layout(
goodJobDataDirectory(t.env, process.platform, t.env.HOME),
);
await eventually(
() => counts(paths),
(value) => value.failed === 1,
);
expect(await counts(paths)).toEqual({
pending: 0,
processing: 0,
failed: 1,
records: 0,
});
expect(
t.events.ui
.filter((call) => call.method === "notify")
.map((call) => String(call.args[0]))
.find((message) => message.includes("good-job failed")),
).toContain("without calling submit_learning");
});
it("records first-word wtf feedback while passing it unchanged", async () => {
t = await createTestSession({ extensions: [extensionPath()] });
const runner = t.session.extensionRunner;
const extension = runner.extensions[0];
const input = extension.handlers.get("input")[0];
const base = runner.createContext();
const faux = fauxProvider({
provider: "wtf-test",
models: [{ id: "worker", maxTokens: 4_096 }],
});
faux.setResponses([
learning("Expanded scope without approval", "scope-creep"),
]);
const stream = vi.spyOn(faux.provider, "streamSimple");
base.modelRegistry.registerProvider(faux.provider);
const model = base.modelRegistry.find("wtf-test", "worker");
if (!model) throw new Error("missing wtf faux model");
const ctx = Object.create(base) as typeof base;
Object.defineProperty(ctx, "model", { value: model });
const manager = ctx.sessionManager as any;
manager.appendMessage({
role: "user",
content: "Implement only A",
timestamp: Date.now(),
});
manager.appendMessage(
fauxAssistantMessage("Implemented A and unrelated B."),
);
const result = await withProcessEnv(t.env, () =>
input(
{
type: "input",
text: "wtf you added unrelated B",
source: "interactive",
},
ctx,
),
);
expect(result).toEqual({ action: "continue" });
const root = goodJobDataDirectory(t.env, process.platform, t.env.HOME);
const loaded = await eventually(
() => loadRecords(layout(root)),
(value) => value.records.length === 1,
);
expect(loaded.records[0]).toMatchObject({
kind: "problem",
feedback: "you added unrelated B",
learning: { slug: "scope-creep" },
});
expect(loaded.records[0]?.id).toMatch(/^wtf-[0-9a-hjkmnp-tv-z]{10}$/);
expect(JSON.stringify(stream.mock.calls[0]?.[1].messages)).toContain(
"Implemented A and unrelated B.",
);
});
it("uses optional directory, model, and thinking settings", async () => {
t = await createTestSession({ extensions: [extensionPath()] });
const customRoot = path.join(t.cwd, "shared-gj");
writeUserSettings(t, {
"good-job": {
directory: customRoot,
model: "good-job-override/background",
thinkingLevel: "xhigh",
},
});
const runner = t.session.extensionRunner;
const extension = runner.extensions[0];
const input = extension.handlers.get("input")[0];
const base = runner.createContext();
const faux = fauxProvider({
provider: "good-job-override",
models: [
{
id: "active",
maxTokens: 4_096,
reasoning: true,
thinkingLevelMap: { xhigh: "xhigh" },
},
{
id: "background",
maxTokens: 4_096,
reasoning: true,
thinkingLevelMap: { xhigh: "xhigh" },
},
],
});
faux.setResponses([learning("Used configured model", "configured-model")]);
Object.assign(faux.getModel("active"), {
thinkingLevelMap: { xhigh: "xhigh" },
});
Object.assign(faux.getModel("background"), {
thinkingLevelMap: { xhigh: "xhigh" },
});
const stream = vi.spyOn(faux.provider, "streamSimple");
base.modelRegistry.registerProvider(faux.provider);
const active = base.modelRegistry.find("good-job-override", "active");
if (!active) throw new Error("missing active faux model");
const ctx = Object.create(base) as typeof base;
Object.defineProperty(ctx, "model", { value: active });
const result = await withProcessEnv(t.env, () =>
input({ type: "input", text: "gj", source: "interactive" }, ctx),
);
expect(result).toEqual({ action: "continue" });
const loaded = await eventually(
() => loadRecords(layout(customRoot)),
(value) => value.records.length === 1,
);
expect(loaded.records[0]?.analysisModel).toEqual({
provider: "good-job-override",
id: "background",
});
expect(stream.mock.calls[0]?.[0].id).toBe("background");
expect(stream.mock.calls[0]?.[2]?.reasoning).toBe("xhigh");
});
it("sends only the records path and synthesis instructions to /gj learn", async () => {
t = await createTestSession({ extensions: [extensionPath()] });
const runner = t.session.extensionRunner;
const extension = runner.extensions[0];
const input = extension.handlers.get("input")[0];
const base = runner.createContext();
const faux = fauxProvider({
provider: "learn-test",
models: [{ id: "worker", maxTokens: 4_096 }],
});
faux.setResponses([
learning("Private summary must not be embedded", "private-learning"),
fauxAssistantMessage("Proposed changes."),
]);
base.modelRegistry.registerProvider(faux.provider);
const model = base.modelRegistry.find("learn-test", "worker");
if (!model) throw new Error("missing learn faux model");
const ctx = Object.create(base) as typeof base;
Object.defineProperty(ctx, "model", { value: model });
await withProcessEnv(t.env, () =>
input({ type: "input", text: "gj", source: "interactive" }, ctx),
);
const root = goodJobDataDirectory(t.env, process.platform, t.env.HOME);
await eventually(
() => loadRecords(layout(root)),
(value) => value.records.length === 1,
);
const promptSpy = vi
.spyOn(t.session, "prompt")
.mockResolvedValue(undefined);
await withProcessEnv(t.env, () =>
extension.commands.get("gj").handler("learn", ctx as never),
);
await vi.waitFor(() => expect(promptSpy).toHaveBeenCalled());
const prompt = String(promptSpy.mock.calls[0]?.[0]);
expect(prompt).toContain(layout(root).records);
expect(prompt).toContain("slug (id)");
expect(prompt).toContain("praise and problem records together");
expect(prompt).not.toContain("Private summary must not be embedded");
});
it("reports invalid settings during startup", async () => {
t = await createTestSession({ extensions: [extensionPath()] });
writeUserSettings(t, { "good-job": { thinkingLevel: "invalid" } });
const runner = t.session.extensionRunner;
const start = runner.extensions[0].handlers.get("session_start")[0];
start({ type: "session_start", reason: "startup" }, runner.createContext());
const notification = await eventually(
async () =>
t?.events.ui
.filter((call) => call.method === "notify")
.map((call) => String(call.args[0]))
.find((message) => message.includes("startup failed")),
(value) => value !== undefined,
);
expect(notification).toContain(
"Invalid good-job.thinkingLevel from user settings",
);
});
it("recovers in-flight work when a new session starts", async () => {
t = await createTestSession({ extensions: [extensionPath()] });
const runner = t.session.extensionRunner;
const extension = runner.extensions[0];
const input = extension.handlers.get("input")[0];
const start = extension.handlers.get("session_start")[0];
const base = runner.createContext();
const faux = fauxProvider({
provider: "good-job-restart",
models: [{ id: "worker", maxTokens: 4_096 }],
});
let releaseFirst = () => {};
let markStarted = () => {};
const firstStarted = new Promise<void>((resolve) => {
markStarted = resolve;
});
let firstAborted = false;
faux.setResponses([
async (_context, options) => {
markStarted();
await new Promise<void>((resolve) => {
releaseFirst = resolve;
options?.signal?.addEventListener(
"abort",
() => {
firstAborted = true;
resolve();
},
{ once: true },
);
});
return learning("Stale response", "stale-response");
},
learning("Recovered after session change", "recovered-work"),
]);
base.modelRegistry.registerProvider(faux.provider);
const model = base.modelRegistry.find("good-job-restart", "worker");
if (!model) throw new Error("missing restart faux model");
const ctx = Object.create(base) as typeof base;
Object.defineProperty(ctx, "model", { value: model });
await withProcessEnv(t.env, () =>
input({ type: "input", text: "gj", source: "interactive" }, ctx),
);
await firstStarted;
start({ type: "session_start", reason: "switch" }, ctx);
await new Promise((resolve) => setTimeout(resolve, 50));
releaseFirst();
const root = goodJobDataDirectory(t.env, process.platform, t.env.HOME);
const paths = layout(root);
const loaded = await eventually(
() => loadRecords(paths),
(value) => value.records.length === 1,
);
expect(firstAborted).toBe(true);
expect(loaded.records[0]?.learning.slug).toBe("recovered-work");
expect(await counts(paths)).toEqual({
pending: 0,
processing: 0,
failed: 0,
records: 1,
});
});
it("ignores unrelated and extension-injected text", async () => {
t = await createTestSession({ extensions: [extensionPath()] });
const input =
t.session.extensionRunner.extensions[0].handlers.get("input")[0];
for (const event of [
{ type: "input", text: "great job", source: "interactive" },
{ type: "input", text: "wtf happened", source: "extension" },
])
expect(
await input(event, t.session.extensionRunner.createContext()),
).toEqual({ action: "continue" });
});
});