161 lines
7.0 KiB
TypeScript
161 lines
7.0 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { mkdtemp, writeFile, symlink, rm } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
import { CandidateMetadataBudget, readCandidateMetadata } from "../src/execution-world/metadata.js";
|
|
import { loadCheckedInstallationCandidate } from "../src/execution-world/candidate.js";
|
|
|
|
test(
|
|
"a real checked application can stage its replacement within the default metadata budget",
|
|
{
|
|
skip: !process.env.QX_CHECKED_CANDIDATE,
|
|
},
|
|
async () => {
|
|
const root = process.env.QX_CHECKED_CANDIDATE!;
|
|
const budget = new CandidateMetadataBudget();
|
|
const first = await loadCheckedInstallationCandidate(root, {}, { budget, owner: "reader" });
|
|
try {
|
|
const retained = budget.snapshot().bytes;
|
|
const second = await loadCheckedInstallationCandidate(root, {}, { budget, owner: "reader" });
|
|
try {
|
|
assert.equal(budget.snapshot().bytes, retained * 2);
|
|
assert.deepEqual(second.reference, first.reference, "Compaction preserves exact checked identity");
|
|
assert.equal(second.graphExecution.kernel, second.kernel, "Graph representation shares the validated program");
|
|
assert.equal(second.graphExecution.queries, second.queries);
|
|
// A deliberately tighter pool admits the serving installation but must
|
|
// reject replacement parsing without leaking a candidate or tree lease.
|
|
const tight = new CandidateMetadataBudget({
|
|
bytes: retained + 1,
|
|
ownerBytes: retained + 1,
|
|
candidates: 2,
|
|
});
|
|
const blocker = tight.acquire("reader");
|
|
blocker.charge(retained);
|
|
try {
|
|
await assert.rejects(
|
|
loadCheckedInstallationCandidate(root, {}, { budget: tight, owner: "reader" }),
|
|
/CANDIDATE_METADATA_LIMIT/,
|
|
);
|
|
assert.equal(tight.snapshot().bytes, retained);
|
|
assert.equal(tight.snapshot().candidates, 1);
|
|
} finally {
|
|
blocker.release();
|
|
}
|
|
const other = budget.acquire("other");
|
|
try {
|
|
assert.throws(() => other.charge(budget.limits.bytes), /CANDIDATE_METADATA_LIMIT/);
|
|
assert.equal(budget.snapshot().bytes, retained * 2, "Rejected admission must not leak charges");
|
|
} finally {
|
|
other.release();
|
|
}
|
|
} finally {
|
|
second.metadata.release();
|
|
}
|
|
assert.equal(budget.snapshot().bytes, retained, "The serving candidate survives failed or completed staging");
|
|
} finally {
|
|
first.metadata.release();
|
|
}
|
|
assert.equal(budget.snapshot().bytes, 0);
|
|
assert.equal(budget.snapshot().candidates, 0);
|
|
},
|
|
);
|
|
|
|
test("concurrent candidate reservations enforce aggregate and owner limits and release exactly once", () => {
|
|
const budget = new CandidateMetadataBudget({ bytes: 1000, ownerBytes: 600, candidates: 3 });
|
|
const first = budget.acquire("a"),
|
|
second = budget.acquire("a"),
|
|
third = budget.acquire("b");
|
|
assert.throws(() => budget.acquire("c"), /CANDIDATE_METADATA_LIMIT/);
|
|
first.charge(400);
|
|
second.charge(200);
|
|
third.charge(400);
|
|
assert.throws(() => second.charge(1), /CANDIDATE_METADATA_LIMIT/);
|
|
assert.throws(() => third.charge(1), /CANDIDATE_METADATA_LIMIT/);
|
|
assert.equal(budget.snapshot().bytes, 1000);
|
|
second.release();
|
|
second.release();
|
|
assert.equal(budget.snapshot().bytes, 800);
|
|
assert.throws(() => second.charge(1), /CANDIDATE_METADATA_CLOSED/);
|
|
first.release();
|
|
third.release();
|
|
assert.deepEqual(budget.snapshot(), { bytes: 0, candidates: 0, owners: 0, limits: budget.limits });
|
|
});
|
|
|
|
test("metadata files reserve parse/index space before allocation and refuse oversized or linked inputs", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "qx-metadata-budget-"));
|
|
const budget = new CandidateMetadataBudget({ bytes: 450, ownerBytes: 450, candidates: 2 });
|
|
const scope = budget.acquire("workspace");
|
|
try {
|
|
const file = join(root, "metadata.json");
|
|
await writeFile(file, '{"ok":true}');
|
|
const input = await readCandidateMetadata(file, scope, 100);
|
|
assert.equal(new TextDecoder().decode(input.bytes), '{"ok":true}');
|
|
const withInput = scope.bytes;
|
|
input.releaseInput();
|
|
input.releaseInput();
|
|
const first = scope.bytes;
|
|
assert.equal(withInput - first, (Buffer.byteLength('{"ok":true}') + 1) * 3);
|
|
assert.ok(first > 12 * 3, "Decoded tree and derived metadata are reserved before parsing");
|
|
await assert.rejects(readCandidateMetadata(file, scope, 100), /CANDIDATE_METADATA_LIMIT/);
|
|
assert.equal(scope.bytes, first, "Rejected tree allocation releases its transient input buffer");
|
|
await assert.rejects(readCandidateMetadata(file, scope, 5), /CANDIDATE_METADATA_FILE_LIMIT/);
|
|
await symlink(file, join(root, "linked"));
|
|
await assert.rejects(readCandidateMetadata(join(root, "linked"), scope, 100), { code: "ELOOP" });
|
|
await assert.rejects(readCandidateMetadata(root, scope, 100), /CANDIDATE_METADATA_FILE_LIMIT/);
|
|
assert.equal(scope.bytes, first);
|
|
input.releaseTree();
|
|
input.releaseTree();
|
|
assert.equal(scope.bytes, 0, "Discarded validation trees can release independently of the candidate");
|
|
} finally {
|
|
scope.release();
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
assert.equal(budget.snapshot().bytes, 0);
|
|
});
|
|
|
|
test("JSON structure accounting is independent of indentation and covers strings, values and dense nodes", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "qx-metadata-shape-"));
|
|
const budget = new CandidateMetadataBudget();
|
|
try {
|
|
for (const value of [{}, [null, true, false, 1, -3.5e8], { 'a"b': '雪\\"', nodes: [{}, {}, {}] }]) {
|
|
const measured = [];
|
|
for (const spaces of [0, 4]) {
|
|
const file = join(root, "metadata.json"),
|
|
text = JSON.stringify(value, null, spaces);
|
|
await writeFile(file, text);
|
|
const scope = budget.acquire("test");
|
|
try {
|
|
const input = await readCandidateMetadata(file, scope, 10000);
|
|
assert.deepEqual(JSON.parse(new TextDecoder().decode(input.bytes)), value);
|
|
input.releaseInput();
|
|
measured.push(scope.bytes);
|
|
} finally {
|
|
scope.release();
|
|
}
|
|
}
|
|
assert.equal(measured[0], measured[1], "Whitespace changes input storage, not the allocated JSON shape");
|
|
assert.ok(measured[0] >= 128);
|
|
}
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
assert.equal(budget.snapshot().bytes, 0);
|
|
});
|
|
|
|
test("transient leases release exactly once, including after their parent closes", () => {
|
|
const budget = new CandidateMetadataBudget({ bytes: 1000, ownerBytes: 600, candidates: 3 });
|
|
const scope = budget.acquire("a");
|
|
scope.charge(200);
|
|
const release = scope.reserve(300);
|
|
assert.throws(() => scope.reserve(101), /CANDIDATE_METADATA_LIMIT/);
|
|
release();
|
|
release();
|
|
assert.equal(scope.bytes, 200);
|
|
const afterClose = scope.reserve(100);
|
|
scope.release();
|
|
afterClose();
|
|
assert.equal(scope.bytes, 0);
|
|
assert.equal(budget.snapshot().bytes, 0);
|
|
});
|