Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/decay/__tests__/debug.test.ts

Raw
import { 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",
				}),
			]),
		);
	});
});