fix(discovery): lazy-load bonjour; add tests

This commit is contained in:
Peter Steinberger
2025-12-13 03:55:32 +00:00
parent 47b4d245aa
commit 4b608117a2
5 changed files with 273 additions and 5 deletions

87
src/infra/bonjour.test.ts Normal file
View File

@@ -0,0 +1,87 @@
import os from "node:os";
import { afterEach, describe, expect, it, vi } from "vitest";
const createService = vi.fn();
const shutdown = vi.fn();
vi.mock("@homebridge/ciao", () => {
return {
Protocol: { TCP: "tcp" },
getResponder: () => ({
createService,
shutdown,
}),
};
});
const { startGatewayBonjourAdvertiser } = await import("./bonjour.js");
describe("gateway bonjour advertiser", () => {
const prevEnv = { ...process.env };
afterEach(() => {
for (const key of Object.keys(process.env)) {
if (!(key in prevEnv)) delete process.env[key];
}
for (const [key, value] of Object.entries(prevEnv)) {
process.env[key] = value;
}
createService.mockReset();
shutdown.mockReset();
vi.restoreAllMocks();
});
it("does not block on advertise and publishes expected txt keys", async () => {
// Allow advertiser to run in unit tests.
delete process.env.VITEST;
process.env.NODE_ENV = "development";
vi.spyOn(os, "hostname").mockReturnValue("test-host");
const destroy = vi.fn().mockResolvedValue(undefined);
const advertise = vi.fn().mockImplementation(
async () =>
await new Promise<void>((resolve) => {
setTimeout(resolve, 250);
}),
);
createService.mockReturnValue({ advertise, destroy });
const started = await startGatewayBonjourAdvertiser({
gatewayPort: 18789,
sshPort: 2222,
bridgePort: 18790,
tailnetDns: "host.tailnet.ts.net",
});
expect(createService).toHaveBeenCalledTimes(2);
const [masterCall, bridgeCall] = createService.mock.calls as Array<
[Record<string, unknown>]
>;
expect(masterCall?.[0]?.type).toBe("clawdis-master");
expect(masterCall?.[0]?.port).toBe(2222);
expect((masterCall?.[0]?.txt as Record<string, string>)?.lanHost).toBe(
"test-host.local",
);
expect((masterCall?.[0]?.txt as Record<string, string>)?.sshPort).toBe(
"2222",
);
expect(bridgeCall?.[0]?.type).toBe("clawdis-bridge");
expect(bridgeCall?.[0]?.port).toBe(18790);
expect((bridgeCall?.[0]?.txt as Record<string, string>)?.bridgePort).toBe(
"18790",
);
expect((bridgeCall?.[0]?.txt as Record<string, string>)?.transport).toBe(
"bridge",
);
// We don't await `advertise()`, but it should still be called for each service.
expect(advertise).toHaveBeenCalledTimes(2);
await started.stop();
expect(destroy).toHaveBeenCalledTimes(2);
expect(shutdown).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,7 +1,5 @@
import os from "node:os";
import { type CiaoService, getResponder, Protocol } from "@homebridge/ciao";
export type GatewayBonjourAdvertiser = {
stop: () => Promise<void>;
};
@@ -26,6 +24,11 @@ function safeServiceName(name: string) {
return trimmed.length > 0 ? trimmed : "Clawdis";
}
type BonjourService = {
advertise: () => Promise<void>;
destroy: () => Promise<void>;
};
export async function startGatewayBonjourAdvertiser(
opts: GatewayBonjourAdvertiseOpts,
): Promise<GatewayBonjourAdvertiser> {
@@ -33,6 +36,7 @@ export async function startGatewayBonjourAdvertiser(
return { stop: async () => {} };
}
const { getResponder, Protocol } = await import("@homebridge/ciao");
const responder = getResponder();
const hostname = os.hostname().replace(/\.local$/i, "");
@@ -53,7 +57,7 @@ export async function startGatewayBonjourAdvertiser(
txtBase.tailnetDns = opts.tailnetDns.trim();
}
const services: CiaoService[] = [];
const services: BonjourService[] = [];
// Master beacon: used for discovery (auto-fill SSH/direct targets).
// We advertise a TCP service so clients can resolve the host; the port itself is informational.