Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

pi/agent/skills/pi-sessions/scripts/inspect.test.mjs

Raw
import assert from "node:assert/strict";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import test from "node:test";
import { inspect } from "./inspect.mjs";

const script = fileURLToPath(new URL("./inspect.mjs", import.meta.url));
const stamp = "2026-09-01T12:00:00.000Z";
const entry = (id, parentId, type, rest = {}) => ({ id, parentId, type, timestamp: stamp, ...rest });
const message = (id, parentId, role, rest = {}) =>
    entry(id, parentId, "message", {
        message: { role, content: [{ type: "text", text: `${role} text` }], ...rest },
    });

function fixture(t) {
    const root = mkdtempSync(join(tmpdir(), "pi-sessions-test-"));
    t.after(() => rmSync(root, { recursive: true, force: true }));
    const cwd = join(root, "project");
    const home = join(root, "home");
    const agent = join(home, ".pi/agent");
    const store = join(home, ".local/state/pi/sessions");
    for (const dir of [cwd, agent, store]) mkdirSync(dir, { recursive: true });
    const ctx = { cwd, home, env: {} };
    function json(file, value) {
        mkdirSync(dirname(file), { recursive: true });
        writeFileSync(file, JSON.stringify(value));
    }
    function add(id, name = "task", entries = [message("u", null, "user")], options = {}) {
        const file = join(options.dir ?? store, `${id}.jsonl`);
        const header = {
            type: "session",
            version: 3,
            id,
            cwd: options.cwd ?? cwd,
            timestamp: options.timestamp ?? stamp,
        };
        const records = [header, ...entries];
        if (name !== null) records.push(entry("name", entries.at(-1)?.id ?? null, "session_info", { name }));
        mkdirSync(dirname(file), { recursive: true });
        writeFileSync(file, `${records.map((e) => JSON.stringify(e)).join("\n")}\n`);
        return file;
    }
    return { root, cwd, home, agent, store, ctx, json, add };
}

test("storage precedence: CLI, environment, project, global, then ordered fallbacks", async (t) => {
    const f = fixture(t);
    for (const id of ["cli", "env", "project", "global"]) f.add(id, id, [], { dir: join(f.cwd, id) });
    f.json(join(f.agent, "settings.json"), { sessionDir: "global" });
    f.json(join(f.cwd, ".pi/settings.json"), { sessionDir: "project" });
    assert.equal((await inspect(["list"], f.ctx)).sessions[0].id, "project");
    f.ctx.env.PI_CODING_AGENT_SESSION_DIR = "env";
    assert.equal((await inspect(["list"], f.ctx)).sessions[0].id, "env");
    assert.equal((await inspect(["list", "--session-dir", "cli"], f.ctx)).sessions[0].id, "cli");
    delete f.ctx.env.PI_CODING_AGENT_SESSION_DIR;
    rmSync(join(f.cwd, ".pi/settings.json"));
    assert.equal((await inspect(["list"], f.ctx)).sessions[0].id, "global");
    f.json(join(f.agent, "settings.json"), { sessionDir: "missing" });
    f.add("fallback");
    assert.equal((await inspect(["list"], f.ctx)).total, 0, "configured empty roots never fall through");
    rmSync(join(f.agent, "settings.json"));
    assert.equal((await inspect(["list"], f.ctx)).sessions[0].id, "fallback");
    rmSync(f.store, { recursive: true });
    f.add("agent", "agent", [], { dir: join(f.agent, "sessions/nested") });
    f.add("local", "local", [], { dir: join(f.cwd, ".pi/sessions") });
    assert.equal((await inspect(["list"], f.ctx)).sessions[0].id, "agent");
    rmSync(join(f.agent, "sessions"), { recursive: true });
    assert.equal((await inspect(["list"], f.ctx)).sessions[0].id, "local");
});

test("agent directory and tilde settings resolve without loading other settings fields", async (t) => {
    const f = fixture(t);
    const agent = join(f.root, "agent override");
    f.json(join(agent, "settings.json"), { sessionDir: "~/sessions", irrelevant: "NEVER_EMIT_SETTINGS_CONTENT" });
    f.ctx.env.PI_CODING_AGENT_DIR = agent;
    f.add("custom", "custom", [], { dir: join(f.home, "sessions") });
    const result = await inspect(["list"], f.ctx);
    assert.equal(result.sessions[0].id, "custom");
    assert.doesNotMatch(JSON.stringify(result), /NEVER_EMIT_SETTINGS_CONTENT/);
});

