diff --git a/apps/hub/src/provisioners/process.test.ts b/apps/hub/src/provisioners/process.test.ts new file mode 100644 index 000000000..877df70f4 --- /dev/null +++ b/apps/hub/src/provisioners/process.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, test } from "bun:test"; +import { createProcessSidecarProvisioner, readProcessProvisionerConfig } from "./process"; +import { createFakeSidecarProcessRunner } from "./fake-process-runner"; + +function fingerprint(role: "deployment" | "probe", port: number, dataDir: string, entry: string) { + return createProcessSidecarProvisioner({ + role, + runner: createFakeSidecarProcessRunner(), + config: readProcessProvisionerConfig({ + env: { + PROCESS_PROVISIONER_SIDECAR_ENTRY: entry, + SIDECAR_CREDENTIAL_ENCRYPTION_KEY: "k".repeat(32), + }, + dataDir, + hubWebSocketUrl: `ws://127.0.0.1:${port}/api/sidecars/ws`, + }), + }).bindingFingerprint; +} + +describe("process provisioner binding fingerprint", () => { + test("is stable across hub restarts that change the port or install path", () => { + expect(fingerprint("deployment", 3000, "/srv/a/data", "/srv/a/sidecar.ts")).toBe( + fingerprint("deployment", 4123, "/srv/b/data", "/srv/b/sidecar.ts"), + ); + }); + + test("still distinguishes roles", () => { + expect(fingerprint("deployment", 3000, "/srv/a", "/srv/a/s.ts")).not.toBe( + fingerprint("probe", 3000, "/srv/a", "/srv/a/s.ts"), + ); + }); +}); diff --git a/apps/hub/src/provisioners/process.ts b/apps/hub/src/provisioners/process.ts index 07113025c..5b7328c8a 100644 --- a/apps/hub/src/provisioners/process.ts +++ b/apps/hub/src/provisioners/process.ts @@ -51,9 +51,7 @@ export type ProcessProvisionerConfig = { readonly allocationsDir: string; /** Where the shared allocation state store keeps generation fences. */ readonly stateFilePath: string; - /** The ws(s):// URL provisioned sidecars dial back on; part of the - * binding fingerprint, since a hub moved to a new address is a - * different backend binding even with the same entry point. */ + /** The ws(s):// URL provisioned sidecars dial back on. */ readonly hubWebSocketUrl: string; /** The operator key every spawned sidecar seals its at-rest credentials under. */ readonly sidecarCredentialEncryptionKey: string; @@ -448,9 +446,10 @@ export type CreateProcessSidecarProvisionerOpts = { }; /** - * The binding fingerprint pins entry point and hub URL: a change to either - * is a different backend binding, so allocations bound to the old one - * aren't silently treated as current. + * The binding fingerprint names the backend kind and role only. Hub port, + * install path, and sidecar entry location change across restarts without + * changing what the backend can host; pinning them strands every existing + * allocation, which the reconciler can neither release nor replace. */ export function createProcessSidecarProvisioner( opts: CreateProcessSidecarProvisionerOpts, @@ -462,7 +461,7 @@ export function createProcessSidecarProvisioner( return createSidecarProvisioner({ id: PROCESS_PROVISIONER_ID, apiVersion: PROVISIONER_API_VERSION, - bindingFingerprint: `process:v1:${opts.role}:${config.sidecarEntryPath}:${config.hubWebSocketUrl}`, + bindingFingerprint: `process:v1:${opts.role}`, capabilities: sidecarCapabilityDeclarations("process"), backend: createProcessBackend(runner, config), store,