Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 18 additions & 16 deletions packages/cli/src/__tests__/cmdrun-happy-path.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:te
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { HISTORY_SCHEMA_VERSION } from "../history.js";
import { loadManifest } from "../manifest";
import { isString } from "../shared/type-guards";
import { createConsoleMocks, createMockManifest, mockClackPrompts, restoreMocks } from "./test-helpers";
Expand Down Expand Up @@ -262,9 +263,10 @@ describe("cmdRun happy-path pipeline", () => {

const historyPath = join(historyDir, "history.json");
expect(existsSync(historyPath)).toBe(true);
const records = JSON.parse(readFileSync(historyPath, "utf-8"));
expect(records.length).toBeGreaterThanOrEqual(1);
const record = records[records.length - 1];
const data = JSON.parse(readFileSync(historyPath, "utf-8"));
expect(data.version).toBe(HISTORY_SCHEMA_VERSION);
expect(data.records.length).toBeGreaterThanOrEqual(1);
const record = data.records[data.records.length - 1];
expect(record.agent).toBe("claude");
expect(record.cloud).toBe("sprite");
expect(record.timestamp).toBeDefined();
Expand All @@ -279,8 +281,8 @@ describe("cmdRun happy-path pipeline", () => {
await cmdRun("claude", "sprite", "Fix all bugs");

const historyPath = join(historyDir, "history.json");
const records = JSON.parse(readFileSync(historyPath, "utf-8"));
const record = records[records.length - 1];
const data = JSON.parse(readFileSync(historyPath, "utf-8"));
const record = data.records[data.records.length - 1];
expect(record.prompt).toBe("Fix all bugs");
});

Expand All @@ -293,8 +295,8 @@ describe("cmdRun happy-path pipeline", () => {
await cmdRun("claude", "sprite");

const historyPath = join(historyDir, "history.json");
const records = JSON.parse(readFileSync(historyPath, "utf-8"));
const record = records[records.length - 1];
const data = JSON.parse(readFileSync(historyPath, "utf-8"));
const record = data.records[data.records.length - 1];
expect(record.prompt).toBeUndefined();
});

Expand All @@ -309,8 +311,8 @@ describe("cmdRun happy-path pipeline", () => {
const after = new Date().toISOString();

const historyPath = join(historyDir, "history.json");
const records = JSON.parse(readFileSync(historyPath, "utf-8"));
const record = records[records.length - 1];
const data = JSON.parse(readFileSync(historyPath, "utf-8"));
const record = data.records[data.records.length - 1];
expect(record.timestamp >= before).toBe(true);
expect(record.timestamp <= after).toBe(true);
});
Expand All @@ -331,9 +333,9 @@ describe("cmdRun happy-path pipeline", () => {

const historyPath = join(historyDir, "history.json");
expect(existsSync(historyPath)).toBe(true);
const records = JSON.parse(readFileSync(historyPath, "utf-8"));
expect(records.length).toBeGreaterThanOrEqual(1);
expect(records[records.length - 1].agent).toBe("claude");
const data = JSON.parse(readFileSync(historyPath, "utf-8"));
expect(data.records.length).toBeGreaterThanOrEqual(1);
expect(data.records[data.records.length - 1].agent).toBe("claude");
});

it("should still execute script when history save fails", async () => {
Expand Down Expand Up @@ -381,10 +383,10 @@ describe("cmdRun happy-path pipeline", () => {
await cmdRun("claude", "sprite");

const historyPath = join(historyDir, "history.json");
const records = JSON.parse(readFileSync(historyPath, "utf-8"));
expect(records).toHaveLength(2);
expect(records[0].agent).toBe("codex");
expect(records[1].agent).toBe("claude");
const data = JSON.parse(readFileSync(historyPath, "utf-8"));
expect(data.records).toHaveLength(2);
expect(data.records[0].agent).toBe("codex");
expect(data.records[1].agent).toBe("claude");
});
});

Expand Down
9 changes: 5 additions & 4 deletions packages/cli/src/__tests__/history-trimming.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { filterHistory, loadHistory, saveSpawnRecord } from "../history.js";
import { filterHistory, HISTORY_SCHEMA_VERSION, loadHistory, saveSpawnRecord } from "../history.js";

/**
* Tests for history trimming and boundary behavior.
Expand Down Expand Up @@ -849,12 +849,13 @@ describe("History Trimming and Boundaries", () => {
timestamp: "2026-01-02T00:00:00.000Z",
});

// Read raw file and verify it's valid JSON
// Read raw file and verify it's valid v1 JSON
const raw = readFileSync(join(testDir, "history.json"), "utf-8");
expect(() => JSON.parse(raw)).not.toThrow();
const parsed = JSON.parse(raw);
expect(Array.isArray(parsed)).toBe(true);
expect(parsed).toHaveLength(100);
expect(parsed.version).toBe(HISTORY_SCHEMA_VERSION);
expect(Array.isArray(parsed.records)).toBe(true);
expect(parsed.records).toHaveLength(100);
});

it("should write pretty-printed JSON with trailing newline after trimming", () => {
Expand Down
123 changes: 110 additions & 13 deletions packages/cli/src/__tests__/history.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { filterHistory, getHistoryPath, getSpawnDir, loadHistory, saveSpawnRecord } from "../history.js";
import {
filterHistory,
getHistoryPath,
getSpawnDir,
HISTORY_SCHEMA_VERSION,
loadHistory,
saveSpawnRecord,
} from "../history.js";

describe("history", () => {
let testDir: string;
Expand Down Expand Up @@ -185,6 +192,55 @@ describe("history", () => {
writeFileSync(join(testDir, "history.json"), "");
expect(loadHistory()).toEqual([]);
});

it("loads v1 format: { version: 1, records: [...] }", () => {
const records: SpawnRecord[] = [
{
agent: "claude",
cloud: "sprite",
timestamp: "2026-01-01T00:00:00.000Z",
},
];
writeFileSync(
join(testDir, "history.json"),
JSON.stringify({
version: 1,
records,
}),
);
expect(loadHistory()).toEqual(records);
});

it("returns empty array for v1 format with unknown version", () => {
const records: SpawnRecord[] = [
{
agent: "claude",
cloud: "sprite",
timestamp: "2026-01-01T00:00:00.000Z",
},
];
writeFileSync(
join(testDir, "history.json"),
JSON.stringify({
version: 99,
records,
}),
);
// Unknown version is not a recognized format; treated as invalid non-array
expect(loadHistory()).toEqual([]);
});

it("loads v0 format: bare array (backward compatibility)", () => {
const records: SpawnRecord[] = [
{
agent: "claude",
cloud: "sprite",
timestamp: "2026-01-01T00:00:00.000Z",
},
];
writeFileSync(join(testDir, "history.json"), JSON.stringify(records));
expect(loadHistory()).toEqual(records);
});
});

// ── saveSpawnRecord ─────────────────────────────────────────────────────
Expand All @@ -202,8 +258,9 @@ describe("history", () => {

expect(existsSync(join(nestedDir, "history.json"))).toBe(true);
const data = JSON.parse(readFileSync(join(nestedDir, "history.json"), "utf-8"));
expect(data).toHaveLength(1);
expect(data[0].agent).toBe("claude");
expect(data.version).toBe(HISTORY_SCHEMA_VERSION);
expect(data.records).toHaveLength(1);
expect(data.records[0].agent).toBe("claude");

// Clean up
rmSync(join(homedir(), ".spawn-test"), {
Expand All @@ -229,9 +286,10 @@ describe("history", () => {
});

const data = JSON.parse(readFileSync(join(testDir, "history.json"), "utf-8"));
expect(data).toHaveLength(2);
expect(data[0].agent).toBe("claude");
expect(data[1].agent).toBe("codex");
expect(data.version).toBe(HISTORY_SCHEMA_VERSION);
expect(data.records).toHaveLength(2);
expect(data.records[0].agent).toBe("claude");
expect(data.records[1].agent).toBe("codex");
});

it("saves record with prompt field", () => {
Expand All @@ -243,7 +301,7 @@ describe("history", () => {
});

const data = JSON.parse(readFileSync(join(testDir, "history.json"), "utf-8"));
expect(data[0].prompt).toBe("Fix all linter errors");
expect(data.records[0].prompt).toBe("Fix all linter errors");
});

it("saves record without prompt field", () => {
Expand All @@ -254,7 +312,7 @@ describe("history", () => {
});

const data = JSON.parse(readFileSync(join(testDir, "history.json"), "utf-8"));
expect(data[0].prompt).toBeUndefined();
expect(data.records[0].prompt).toBeUndefined();
});

it("writes pretty-printed JSON with trailing newline", () => {
Expand All @@ -281,9 +339,47 @@ describe("history", () => {
}

const data = JSON.parse(readFileSync(join(testDir, "history.json"), "utf-8"));
expect(data).toHaveLength(5);
expect(data[0].agent).toBe("agent-0");
expect(data[4].agent).toBe("agent-4");
expect(data.version).toBe(HISTORY_SCHEMA_VERSION);
expect(data.records).toHaveLength(5);
expect(data.records[0].agent).toBe("agent-0");
expect(data.records[4].agent).toBe("agent-4");
});

it("writes v1 format with version and records fields", () => {
saveSpawnRecord({
agent: "claude",
cloud: "sprite",
timestamp: "2026-01-01T00:00:00.000Z",
});

const data = JSON.parse(readFileSync(join(testDir, "history.json"), "utf-8"));
expect(data.version).toBe(HISTORY_SCHEMA_VERSION);
expect(Array.isArray(data.records)).toBe(true);
});

it("migrates v0 bare array to v1 format on next save", () => {
const existing: SpawnRecord[] = [
{
agent: "claude",
cloud: "sprite",
timestamp: "2026-01-01T00:00:00.000Z",
},
];
// Write v0 bare array
writeFileSync(join(testDir, "history.json"), JSON.stringify(existing));

// Trigger a write via saveSpawnRecord
saveSpawnRecord({
agent: "codex",
cloud: "hetzner",
timestamp: "2026-01-02T00:00:00.000Z",
});

const data = JSON.parse(readFileSync(join(testDir, "history.json"), "utf-8"));
expect(data.version).toBe(HISTORY_SCHEMA_VERSION);
expect(data.records).toHaveLength(2);
expect(data.records[0].agent).toBe("claude");
expect(data.records[1].agent).toBe("codex");
});

it("recovers from corrupted existing history file", () => {
Expand All @@ -297,8 +393,9 @@ describe("history", () => {

// loadHistory returns [] for corrupted files, so saveSpawnRecord starts fresh
const data = JSON.parse(readFileSync(join(testDir, "history.json"), "utf-8"));
expect(data).toHaveLength(1);
expect(data[0].agent).toBe("claude");
expect(data.version).toBe(HISTORY_SCHEMA_VERSION);
expect(data.records).toHaveLength(1);
expect(data.records[0].agent).toBe("claude");
});
});

Expand Down
Loading