test("list filters literal names, exact cwd, UTC dates and pages with stable ordering", async (t) => {
    const f = fixture(t);
    f.add("bbb", "same [literal]", [], { timestamp: "2026-09-02T12:00:00Z" });
    f.add("aaa", "same [literal]", [], { timestamp: "2026-09-02T12:00:00Z" });
    f.add("old", "same [literal]", [], { timestamp: "2026-08-31T12:00:00Z" });
    f.add("other", "another", [], { cwd: join(f.cwd, "other") });
    const r = await inspect(
        [
            "list",
            "[literal]",
            "--cwd",
            f.cwd,
            "--since",
            "2026-09-01",
            "--until",
            "2026-09-02",
            "--offset",
            "1",
            "--limit",
            "1",
        ],
        f.ctx,
    );
    assert.equal(r.total, 2);
    assert.equal(r.sessions.length, 1);
    assert.equal(r.sessions[0].id, "bbb");
    f.add("utc", "offset date", [], { timestamp: "2026-09-01T01:00:00+02:00" });
    const utc = await inspect(["list", "offset date", "--until", "2026-08-31"], f.ctx);
    assert.equal(utc.total, 1);
    assert.equal(utc.sessions[0].started, "2026-08-31T23:00:00.000Z");
    await assert.rejects(inspect(["list", "--since", "2026-02-30"], f.ctx), /date/i);
    await assert.rejects(inspect(["list", "--limit", "101"], f.ctx), /limit/i);
    await assert.rejects(inspect(["list", "--offset", "-1"], f.ctx), /offset/i);
});

test("scoped lists skip only valid headers proven outside date and cwd bounds", async (t) => {
    const f = fixture(t);
    f.add("inside", "inside", [], { timestamp: "2026-09-01T00:00:00Z" });
    const before = f.add("before", "before", [], { timestamp: "2026-08-31T23:59:59Z" });
    const after = f.add("after", "after", [], { timestamp: "2026-09-03T00:00:00Z" });
    const other = f.add("other", "other", [], { cwd: join(f.cwd, "other") });
    for (const file of [before, after, other])
        writeFileSync(file, `${readFileSync(file, "utf8")}{"MALFORMED_BODY":`);

    const scoped = await inspect(
        ["list", "--cwd", f.cwd, "--since", "2026-09-01", "--until", "2026-09-02"],
        f.ctx,
    );
    assert.equal(scoped.complete, true);
    assert.equal(scoped.scanned, 4);
    assert.equal(scoped.parsed, 1);
    assert.equal(scoped.skipped, 3);
    assert.equal(scoped.errorCount, 0);
    assert.equal(scoped.sessions[0].id, "inside");
    assert.equal(scoped.scanned, scoped.parsed + scoped.skipped + scoped.errorCount);

    const unscoped = await inspect(["list"], f.ctx);
    assert.equal(unscoped.complete, false);
    assert.equal(unscoped.scanned, 4);
    assert.equal(unscoped.parsed, 1);
    assert.equal(unscoped.skipped, 0);
    assert.equal(unscoped.errorCount, 3);
    assert.equal(unscoped.scanned, unscoped.parsed + unscoped.skipped + unscoped.errorCount);
    await assert.rejects(inspect(["show", before, "--since", "2026-09-01"], f.ctx), /line 3/);
});

test("scoped lists retain unknown and in-scope errors before query filtering", async (t) => {
    const f = fixture(t);
    f.add("name-hit", "wanted by name", [], { timestamp: "2026-09-01T12:00:00Z" });
    const broken = f.add("broken", "unrelated", [], { timestamp: "2026-09-01T12:00:00Z" });
    writeFileSync(broken, `${readFileSync(broken, "utf8")}{"MALFORMED_BODY":`);
    const unknown = f.add("unknown", "unknown", [], { timestamp: "2026-08-01T12:00:00Z" });
    f.json(unknown, {
        type: "session",
        version: 99,
        id: "unknown",
        cwd: f.cwd,
        timestamp: "2026-08-01T12:00:00Z",
    });

    const result = await inspect(["list", "wanted by name", "--since", "2026-09-01"], f.ctx);
    assert.equal(result.complete, false);
    assert.equal(result.scanned, 3);
    assert.equal(result.parsed, 1);
    assert.equal(result.skipped, 0);
    assert.equal(result.errorCount, 2);
    assert.equal(result.sessions[0].id, "name-hit");
    assert.match(result.errors.find((error) => error.file === broken).error, /line 3/);
    assert.match(result.errors.find((error) => error.file === unknown).error, /version/i);
    assert.equal(result.scanned, result.parsed + result.skipped + result.errorCount);
});

