78 lines
3.2 KiB
TypeScript
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);
|
|
});
|
|
});
|