129 lines
6.8 KiB
JavaScript
129 lines
6.8 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { DatabaseSync } from "node:sqlite";
|
|
import { test } from "node:test";
|
|
import { ContinuityStore } from "../src/continuity.js";
|
|
import { reciprocalRankFusion } from "../src/rank-fusion.js";
|
|
|
|
const owner = "device:test";
|
|
function event(eventId = "e1", overrides = {}) {
|
|
return { event_id: eventId, subject_id: "task:reimburse", task_id: "reimburse", kind: "task_completed",
|
|
source: "runtime", observed_at: Date.now(), text: "十月报销的发票已经整理到报销区,尚未提交审批。", ...overrides };
|
|
}
|
|
function setup(t) {
|
|
const db = new DatabaseSync(":memory:");
|
|
const store = new ContinuityStore(db);
|
|
t.after(() => db.close());
|
|
return store;
|
|
}
|
|
function ingest(store, events) { return store.ingest({ version: 1, owner, events }); }
|
|
function proposal(job, content = "十月报销发票已整理,尚未提交审批。") {
|
|
return { job_id: job.job_id, token: job.token, episodes: [{ subject_id: job.events[0].subject_id, content,
|
|
cues: ["每月报销整理发票"], source_refs: job.events.map((item) => item.event_id) }], candidates: [], discarded_event_ids: [] };
|
|
}
|
|
|
|
test("idempotent ingest rejects conflicting IDs and rolls back the whole envelope", (t) => {
|
|
const store = setup(t);
|
|
const first = event();
|
|
assert.equal(ingest(store, [first]).stored, 1);
|
|
assert.equal(ingest(store, [first]).stored, 0);
|
|
assert.throws(() => ingest(store, [event("e2"), { ...first, text: "changed" }]), /collision/);
|
|
assert.equal(store.db.prepare("SELECT count(*) AS n FROM continuity_events").get().n, 1);
|
|
assert.throws(() => ingest(store, [event("forged", { source: "user" })]), /provenance/);
|
|
});
|
|
|
|
test("leases recover the same batch, reject stale workers, commit atomically and replay receipts", (t) => {
|
|
const store = setup(t);
|
|
ingest(store, [event()]);
|
|
const now = Date.now();
|
|
const job = store.claim({ owner }, now);
|
|
assert.equal(store.claim({ owner }, now + 1), null);
|
|
const restored = new ContinuityStore(store.db).claim({ owner }, now + 120_001);
|
|
assert.equal(job.job_id, restored.job_id);
|
|
assert.notEqual(job.token, restored.token);
|
|
assert.throws(() => store.commit(proposal(job), now + 120_002), /Stale/);
|
|
const receipt = store.commit(proposal(restored), now + 120_002);
|
|
assert.equal(receipt.consumed, 1);
|
|
assert.deepEqual(store.commit(proposal(restored), now + 999_999), receipt);
|
|
assert.equal(store.claim({ owner }, now + 1_000_000), null);
|
|
});
|
|
|
|
test("missing or invented evidence never consumes an observation batch", (t) => {
|
|
const store = setup(t);
|
|
ingest(store, [event("e1"), event("e2", { text: "另一项没有引用的独立结果" })]);
|
|
const job = store.claim({ owner });
|
|
const draft = proposal(job);
|
|
draft.episodes[0].source_refs = ["e1"];
|
|
assert.throws(() => store.commit(draft), /all source events/);
|
|
draft.episodes[0].source_refs = ["invented"];
|
|
assert.throws(() => store.commit(draft), /outside/);
|
|
assert.equal(store.db.prepare("SELECT count(*) AS n FROM continuity_records").get().n, 0);
|
|
assert.equal(store.db.prepare("SELECT sum(consumed) AS n FROM continuity_events").get().n, 0);
|
|
assert.equal(store.fail(job).released, true);
|
|
assert.equal(store.claim({ owner }), null);
|
|
});
|
|
|
|
test("background proposals cannot promote executor text to preferences or change another subject", (t) => {
|
|
const store = setup(t);
|
|
ingest(store, [event()]);
|
|
const job = store.claim({ owner });
|
|
const draft = proposal(job);
|
|
draft.candidates = [{ ...draft.episodes[0], kind: "preference", user_quote: "报销" }];
|
|
assert.throws(() => store.commit(draft), /exact user quote/);
|
|
draft.candidates = [];
|
|
draft.episodes[0].subject_id = "project:other";
|
|
assert.throws(() => store.commit(draft), /no evidence/);
|
|
});
|
|
|
|
test("explicit corrections remain sourced candidates, not automatically active global rules", (t) => {
|
|
const store = setup(t);
|
|
ingest(store, [event("correction", { source: "user", kind: "user_message", text: "报销前先让我确认发票清单。" })]);
|
|
const job = store.claim({ owner }, Date.now() + 31_000);
|
|
store.commit({ job_id: job.job_id, token: job.token, episodes: [], candidates: [{ subject_id: "task:reimburse",
|
|
kind: "correction", content: "报销前须确认发票清单", user_quote: "报销前先让我确认发票清单。",
|
|
source_refs: ["correction"], cues: ["报销确认"] }], discarded_event_ids: [] }, Date.now() + 31_001);
|
|
const record = store.recall({ owner, query: "报销确认" }).hits[0];
|
|
assert.equal(record.status, "candidate");
|
|
assert.deepEqual(record.source_refs, ["correction"]);
|
|
});
|
|
|
|
test("Chinese recall finds dormant software beyond a recent-history window and respects owner scope", (t) => {
|
|
const store = setup(t);
|
|
ingest(store, [event("software", { subject_id: "asset:billing", text: "账单软件升级后导出发票失败,需要回滚模板。" })]);
|
|
store.commit(proposal(store.claim({ owner }), "账单软件导出发票故障:模板字段变更,回滚后恢复。"));
|
|
for (let i = 0; i < 110; i++) {
|
|
ingest(store, [event(`noise${i}`, { subject_id: `task:noise${i}` })]);
|
|
store.commit(proposal(store.claim({ owner }), `天气查看任务编号${i}`));
|
|
}
|
|
const recalled = store.recall({ owner, query: "账单软件导出发票又失败" });
|
|
assert.match(recalled.hits[0].content, /模板字段/);
|
|
assert.equal(recalled.hits[0].subject_id, "asset:billing");
|
|
assert.deepEqual(store.recall({ owner: "device:other", query: "账单软件" }).hits, []);
|
|
assert.deepEqual(store.evidence({ owner: "device:other", event_ids: ["software"] }), []);
|
|
assert.equal(store.evidence({ owner, event_ids: ["software"] })[0].source, "runtime");
|
|
});
|
|
|
|
test("secrets are redacted before the model, and job size is bounded in UTF-8 bytes", (t) => {
|
|
const store = setup(t);
|
|
ingest(store, [event("secret", { text: "密码=do-not-retain" }), ...Array.from({ length: 19 }, (_, i) => event(`long${i}`, { text: "中文观察".repeat(2000) }))]);
|
|
const job = store.claim({ owner });
|
|
assert.ok(!JSON.stringify(job).includes("do-not-retain"));
|
|
assert.ok(Buffer.byteLength(JSON.stringify(job.events)) < 12_000);
|
|
assert.ok(job.events.length < 20);
|
|
});
|
|
|
|
test("RRF fuses channels deterministically without rewarding duplicates within one channel", () => {
|
|
const ranked = reciprocalRankFusion([["b", "a"], ["a", "c"]]);
|
|
assert.equal(ranked[0].id, "a");
|
|
assert.equal(reciprocalRankFusion([["a", "a"]])[0].score, 1 / 61);
|
|
});
|
|
|
|
test("repeated observations collapse for the model but retain every evidence reference", (t) => {
|
|
const store = setup(t);
|
|
ingest(store, [event("one"), event("two")]);
|
|
const job = store.claim({ owner });
|
|
assert.equal(job.events.length, 1);
|
|
assert.equal(job.events[0].repeat_count, 2);
|
|
assert.equal(store.commit(proposal(job)).consumed, 2);
|
|
assert.deepEqual(store.recall({ owner, query: "报销" }).hits[0].source_refs, ["one", "two"]);
|
|
});
|