140 lines
4.9 KiB
TypeScript
140 lines
4.9 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import { NetworkFaults } from "./network";
|
|
import { AssertableState } from "../deterministic-tests/src/utils/assertable-state";
|
|
import { TESTS } from "../deterministic-tests/src/test-registry";
|
|
import type { ExpectedDocument } from "../deterministic-tests/src/test-definition";
|
|
|
|
const id = "00000000-0000-4000-8000-000000000001";
|
|
function state(
|
|
files: Record<string, string>,
|
|
identities = Object.fromEntries(
|
|
Object.keys(files).map((path, i) => [
|
|
i ? id.replace(/1$/, String(i + 1)) : id,
|
|
path
|
|
])
|
|
)
|
|
) {
|
|
const map = new Map(Object.entries(files));
|
|
return new AssertableState({
|
|
files: map,
|
|
clientFiles: [map, new Map(map)],
|
|
canonical: identities,
|
|
manifests: [identities, identities],
|
|
bytes: new Map(
|
|
[...map].map(([path, text]) => [
|
|
path,
|
|
new TextEncoder().encode(text)
|
|
])
|
|
)
|
|
});
|
|
}
|
|
|
|
test("scripted verifiers reject the concrete shared-loss and duplication counterexamples", () => {
|
|
const cases = [
|
|
[
|
|
"multi-file-operations",
|
|
{ "B.md": "updated", "C.md": "CORRUPTED", "unexpected.md": "EXTRA" }
|
|
],
|
|
[
|
|
"concurrent-edit-exact-same-position",
|
|
{ "doc.md": "slow slow fast brown fox CORRUPTED" }
|
|
]
|
|
] as const;
|
|
for (const [name, files] of cases) {
|
|
const step = TESTS[name]!.steps.filter(
|
|
(s) => s.type === "assert-consistent"
|
|
).at(-1)!;
|
|
assert.throws(() => step.verify!(state(files)));
|
|
}
|
|
const partial = state({ "a.md": "A", "b.md": "B" });
|
|
partial.assertContent("a.md", "A");
|
|
assert.throws(() => partial.assertComplete(), /complete file set/);
|
|
});
|
|
|
|
test("complete document oracles kill lost-byte, duplicate-file and identity-reuse mutations", () => {
|
|
const expected: ExpectedDocument[] = [
|
|
{ key: "a", path: "a.md", content: "exact bytes 🦊\r\n" }
|
|
];
|
|
const identities = new Map<string, string>();
|
|
state({ "a.md": "exact bytes 🦊\r\n" }).assertDocuments(
|
|
expected,
|
|
identities
|
|
);
|
|
const corruptions: Record<string, string>[] = [
|
|
{ "a.md": "exact bytes 🦊\n" },
|
|
{ "a.md": "exact bytes 🦊\r\nexact bytes 🦊\r\n" },
|
|
{ "a.md": "exact bytes 🦊\r\n", "extra.md": "EXTRA" }
|
|
];
|
|
for (const corrupted of corruptions)
|
|
assert.throws(() =>
|
|
state(corrupted).assertDocuments(expected, identities)
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
state(
|
|
{ "a.md": "exact bytes 🦊\r\n" },
|
|
{ [id.replace(/1$/, "2")]: "a.md" }
|
|
).assertDocuments(expected, identities),
|
|
/identity changed/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
state({ "a.md": "exact bytes 🦊\r\n" }).assertDocuments(
|
|
[{ ...expected[0], key: "recreated" }],
|
|
identities
|
|
),
|
|
/reused/
|
|
);
|
|
});
|
|
|
|
test("request checkpoints hold the exact boundary and reject unexercised response predicates", async () => {
|
|
const network = new NetworkFaults();
|
|
let sent = 0,
|
|
completed = false;
|
|
const input = "http://localhost/vaults/test/documents/document";
|
|
const init = {
|
|
method: "PUT",
|
|
body: JSON.stringify({ requestId: "one", parentVersionId: 1 })
|
|
};
|
|
const fetcher = network.wrap(async () => {
|
|
sent++;
|
|
return Response.json({ type: "StaleBase" });
|
|
});
|
|
network.hold({ kind: "content", point: "before" });
|
|
assert.throws(() => network.resumeRequest(), /before it is reached/);
|
|
const request = fetcher(input, init).then(() => {
|
|
completed = true;
|
|
});
|
|
await network.waitForHeldRequest();
|
|
assert.equal(sent, 0);
|
|
assert.equal(completed, false);
|
|
assert.throws(() => network.assertConsumed(), /never released/);
|
|
network.resumeRequest();
|
|
await request;
|
|
network.assertConsumed();
|
|
assert.equal(network.requests[0].responseType, "StaleBase");
|
|
network.hold({ kind: "content", point: "after", responseType: "Accepted" });
|
|
assert.equal(
|
|
network.requestsSinceCheckpoint().length,
|
|
0,
|
|
"An earlier response must not satisfy a new race assertion"
|
|
);
|
|
await fetcher(input, init);
|
|
assert.throws(() => network.assertConsumed(), /never reached/);
|
|
network.releaseRequests();
|
|
});
|
|
|
|
test("scripted races have no sleep steps and the stale-content regression asserts its response", () => {
|
|
for (const [name, scenario] of Object.entries(TESTS))
|
|
assert(
|
|
!scenario!.steps.some((s) => s.type === "sleep"),
|
|
`${name} still relies on timing`
|
|
);
|
|
assert(
|
|
TESTS["merging-update-response-survives-user-rename"]!.steps.some(
|
|
(s) =>
|
|
s.type === "assert-response" && s.responseType === "StaleBase"
|
|
)
|
|
);
|
|
});
|