cultcache-ts and cultcache-rs write different single-file store layouts, and the TypeScript client cannot read the Rust one at all -- its schema rejects it with "expected object, received array". Verified against the real enrolled identity on yggdrasil, not inferred. cultcache-rs [ [key, type, payload, stored_at, schema_id], ... ] cultcache-ts [ formatVersion, catalog, records ] with named envelopes Both readers I wrote for Idunn-written files assumed the named-object shape. They were wrong, and their tests passed only because the fixtures were built to match the assumption rather than the writer -- the same failure the census keeps recording, where the check agrees with the guess. src/idunn-store.ts now owns that decode, consumed by the write-lease reader and the presence-identity reader, and says plainly that it exists because the two CultLib forks disagree. If they are reconciled, it should be deleted in favour of the client. The write-lease fixture is rebuilt as a positional envelope, and the descriptor test now wraps a genuine machine-bound payload the way cultcache-rs writes it instead of enrolling through the TypeScript client and reading it back. 55 pass, 1 skipped, 12 files; tsc clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
153 lines
5.7 KiB
TypeScript
153 lines
5.7 KiB
TypeScript
import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
|
|
import { encode } from "@msgpack/msgpack";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import { resolveWriteLease } from "../src/process-write-lease.js";
|
|
|
|
/**
|
|
* Idunn writes CultCache envelopes whose payload is the canonical positional
|
|
* MessagePack encoding of the record. These helpers reproduce that shape, so
|
|
* the tests exercise the same bytes the daemon will actually be handed rather
|
|
* than a convenient object.
|
|
*/
|
|
function envelope(key: string, type: string, payload: Uint8Array): Uint8Array {
|
|
// cultcache-rs writes positional envelopes: [key, type, payload, stored_at,
|
|
// schema_id]. Building the fixture the way Idunn actually writes it is the
|
|
// point -- an object-shaped fixture agrees with a wrong reader.
|
|
return encode([[key, type, payload, "2026-09-06T00:00:00.000Z", type]]);
|
|
}
|
|
|
|
function expectedIncarnation(target: string, incarnationId: string): Uint8Array {
|
|
const fields = new Array(12).fill("x");
|
|
fields[0] = "idunn.expected_incarnation.v2";
|
|
fields[1] = target;
|
|
fields[3] = incarnationId;
|
|
return encode(fields);
|
|
}
|
|
|
|
function writeLease(target: string, incarnationId: string, epoch = 3): Uint8Array {
|
|
const fields: unknown[] = new Array(14).fill("x");
|
|
fields[0] = "idunn.process_write_lease.v1";
|
|
fields[1] = target;
|
|
fields[4] = incarnationId;
|
|
fields[10] = "runtime-instance-1";
|
|
fields[12] = epoch;
|
|
fields[13] = 1757116800000;
|
|
return encode(fields);
|
|
}
|
|
|
|
async function bundleWith(
|
|
expectedBytes: Uint8Array | undefined,
|
|
leaseBytes: Uint8Array | undefined
|
|
): Promise<{ bundle: string; lease: string }> {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "heimdall-lease-"));
|
|
const bundle = path.join(root, "runtime-instance-1");
|
|
await mkdir(bundle, { recursive: true });
|
|
|
|
if (expectedBytes) {
|
|
await writeFile(
|
|
path.join(bundle, "expected.cc"),
|
|
envelope("heimdall", "idunn.expected_incarnation", expectedBytes)
|
|
);
|
|
}
|
|
|
|
const lease = path.join(root, "process-write-lease.cc");
|
|
if (leaseBytes) {
|
|
await writeFile(lease, envelope("heimdall", "idunn.process_write_lease", leaseBytes));
|
|
}
|
|
|
|
return { bundle, lease };
|
|
}
|
|
|
|
describe("Idunn process write lease", () => {
|
|
it("permits writes when not launched by Idunn", async () => {
|
|
// A developer run and the current plain systemd unit have no second
|
|
// generation to collide with.
|
|
await expect(resolveWriteLease({})).resolves.toMatchObject({
|
|
mayWrite: true,
|
|
reason: "not-under-idunn",
|
|
});
|
|
});
|
|
|
|
it("permits writes when the lease names this incarnation", async () => {
|
|
const { bundle, lease } = await bundleWith(
|
|
expectedIncarnation("heimdall", "incarnation-a"),
|
|
writeLease("heimdall", "incarnation-a", 7)
|
|
);
|
|
|
|
await expect(
|
|
resolveWriteLease({ leasePath: lease, runtimeBundlePath: bundle })
|
|
).resolves.toMatchObject({ mayWrite: true, reason: "held", incarnationId: "incarnation-a", leaseEpoch: 7 });
|
|
});
|
|
|
|
it("refuses writes when the lease names the other generation", async () => {
|
|
// The case the lease exists for: a warming candidate while the incumbent
|
|
// still holds the store.
|
|
const { bundle, lease } = await bundleWith(
|
|
expectedIncarnation("heimdall", "incarnation-candidate"),
|
|
writeLease("heimdall", "incarnation-incumbent")
|
|
);
|
|
|
|
const decision = await resolveWriteLease({ leasePath: lease, runtimeBundlePath: bundle });
|
|
expect(decision.mayWrite).toBe(false);
|
|
expect(decision.reason).toContain("incarnation-incumbent");
|
|
});
|
|
|
|
it("refuses writes when no lease has been granted yet", async () => {
|
|
const { bundle, lease } = await bundleWith(
|
|
expectedIncarnation("heimdall", "incarnation-a"),
|
|
undefined
|
|
);
|
|
|
|
const decision = await resolveWriteLease({ leasePath: lease, runtimeBundlePath: bundle });
|
|
expect(decision.mayWrite).toBe(false);
|
|
expect(decision.reason).toContain("no readable write lease");
|
|
});
|
|
|
|
it("refuses writes when the lease belongs to another target", async () => {
|
|
const { bundle, lease } = await bundleWith(
|
|
expectedIncarnation("heimdall", "incarnation-a"),
|
|
writeLease("ghostlight", "incarnation-a")
|
|
);
|
|
|
|
const decision = await resolveWriteLease({ leasePath: lease, runtimeBundlePath: bundle });
|
|
expect(decision.mayWrite).toBe(false);
|
|
expect(decision.reason).toContain("ghostlight");
|
|
});
|
|
|
|
it("refuses writes on a wrong-length or wrong-schema lease", async () => {
|
|
const short = encode(new Array(9).fill("x"));
|
|
const wrongSchema = encode(
|
|
Object.assign(new Array(14).fill("x"), { 0: "idunn.process_write_lease.v0", 12: 1 })
|
|
);
|
|
|
|
for (const bytes of [short, wrongSchema]) {
|
|
const { bundle, lease } = await bundleWith(
|
|
expectedIncarnation("heimdall", "incarnation-a"),
|
|
bytes
|
|
);
|
|
const decision = await resolveWriteLease({ leasePath: lease, runtimeBundlePath: bundle });
|
|
expect(decision.mayWrite).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("refuses writes when a lease path is set without a runtime bundle", async () => {
|
|
// Launch inputs are incoherent; declining is the safe reading.
|
|
const decision = await resolveWriteLease({ leasePath: "/nonexistent/lease.cc" });
|
|
expect(decision.mayWrite).toBe(false);
|
|
expect(decision.reason).toContain("GAMECULT_IDUNN_RUNTIME_BUNDLE");
|
|
});
|
|
|
|
it("refuses writes when the runtime bundle is unreadable", async () => {
|
|
const { lease } = await bundleWith(undefined, writeLease("heimdall", "incarnation-a"));
|
|
const decision = await resolveWriteLease({
|
|
leasePath: lease,
|
|
runtimeBundlePath: "/nonexistent/bundle",
|
|
});
|
|
expect(decision.mayWrite).toBe(false);
|
|
expect(decision.reason).toContain("unreadable runtime bundle");
|
|
});
|
|
});
|