Files
Sybil-2/web/tests/agent-optimistic-outbox.test.mjs
T

140 lines
7.3 KiB
JavaScript
Raw Normal View History

import assert from "node:assert/strict";
import test from "node:test";
import { createAgentOptimisticOutbox } from "../src/lib/agent-optimistic-outbox.ts";
const canonical = (id, content = "Hello", time = "2026-10-14T12:01:00.000Z") => ({
id, content, createdAt: time, role: "user", name: "buzzert",
metadata: { kind: "message", entryId: 123, status: "complete", channel: "sybil" },
});
const deferred = () => { let resolve, reject; const promise = new Promise((yes, no) => { resolve = yes; reject = no; }); return { promise, resolve, reject }; };
function fixture(submit, legacy = {}) {
let stored = null;
let serial = 0;
const changes = [];
let box;
const storage = { read: () => structuredClone(stored), write: rows => { stored = structuredClone(rows); }, ...legacy };
const open = () => {
box = createAgentOptimisticOutbox({ submit, storage, newId: () => `request-${++serial}`, now: () => Date.parse("2026-10-14T12:00:00.000Z"), changed: () => changes.push(box?.snapshot()) });
return box;
};
return { open, changes, stored: () => stored, storage };
}
test("send publishes and persists one canonical row before POST, retaining it through receipt and echo", async () => {
const network = deferred(); let called = false;
const state = fixture(async (_content, requestId) => { called = true; assert.equal(requestId, "request-1"); return network.promise; });
const box = state.open();
const done = box.send("Hello");
const first = box.display([])[0];
assert.equal(called, false, "network starts after synchronous local publication");
assert.equal(first.id, "inbound:sybil:request-1");
assert.equal(first.metadata.status, "sending");
assert.equal(state.stored()[0].id, first.id);
assert.equal(state.changes.length, 1);
network.resolve({ status: "queued", messageId: first.id }); await done;
assert.equal(box.display([])[0].metadata.status, "queued");
assert.equal(box.display([])[0].createdAt, first.createdAt);
const echo = canonical(first.id);
box.reconcile({ messages: [echo] });
const displayed = box.display([echo]);
assert.equal(displayed.length, 1);
assert.equal(displayed[0].id, first.id);
assert.equal(displayed[0].createdAt, first.createdAt, "server receipt time cannot reorder the optimistic bubble");
assert.equal(displayed[0].metadata.status, "complete");
assert.deepEqual(state.stored(), []);
assert.equal(box.display([]).length, 1, "even a stale display snapshot cannot create an acknowledgment gap");
});
test("canonical echo before an HTTP failure wins and never paints a delivered bubble failed", async () => {
const network = deferred(); const state = fixture(() => network.promise); const box = state.open();
const done = box.send("Hello"); const row = box.display([])[0];
const echo = canonical(row.id);
box.reconcile({ messages: [echo] });
network.reject(new Error("response lost"));
await done;
assert.equal(box.display([echo])[0].metadata.status, "complete");
assert.equal(box.display([echo])[0].metadata.error, undefined);
assert.deepEqual(state.stored(), []);
});
test("queue echo before HTTP acknowledgment preserves one row when the queue snapshot disappears", async () => {
const network = deferred(); const state = fixture(() => network.promise); const box = state.open();
const done = box.send("Hello"); const row = box.display([])[0];
const queued = { ...canonical(row.id), metadata: { kind: "message", status: "queued" } };
box.reconcile({ messages: [], activity: { queuedMessages: [queued] } });
assert.equal(box.display([queued]).length, 1);
box.reconcile({ messages: [], activity: { queuedMessages: [] } });
assert.equal(box.display([])[0].id, row.id, "queue-to-history transfer must not hide the bubble");
network.reject(new Error("HTTP acknowledgment lost")); await done;
assert.equal(box.display([])[0].metadata.status, "queued");
});
test("failed retry and reload preserve the message ID, timestamp, and original receipt key", async () => {
const requests = []; let fail = true;
const state = fixture(async (content, requestId) => {
requests.push({ content, requestId });
if (fail) throw new Error("network down");
return { status: "placed", messageId: `inbound:sybil:${requestId}` };
});
let box = state.open();
await assert.rejects(box.send("Hello"), /network down/);
const failed = box.display([])[0];
assert.equal(failed.metadata.status, "failed");
assert.equal(failed.metadata.error, "network down");
box = state.open();
assert.equal(requests.length, 1, "restoring drafts never sends automatically");
assert.equal(box.display([])[0].metadata.status, "failed");
assert.equal(box.display([])[0].createdAt, failed.createdAt);
fail = false;
const retry = box.retry(failed.id);
assert.equal(box.display([])[0].metadata.status, "sending");
await retry;
assert.deepEqual(requests.map(row => row.requestId), ["request-1", "request-1"]);
assert.equal(box.display([]).length, 1);
assert.equal(box.display([])[0].metadata.status, "complete");
assert.equal(box.display([])[0].createdAt, failed.createdAt);
});
test("in-flight reload becomes unconfirmed and two identical sends remain independent rows", async () => {
const first = deferred(), second = deferred(); let calls = 0;
const state = fixture(() => ++calls === 1 ? first.promise : second.promise); const box = state.open();
const send1 = box.send("Same content"), send2 = box.send("Same content");
const rows = box.display([]);
assert.equal(rows.length, 2);
assert.notEqual(rows[0].id, rows[1].id);
assert.ok(rows[0].createdAt < rows[1].createdAt, "same-millisecond sends retain insertion order");
const reloaded = state.open();
assert.deepEqual(reloaded.display([]).map(row => row.metadata.status), ["unconfirmed", "unconfirmed"]);
assert.deepEqual(reloaded.display([]).map(row => row.id), rows.map(row => row.id));
first.resolve({ status: "queued" }); second.resolve({ status: "queued" });
await Promise.all([send1, send2]);
});
test("legacy failed cards join their saved receipt keys without automatic delivery", () => {
const state = fixture(() => assert.fail("migration must not submit"), {
legacyRows: () => [{ content: "old message", status: "failed", error: "old error" }],
legacyIds: () => [{ content: "old message", requestId: "old-request" }],
});
const box = state.open();
assert.equal(box.display([])[0].id, "inbound:sybil:old-request");
assert.equal(box.display([])[0].metadata.status, "failed");
assert.equal(box.display([])[0].metadata.error, "old error");
const migrated = state.stored()[0];
assert.equal(migrated.id, "inbound:sybil:old-request");
assert.equal(state.open().display([])[0].createdAt, migrated.createdAt, "migration is saved before any retry");
});
test("storage failures still leave a failed inline row and prevent untracked POST", async () => {
let calls = 0, visible = 0;
const box = createAgentOptimisticOutbox({ submit: async () => { calls++; return { status: "queued" }; },
storage: { read: () => null, write: () => { throw new Error("storage unavailable"); } },
newId: () => "request", changed: () => visible++,
});
const sending = box.send("Do not lose this text");
assert.equal(box.display([])[0].metadata.status, "failed");
assert.equal(box.display([])[0].content, "Do not lose this text");
assert.equal(visible, 1);
await assert.rejects(sending, /storage unavailable/);
assert.equal(calls, 0);
});