Replace sqlite3 subprocess with bun:sqlite native driver, add AGENTS.md and tests
Rewrites history/queries.ts to use bun:sqlite with readonly mode and parameterized queries instead of spawning sqlite3. Consolidates threshold constants into a single source of truth. Adds AGENTS.md as the canonical operational doc, moves architecture to docs/, and adds initial test suite.
This commit is contained in:
26
tests/compaction.test.ts
Normal file
26
tests/compaction.test.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { getCompactionPrompt } from "../src/compaction/prompt";
|
||||
|
||||
describe("compaction prompt", () => {
|
||||
test("returns non-empty string", () => {
|
||||
const prompt = getCompactionPrompt();
|
||||
expect(typeof prompt).toBe("string");
|
||||
expect(prompt.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("emphasizes self-continuity, not another agent", () => {
|
||||
const prompt = getCompactionPrompt();
|
||||
expect(prompt).toContain("yourself");
|
||||
expect(prompt).toContain("not another agent");
|
||||
});
|
||||
|
||||
test("includes key sections", () => {
|
||||
const prompt = getCompactionPrompt();
|
||||
expect(prompt).toContain("## Goal");
|
||||
expect(prompt).toContain("## Instructions");
|
||||
expect(prompt).toContain("## Discoveries");
|
||||
expect(prompt).toContain("## Accomplished");
|
||||
expect(prompt).toContain("## Relevant files");
|
||||
expect(prompt).toContain("## Notes");
|
||||
});
|
||||
});
|
||||
64
tests/format.test.ts
Normal file
64
tests/format.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { formatMessageList, formatSessionList } from "../src/history/format";
|
||||
|
||||
describe("formatSessionList", () => {
|
||||
test("returns message for empty list", () => {
|
||||
expect(formatSessionList([])).toBe("No sessions found.");
|
||||
});
|
||||
|
||||
test("formats sessions as markdown table", () => {
|
||||
const rows = [
|
||||
{ id: "ses_abc123def", title: "Test Session", updated: "2024-01-15 10:30:00", msgs: 12 },
|
||||
];
|
||||
const result = formatSessionList(rows);
|
||||
expect(result).toContain("# Recent Sessions");
|
||||
expect(result).toContain("| ID | Title | Updated | Messages |");
|
||||
expect(result).toContain("ses_abc123de...");
|
||||
expect(result).toContain("Test Session");
|
||||
});
|
||||
|
||||
test("truncates long IDs and titles", () => {
|
||||
const rows = [
|
||||
{
|
||||
id: "ses_verylongidthatshouldbetruncated1234567890",
|
||||
title: "A very long title that should be truncated for display",
|
||||
updated: "2024-01-15",
|
||||
msgs: 5,
|
||||
},
|
||||
];
|
||||
const result = formatSessionList(rows);
|
||||
expect(result).toContain("ses_verylong...");
|
||||
});
|
||||
|
||||
test("handles untitled sessions", () => {
|
||||
const rows = [{ id: "ses_1", title: null, updated: "2024-01-15", msgs: 0 }];
|
||||
const result = formatSessionList(rows);
|
||||
expect(result).toContain("untitled");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatMessageList", () => {
|
||||
test("returns message for empty list", () => {
|
||||
expect(formatMessageList([])).toBe("No messages found.");
|
||||
});
|
||||
|
||||
test("formats messages with roles and timestamps", () => {
|
||||
const rows = [
|
||||
{ role: "user", time: "2024-01-15 10:00:00", text: "Hello" },
|
||||
{ role: "assistant", time: "2024-01-15 10:00:05", text: "Hi there" },
|
||||
];
|
||||
const result = formatMessageList(rows);
|
||||
expect(result).toContain("# Conversation");
|
||||
expect(result).toContain("user");
|
||||
expect(result).toContain("assistant");
|
||||
expect(result).toContain("Hello");
|
||||
expect(result).toContain("Hi there");
|
||||
});
|
||||
|
||||
test("truncates long text", () => {
|
||||
const longText = "x".repeat(3000);
|
||||
const rows = [{ role: "assistant", time: "2024-01-15", text: longText }];
|
||||
const result = formatMessageList(rows);
|
||||
expect(result.length).toBeLessThan(longText.length + 200);
|
||||
});
|
||||
});
|
||||
76
tests/queries.test.ts
Normal file
76
tests/queries.test.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Database } from "bun:sqlite";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { closeDb, runQuery } from "../src/history/queries";
|
||||
|
||||
describe("queries module with bun:sqlite", () => {
|
||||
const tmpDir = mkdtempSync(path.join(os.tmpdir(), "open-memory-test-"));
|
||||
const opencodeDir = path.join(tmpDir, "opencode");
|
||||
const dbPath = path.join(opencodeDir, "opencode.db");
|
||||
const originalXdg = process.env.XDG_DATA_HOME;
|
||||
|
||||
const setup = (): void => {
|
||||
mkdirSync(opencodeDir, { recursive: true });
|
||||
const db = new Database(dbPath);
|
||||
db.run(
|
||||
"CREATE TABLE IF NOT EXISTS project (id TEXT PRIMARY KEY, worktree TEXT, name TEXT, time_updated INTEGER)",
|
||||
);
|
||||
db.run(
|
||||
"CREATE TABLE IF NOT EXISTS session (id TEXT, project_id TEXT, parent_id TEXT, title TEXT, summary TEXT, time_created INTEGER, time_updated INTEGER)",
|
||||
);
|
||||
db.run(
|
||||
"INSERT OR REPLACE INTO project VALUES ('proj1', '/tmp/test', 'test-project', 1700000000000)",
|
||||
);
|
||||
db.run(
|
||||
"INSERT OR REPLACE INTO session VALUES ('ses_1', 'proj1', NULL, 'Test Session', NULL, 1700000000000, 1700001000000)",
|
||||
);
|
||||
db.close();
|
||||
process.env.XDG_DATA_HOME = opencodeDir;
|
||||
};
|
||||
|
||||
const cleanup = (): void => {
|
||||
closeDb();
|
||||
process.env.XDG_DATA_HOME = originalXdg;
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
};
|
||||
|
||||
test("runQuery without parameters", () => {
|
||||
setup();
|
||||
try {
|
||||
type CountRow = { label: string; cnt: number };
|
||||
const rows = runQuery<CountRow>("SELECT 'projects' AS label, COUNT(*) AS cnt FROM project");
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows[0].cnt).toBe(1);
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("runQuery with named parameters", () => {
|
||||
setup();
|
||||
try {
|
||||
type SessionRow = { id: string; title: string };
|
||||
const rows = runQuery<SessionRow>("SELECT id, title FROM session WHERE id = $sessionId", {
|
||||
$sessionId: "ses_1",
|
||||
});
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows[0].id).toBe("ses_1");
|
||||
expect(rows[0].title).toBe("Test Session");
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("runQuery opens database read-only", () => {
|
||||
setup();
|
||||
try {
|
||||
type Row = { cnt: number };
|
||||
const rows = runQuery<Row>("SELECT COUNT(*) AS cnt FROM session");
|
||||
expect(rows[0].cnt).toBe(1);
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
32
tests/thresholds.test.ts
Normal file
32
tests/thresholds.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { type ContextStatus, getStatusLabel, THRESHOLDS } from "../src/context/thresholds";
|
||||
|
||||
describe("thresholds", () => {
|
||||
test("THRESHOLDS constants", () => {
|
||||
expect(THRESHOLDS.yellow).toBe(70);
|
||||
expect(THRESHOLDS.red).toBe(85);
|
||||
expect(THRESHOLDS.critical).toBe(92);
|
||||
});
|
||||
|
||||
test("getStatusLabel returns correct status", () => {
|
||||
expect(getStatusLabel(0)).toBe("green");
|
||||
expect(getStatusLabel(50)).toBe("green");
|
||||
expect(getStatusLabel(69)).toBe("green");
|
||||
expect(getStatusLabel(70)).toBe("yellow");
|
||||
expect(getStatusLabel(75)).toBe("yellow");
|
||||
expect(getStatusLabel(84)).toBe("yellow");
|
||||
expect(getStatusLabel(85)).toBe("red");
|
||||
expect(getStatusLabel(90)).toBe("red");
|
||||
expect(getStatusLabel(91)).toBe("red");
|
||||
expect(getStatusLabel(92)).toBe("critical");
|
||||
expect(getStatusLabel(99)).toBe("critical");
|
||||
expect(getStatusLabel(100)).toBe("critical");
|
||||
});
|
||||
|
||||
test("ContextStatus type accepts all valid values", () => {
|
||||
const statuses: ContextStatus[] = ["green", "yellow", "red", "critical"];
|
||||
for (const s of statuses) {
|
||||
expect(typeof s).toBe("string");
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user