test("duplicate names and ID prefixes require disambiguation; discovered paths work", async (t) => {
    const f = fixture(t);
    const file = f.add("abc-1", "duplicate");
    f.add("abc-2", "duplicate");
    await assert.rejects(inspect(["show", "duplicate"], f.ctx), /ambiguous/i);
    await assert.rejects(inspect(["show", "abc"], f.ctx), /ambiguous/i);
    assert.equal((await inspect(["show", "abc-1"], f.ctx)).id, "abc-1");
    assert.equal((await inspect(["show", file], f.ctx)).file, file);
    await assert.rejects(inspect(["show", "missing"], f.ctx), /not found/i);
});

test("persisted branch excludes abandoned errors; explicit leaf recovers them", async (t) => {
    const f = fixture(t);
    f.add("branches", null, [
        message("u", null, "user"),
        message("old", "u", "assistant", { stopReason: "error" }),
        message("new", "u", "assistant", {
            stopReason: "stop",
            content: [{ type: "toolCall", id: "call1", name: "read", arguments: { path: "ARGUMENT_MUST_NOT_APPEAR" } }],
        }),
        message("result", "new", "toolResult", { toolName: "read", isError: true }),
        message("abort", "result", "assistant", { stopReason: "aborted" }),
        entry("exec", "abort", "message", {
            message: { role: "bashExecution", exitCode: 1, output: "OUTPUT_MUST_NOT_APPEAR" },
        }),
    ]);
    const r = await inspect(["show", "branches"], f.ctx);
    assert.equal(r.leafId, "exec");
    assert.equal(r.heads.total, 2);
    assert.equal(r.branch.counts.assistantErrors, 0);
    assert.equal(r.branch.counts.toolErrors, 1);
    assert.equal(r.branch.counts.toolCalls, 1);
    assert.equal(r.branch.counts.aborts, 1);
    assert.equal(r.branch.counts.bashErrors, 1);
    assert.deepEqual(
        r.timeline.map((e) => e.id),
        ["u", "new", "result", "abort", "exec"],
    );
    assert.equal(r.timeline[1].line, 4);
    const old = await inspect(["show", "branches", "--leaf", "old"], f.ctx);
    assert.equal(old.branch.counts.assistantErrors, 1);
    assert.equal(old.counts.assistantErrors, 1);
    assert.doesNotMatch(JSON.stringify(r), /ARGUMENT_MUST_NOT_APPEAR|OUTPUT_MUST_NOT_APPEAR|user text/);
    await assert.rejects(inspect(["show", "branches", "--leaf", "unknown"], f.ctx), /leaf/i);
});

test("retained-tail compaction supplies context once, without inflating branch turns", async (t) => {
    const f = fixture(t);
    f.add("tail", null, [
        message("u1", null, "user"),
        message("u2", "u1", "user"),
        entry("c", "u2", "compaction", {
            summary: "SUMMARY_TEXT",
            tokensBefore: 9000,
            retainedTail: [
                { role: "user", content: "retained user" },
                { role: "assistant", content: [{ type: "text", text: "retained answer" }] },
            ],
        }),
        message("a", "c", "assistant"),
    ]);
    const r = await inspect(["show", "tail", "--view", "context", "--text"], f.ctx);
    assert.equal(r.branch.counts.users, 2);
    assert.equal(r.context.counts.users, 1);
    assert.equal(r.context.counts.assistants, 2);
    assert.deepEqual(
        r.timeline.map((e) => e.id),
        ["c", "c:tail:0", "c:tail:1", "a"],
    );
    assert.equal(r.timeline[0].text, "SUMMARY_TEXT");
    assert.equal(r.timeline[1].line, 4);
    assert.equal(r.timeline[1].text, "retained user");
});

