Files
learnharness/tests/chat-store-events.test.ts

78 lines
3.2 KiB
TypeScript

/**
* CHAT STORE EVENT PINS (push-architecture S6, 2026-10-02): the sidebar's
* 2.5s poll died — list truth now rides chats-changed. The store's law:
* list-SHAPE writes (create/rename/delete) announce post-write; content
* writes (messages/usage/settings/tree) are DELIBERATELY silent — the
* silence law is pinned here so it reads as law, not oversight. Runs
* under an isolated LH_DATA_DIR (the chat-birth-stamp discipline: the
* seam is set before any config-loading import, so every app-module
* import is dynamic).
*/
import assert from "node:assert/strict";
import fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { after, describe, it } from "node:test";
// ── the seam, BEFORE any config-loading import ──────────────────────────────
const TMP_DATA = fs.mkdtempSync(path.join(os.tmpdir(), "lh-chat-store-events-"));
process.env.LH_DATA_DIR = TMP_DATA;
const { chatStore } = await import("../src/store/chats.ts");
const { subscribe } = await import("../src/core/events.ts");
after(() => {
fs.rmSync(TMP_DATA, { recursive: true, force: true });
});
describe("chat store events (S6: the silence law)", () => {
it("createChat, renameChat, deleteChat each announce chats-changed", () => {
let announces = 0;
const off = subscribe((e) => {
if (e.type === "chats-changed") announces++;
});
const chat = chatStore.createChat("Store events fixture");
assert.equal(announces, 1, "create announces post-write");
chatStore.renameChat(chat.id, "Renamed");
assert.equal(announces, 2, "rename announces (a list-shape write)");
assert.equal(chatStore.deleteChat(chat.id), true);
assert.equal(announces, 3, "a real delete announces");
off();
});
it("deleteChat on a missing id stays silent", () => {
let announces = 0;
const off = subscribe((e) => {
if (e.type === "chats-changed") announces++;
});
assert.equal(chatStore.deleteChat("chat_never_existed"), false);
assert.equal(announces, 0, "nothing was written — nothing announced");
off();
});
it("content writes are DELIBERATELY silent (the law)", () => {
const chat = chatStore.createChat("Silent content fixture");
let announces = 0;
const off = subscribe((e) => {
if (e.type === "chats-changed") announces++;
});
// saveMessages: fires per turn-persist — would spam the bus during
// turns; its list-visible effects arrive via turn-started/settled.
chatStore.saveMessages(chat.id, [{ id: "m1", role: "user", content: "hello", timestamp: 1 } as never]);
// usage appends + settings + tree: record CONTENT, not list shape.
chatStore.appendTurnUsage(chat.id, { id: "t1", messageId: "m1", at: "2026-10-02T00:00:00.000Z", model: "x:y", steps: [] });
chatStore.appendExtractionUsage(chat.id, { startedAt: 1, at: "2026-10-02T00:00:00.000Z", durationMs: 1, stages: { extractor: { model: "x:y", steps: [] }, auditor: { model: "x:y", steps: [] } } });
chatStore.saveSettings(chat.id, {});
chatStore.saveTree(chat.id, { nodes: [], edges: [], version: 1 });
assert.equal(announces, 0, "the silence law: content writes never announce");
off();
chatStore.deleteChat(chat.id);
});
});