deskpaw-memory/test/protocol.test.js

72 lines
3.8 KiB
JavaScript

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);
});