← octto
// tests/state/persistence.test.ts
import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test";
import { existsSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createStatePersistence, type StatePersistence } from "../../src/state/persistence";
import type { BrainstormState } from "../../src/state/types";
describe("createStatePersistence", () => {
let persistence: StatePersistence;
let testDir: string;
beforeEach(() => {
testDir = mkdtempSync(join(tmpdir(), "octto-persistence-test-"));
persistence = createStatePersistence(testDir);
});
afterEach(() => {
rmSync(testDir, { recursive: true, force: true });
});
describe("save and load", () => {
it("should save state to file and load it back", async () => {
const state: BrainstormState = {
session_id: "ses_test123",
browser_session_id: "ses_browser1",
request: "Add healthcheck endpoints",
created_at: Date.now(),
updated_at: Date.now(),
branches: {
infrastructure: {
id: "infrastructure",
scope: "Which services need healthchecks",
status: "exploring",
questions: [],
finding: null,
},
},
branch_order: ["infrastructure"],
};
await persistence.save(state);
const loaded = await persistence.load("ses_test123");
expect(loaded).not.toBeNull();
expect(loaded!.session_id).toBe("ses_test123");
expect(loaded!.request).toBe("Add healthcheck endpoints");
expect(loaded!.branches.infrastructure.scope).toBe("Which services need healthchecks");
});
it("should return null for non-existent session", async () => {
const loaded = await persistence.load("ses_nonexistent");
expect(loaded).toBeNull();
});
it("should return null for valid JSON with invalid schema", async () => {
const errorSpy = spyOn(console, "error").mockImplementation(() => {});
// Write a valid JSON file that doesn't match BrainstormState schema
const filePath = join(testDir, "ses_corrupt.json");
await Bun.write(filePath, JSON.stringify({ not_a: "valid_state" }));
const loaded = await persistence.load("ses_corrupt");
expect(loaded).toBeNull();
expect(errorSpy).toHaveBeenCalled();
errorSpy.mockRestore();
});
it("should create directory if it does not exist", async () => {
const state: BrainstormState = {
session_id: "ses_new",
browser_session_id: null,
request: "Test",
created_at: Date.now(),
updated_at: Date.now(),
branches: {},
branch_order: [],
};
await persistence.save(state);
expect(existsSync(testDir)).toBe(true);
});
});
describe("delete", () => {
it("should delete state file", async () => {
const state: BrainstormState = {
session_id: "ses_delete",
browser_session_id: null,
request: "Delete me",
created_at: Date.now(),
updated_at: Date.now(),
branches: {},
branch_order: [],
};
await persistence.save(state);
expect(await persistence.load("ses_delete")).not.toBeNull();
await persistence.delete("ses_delete");
expect(await persistence.load("ses_delete")).toBeNull();
});
});
describe("session ID validation", () => {
it("should reject session IDs with path traversal sequences", async () => {
const state: BrainstormState = {
session_id: "../etc/passwd",
browser_session_id: null,
request: "Malicious",
created_at: Date.now(),
updated_at: Date.now(),
branches: {},
branch_order: [],
};
expect(() => persistence.save(state)).toThrow("Invalid session ID: ../etc/passwd");
});
it("should reject session IDs with slashes", async () => {
await expect(persistence.load("foo/bar")).rejects.toThrow("Invalid session ID: foo/bar");
});
it("should reject session IDs with dots", async () => {
await expect(persistence.delete("session.id")).rejects.toThrow("Invalid session ID: session.id");
});
it("should accept valid session IDs with alphanumeric, underscore, and hyphen", async () => {
const state: BrainstormState = {
session_id: "ses_valid-123_ABC",
browser_session_id: null,
request: "Valid",
created_at: Date.now(),
updated_at: Date.now(),
branches: {},
branch_order: [],
};
await persistence.save(state);
const loaded = await persistence.load("ses_valid-123_ABC");
expect(loaded).not.toBeNull();
expect(loaded!.session_id).toBe("ses_valid-123_ABC");
});
});
describe("list", () => {
it("should list all session IDs", async () => {
const state1: BrainstormState = {
session_id: "ses_list1",
browser_session_id: null,
request: "First",
created_at: Date.now(),
updated_at: Date.now(),
branches: {},
branch_order: [],
};
const state2: BrainstormState = {
session_id: "ses_list2",
browser_session_id: null,
request: "Second",
created_at: Date.now(),
updated_at: Date.now(),
branches: {},
branch_order: [],
};
await persistence.save(state1);
await persistence.save(state2);
const ids = await persistence.list();
expect(ids).toContain("ses_list1");
expect(ids).toContain("ses_list2");
});
});
});