316 lines
11 KiB
TypeScript
316 lines
11 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import { MemoryDisk, MemoryPersistence } from "./storage";
|
|
import { NetworkFaults, classifyRequest } from "./network";
|
|
import {
|
|
ContentLedger,
|
|
assertManifest,
|
|
assertSameFiles,
|
|
pathDecision
|
|
} from "./oracles";
|
|
import { Random } from "./random";
|
|
import { TestRunner } from "../deterministic-tests/src/test-runner";
|
|
import type { ServerControl } from "../deterministic-tests/src/server-control";
|
|
import { Logger } from "sync-client";
|
|
import { ManagedWebSocketFactory } from "../deterministic-tests/src/managed-websocket";
|
|
|
|
const bytes = (text: string) => new TextEncoder().encode(text);
|
|
|
|
test("consistency oracle rejects self comparison, divergent paths and invalid UTF-8 bytes", () => {
|
|
const first = new Map([["a.bin", new Uint8Array([0xff])]]);
|
|
assert.throws(() => assertSameFiles([first, first]), /itself/);
|
|
assert.throws(() =>
|
|
assertSameFiles([first, new Map([["a.bin", new Uint8Array([0xfe])]])])
|
|
);
|
|
assert.throws(() => assertSameFiles([first, new Map()]));
|
|
assertSameFiles([first, structuredClone(first)]);
|
|
});
|
|
|
|
test("ledger catches shared loss, duplicated bytes, and unrelated loss in delete workloads", () => {
|
|
const ledger = new ContentLedger();
|
|
ledger.add("first-marker");
|
|
ledger.add("second-marker");
|
|
ledger.removedBy("first-marker", "explicit delete of doc A");
|
|
assert.throws(
|
|
() => ledger.assertPreserved(new Map()),
|
|
/second-marker.*lost/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
ledger.assertPreserved(
|
|
new Map([["a", bytes("second-marker second-marker")]])
|
|
),
|
|
/duplicated/
|
|
);
|
|
ledger.assertPreserved(new Map([["a", bytes("second-marker")]]));
|
|
});
|
|
|
|
test("memory adapter enforces v4 operations and snapshot ownership", async () => {
|
|
const disk = new MemoryDisk();
|
|
await disk.userWrite("folder/a", bytes("A"));
|
|
const snap = (await disk.readSnapshot("folder/a"))!;
|
|
snap.content[0] = 0;
|
|
assert.equal(
|
|
Buffer.from((await disk.readSnapshot("folder/a"))!.content).toString(),
|
|
"A"
|
|
);
|
|
await assert.rejects(
|
|
disk.write("folder/a", { content: bytes("B") }),
|
|
/exists/
|
|
);
|
|
await disk.userWrite("folder/b", bytes("B"));
|
|
await assert.rejects(disk.rename("folder/a", "folder/b"), /exists/);
|
|
await assert.rejects(disk.delete("folder/a"), /regular file/);
|
|
await assert.rejects(disk.delete("folder"), /regular file/);
|
|
await assert.rejects(disk.readSnapshot("../outside"), /Unsafe/);
|
|
await assert.rejects(disk.stat("folder/a/child"), /ancestor/);
|
|
await assert.rejects(disk.readSnapshot("folder"), /directory/);
|
|
});
|
|
|
|
for (const phase of ["before", "visible", "durable"] as const) {
|
|
for (const powerLoss of [false, true]) {
|
|
test(`${powerLoss ? "power loss" : "process death"} at ${phase} rename boundary`, async () => {
|
|
const disk = new MemoryDisk();
|
|
await disk.userWrite("a", bytes("A"));
|
|
const session = disk.session();
|
|
let fired = false;
|
|
disk.boundary = (label) => {
|
|
if (label === `${phase}:rename:a->b`) {
|
|
fired = true;
|
|
disk.crash(powerLoss);
|
|
throw new Error("crash");
|
|
}
|
|
};
|
|
await assert.rejects(session.rename("a", "b"), /crash/);
|
|
assert(fired);
|
|
disk.boundary = () => {};
|
|
const moved =
|
|
phase === "durable" || (phase === "visible" && !powerLoss);
|
|
assert.deepEqual([...disk.userFiles().keys()], [moved ? "b" : "a"]);
|
|
assert.throws(
|
|
() => session.write("stale", { content: bytes("bad") }),
|
|
/crashed/
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
test("persistence has deep ownership and both old/new uncertain save outcomes", async () => {
|
|
for (const phase of ["before", "durable"]) {
|
|
const store = new MemoryPersistence({ settings: { vaultName: "old" } });
|
|
const loaded = await store.load();
|
|
loaded.settings!.vaultName = "mutated";
|
|
assert.equal(store.snapshot().settings!.vaultName, "old");
|
|
store.boundary = (label) => {
|
|
if (label === `${phase}:save`) throw new Error("save failed");
|
|
};
|
|
await assert.rejects(
|
|
store.save({ settings: { vaultName: "new" } }),
|
|
/save failed/
|
|
);
|
|
assert.equal(
|
|
store.snapshot().settings!.vaultName,
|
|
phase === "before" ? "old" : "new"
|
|
);
|
|
}
|
|
});
|
|
|
|
test("CAS hooks match v4 create, update and manifest and preserve retry payload", async () => {
|
|
for (const kind of ["create", "content", "manifest"] as const) {
|
|
const net = new NetworkFaults();
|
|
const url = `http://test/vaults/test/${kind === "manifest" ? "file-manifest" : "documents/uuid"}`;
|
|
const body = {
|
|
requestId: "same",
|
|
parentVersionId: kind === "create" ? null : 3
|
|
};
|
|
const init = { method: "PUT", body: JSON.stringify(body) };
|
|
assert.equal(classifyRequest(url, init)?.kind, kind);
|
|
let committed = 0;
|
|
const wrapped = net.wrap(async () => {
|
|
committed++;
|
|
return new Response('{"type":"Accepted"}');
|
|
});
|
|
net.arm(kind);
|
|
await assert.rejects(wrapped(url, init), /Injected after-commit/);
|
|
await net.wait();
|
|
assert.throws(() => net.assertConsumed(), /never retried/);
|
|
assert.equal(committed, 1);
|
|
await wrapped(url, init);
|
|
net.assertConsumed();
|
|
await assert.rejects(
|
|
wrapped(url, {
|
|
...init,
|
|
body: JSON.stringify({ ...body, changed: true })
|
|
}),
|
|
/Retry changed/
|
|
);
|
|
}
|
|
assert.equal(
|
|
classifyRequest("http://test/documents", { method: "POST" }),
|
|
undefined
|
|
);
|
|
});
|
|
|
|
test("unconsumed and before-send faults cannot silently pass", async () => {
|
|
const net = new NetworkFaults();
|
|
net.arm("manifest", "before");
|
|
assert.throws(() => net.assertConsumed(), /never fired/);
|
|
let called = false;
|
|
await assert.rejects(
|
|
net.wrap(async () => {
|
|
called = true;
|
|
return new Response();
|
|
})("http://test/file-manifest", {
|
|
method: "PUT",
|
|
body: '{"requestId":"r"}'
|
|
})
|
|
);
|
|
assert(!called);
|
|
assert.throws(() => net.assertConsumed(), /never retried/);
|
|
await net.wrap(async () => new Response())("http://test/file-manifest", {
|
|
method: "PUT",
|
|
body: '{"requestId":"r"}'
|
|
});
|
|
net.assertConsumed();
|
|
});
|
|
|
|
test("HTTP observation checkpoint blocks catchup, resumes, and respects abort", async () => {
|
|
const network = new NetworkFaults();
|
|
const wrapped = network.wrap(async () => new Response("{}"));
|
|
network.pauseObservation();
|
|
let delivered = false;
|
|
const request = wrapped("http://test/events-since?after=0").then(() => {
|
|
delivered = true;
|
|
});
|
|
await network.waitForObservation();
|
|
assert(!delivered, "Catchup bypassed the observation checkpoint");
|
|
network.resumeObservation();
|
|
await request;
|
|
network.assertConsumed();
|
|
network.pauseObservation();
|
|
const controller = new AbortController();
|
|
const interrupted = wrapped("http://test/vault-snapshot", {
|
|
signal: controller.signal
|
|
});
|
|
const rejected = assert.rejects(interrupted, /abort/i);
|
|
await network.waitForObservation();
|
|
controller.abort();
|
|
await rejected;
|
|
network.resumeObservation();
|
|
network.assertConsumed();
|
|
});
|
|
|
|
test("independent path decision truth table covers create, delete and rename", () => {
|
|
for (const base of [undefined, "a", "b"])
|
|
for (const local of [undefined, "a", "b"])
|
|
for (const remote of [undefined, "a", "b"]) {
|
|
const result = pathDecision(base, local, remote);
|
|
assert.equal(result, remote !== base ? remote : local);
|
|
}
|
|
assertManifest({ a: "folder/é.md", b: "other.bin" });
|
|
for (const entries of [
|
|
{ a: "A", b: "a" },
|
|
{ a: "é", b: "e\u0301" },
|
|
{ a: "x", b: "x/y" }
|
|
])
|
|
assert.throws(() => assertManifest(entries));
|
|
});
|
|
|
|
test("seeded actions are repeatable, including seed zero", () => {
|
|
for (const seed of [0, 1, 12345, 0xffffffff]) {
|
|
const first = new Random(seed);
|
|
const second = new Random(seed);
|
|
assert.deepEqual(
|
|
Array.from({ length: 100 }, () => first.int(100)),
|
|
Array.from({ length: 100 }, () => second.int(100))
|
|
);
|
|
}
|
|
});
|
|
|
|
test("runner cleanup errors fail an otherwise successful test", async () => {
|
|
const server = {
|
|
isRunning: () => true,
|
|
resume: () => {}
|
|
} as unknown as ServerControl;
|
|
const runner = new TestRunner(
|
|
server,
|
|
new Logger(),
|
|
"unused",
|
|
"http://unused"
|
|
);
|
|
let cleaned = 0;
|
|
Object.assign(runner, {
|
|
initializeAgents: async () => {},
|
|
agents: [
|
|
{
|
|
clientId: 0,
|
|
database: () => ({ evidence: "before cleanup" }),
|
|
disk: { image: () => ({ bytes: [0, 255] }) },
|
|
network: { requests: [{ body: { requestId: "original" } }] },
|
|
cleanup: async () => {
|
|
cleaned++;
|
|
throw new Error("late background error");
|
|
}
|
|
}
|
|
]
|
|
});
|
|
const result = await runner.runTest("cleanup-probe", {
|
|
clients: 2,
|
|
steps: []
|
|
});
|
|
assert.equal(result.success, false);
|
|
assert.match(result.error!, /late background error/);
|
|
assert.equal(cleaned, 1, "Failed cleanup was run twice");
|
|
assert.deepEqual(result.diagnostics, [
|
|
{
|
|
client: 0,
|
|
database: { evidence: "before cleanup" },
|
|
disk: { bytes: [0, 255] },
|
|
requests: [{ body: { requestId: "original" } }]
|
|
}
|
|
]);
|
|
});
|
|
|
|
test("disposed harness rejects late websocket reconnects", async () => {
|
|
const factory = new ManagedWebSocketFactory();
|
|
const Socket = factory.constructorFn;
|
|
await factory.finish();
|
|
assert.throws(() => new Socket("ws://unused"), /after harness cleanup/);
|
|
});
|
|
|
|
test("runner preserves primary failure and cleanup diagnostics", async () => {
|
|
const server = {
|
|
isRunning: () => true,
|
|
resume: () => {}
|
|
} as unknown as ServerControl;
|
|
const runner = new TestRunner(
|
|
server,
|
|
new Logger(),
|
|
"unused",
|
|
"http://unused"
|
|
);
|
|
Object.assign(runner, {
|
|
initializeAgents: async () => {},
|
|
executeStep: async () => {
|
|
throw new Error("primary assertion failed");
|
|
},
|
|
agents: [
|
|
{
|
|
clientId: 0,
|
|
database: () => ({}),
|
|
disk: { image: () => ({}) },
|
|
network: { requests: [] },
|
|
cleanup: async () => {
|
|
throw new Error("cleanup failed too");
|
|
}
|
|
}
|
|
]
|
|
});
|
|
const result = await runner.runTest("cleanup-probe", {
|
|
clients: 2,
|
|
steps: [{ type: "barrier" }]
|
|
});
|
|
assert.equal(result.success, false);
|
|
assert.match(result.error!, /primary assertion failed/);
|
|
assert.match(result.error!, /cleanup failed too/);
|
|
});
|