mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
194 lines
7.9 KiB
TypeScript
194 lines
7.9 KiB
TypeScript
import { join } from "node:path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import {
|
|
createNodeSqliteFactory,
|
|
createSqliteSessionSearch,
|
|
createSqliteSessionStore,
|
|
type SqliteSessionMetadata,
|
|
} from "../../../storage/sqlite-node/src/index.ts";
|
|
import { NodeExecutionEnv } from "../../src/harness/env/nodejs.ts";
|
|
import { createJsonlSessionStore } from "../../src/harness/session/jsonl-store.ts";
|
|
import { createSessionRepository } from "../../src/harness/session/repository.ts";
|
|
import { createScanningSessionSearch } from "../../src/harness/session/search-backend.ts";
|
|
import type { SessionSearch, SessionSearchHit, SessionSearchOptions } from "../../src/harness/types.ts";
|
|
import { createTempDir, createUserMessage } from "./session-test-utils.ts";
|
|
|
|
const ownedStores: AsyncDisposable[] = [];
|
|
|
|
afterEach(async () => {
|
|
for (const store of ownedStores.splice(0)) await store[Symbol.asyncDispose]();
|
|
});
|
|
|
|
function createSqliteRepository(options: Parameters<typeof createSqliteSessionStore>[0]) {
|
|
const store = createSqliteSessionStore(options);
|
|
ownedStores.push(store);
|
|
return createSessionRepository({
|
|
store,
|
|
search: createSqliteSessionSearch(options),
|
|
});
|
|
}
|
|
|
|
function createJsonlRepository(options: Parameters<typeof createJsonlSessionStore>[0]) {
|
|
const store = createJsonlSessionStore(options);
|
|
ownedStores.push(store);
|
|
return createSessionRepository({ store, search: createScanningSessionSearch(store) });
|
|
}
|
|
|
|
describe("JsonlSessionStore with scanning search", () => {
|
|
it("searches canonical session entries by scanning", async () => {
|
|
const root = createTempDir();
|
|
const env = new NodeExecutionEnv({ cwd: root });
|
|
const repo = createJsonlRepository({ fs: env, sessionsRoot: join(root, "sessions") });
|
|
const included = await repo.create({ cwd: root, id: "included" });
|
|
const excluded = await repo.create({ cwd: `${root}/other`, id: "excluded" });
|
|
const entryId = await included.appendMessage(createUserMessage("Find the auth defect"));
|
|
await excluded.appendMessage(createUserMessage("Find the auth defect"));
|
|
|
|
await expect(repo.search({ text: "AUTH", cwd: root })).resolves.toEqual([
|
|
expect.objectContaining({ entryId, metadata: expect.objectContaining({ id: "included" }) }),
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("SqliteSessionStore with explicit SQLite FTS5 search", () => {
|
|
it("uses SQLite FTS5 when composed with its search store", async () => {
|
|
const root = createTempDir();
|
|
const env = new NodeExecutionEnv({ cwd: root });
|
|
const sqlite = createNodeSqliteFactory();
|
|
const databasePath = join(root, "sessions.sqlite");
|
|
const repo = createSqliteRepository({ env, sqlite, databasePath });
|
|
const included = await repo.create({ cwd: root, id: "included" });
|
|
const excluded = await repo.create({ cwd: `${root}/other`, id: "excluded" });
|
|
const metadata = await included.getMetadata();
|
|
const entryId = await included.appendMessage(createUserMessage("Find the auth defect"));
|
|
await excluded.appendMessage(createUserMessage("Find the auth defect"));
|
|
|
|
await expect(repo.search({ text: "auth", cwd: root })).resolves.toEqual([
|
|
expect.objectContaining({ entryId, metadata: expect.objectContaining({ id: "included" }) }),
|
|
]);
|
|
await expect(repo.search({ text: "uth", cwd: root })).resolves.toEqual([
|
|
expect.objectContaining({ entryId, metadata: expect.objectContaining({ id: "included" }) }),
|
|
]);
|
|
|
|
const db = await sqlite.open(databasePath);
|
|
try {
|
|
const tables = await db
|
|
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name")
|
|
.all<{ name: string }>();
|
|
expect(tables.map((row) => row.name)).toContain("session_search_fts");
|
|
expect(tables.map((row) => row.name)).not.toContain("session_search_records");
|
|
} finally {
|
|
await db.close();
|
|
}
|
|
|
|
await repo.delete(metadata);
|
|
await expect(repo.search({ text: "auth", cwd: root })).resolves.toEqual([]);
|
|
});
|
|
|
|
it("creates an empty canonical session without initializing FTS", async () => {
|
|
const root = createTempDir();
|
|
const env = new NodeExecutionEnv({ cwd: root });
|
|
const sqlite = createNodeSqliteFactory();
|
|
const databasePath = join(root, "sessions.sqlite");
|
|
const repo = createSqliteRepository({ env, sqlite, databasePath });
|
|
const session = await repo.create({ cwd: root, id: "session-1" });
|
|
|
|
const db = await sqlite.open(databasePath);
|
|
try {
|
|
const fts = await db
|
|
.prepare("SELECT 1 AS found FROM sqlite_master WHERE type = 'table' AND name = 'session_search_fts'")
|
|
.get<{ found: number }>();
|
|
expect(fts).toBeUndefined();
|
|
} finally {
|
|
await db.close();
|
|
}
|
|
await expect(session.appendMessage(createUserMessage("still writable"))).resolves.toBeTypeOf("string");
|
|
});
|
|
|
|
it("rolls back canonical appends when co-located FTS trigger writes fail", async () => {
|
|
const root = createTempDir();
|
|
const env = new NodeExecutionEnv({ cwd: root });
|
|
const sqlite = createNodeSqliteFactory();
|
|
const databasePath = join(root, "sessions.sqlite");
|
|
const repo = createSqliteRepository({ env, sqlite, databasePath });
|
|
await repo.search({ text: "initialize" });
|
|
const session = await repo.create({ cwd: root, id: "session-1" });
|
|
|
|
const db = await sqlite.open(databasePath);
|
|
try {
|
|
await db.exec("DROP TABLE session_search_fts");
|
|
} finally {
|
|
await db.close();
|
|
}
|
|
|
|
await expect(session.appendMessage(createUserMessage("must roll back"))).rejects.toThrow();
|
|
await expect(session.getEntries()).resolves.toEqual([]);
|
|
});
|
|
|
|
it("rolls back canonical deletion when co-located FTS cleanup fails", async () => {
|
|
const root = createTempDir();
|
|
const env = new NodeExecutionEnv({ cwd: root });
|
|
const sqlite = createNodeSqliteFactory();
|
|
const databasePath = join(root, "sessions.sqlite");
|
|
const repo = createSqliteRepository({ env, sqlite, databasePath });
|
|
await repo.search({ text: "initialize" });
|
|
const session = await repo.create({ cwd: root, id: "session-1" });
|
|
await session.appendMessage(createUserMessage("must remain"));
|
|
const metadata = await session.getMetadata();
|
|
|
|
const db = await sqlite.open(databasePath);
|
|
try {
|
|
await db.exec("DROP TABLE session_search_fts");
|
|
} finally {
|
|
await db.close();
|
|
}
|
|
|
|
await expect(repo.delete(metadata)).rejects.toThrow();
|
|
const reopened = await repo.open(metadata);
|
|
await expect(reopened.getEntries()).resolves.toHaveLength(1);
|
|
});
|
|
|
|
it("initializes canonical storage when searched before the first session is created", async () => {
|
|
const root = createTempDir();
|
|
const env = new NodeExecutionEnv({ cwd: root });
|
|
const repo = createSqliteRepository({
|
|
env,
|
|
sqlite: createNodeSqliteFactory(),
|
|
databasePath: join(root, "sessions.sqlite"),
|
|
});
|
|
|
|
await expect(repo.search({ text: "auth" })).resolves.toEqual([]);
|
|
const session = await repo.create({ cwd: root, id: "session-1" });
|
|
const entryId = await session.appendMessage(createUserMessage("Find the auth defect"));
|
|
|
|
await expect(repo.search({ text: "auth" })).resolves.toEqual([
|
|
expect.objectContaining({ entryId, metadata: expect.objectContaining({ id: "session-1" }) }),
|
|
]);
|
|
await expect(session.appendMessage(createUserMessage("Still writable"))).resolves.toBeTypeOf("string");
|
|
});
|
|
});
|
|
|
|
describe("SqliteSessionStore with custom search", () => {
|
|
it("can swap out the default search implementation", async () => {
|
|
const root = createTempDir();
|
|
const searches: SessionSearchOptions[] = [];
|
|
const search: SessionSearch<SqliteSessionMetadata> = {
|
|
async search(options): Promise<SessionSearchHit<SqliteSessionMetadata>[]> {
|
|
searches.push(options);
|
|
return [];
|
|
},
|
|
};
|
|
const env = new NodeExecutionEnv({ cwd: root });
|
|
const store = createSqliteSessionStore({
|
|
env,
|
|
sqlite: createNodeSqliteFactory(),
|
|
databasePath: join(root, "sessions.sqlite"),
|
|
});
|
|
const repo = createSessionRepository({ store, search });
|
|
const session = await repo.create({ cwd: root, id: "session-1" });
|
|
await session.appendMessage(createUserMessage("stored canonically"));
|
|
|
|
await expect(repo.search({ text: "custom query" })).resolves.toEqual([]);
|
|
expect(searches).toEqual([{ text: "custom query" }]);
|
|
});
|
|
});
|