repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/decay/__tests__/debug.test.ts
Rawimport { mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import {
fauxAssistantMessage,
fauxProvider,
fauxToolCall,
} from "@earendil-works/pi-ai";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
createTestSession,
sandboxEnv,
type TestSession,
withProcessEnv,
} from "../../../test/harness";
describe("decay debug", () => {
let root: string | undefined;
let session: TestSession | undefined;
afterEach(() => {
session?.dispose();
if (root) rmSync(root, { recursive: true, force: true });
session = undefined;
root = undefined;
});
it("writes its lifecycle JSONL through the standalone factory", async () => {
root = mkdtempSync(path.join(tmpdir(), "pi-ext-decay-debug-"));
const env = sandboxEnv(root, { env: { PI_DECAY_DEBUG: "1" } });
vi.resetModules();
const factory = await withProcessEnv(
env,
async () => (await import("../index.ts")).default,
);
session = await createTestSession({
cwd: root,
extensionFactories: [factory],
env: { PI_DECAY_DEBUG: "1" },
});
await session.session.extensionRunner.emit({
type: "session_start",
reason: "startup",
});
await session.session.extensionRunner.emit({
type: "session_shutdown",
reason: "quit",
});
if (!env.XDG_STATE_HOME) throw new Error("missing sandbox state home");
const directory = path.join(env.XDG_STATE_HOME, "pi-ext", "debug", "decay");
const [file] = readdirSync(directory);
const events = readFileSync(path.join(directory, file), "utf8")
.trim()
.split("\n")
.map((line) => JSON.parse(line).event);
expect(events).toEqual(
expect.arrayContaining(["session.start", "session.shutdown"]),
);
});
it("logs bounded shard, retry, allocation, and model usage metadata without source content", async () => {
root = mkdtempSync(path.join(tmpdir(), "pi-ext-decay-debug-"));
const env = sandboxEnv(root, { env: { PI_DECAY_DEBUG: "1" } });
vi.resetModules();
const factory = await withProcessEnv(
env,
async () => (await import("../index.ts")).default,
);
session = await createTestSession({
cwd: root,
extensionFactories: [factory],
env: { PI_DECAY_DEBUG: "1" },
});
const faux = fauxProvider({
provider: "openai",
models: [{ id: "gpt-4o", maxTokens: 16_384 }],
});
session.session.extensionRunner
.createContext()
.modelRegistry.registerProvider(faux.provider);
const first = fauxToolCall("record_chunk", {
chunk: 1,
of: 2,
atoms: [
{
key: "SECRET_KEY",
text: "SECRET_ATOM",
kind: "state",
status: "active",
},
],
});
const second = fauxToolCall("record_chunk", {
chunk: 2,
of: 2,
atoms: [
{ key: "second", text: "Done", kind: "decision", status: "active" },
],
});
faux.setResponses([
fauxAssistantMessage(first, { stopReason: "toolUse" }),
fauxAssistantMessage(second, { stopReason: "length" }),
fauxAssistantMessage(second, { stopReason: "toolUse" }),
fauxAssistantMessage("## Goal\n\nSECRET_SUMMARY"),
]);
const event = {
type: "session_before_compact" as const,
reason: "manual" as const,
willRetry: false,
signal: new AbortController().signal,
customInstructions: "SECRET_INSTRUCTION",
branchEntries: [],
preparation: {
firstKeptEntryId: "kept",
messagesToSummarize: [
{ role: "user", content: "SECRET_USER_ONE", timestamp: 1 },
{
role: "assistant",
content: [{ type: "text", text: "done" }],
timestamp: 2,
},
{ role: "user", content: "SECRET_USER_TWO", timestamp: 3 },
] as never,
turnPrefixMessages: [],
isSplitTurn: false,
tokensBefore: 42_000,
previousSummary: undefined,
fileOps: {
read: new Set(["SECRET_PATH"]),
written: new Set<string>(),
edited: new Set<string>(),
},
settings: {
enabled: true,
reserveTokens: 16_384,
keepRecentTokens: 20_000,
},
},
};
const result = await session.session.extensionRunner.emit(event);
expect(result.compaction.summary).toContain("SECRET_SUMMARY");
faux.setResponses([
fauxAssistantMessage([], {
stopReason: "error",
errorMessage: "SECRET_PROVIDER_ERROR",
}),
]);
expect(await session.session.extensionRunner.emit(event)).toBeUndefined();
await session.session.extensionRunner.emit({
type: "session_shutdown",
reason: "quit",
});
if (!env.XDG_STATE_HOME) throw new Error("missing sandbox state home");
const directory = path.join(env.XDG_STATE_HOME, "pi-ext", "debug", "decay");
const text = readdirSync(directory)
.map((file) => readFileSync(path.join(directory, file), "utf8"))
.join("\n");
expect(text).not.toMatch(
/SECRET_(USER|INSTRUCTION|ATOM|KEY|PATH|SUMMARY|PROVIDER)/,
);
expect(text).not.toContain("gpt-4o");
const entries = text
.trim()
.split("\n")
.filter(Boolean)
.map((line) => JSON.parse(line));
expect(
entries.find((entry) => entry.event === "compaction.plan"),
).toMatchObject({
runIndex: 1,
chunkCount: 2,
previousAtoms: 0,
tokensBefore: 42_000,
});
const starts = entries.filter(
(entry) =>
entry.event === "classifier.request.start" && entry.runIndex === 1,
);
const finishes = entries.filter(
(entry) =>
entry.event === "classifier.request.finish" && entry.runIndex === 1,
);
const errors = entries.filter(
(entry) =>
entry.event === "classifier.request.error" && entry.runIndex === 1,
);
expect(starts).toHaveLength(3);
expect(starts[0]).toMatchObject({
catalogAvailableAtoms: 0,
catalogAtoms: 0,
catalogChars: 2,
});
expect(starts[1]).toMatchObject({
catalogAvailableAtoms: 1,
catalogAtoms: 1,
catalogChars: expect.any(Number),
});
expect(finishes).toHaveLength(2);
expect(errors).toHaveLength(1);
expect(errors[0]).toMatchObject({
runIndex: 1,
chunkIndex: 2,
attempt: 1,
requestResult: "output_limit",
validRecords: 1,
committedRecords: 0,
stopReason: "length",
});
expect(finishes[0]).toEqual(
expect.objectContaining({
requestResult: "accepted",
recordAtoms: 1,
reportedInputTokens: expect.any(Number),
reportedCost: expect.any(Number),
}),
);
expect(starts[0].spanId).toBe(finishes[0].spanId);
expect(starts[1].spanId).toBe(errors[0].spanId);
expect(entries).toEqual(
expect.arrayContaining([
expect.objectContaining({
event: "classifier.decision",
decision: "retry",
chunkIndex: 2,
}),
expect.objectContaining({
event: "compaction.memory",
memoryAtoms: 2,
activeAtoms: 2,
atomStates: 1,
atomDecisions: 1,
}),
expect.objectContaining({
event: "summarizer.request.finish",
requestResult: "accepted",
}),
expect.objectContaining({
event: "compaction.outcome",
outcome: "success",
}),
expect.objectContaining({
event: "classifier.request.error",
runIndex: 2,
requestResult: "provider_error",
}),
expect.objectContaining({
event: "compaction.outcome",
runIndex: 2,
outcome: "fallback",
stage: "classifier",
}),
]),
);
});
});