test("firstKeptEntryId and branch summaries reconstruct the documented context order", async (t) => {
    const f = fixture(t);
    f.add("kept", null, [
        message("u1", null, "user"),
        message("u2", "u1", "user"),
        entry("c", "u2", "compaction", { summary: "summary", tokensBefore: 5000, firstKeptEntryId: "u2" }),
        entry("b", "c", "branch_summary", { summary: "branch text", fromId: "u1" }),
        message("a", "b", "assistant"),
    ]);
    const r = await inspect(["show", "kept", "--view", "context"], f.ctx);
    assert.deepEqual(
        r.timeline.map((e) => e.id),
        ["c", "u2", "b", "a"],
    );
    assert.equal(r.context.counts.users, 1);
    assert.equal(r.context.counts.compactions, 1);
});

test("text is bounded while content exposes exact persisted payloads on request", async (t) => {
    const f = fixture(t);
    f.add("bounded", null, [
        message("u", null, "user", { content: "x".repeat(10000) }),
        message("a", "u", "assistant", {
            errorMessage: "EXACT_ERROR",
            content: [
                { type: "thinking", thinking: "EXACT_THINKING" },
                { type: "image", data: "EXACT_IMAGE", mimeType: "image/png" },
                { type: "text", text: "visible answer" },
                { type: "toolCall", name: "bash", id: "t", arguments: { command: "EXACT_ARGUMENTS" } },
            ],
        }),
        message("r", "a", "toolResult", {
            content: [{ type: "text", text: "EXACT_RESULT" }],
            details: { reason: "EXACT_DETAILS" },
        }),
        entry("custom", "r", "custom", { customType: "state", data: "EXACT_STATE" }),
    ]);
    const r = await inspect(["show", "bounded", "--text", "--limit", "1"], f.ctx);
    assert.equal(r.total, 4);
    assert.equal(r.timeline.length, 1);
    assert.ok(r.timeline[0].text.length <= 500);
    assert.equal(r.timeline[0].textTruncated, true);
    const text = await inspect(["show", "bounded", "--text"], f.ctx);
    assert.doesNotMatch(JSON.stringify(text), /EXACT_/);
    assert.equal(text.timeline[1].text, "visible answer");
    const content = await inspect(["show", "bounded", "--content"], f.ctx);
    assert.equal(content.timeline[1].payload.message.errorMessage, "EXACT_ERROR");
    assert.equal(content.timeline[1].payload.message.content[0].thinking, "EXACT_THINKING");
    assert.equal(content.timeline[1].payload.message.content[1].data, "EXACT_IMAGE");
    assert.equal(content.timeline[1].payload.message.content[3].arguments.command, "EXACT_ARGUMENTS");
    assert.equal(content.timeline[2].payload.message.content[0].text, "EXACT_RESULT");
    assert.equal(content.timeline[2].payload.message.details.reason, "EXACT_DETAILS");
    assert.equal(content.timeline[3].payload.data, "EXACT_STATE");
    const page = await inspect(["show", "bounded", "--offset", "1", "--limit", "1"], f.ctx);
    assert.equal(page.timeline[0].id, "a");
    assert.equal(page.timeline[0].text, undefined);
    assert.equal(page.timeline[0].payload, undefined);
});

test("malformed lines and partial tails report locations, never parser content; listing is explicitly partial", async (t) => {
    const f = fixture(t);
    f.add("valid");
    const broken = f.add("broken", null, []);
    writeFileSync(broken, `${readFileSync(broken, "utf8")}{"NEVER_ECHO_BAD_JSON":`);
    const r = await inspect(["list"], f.ctx);
    assert.equal(r.complete, false);
    assert.equal(r.errorCount, 1);
    assert.equal(r.sessions[0].id, "valid");
    assert.match(r.errors[0].error, /line 2/);
    assert.doesNotMatch(JSON.stringify(r), /NEVER_ECHO_BAD_JSON/);
    await assert.rejects(inspect(["show", broken], f.ctx), /line 2/);
    const settings = join(f.agent, "settings.json");
    writeFileSync(settings, '{"NEVER_ECHO_SETTINGS":');
    await assert.rejects(
        inspect(["list"], f.ctx),
        (error) => /settings/.test(error.message) && !error.message.includes("NEVER_ECHO_SETTINGS"),
    );
});

test("unsupported versions, duplicate IDs and missing parents fail, rather than guessing", async (t) => {
    const f = fixture(t);
    const file = f.add("invalid", null, [message("u", "absent", "user")]);
    await assert.rejects(inspect(["show", file], f.ctx), /parent/i);
    f.add("invalid", null, [message("u", null, "user"), message("u", null, "user")]);
    await assert.rejects(inspect(["show", file], f.ctx), /duplicate/i);
    f.json(file, { type: "session", version: 99, id: "invalid", cwd: f.cwd, timestamp: stamp });
    await assert.rejects(inspect(["show", file], f.ctx), /version/i);
});

