Test fixtures: the Rust owner's Idunn runtime records, and one fake Idunn RUDP endpoint
tests/fixtures/idunn-runtime holds expected.cc, activation.cc, the activation seed, the presence identity credential and the machine-id it is bound to, copied from CultLib 217a9480 (Rust-written; test keys only). fake-idunn-rudp.ts is the accept server idunn-rudp-health.test.ts built inline, now keeping a session per remote and every delivered frame, so a boot test can receive several pulses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e8548614c8
commit
c0d30e8081
9 changed files with 83 additions and 36 deletions
64
tests/fixtures/fake-idunn-rudp.ts
vendored
Normal file
64
tests/fixtures/fake-idunn-rudp.ts
vendored
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
/**
|
||||||
|
* A stand-in for Idunn's health endpoint: it accepts each RUDP connect, acks
|
||||||
|
* the frame that follows and keeps every delivered frame. Heimdall opens a
|
||||||
|
* fresh socket and session per publication, so sessions are kept per remote.
|
||||||
|
*/
|
||||||
|
import dgram from "node:dgram";
|
||||||
|
import {
|
||||||
|
CultNetRudpSession,
|
||||||
|
decodeRudpPacket,
|
||||||
|
encodeRudpPacket,
|
||||||
|
type CultNetTransportFrame,
|
||||||
|
} from "cultnet-ts";
|
||||||
|
|
||||||
|
export interface FakeIdunnRudp {
|
||||||
|
port: number;
|
||||||
|
frames: CultNetTransportFrame[];
|
||||||
|
/** Resolves with the nth delivered frame (0-based) once it has arrived. */
|
||||||
|
frame(index: number): Promise<CultNetTransportFrame>;
|
||||||
|
close(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startFakeIdunnRudp(): Promise<FakeIdunnRudp> {
|
||||||
|
const server = dgram.createSocket("udp4");
|
||||||
|
const sessions = new Map<string, CultNetRudpSession>();
|
||||||
|
const frames: CultNetTransportFrame[] = [];
|
||||||
|
const waiting: Array<{ index: number; resolve: (frame: CultNetTransportFrame) => void }> = [];
|
||||||
|
|
||||||
|
server.on("message", (wire, remote) => {
|
||||||
|
const packet = decodeRudpPacket(wire);
|
||||||
|
const key = `${remote.address}:${remote.port}`;
|
||||||
|
if (packet.packetType === "connect") {
|
||||||
|
const session = new CultNetRudpSession({
|
||||||
|
connectionId: 0x1d0d0001,
|
||||||
|
initialSequence: 1,
|
||||||
|
resendDelayMs: 100,
|
||||||
|
});
|
||||||
|
sessions.set(key, session);
|
||||||
|
server.send(encodeRudpPacket(session.acceptConnect(packet, Date.now())), remote.port, remote.address);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const session = sessions.get(key);
|
||||||
|
if (!session) return;
|
||||||
|
const result = session.receive(packet, Date.now());
|
||||||
|
if (result.reply) server.send(encodeRudpPacket(result.reply), remote.port, remote.address);
|
||||||
|
for (const frame of result.delivered) {
|
||||||
|
frames.push(frame);
|
||||||
|
for (const waiter of waiting.filter((entry) => entry.index === frames.length - 1)) waiter.resolve(frame);
|
||||||
|
}
|
||||||
|
if (result.delivered.length > 0) {
|
||||||
|
server.send(encodeRudpPacket(session.createAck()), remote.port, remote.address);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await new Promise<void>((resolve) => server.bind(0, "127.0.0.1", resolve));
|
||||||
|
|
||||||
|
return {
|
||||||
|
port: server.address().port,
|
||||||
|
frames,
|
||||||
|
frame: (index) =>
|
||||||
|
frames[index]
|
||||||
|
? Promise.resolve(frames[index]!)
|
||||||
|
: new Promise((resolve) => waiting.push({ index, resolve })),
|
||||||
|
close: () => server.close(),
|
||||||
|
};
|
||||||
|
}
|
||||||
1
tests/fixtures/idunn-runtime/.gitattributes
vendored
Normal file
1
tests/fixtures/idunn-runtime/.gitattributes
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
* -text
|
||||||
11
tests/fixtures/idunn-runtime/README.md
vendored
Normal file
11
tests/fixtures/idunn-runtime/README.md
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Idunn runtime fixtures
|
||||||
|
|
||||||
|
Written by the Rust owner, not by Heimdall's tests. Copied byte for byte from CultLib
|
||||||
|
`217a9480`, `packages/cultnet-ts/test/fixtures/idunn-runtime/web` (`expected.cc`,
|
||||||
|
`activation.cc`, `activation-key.seed`, `provider-identity.credential`) and that directory's
|
||||||
|
`machine-id`. CultLib regenerates them with `packages/cultnet-rs/examples/idunn_runtime_fixture.rs`
|
||||||
|
(its own README has the Yggdrasil command); the provider identity is bound to `machine-id`, so a
|
||||||
|
test that decodes it supplies that machine id instead of `/etc/machine-id`.
|
||||||
|
|
||||||
|
Every key here is a test key. `provider-identity.credential` is the shape Idunn passes on the
|
||||||
|
presence-identity descriptor: key fields are 32-integer arrays, which only the Rust owner writes.
|
||||||
1
tests/fixtures/idunn-runtime/activation-key.seed
vendored
Normal file
1
tests/fixtures/idunn-runtime/activation-key.seed
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
¯ÂOd×f¯l*oµ‘ÀFªv‰n<E280B0>hLûÑpšS´p
|
||||||
BIN
tests/fixtures/idunn-runtime/activation.cc
vendored
Normal file
BIN
tests/fixtures/idunn-runtime/activation.cc
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/idunn-runtime/expected.cc
vendored
Normal file
BIN
tests/fixtures/idunn-runtime/expected.cc
vendored
Normal file
Binary file not shown.
1
tests/fixtures/idunn-runtime/machine-id
vendored
Normal file
1
tests/fixtures/idunn-runtime/machine-id
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1
|
||||||
BIN
tests/fixtures/idunn-runtime/provider-identity.credential
vendored
Normal file
BIN
tests/fixtures/idunn-runtime/provider-identity.credential
vendored
Normal file
Binary file not shown.
|
|
@ -1,18 +1,12 @@
|
||||||
import dgram from "node:dgram";
|
|
||||||
import { mkdtemp } from "node:fs/promises";
|
import { mkdtemp } from "node:fs/promises";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { decode } from "@msgpack/msgpack";
|
import { decode } from "@msgpack/msgpack";
|
||||||
import {
|
import { parseCultNetMessage } from "cultnet-ts";
|
||||||
CultNetRudpSession,
|
|
||||||
decodeRudpPacket,
|
|
||||||
encodeRudpPacket,
|
|
||||||
parseCultNetMessage,
|
|
||||||
type CultNetTransportFrame,
|
|
||||||
} from "cultnet-ts";
|
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { loadConfig } from "../src/config.js";
|
import { loadConfig } from "../src/config.js";
|
||||||
import { publishIdunnRudpHealth } from "../src/idunn-rudp-health.js";
|
import { publishIdunnRudpHealth } from "../src/idunn-rudp-health.js";
|
||||||
|
import { startFakeIdunnRudp } from "./fixtures/fake-idunn-rudp.js";
|
||||||
|
|
||||||
vi.mock("node:fs/promises", async (importOriginal) => {
|
vi.mock("node:fs/promises", async (importOriginal) => {
|
||||||
const original = await importOriginal<typeof import("node:fs/promises")>();
|
const original = await importOriginal<typeof import("node:fs/promises")>();
|
||||||
|
|
@ -27,37 +21,12 @@ vi.mock("node:fs/promises", async (importOriginal) => {
|
||||||
|
|
||||||
describe("Heimdall Idunn health publication", () => {
|
describe("Heimdall Idunn health publication", () => {
|
||||||
it("completes the canonical RUDP handshake before publishing its signed frame", async () => {
|
it("completes the canonical RUDP handshake before publishing its signed frame", async () => {
|
||||||
const server = dgram.createSocket("udp4");
|
const server = await startFakeIdunnRudp();
|
||||||
const session = new CultNetRudpSession({
|
|
||||||
connectionId: 0x1d0d0001,
|
|
||||||
initialSequence: 1,
|
|
||||||
resendDelayMs: 100,
|
|
||||||
});
|
|
||||||
let resolveFrame!: (frame: CultNetTransportFrame) => void;
|
|
||||||
const frameReceived = new Promise<CultNetTransportFrame>((resolve) => {
|
|
||||||
resolveFrame = resolve;
|
|
||||||
});
|
|
||||||
|
|
||||||
server.on("message", (wire, remote) => {
|
|
||||||
const packet = decodeRudpPacket(wire);
|
|
||||||
if (packet.packetType === "connect") {
|
|
||||||
server.send(encodeRudpPacket(session.acceptConnect(packet, Date.now())), remote.port, remote.address);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const result = session.receive(packet, Date.now());
|
|
||||||
if (result.reply) server.send(encodeRudpPacket(result.reply), remote.port, remote.address);
|
|
||||||
for (const frame of result.delivered) resolveFrame(frame);
|
|
||||||
if (result.delivered.length > 0) {
|
|
||||||
server.send(encodeRudpPacket(session.createAck()), remote.port, remote.address);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
await new Promise<void>((resolve) => server.bind(0, "127.0.0.1", resolve));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tmp = await mkdtemp(path.join(os.tmpdir(), "heimdall-idunn-rudp-"));
|
const tmp = await mkdtemp(path.join(os.tmpdir(), "heimdall-idunn-rudp-"));
|
||||||
const address = server.address();
|
|
||||||
const config = loadConfig({
|
const config = loadConfig({
|
||||||
GC_ACCESS_IDUNN_RUDP_HEALTH: `127.0.0.1:${address.port}`,
|
GC_ACCESS_IDUNN_RUDP_HEALTH: `127.0.0.1:${server.port}`,
|
||||||
GC_ACCESS_PROVIDER_HEALTH_IDENTITY_PATH: path.join(tmp, "provider-health-identity.cc"),
|
GC_ACCESS_PROVIDER_HEALTH_IDENTITY_PATH: path.join(tmp, "provider-health-identity.cc"),
|
||||||
});
|
});
|
||||||
const observedAt = "2026-08-22T00:45:00.000Z";
|
const observedAt = "2026-08-22T00:45:00.000Z";
|
||||||
|
|
@ -78,7 +47,7 @@ describe("Heimdall Idunn health publication", () => {
|
||||||
observedAt,
|
observedAt,
|
||||||
presence,
|
presence,
|
||||||
});
|
});
|
||||||
const frame = await frameReceived;
|
const frame = await server.frame(0);
|
||||||
const message = parseCultNetMessage(decode(frame.payload), "cultnet.schema.v0");
|
const message = parseCultNetMessage(decode(frame.payload), "cultnet.schema.v0");
|
||||||
|
|
||||||
expect(frame.channelId).toBe("schema");
|
expect(frame.channelId).toBe("schema");
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue