Three changes, found by one failing test. The forge failed three runs in a row as "status hangs", and it was diagnosed twice as contention — real defects, fixed, and not the cause. The heartbeats told the truth in the end: every exec on anchor crawled from 15s to 105s, because eleven containers plus a database pull were running in a 1GiB machine. Starvation presents as whatever you were doing when the page-outs start, which is why it wore two other bugs' clothes first. So machine size is now the scenario's to declare — memory and cpus per machine, default unchanged. The anchor that carries the whole substrate is bigger than the laptop that joins it, and the comment on the scenario says why in terms of what lands there. `egress: true` gives a machine one extra interface on a lab-supplied NAT network, addressed by DHCP because the one address a scenario has no business choosing is on the host's side of the fence. Declared per machine and off by default: a closed scenario stays the rule (novox/hq ADR 0016), and the exception exists because a first node fetches its images before any mesh can serve them — which is now the tested path (04-ISSUES/029), and a lab that can never reach upstream cannot prove the bootstrap it exists to prove. The uplink route is metric-4096, so it never shadows a route the scenario declared. A detached machine declaring egress is refused, not ignored. And settled() treats a poll that threw as a poll that missed. An exec timeout at minute four of a wait is "could not ask", not a verdict on the machine.
97 lines
4.2 KiB
TypeScript
97 lines
4.2 KiB
TypeScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { parseScenario } from "../src/declaration/parse.ts";
|
|
import { assertSupported, UnsupportedError } from "../src/lifecycle/supported.ts";
|
|
|
|
/**
|
|
* A declaration the runtime silently ignores is the fault this lab exists to catch —
|
|
* novox/hq 04-ISSUES/003, where a firewall key is declared in five manifests and read by no
|
|
* code. These tests exist so the lab never commits it, and they move as the runtime catches
|
|
* up with the model.
|
|
*/
|
|
|
|
const withGateway = `scenario: x
|
|
segments:
|
|
pub: { kind: public, cidr: [192.0.2.0/24] }
|
|
home: { kind: private, cidr: [192.168.1.0/24], gateway: { to: pub, address: [192.0.2.5], nat: [v4] } }
|
|
machines: { a: { at: { segment: home, address: [192.168.1.9] } } }`;
|
|
|
|
test("a plain scenario is raisable", () => {
|
|
const scenario = parseScenario(`scenario: x
|
|
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
|
|
machines: { a: { at: { segment: net, address: [192.0.2.1] } } }`);
|
|
assert.doesNotThrow(() => assertSupported(scenario));
|
|
});
|
|
|
|
test("gateways are implemented — a router is materialised for them", () => {
|
|
assert.doesNotThrow(() => assertSupported(parseScenario(withGateway)));
|
|
});
|
|
|
|
test("published ports and policy are implemented", () => {
|
|
const scenario = parseScenario(`scenario: x
|
|
segments:
|
|
pub: { kind: public, cidr: [192.0.2.0/24] }
|
|
home: { kind: private, cidr: [192.168.1.0/24], gateway: { to: pub, address: [192.0.2.5], nat: [v4] } }
|
|
iot: { kind: private, cidr: [192.168.30.0/24], gateway: { to: pub, address: [192.0.2.5], nat: [v4] } }
|
|
policy: [{ from: iot, to: home, allow: false }]
|
|
machines:
|
|
a:
|
|
at: { segment: home, address: [192.168.1.9] }
|
|
published: [{ port: 443, on: home }]`);
|
|
assert.doesNotThrow(() => assertSupported(scenario));
|
|
});
|
|
|
|
test("inbound: deny is implemented — a host firewall is applied and read back", () => {
|
|
const scenario = parseScenario(`scenario: x
|
|
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
|
|
machines: { a: { at: { segment: net, address: [192.0.2.1] }, inbound: deny } }`);
|
|
assert.doesNotThrow(() => assertSupported(scenario));
|
|
});
|
|
|
|
test("the host is placeable — it used to be refused, and tier 0 now exists", () => {
|
|
// These two tests failed the moment placement worked, which is what they were for. They
|
|
// defended "there is nothing to place yet" while that was true; the decision changed, so
|
|
// they change with it rather than being deleted (novox/hq ADR 0017).
|
|
const scenario = parseScenario(`scenario: x
|
|
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
|
|
machines: { a: { at: { segment: net, address: [192.0.2.1] } } }
|
|
place: { all: [host] }`);
|
|
assert.doesNotThrow(() => assertSupported(scenario));
|
|
});
|
|
|
|
test("a tier above 0 is still refused, and named", () => {
|
|
// The refusal narrowed rather than disappearing. A scenario placing a host AND a substrate
|
|
// must be told which half is missing — not that `place:` is unsupported, when half of it
|
|
// now works.
|
|
const scenario = parseScenario(`scenario: x
|
|
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
|
|
machines: { a: { at: { segment: net, address: [192.0.2.1] } } }
|
|
place: { all: [host, substrate] }`);
|
|
try {
|
|
assertSupported(scenario);
|
|
assert.fail("should have refused");
|
|
} catch (err) {
|
|
assert.ok(err instanceof UnsupportedError);
|
|
assert.equal(err.missing.length, 1, `expected only the substrate: ${err.missing.join(", ")}`);
|
|
assert.match(err.missing[0] ?? "", /substrate/);
|
|
assert.match(err instanceof Error ? err.message : "", /silently lacks them/);
|
|
}
|
|
});
|
|
|
|
test("inbound: allow is not a gap — only deny needs enforcing", () => {
|
|
const scenario = parseScenario(`scenario: x
|
|
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
|
|
machines: { a: { at: { segment: net, address: [192.0.2.1] }, inbound: allow } }`);
|
|
assert.doesNotThrow(() => assertSupported(scenario));
|
|
});
|
|
|
|
test("egress is parsed, and off unless asked for", () => {
|
|
const scenario = parseScenario(`scenario: x
|
|
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
|
|
machines:
|
|
a: { at: { segment: net, address: [192.0.2.1] }, egress: true }
|
|
b: { at: { segment: net, address: [192.0.2.2] } }`);
|
|
assert.equal(scenario.machines["a"]?.egress, true);
|
|
assert.equal(scenario.machines["b"]?.egress, undefined);
|
|
});
|