// The hosts file's verbs over files shaped like the workstation's on 2026-10-03 (novox/hq ADR 0199): // distribution lines, an operator's development names, the mesh's block and another tool's. import { test } from "node:test"; import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { HOSTS_FILE, escalated, parse, withAdded, withRemoved } from "../client.ts"; const FILE = "# Static table lookup for hostnames.\n" + "127.0.0.1\tlocaldev.example.com\n" + "127.0.0.1 a.example.com b.example.com\n" + "# BEGIN mesh hosts.own\n" + "127.0.0.1\tlocalhost\n" + "::1\tlocalhost\n" + "# END mesh hosts.own\n" + "# BEGIN other-tool\n" + "192.0.2.7\tproject.test\n" + "# END other-tool\n"; const blocks = (text: string) => parse(text).filter((l) => l.owner !== "operator").map((l) => l.text); test("every line says whose it is", () => { const lines = parse(FILE); assert.equal(lines.length, 10); assert.deepEqual(lines[1], { text: "127.0.0.1\tlocaldev.example.com", owner: "operator", address: "127.0.0.1", names: ["localdev.example.com"] }); assert.equal(lines[4].owner, "mesh hosts.own"); assert.equal(lines[8].owner, "other-tool"); assert.deepEqual(lines[8].names, ["project.test"]); }); test("add appends an operator line, and is a no-op when the names are there", () => { const after = withAdded(FILE, "192.0.2.9", ["lab.test", "www.lab.test"]); assert.ok(after.endsWith("192.0.2.9\tlab.test www.lab.test\n")); assert.deepEqual(blocks(after), blocks(FILE)); assert.equal(withAdded(FILE, "127.0.0.1", ["a.example.com"]), FILE); assert.ok(withAdded(FILE, "127.0.0.1", ["a.example.com", "c.example.com"]).endsWith("127.0.0.1\tc.example.com\n")); }); test("add refuses what is not an address or a host name", () => { assert.throws(() => withAdded(FILE, "not-an-ip", ["x.test"]), /not an IPv4 or IPv6 address/); assert.throws(() => withAdded(FILE, "192.0.2.9", ["bad name\n10.0.0.1 evil"]), /not a host name/); assert.throws(() => withAdded(FILE, "192.0.2.9", []), /at least one name/); }); test("remove takes one name or one address from the operator's lines, and blocks stay byte for byte", () => { const one = withRemoved(FILE, "a.example.com"); assert.equal(one.removed, 1); assert.ok(one.text.includes("127.0.0.1\tb.example.com\n")); assert.ok(!one.text.includes("a.example.com")); assert.deepEqual(blocks(one.text), blocks(FILE)); const all = withRemoved(FILE, "127.0.0.1"); assert.equal(all.removed, 2); assert.ok(all.text.includes("# BEGIN mesh hosts.own\n127.0.0.1\tlocalhost\n"), "the mesh's own localhost is not the operator's to remove"); }); test("remove refuses a name only a block writes, naming whose", () => { assert.throws(() => withRemoved(FILE, "project.test"), /written by other-tool/); assert.equal(withRemoved(FILE, "nowhere.test").removed, 0); }); test("the file is written as root through sudo where the account is not root", () => { assert.deepEqual(escalated("install", ["x"], 1000), ["sudo", ["-n", "install", "x"]]); assert.deepEqual(escalated("install", ["x"], 0), ["install", ["x"]]); }); test("the path the code writes is the path the manifest's resource declares", () => { const manifest = JSON.parse(readFileSync(new URL("../module.json", import.meta.url), "utf8")); assert.equal(manifest.resources.find((r: { id: string }) => r.id === "own").path, HOSTS_FILE); });