import assert from "node:assert/strict"; import { spawn } from "node:child_process"; import { mkdtempSync, rmSync } from "node:fs"; import { createConnection } from "node:net"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { test } from "node:test"; test("real socket protocol preserves observations and provenance across service restart", async (t) => { const dir = mkdtempSync(join(tmpdir(), "dp-memory-protocol-")); const socketPath = join(dir, "memory.sock"); let child; async function start() { child = spawn(process.execPath, [new URL("../src/server.js", import.meta.url).pathname], { env: { DESKPAW_STATE_DIR: dir, DESKPAW_MEMORY_SOCKET: socketPath, DESKPAW_MEMORY_DB: join(dir, "data.sqlite"), DESKPAW_MEMORY_BUNDLES: join(dir, "bundles") }, stdio: ["ignore", "pipe", "pipe"], }); await new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error("Isolated memory server did not start")), 5000); child.once("error", (error) => { clearTimeout(timeout); reject(error); }); child.once("exit", (code) => { clearTimeout(timeout); reject(new Error(`Server exited: ${code}`)); }); child.stdout.once("data", () => { clearTimeout(timeout); resolve(); }); }); } async function stop() { if (!child || child.exitCode !== null) return; const exited = new Promise((resolve) => child.once("exit", resolve)); child.kill("SIGTERM"); const timeout = setTimeout(() => child.kill("SIGKILL"), 2000); await exited; clearTimeout(timeout); } function call(method, params) { return new Promise((resolve, reject) => { const socket = createConnection(socketPath); let text = ""; socket.setTimeout(3000, () => socket.destroy(new Error("Test socket timed out"))); socket.once("connect", () => socket.write(JSON.stringify({ id: "test", method, params }) + "\n")); socket.on("data", (chunk) => { text += chunk; }); socket.once("end", () => { try { const response = JSON.parse(text); if (!response.ok) throw new Error(response.error); resolve(response.result); } catch (error) { reject(error); } }); socket.once("error", reject); }); } t.after(async () => { await stop(); rmSync(dir, { recursive: true, force: true }); }); await start(); const owner = "device:protocol"; const event = { event_id: "run:1", subject_id: "schedule:monthly", task_id: "run1", kind: "task_completed", source: "runtime", observed_at: Date.now(), text: "十月发票归档完成,等待确认。" }; assert.equal((await call("memory.events.ingest", { version: 1, owner, events: [event] })).stored, 1); const job = await call("memory.observation.claim", { owner }); const proposal = { job_id: job.job_id, token: job.token, episodes: [{ subject_id: event.subject_id, content: "十月发票已整理,等待用户确认。", source_refs: [event.event_id], cues: ["每月报销"] }], candidates: [], discarded_event_ids: [] }; const receipt = await call("memory.observation.commit", proposal); await stop(); await start(); assert.deepEqual(await call("memory.observation.commit", proposal), receipt); const recalled = await call("memory.continuity.recall", { owner, query: "发票报销" }); assert.equal(recalled.hits.length, 1); const context = await call("context.prepare", { owner, task_id: "run2", objective: "继续报销", schedule_id: "monthly" }); assert.equal(context.continuity.hits[0].source_refs[0], "run:1"); assert.equal((await call("memory.evidence.get", { owner, event_ids: ["run:1"] }))[0].text, event.text); assert.deepEqual(await call("memory.evidence.get", { owner: "device:another", event_ids: ["run:1"] }), []); assert.equal(await call("memory.observation.claim", { owner }), null); });