Files
lab/bastion/src/labd/tests/bastions-machines.test.ts
Michal d6e1f3c74d fix(labd): preserve machine identity across bastion restarts
The worker0-k8s0 bug: when labd restarts, the in-memory installed map
is lost. The next DHCP/PXE re-discovery for that MAC ran an upsert that
wrote status="discovered", silently downgrading the DB record from
"online" or "offline" and erasing the machine's known hostname/role
identity from the CLI view.

- server.ts: drop status="discovered" from the upsert update branch so
  re-discovery cannot downgrade an installed record.
- routes/bastions.ts (/api/machines): when the DB knows a real
  hostname+role for a MAC currently only in live.discovered, promote
  it back to live.installed so the CLI sees the right state. Also
  reordered the live-vs-DB fallback so DB online/offline maps to
  live.installed and the discovered branch is the else.
- tests: 3 new vitest cases covering promotion, fresh-discovery, and
  unknown-MAC fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:09:24 +01:00

145 lines
4.9 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import Fastify from "fastify";
import { registerBastionRoutes } from "../src/routes/bastions.js";
import { bastionRegistry } from "../src/services/bastion-registry.js";
import type { DbClient } from "../src/server.js";
import type { BastionState } from "@lab/shared";
function createMockDb(servers: unknown[] = []): DbClient {
return {
$queryRaw: vi.fn().mockResolvedValue([{ "?column?": 1 }]),
server: {
findMany: vi.fn().mockResolvedValue(servers),
findUnique: vi.fn().mockResolvedValue(null),
upsert: vi.fn().mockResolvedValue({}),
},
joinToken: {
findUnique: vi.fn().mockResolvedValue(null),
findMany: vi.fn().mockResolvedValue([]),
create: vi.fn().mockResolvedValue({ id: "t" }),
update: vi.fn().mockResolvedValue({}),
},
bastion: {
upsert: vi.fn().mockResolvedValue({}),
findMany: vi.fn().mockResolvedValue([]),
findUnique: vi.fn().mockResolvedValue(null),
update: vi.fn().mockResolvedValue({}),
},
};
}
function registerFakeBastion(bastionId: string, state: BastionState): void {
bastionRegistry.register({
bastionId,
hostname: "fake",
network: "192.168.8.0/24",
serverIp: "192.168.8.11",
// socket is referenced only on commands, not during aggregation
socket: { on: () => undefined, off: () => undefined, send: () => undefined, close: () => undefined } as never,
connectedAt: new Date(),
lastHeartbeat: new Date(),
state,
});
}
describe("GET /api/machines aggregation", () => {
beforeEach(() => {
for (const b of bastionRegistry.getAll()) bastionRegistry.unregister(b.bastionId);
});
it("promotes a live-discovered MAC to installed when the DB has a real hostname+role for it", async () => {
// Simulates the worker0-k8s0 bug: bastion restarted, lost its installed map,
// rediscovered the machine via DHCP/PXE. DB still has hostname=worker0-k8s0,
// role=infra, ip=192.168.8.23. Without the fix, the CLI sees a "discovered"
// row with no hostname/role/IP. With the fix, the row is promoted to
// "installed" with full identity preserved.
const mac = "78:55:36:08:28:fb";
registerFakeBastion("b1", {
discovered: {
[mac]: {
mac, product: "SER", board: "SER", serial: "x", manufacturer: "AZW",
cpu_model: "AMD Ryzen 7 255", cpu_cores: 16, memory_gb: 58, arch: "x86_64",
disks: [], nics: [], first_seen: "", last_seen: "",
},
},
install_queue: {},
installed: {},
debug: {},
});
const app = Fastify({ logger: false });
const db = createMockDb([
{ mac, hostname: "worker0-k8s0", role: "infra", ip: "192.168.8.23", status: "discovered", labels: {} },
]);
registerBastionRoutes(app, db);
const res = await app.inject({ method: "GET", url: "/api/machines" });
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.discovered[mac]).toBeUndefined();
expect(body.installed[mac]).toMatchObject({
hostname: "worker0-k8s0",
role: "infra",
ip: "192.168.8.23",
cpu_model: "AMD Ryzen 7 255",
cpu_cores: 16,
memory_gb: 58,
});
await app.close();
});
it("leaves a fresh-discovery MAC in discovered when DB only has a discovery-shaped record", async () => {
const mac = "aa:bb:cc:dd:ee:ff";
registerFakeBastion("b1", {
discovered: {
[mac]: {
mac, product: "SER", board: "SER", serial: "x", manufacturer: "AZW",
cpu_model: "AMD Ryzen 7", cpu_cores: 8, memory_gb: 32, arch: "x86_64",
disks: [], nics: [], first_seen: "", last_seen: "",
},
},
install_queue: {},
installed: {},
debug: {},
});
const app = Fastify({ logger: false });
// Matches what labd writes on first discovery: hostname=product, role="unknown"
const db = createMockDb([
{ mac, hostname: "SER", role: "unknown", ip: null, status: "discovered", labels: {} },
]);
registerBastionRoutes(app, db);
const res = await app.inject({ method: "GET", url: "/api/machines" });
const body = JSON.parse(res.body);
expect(body.discovered[mac]).toBeDefined();
expect(body.installed[mac]).toBeUndefined();
await app.close();
});
it("falls back to DB for MACs not in any live bucket", async () => {
const mac = "11:22:33:44:55:66";
// No bastions connected
const app = Fastify({ logger: false });
const db = createMockDb([
{ mac, hostname: "worker1-k8s0", role: "infra", ip: "192.168.8.13", status: "online", labels: {} },
]);
registerBastionRoutes(app, db);
const res = await app.inject({ method: "GET", url: "/api/machines" });
const body = JSON.parse(res.body);
expect(body.installed[mac]).toMatchObject({
hostname: "worker1-k8s0",
role: "infra",
ip: "192.168.8.13",
});
await app.close();
});
});