test("latest compaction wins, including an empty retained tail", async (t) => {
    const f = fixture(t);
    f.add("latest", null, [
        message("u", null, "user"),
        entry("c1", "u", "compaction", { summary: "old", retainedTail: [{ role: "user", content: "old user" }] }),
        entry("c2", "c1", "compaction", { summary: "new", retainedTail: [], firstKeptEntryId: "u" }),
    ]);
    const r = await inspect(["show", "latest", "--view", "context"], f.ctx);
    assert.deepEqual(
        r.timeline.map((e) => e.id),
        ["c2"],
    );
    assert.equal(r.context.counts.users, 0);
    assert.equal(r.branch.counts.compactions, 2);
});

test("invalid compaction metadata is rejected without serializing its payload", async (t) => {
    const f = fixture(t);
    const file = f.add("bad-compaction", null, [
        entry("c", null, "compaction", { retainedTail: [], firstKeptEntryId: { value: "HIDDEN_METADATA" } }),
    ]);
    await assert.rejects(
        inspect(["show", file], f.ctx),
        (e) => /line 2/.test(e.message) && !e.message.includes("HIDDEN_METADATA"),
    );
    f.add("bad-compaction", null, [entry("c", null, "compaction", { firstKeptEntryId: "absent" })]);
    await assert.rejects(inspect(["show", file], f.ctx), (e) => e.message.includes(file) && /line 2/.test(e.message));
});

test("empty stores, header-only sessions and valid unterminated final lines are read-only", async (t) => {
    const f = fixture(t);
    assert.equal((await inspect(["list"], f.ctx)).total, 0);
    const file = f.add("header", null, []);
    const text = readFileSync(file, "utf8").trimEnd();
    writeFileSync(file, text);
    const result = await inspect(["show", file], f.ctx);
    assert.equal(result.total, 0);
    assert.equal(result.leafId, null);
    assert.equal(result.scanned, 1);
    assert.equal(readFileSync(file, "utf8"), text);
    writeFileSync(file, "");
    await assert.rejects(inspect(["show", file], f.ctx), /empty session/);
});

test("file symlinks are not followed and the input file is unchanged", async (t) => {
    const f = fixture(t);
    const file = f.add("real");
    const before = readFileSync(file);
    const time = statSync(file).mtimeMs;
    if (process.platform !== "win32") symlinkSync(file, join(f.store, "alias.jsonl"));
    const r = await inspect(["list"], f.ctx);
    assert.equal(r.total, 1);
    await inspect(["show", file, "--view", "context", "--text"], f.ctx);
    assert.deepEqual(readFileSync(file), before);
    assert.equal(statSync(file).mtimeMs, time);
});

test("CLI prints JSON, reports partial failures through exit status, and rejects unsupported options", (t) => {
    const f = fixture(t);
    f.add("cli");
    const args = [script, "list", "--session-dir", f.store];
    const env = { ...process.env, PI_CODING_AGENT_DIR: f.agent, PI_CODING_AGENT_SESSION_DIR: "" };
    const good = spawnSync(process.execPath, args, { cwd: f.cwd, env, encoding: "utf8" });
    assert.equal(good.status, 0, good.stderr);
    assert.equal(JSON.parse(good.stdout).sessions[0].id, "cli");
    writeFileSync(join(f.store, "broken.jsonl"), "bad");
    const partial = spawnSync(process.execPath, args, { cwd: f.cwd, env, encoding: "utf8" });
    assert.equal(partial.status, 1);
    assert.equal(JSON.parse(partial.stdout).complete, false);
    const invalid = spawnSync(process.execPath, [script, "--unsupported"], { encoding: "utf8" });
    assert.equal(invalid.status, 1);
    assert.ok(JSON.parse(invalid.stderr).error);
    const help = spawnSync(process.execPath, [script, "--help"], { encoding: "utf8" });
    assert.equal(help.status, 0);
    assert.match(help.stdout, /show/);
    if (process.platform !== "win32") {
        const linkedScript = join(f.cwd, "inspect-link.mjs");
        symlinkSync(script, linkedScript);
        const linked = spawnSync(process.execPath, [linkedScript, "--help"], { encoding: "utf8" });
        assert.equal(linked.status, 0);
        assert.match(linked.stdout, /show/);
    }
});