test: the re-render test proves SyncExec rendered and checks its errors, the moves tests print lengths, and a flat row's Start is pinned end to end (end sweep, tasks 3 and 7)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 files changed,
+66
−9
internal/activator/instancekey_test.go+31 −3internal/appconfig/syncexec_instancekey_test.go+31 −4internal/moves/archive/instancekey_test.go+1 −1internal/moves/envreport_instancekey_test.go+3 −1
diff --git a/internal/activator/instancekey_test.go b/internal/activator/instancekey_test.go| index 649c13c23..aab0d36a0 100644 |
| --- a/internal/activator/instancekey_test.go |
| +++ b/internal/activator/instancekey_test.go |
| @@ -646,11 +646,17 @@ func TestEnsureInstanceKeySymlinkErrorNamesThePathOnce(t *testing.T) { |
| } |
| // startRecorded starts host on b with the recording binary and returns the |
| -// environment the child saw, or nil when no child ran within a second. |
| +// environment the child saw, or nil when no child ran within a second. The |
| +// route's socket is a temp path, so the row classifies as foreign; |
| +// startRecordedRoute takes the row as given. |
| func startRecorded(t *testing.T, b *ExecBackend, host string) (map[string]string, error) { |
| t.Helper() |
| - _, envPath := stageRecordingLiveFullEnv(t, b, host, "v1") |
| - rt := registry.Route{Host: host, Kind: "instance", Socket: tmpSock(t)} |
| + return startRecordedRoute(t, b, registry.Route{Host: host, Kind: "instance", Socket: tmpSock(t)}) |
| +} |
| + |
| +func startRecordedRoute(t *testing.T, b *ExecBackend, rt registry.Route) (map[string]string, error) { |
| + t.Helper() |
| + _, envPath := stageRecordingLiveFullEnv(t, b, rt.Host, "v1") |
| ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) |
| defer cancel() |
| proc, _, err := b.Start(ctx, rt) // the fake binary never binds: the health wait fails, as in the sibling tests |
| @@ -691,6 +697,28 @@ func TestExecBackendStartHandsTheChildTheMintedInstanceKey(t *testing.T) { |
| } |
| } |
| +// Task 3 review (M4): the tests above launch through the foreign arm, since |
| +// a temp socket is neither default. A row that classifies as flat goes |
| +// through the same spawn body, and this pins it end to end: Start mints |
| +// into <DataDir>/<host>.env and hands the child that key. |
| +func TestExecBackendStartHandsAFlatRowsChildTheMintedInstanceKey(t *testing.T) { |
| + b := testExecBackend(t) |
| + b.RunDirOverride = shortRunDir(t) |
| + host := "shop.example.com" |
| + rt := flatGenRoute(b, host) |
| + if l, why := b.Layout(rt); l != LayoutFlat { |
| + t.Fatalf("fixture classifies %q (%s), want flat", l, why) |
| + } |
| + child, _ := startRecordedRoute(t, b, rt) |
| + minted, err := readEnvFile(filepath.Join(b.DataDir, host+".env")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if child == nil || !wellFormedInstanceKey(minted[InstanceKeyVar]) || child[InstanceKeyVar] != minted[InstanceKeyVar] { |
| + t.Fatal("a flat row's child was not handed the key minted into <host>.env") |
| + } |
| +} |
| + |
| // Finding 2 through a launch: while the key's sync fails, Start fails and |
| // no child runs. Once the sync succeeds, the child is handed the key the |
| // failed attempt wrote, never a second one. |
diff --git a/internal/appconfig/syncexec_instancekey_test.go b/internal/appconfig/syncexec_instancekey_test.go| index cabb16f72..99f496c4f 100644 |
| --- a/internal/appconfig/syncexec_instancekey_test.go |
| +++ b/internal/appconfig/syncexec_instancekey_test.go |
| @@ -3,6 +3,7 @@ package appconfig |
| import ( |
| "os" |
| "path/filepath" |
| + "strings" |
| "testing" |
| "github.com/carlosframework/platform/internal/registry" |
| @@ -13,13 +14,17 @@ import ( |
| // name in the app's configuration (L21, never overwrite: re-render). |
| func TestSyncExecLeavesTheInstanceKeyAlone(t *testing.T) { |
| ctx, s, reg, dd, priv, _ := execKit(t) |
| - pk, _ := ParsePrivateKey(priv, "test") |
| + pk, err := ParsePrivateKey(priv, "test") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| own := filepath.Join(dd, "shop.example.com.env") |
| body := []byte("CARLOS_ADMIN_TOKEN=tok\nCARLOS_INSTANCE_KEY=q+/Zk3vJ0aW1Yx9sR7tUe2oPl4mN8bC6dF5gH0iJ1kA=\n") |
| if err := os.WriteFile(own, body, 0o600); err != nil { |
| t.Fatal(err) |
| } |
| - if err := reg.Upsert(registry.Route{Host: "shop.example.com", Kind: "instance", App: "shop", Account: "acct", Status: "ready", Socket: "/run/carlos/shop.sock"}); err != nil { |
| + rt := registry.Route{Host: "shop.example.com", Kind: "instance", App: "shop", Account: "acct", Status: "ready", Socket: "/run/carlos/shop.sock"} |
| + if err := reg.Upsert(rt); err != nil { |
| t.Fatal(err) |
| } |
| for _, vars := range []map[string]string{{"A": "1"}, {"A": "2", "CARLOS_INSTANCE_KEY": "from-config"}} { |
| @@ -29,8 +34,30 @@ func TestSyncExecLeavesTheInstanceKeyAlone(t *testing.T) { |
| if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }, "", nil); err != nil { |
| t.Fatal(err) |
| } |
| - if got, _ := os.ReadFile(own); string(got) != string(body) { |
| - t.Fatalf("a re-render changed the instance's own env file: %q", got) |
| + // The render happened: <host>.config.env carries this pass's |
| + // variables, the configured key included. Without this the test |
| + // would pass against a SyncExec that skipped the route. |
| + rendered, err := os.ReadFile(ConfigEnvPath(dd, rt)) |
| + if err != nil { |
| + t.Fatalf("no rendered configuration after SyncExec: %v", err) |
| + } |
| + got := map[string]string{} |
| + for _, line := range strings.Split(string(rendered), "\n") { |
| + if k, v, ok := strings.Cut(line, "="); ok { |
| + got[k] = v |
| + } |
| + } |
| + for k, v := range vars { |
| + if got[k] != v { |
| + t.Fatalf("rendered configuration lacks %s=%s (%d bytes, %d variables)", k, v, len(rendered), len(got)) |
| + } |
| + } |
| + after, err := os.ReadFile(own) |
| + if err != nil { |
| + t.Fatalf("own env file after the render: %v", err) |
| + } |
| + if string(after) != string(body) { |
| + t.Fatalf("a re-render changed the instance's own env file: %d bytes, want %d", len(after), len(body)) |
| } |
| } |
| } |
diff --git a/internal/moves/archive/instancekey_test.go b/internal/moves/archive/instancekey_test.go| index 1f7abb192..2d5114593 100644 |
| --- a/internal/moves/archive/instancekey_test.go |
| +++ b/internal/moves/archive/instancekey_test.go |
| @@ -25,6 +25,6 @@ func TestArchiveCarriesTheInstanceKeyByteForByte(t *testing.T) { |
| } |
| got, err := os.ReadFile(outEnv) |
| if err != nil || !bytes.Equal(got, body) { |
| - t.Fatalf("extracted env differs from the source's (err %v)", err) |
| + t.Fatalf("extracted env differs from the source's: %d bytes, want %d (err %v)", len(got), len(body), err) |
| } |
| } |
diff --git a/internal/moves/envreport_instancekey_test.go b/internal/moves/envreport_instancekey_test.go| index 141a19e0d..f38e07f42 100644 |
| --- a/internal/moves/envreport_instancekey_test.go |
| +++ b/internal/moves/envreport_instancekey_test.go |
| @@ -19,8 +19,10 @@ func TestEnvReportComparesTheInstanceKey(t *testing.T) { |
| {map[string]string{name: "a2V5LXR3bw==", "CARLOS_REGION": "uk"}, "differs"}, |
| {map[string]string{"CARLOS_REGION": "uk"}, "missing"}, |
| } { |
| + // The kind is the whole check: T3 (movesreport.go) fails on every |
| + // kind but advisory-differs, which only an advisory variable gets. |
| diffs := CompareEnv(src, ReportEnv(key, tc.target), false) |
| - if len(diffs) != 1 || diffs[0].Name != name || diffs[0].Kind != tc.want || diffs[0].Note() { |
| + if len(diffs) != 1 || diffs[0].Name != name || diffs[0].Kind != tc.want { |
| t.Fatalf("diffs = %+v, want one %s on %s", diffs, tc.want, name) |
| } |
| } |