From 840db9bd4f22e506a533aa912e542efe999e8823 Mon Sep 17 00:00:00 2001 From: mptyl Date: Wed, 5 Aug 2026 16:49:56 +0200 Subject: [PATCH] fix: bootstrap registry on first workspace list --- backend/src/workspaces/registry.ts | 6 ++++++ backend/test/workspace-registry.test.ts | 15 +++++++++++++ scripts/unified-deployment-smoke.sh | 28 +++++++++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/backend/src/workspaces/registry.ts b/backend/src/workspaces/registry.ts index f47efd38..0af1b086 100644 --- a/backend/src/workspaces/registry.ts +++ b/backend/src/workspaces/registry.ts @@ -155,6 +155,12 @@ export class WorkspaceRegistry { } async list(): Promise { + const active = await this.tryActiveState(); + if (active) return active.revisions; + // A clean installation has no active snapshot until the first registry operation. Keep + // this lazy so health/startup remain available when Git is temporarily unreachable, while + // still refusing corrupted existing state (tryActiveState throws instead of returning none). + await this.bootstrap(); return (await this.activeState()).revisions; } diff --git a/backend/test/workspace-registry.test.ts b/backend/test/workspace-registry.test.ts index fef26e14..ad427f9d 100644 --- a/backend/test/workspace-registry.test.ts +++ b/backend/test/workspace-registry.test.ts @@ -254,6 +254,21 @@ test("bootstraps a checkout and activates a validated immutable snapshot", async }); }); +test("first list lazily bootstraps a clean registry", async () => { + const remote = await fixture(); + const root = join(remote.root, "registry"); + const registry = new WorkspaceRegistry(config(root, remote.remote)); + + await expect(registry.list()).resolves.toEqual([ + expect.objectContaining({ + id: "psd-clinical", + commit: remote.initialCommit, + state: "operational", + }), + ]); + expect(existsSync(join(root, "state", "active.json"))).toBe(true); +}); + test("publishes create, update, and delete with the configured Git author identity", async () => { const remote = await fixture(); const registry = new WorkspaceRegistry(config(join(remote.root, "registry"), remote.remote, { diff --git a/scripts/unified-deployment-smoke.sh b/scripts/unified-deployment-smoke.sh index eb6ea095..c1d20a3f 100755 --- a/scripts/unified-deployment-smoke.sh +++ b/scripts/unified-deployment-smoke.sh @@ -630,6 +630,27 @@ task13_report_server_workspace_failure() { else printf '%s\n' '(empty response)' >&2 fi + printf '%s\n' '--- sanitized registry integrity probe ---' >&2 + { + task13_compose exec -T core node --input-type=module -e ' + const { loadConfig } = await import("/app/backend/dist/config.js"); + const { WorkspaceRegistry } = await import("/app/backend/dist/workspaces/registry.js"); + const registry = new WorkspaceRegistry(loadConfig(process.env).workspaceRegistry); + try { + const revisions = await registry.list(); + for (const revision of revisions) await registry.read(revision.id); + console.log(JSON.stringify({ ok: true, revisions: revisions.length })); + } catch (error) { + console.log(JSON.stringify({ + ok: false, + name: error instanceof Error ? error.name : "UnknownError", + code: error && typeof error === "object" && "code" in error ? error.code : "unknown", + message: error instanceof Error ? error.message : "Unknown registry failure", + })); + process.exitCode = 1; + } + ' 2>&1 || printf '%s\n' '(registry integrity probe unavailable)' + } | tail -n 20 | task13_sanitize >&2 printf '%s\n' '--- sanitized core logs (last 100 lines) ---' >&2 { task13_compose logs --no-color --tail 100 core 2>&1 \ @@ -1063,6 +1084,11 @@ task13_self_test_server_workspace_diagnostics() { printf '%s\n' \ '{"error":"workspace_invalid","detail":"password=fixture-known-secret"}' >"$response" task13_compose() { + if [[ "$*" == "exec -T core node --input-type=module -e "* ]]; then + printf '%s\n' \ + '{"name":"WorkspaceRegistryError","code":"workspace_invalid","message":"Workspace snapshot integrity check failed"}' + return 0 + fi [[ "$*" == "logs --no-color --tail 100 core" ]] \ || task13_fail "server diagnostics requested an unexpected Compose command" while [[ "$i" -le 150 ]]; do @@ -1083,6 +1109,8 @@ task13_self_test_server_workspace_diagnostics() { || task13_fail "server diagnostics omit the unexpected HTTP status" [[ "$output" == *'workspace_invalid'* ]] \ || task13_fail "server diagnostics omit the generic response" + [[ "$output" == *'Workspace snapshot integrity check failed'* ]] \ + || task13_fail "server diagnostics omit the bounded internal registry reason" [[ "$output" == *'core-log-051'* && "$output" != *'core-log-050'* ]] \ || task13_fail "server diagnostics do not bound core logs to the last 100 lines" count="$(grep -Ec '^core-log-[0-9]{3}' <<<"$output")"