From c79c34c57888e94c66280eb4727ccc1bd9026e37 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Wed, 10 Jun 2026 15:38:35 +0000 Subject: [PATCH] fix(provisioner): SSOT workspace volume naming + e2e-names CLI (SEV-2499 follow-up) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add WorkspaceVolumeName() helper and wire it through buildWorkspaceMount() to eliminate inline fmt.Sprintf duplication. Introduce workspace-server/cmd/e2e-names — a small SSOT CLI that exports the same Go naming helpers the provisioner uses, so E2E shell scripts can never drift from the real naming convention again. Changes: - provisioner.go: add WorkspaceVolumeName(), refactor buildWorkspaceMount() - provisioner_test.go: unit tests + KI-013 same-prefix-12 regression guard - cmd/e2e-names/main.go: new CLI (container, config-volume, session-volume, workspace-volume subcommands) - test_local_provision_lifecycle_e2e.sh: use e2e-names for all name lookups - local-provision-e2e.yml: build e2e-names + connect job container to molecule-core-net so workspace containers can reach the platform server Relates: SEV-2499 / mc#2499 --- .gitea/workflows/local-provision-e2e.yml | 17 +++++++ .../e2e/test_local_provision_lifecycle_e2e.sh | 30 ++++++++++--- workspace-server/cmd/e2e-names/main.go | 45 +++++++++++++++++++ .../internal/provisioner/provisioner.go | 12 +++-- .../internal/provisioner/provisioner_test.go | 28 ++++++++++++ 5 files changed, 122 insertions(+), 10 deletions(-) create mode 100644 workspace-server/cmd/e2e-names/main.go diff --git a/.gitea/workflows/local-provision-e2e.yml b/.gitea/workflows/local-provision-e2e.yml index 43a9063a7..9ba542cc5 100644 --- a/.gitea/workflows/local-provision-e2e.yml +++ b/.gitea/workflows/local-provision-e2e.yml @@ -92,6 +92,10 @@ jobs: cache: true cache-dependency-path: workspace-server/go.sum + - name: Build e2e-names SSOT CLI + working-directory: workspace-server + run: go build -o /usr/local/bin/e2e-names ./cmd/e2e-names + - name: Ensure provisioner network + pre-pull alpine run: | # The local provisioner attaches workspace containers to @@ -100,6 +104,19 @@ jobs: # named config volume. Pre-pull + ensure the bridge (idempotent). docker pull alpine:3 >/dev/null docker network create molecule-core-net >/dev/null 2>&1 || true + # SEV-2499 / mc#2499: connect the act_runner job container to + # molecule-core-net so workspace containers can reach the platform + # server. Without this, the platform runs in the job container's + # namespace and workspace containers on molecule-core-net time out + # trying to reach the gateway IP (the host cannot forward to an + # unpublished container port). + JOB_CID="$(hostname)" + echo "JOB_CID=${JOB_CID}" >> "$GITHUB_ENV" + if docker network connect molecule-core-net "$JOB_CID" >/dev/null 2>&1; then + echo "Connected job container ($JOB_CID) to molecule-core-net." + else + echo "WARN: could not connect job container to molecule-core-net (fallback to gateway)." + fi echo "alpine:3 pre-pulled; molecule-core-net ensured." - name: Start Postgres (docker, ephemeral host port) diff --git a/tests/e2e/test_local_provision_lifecycle_e2e.sh b/tests/e2e/test_local_provision_lifecycle_e2e.sh index 1fa325799..10336a0b9 100755 --- a/tests/e2e/test_local_provision_lifecycle_e2e.sh +++ b/tests/e2e/test_local_provision_lifecycle_e2e.sh @@ -190,8 +190,26 @@ except Exception: print('')" } +# SEV-2499 SSOT: use the same Go naming helpers the provisioner uses so the +# e2e script can never drift from the real naming convention. The CI job +# pre-builds e2e-names to /usr/local/bin; local runs must build it once: +# go build -o /usr/local/bin/e2e-names ./cmd/e2e-names +_e2e_name() { + local kind="$1" wid="$2" + if [ -x "$(command -v e2e-names)" ]; then + e2e-names "$kind" "$wid" + else + echo "SEV-2499: e2e-names not found in PATH — build it with: go build -o /usr/local/bin/e2e-names ./cmd/e2e-names" >&2 + exit 2 + fi +} +config_volume_name() { _e2e_name config-volume "$1"; } +session_volume_name() { _e2e_name session-volume "$1"; } +workspace_volume_name(){ _e2e_name workspace-volume "$1"; } +container_name() { _e2e_name container "$1"; } + container_running() { # container_running -> echoes name if running - docker ps --filter "name=ws-${1}" --filter "status=running" --format '{{.Names}}' 2>/dev/null | head -1 + docker ps --filter "name=$(container_name "$1")" --filter "status=running" --format '{{.Names}}' 2>/dev/null | head -1 } diagnose_provision() { @@ -224,11 +242,11 @@ cleanup() { # SCOPED teardown — only the workspace this test created. Never a blanket # sweep (other dev workspaces may be live on this shared daemon). e2e_delete_workspace "$WSID" "" >/dev/null 2>&1 || true - docker rm -f "ws-${WSID}" >/dev/null 2>&1 || true + docker rm -f "$(container_name "$WSID")" >/dev/null 2>&1 || true docker volume rm -f \ - "ws-${WSID}-configs" "ws-${WSID}-claude-sessions" \ - "ws-${WSID}-workspace" >/dev/null 2>&1 || true - echo "cleaned workspace $WSID + ws-${WSID} container/volumes" + "$(config_volume_name "$WSID")" "$(session_volume_name "$WSID")" \ + "$(workspace_volume_name "$WSID")" >/dev/null 2>&1 || true + echo "cleaned workspace $WSID + $(container_name "$WSID") container/volumes" fi # Restore the cache tag to whatever it pointed at before we retagged it, so a # stub run doesn't leave the real claude-code tag aliased to the stub. @@ -347,7 +365,7 @@ if [ -z "$WSID" ]; then exit 1 fi pass "workspace created: $WSID" -CONFIG_VOL="ws-${WSID}-configs" +CONFIG_VOL="$(config_volume_name "$WSID")" # Mint a workspace bearer for the WorkspaceAuth-gated secret + /restart calls. WTOKEN=$(e2e_mint_workspace_token "$WSID" || true) diff --git a/workspace-server/cmd/e2e-names/main.go b/workspace-server/cmd/e2e-names/main.go new file mode 100644 index 000000000..ef0de943a --- /dev/null +++ b/workspace-server/cmd/e2e-names/main.go @@ -0,0 +1,45 @@ +// e2e-names prints Docker container/volume names for a workspace ID. +// +// This is the shell-side single source of truth (SSOT) for SEV-2499: +// it imports the same Go naming helpers the provisioner uses, so E2E +// scripts can never drift from the real naming convention again. +// +// Usage: +// +// e2e-names container +// e2e-names config-volume +// e2e-names session-volume +// e2e-names workspace-volume +package main + +import ( + "fmt" + "os" + + "git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/provisioner" +) + +func main() { + if len(os.Args) < 3 { + fmt.Fprintln(os.Stderr, "usage: e2e-names ") + fmt.Fprintln(os.Stderr, " kinds: container, config-volume, session-volume, workspace-volume") + os.Exit(1) + } + + kind := os.Args[1] + wsid := os.Args[2] + + switch kind { + case "container": + fmt.Println(provisioner.ContainerName(wsid)) + case "config-volume": + fmt.Println(provisioner.ConfigVolumeName(wsid)) + case "session-volume": + fmt.Println(provisioner.ClaudeSessionVolumeName(wsid)) + case "workspace-volume": + fmt.Println(provisioner.WorkspaceVolumeName(wsid)) + default: + fmt.Fprintf(os.Stderr, "e2e-names: unknown kind %q\n", kind) + os.Exit(1) + } +} diff --git a/workspace-server/internal/provisioner/provisioner.go b/workspace-server/internal/provisioner/provisioner.go index 2d16c010b..ee5401e75 100644 --- a/workspace-server/internal/provisioner/provisioner.go +++ b/workspace-server/internal/provisioner/provisioner.go @@ -219,6 +219,12 @@ func ClaudeSessionVolumeName(workspaceID string) string { return fmt.Sprintf("ws-%s-claude-sessions", workspaceID) } +// WorkspaceVolumeName returns the Docker named volume for a workspace's +// /workspace mount. +func WorkspaceVolumeName(workspaceID string) string { + return fmt.Sprintf("ws-%s-workspace", workspaceID) +} + // legacyClaudeSessionVolumeName returns the pre-KI-013 truncated session volume name. func legacyClaudeSessionVolumeName(workspaceID string) string { id := workspaceID @@ -790,15 +796,13 @@ func (p *Provisioner) Start(ctx context.Context, cfg WorkspaceConfig) (string, e func buildWorkspaceMount(cfg WorkspaceConfig) string { // Named volume when no host path is configured. if cfg.WorkspacePath == "" { - volumeName := fmt.Sprintf("ws-%s-workspace", cfg.WorkspaceID) - return fmt.Sprintf("%s:/workspace", volumeName) + return fmt.Sprintf("%s:/workspace", WorkspaceVolumeName(cfg.WorkspaceID)) } // Host bind mount. Append :ro for read-only mode; otherwise default // (implicit read-write). "none" explicitly opts out of the mount // even when a path is set. if cfg.WorkspaceAccess == WorkspaceAccessNone { - volumeName := fmt.Sprintf("ws-%s-workspace", cfg.WorkspaceID) - return fmt.Sprintf("%s:/workspace", volumeName) + return fmt.Sprintf("%s:/workspace", WorkspaceVolumeName(cfg.WorkspaceID)) } if cfg.WorkspaceAccess == WorkspaceAccessReadOnly { return fmt.Sprintf("%s:/workspace:ro", cfg.WorkspacePath) diff --git a/workspace-server/internal/provisioner/provisioner_test.go b/workspace-server/internal/provisioner/provisioner_test.go index 12f671f6f..914586e53 100644 --- a/workspace-server/internal/provisioner/provisioner_test.go +++ b/workspace-server/internal/provisioner/provisioner_test.go @@ -482,6 +482,34 @@ func TestConfigVolumeName_DistinctSamePrefix12(t *testing.T) { } } +// TestWorkspaceVolumeName verifies workspace volume naming. +func TestWorkspaceVolumeName(t *testing.T) { + tests := []struct { + id string + want string + }{ + {"short", "ws-short-workspace"}, + {"exactly12ch", "ws-exactly12ch-workspace"}, + {"longer-than-twelve-characters", "ws-longer-than-twelve-characters-workspace"}, + {"abc", "ws-abc-workspace"}, + } + for _, tt := range tests { + got := WorkspaceVolumeName(tt.id) + if got != tt.want { + t.Errorf("WorkspaceVolumeName(%q) = %q, want %q", tt.id, got, tt.want) + } + } +} + +// TestWorkspaceVolumeName_DistinctSamePrefix12 is a regression guard for KI-013. +func TestWorkspaceVolumeName_DistinctSamePrefix12(t *testing.T) { + id1 := "123456789abc-4def-1234-567890abcdef" + id2 := "123456789abc-4def-1234-567890abcdf0" + if WorkspaceVolumeName(id1) == WorkspaceVolumeName(id2) { + t.Fatalf("WorkspaceVolumeName must differ for same-first-12 UUIDs: both = %q", WorkspaceVolumeName(id1)) + } +} + // ---------- #12 — claude-sessions volume naming ---------- // TestClaudeSessionVolumeName_Deterministic: same ID → same volume name, and -- 2.52.0