| index 1fedbc201..45a205673 100644 |
| --- a/internal/appconfig/sync_test.go |
| +++ b/internal/appconfig/sync_test.go |
| @@ -64,7 +64,7 @@ func TestSyncWritesFileAndRestartsOnChange(t *testing.T) { |
| if err != nil { |
| t.Fatalf("read rendered env: %v", err) |
| } |
| - want := "LOG_LEVEL=debug\nSES_SECRET=shh\n" |
| + want := "LOG_LEVEL=debug\nSES_SECRET=shh\nCARLOS_PURGE=header\n" |
| if string(got) != want { |
| t.Fatalf("rendered env = %q, want %q", got, want) |
| } |
| @@ -164,7 +164,9 @@ func TestSyncLeavesHandStagedFileAloneWhenNoManifestExists(t *testing.T) { |
| func TestSyncConvergesToEmptyAfterEveryVarIsUnset(t *testing.T) { |
| // The mirror of the case above: the manifest object DOES exist, it just |
| // has no vars left. That is a real, operator-intended state and must |
| - // converge — the file is emptied and the unit restarted. |
| + // converge — every tenant var is dropped and the unit restarted. The |
| + // file is not fully emptied: CARLOS_PURGE=header is agent-owned, not a |
| + // tenant var, and applyAgentVars appends it unconditionally. |
| s := release.NewDirStore(t.TempDir()) |
| ctx := context.Background() |
| reg := testRegistry(t) |
| @@ -192,8 +194,8 @@ func TestSyncConvergesToEmptyAfterEveryVarIsUnset(t *testing.T) { |
| t.Fatalf("Sync: %v", err) |
| } |
| path := filepath.Join(envDir, "keymail.example.com.env") |
| - if got, err := os.ReadFile(path); err != nil || string(got) != "LOG_LEVEL=debug\n" { |
| - t.Fatalf("after set: env = %q, err = %v; want LOG_LEVEL=debug", got, err) |
| + if got, err := os.ReadFile(path); err != nil || string(got) != "LOG_LEVEL=debug\nCARLOS_PURGE=header\n" { |
| + t.Fatalf("after set: env = %q, err = %v; want LOG_LEVEL=debug\\nCARLOS_PURGE=header", got, err) |
| } |
| |
| if _, err := Unset(ctx, s, "bab", "keymail", []string{"LOG_LEVEL"}); err != nil { |
| @@ -207,8 +209,10 @@ func TestSyncConvergesToEmptyAfterEveryVarIsUnset(t *testing.T) { |
| if err != nil { |
| t.Fatalf("read env after unset: %v", err) |
| } |
| - if len(got) != 0 { |
| - t.Fatalf("after full unset: env = %q, want empty", got) |
| + // Convergence to empty is now "just the agent-owned line" — the agent |
| + // still owns CARLOS_PURGE regardless of what the tenant's manifest holds. |
| + if string(got) != "CARLOS_PURGE=header\n" { |
| + t.Fatalf("after full unset: env = %q, want just CARLOS_PURGE=header", got) |
| } |
| if len(restarted) != 1 || restarted[0] != "keymail.example.com" { |
| t.Fatalf("restarted = %v, want exactly [keymail.example.com]", restarted) |
| @@ -489,9 +493,9 @@ func TestSyncRendersEachRoutesBoundEnvironment(t *testing.T) { |
| } |
| |
| want := map[string]string{ |
| - "plain.example.com": "LOG_LEVEL=info\nSHARED=yes\n", |
| - "demo.example.com": "DEMO_ONLY=1\nLOG_LEVEL=debug\nSHARED=yes\n", |
| - "staging.example.com": "LOG_LEVEL=trace\nSHARED=yes\nSTAGING_ONLY=1\n", |
| + "plain.example.com": "LOG_LEVEL=info\nSHARED=yes\nCARLOS_PURGE=header\n", |
| + "demo.example.com": "DEMO_ONLY=1\nLOG_LEVEL=debug\nSHARED=yes\nCARLOS_PURGE=header\n", |
| + "staging.example.com": "LOG_LEVEL=trace\nSHARED=yes\nSTAGING_ONLY=1\nCARLOS_PURGE=header\n", |
| } |
| for host, body := range want { |
| got, err := os.ReadFile(filepath.Join(envDir, host+".env")) |
| @@ -612,8 +616,8 @@ func TestSyncLeavesEnvFileUntouchedWhenBoundEnvironmentIsMissing(t *testing.T) { |
| if err != nil { |
| t.Fatalf("sibling route not materialized: %v", err) |
| } |
| - if string(sibling) != "LOG_LEVEL=info\n" { |
| - t.Fatalf("sibling env = %q, want LOG_LEVEL=info", sibling) |
| + if string(sibling) != "LOG_LEVEL=info\nCARLOS_PURGE=header\n" { |
| + t.Fatalf("sibling env = %q, want LOG_LEVEL=info + CARLOS_PURGE=header", sibling) |
| } |
| if len(restarted) != 1 || restarted[0] != "ok.example.com" { |
| t.Fatalf("restarted = %v, want exactly [ok.example.com]", restarted) |
| @@ -733,8 +737,8 @@ func TestSyncRefusesToReplaceStagedFileWhenAppHasNoDefaultBundle(t *testing.T) { |
| if err != nil { |
| t.Fatalf("fresh route not materialized: %v", err) |
| } |
| - if string(fresh) != "LOG_LEVEL=debug\n" { |
| - t.Fatalf("fresh route env = %q, want LOG_LEVEL=debug", fresh) |
| + if string(fresh) != "LOG_LEVEL=debug\nCARLOS_PURGE=header\n" { |
| + t.Fatalf("fresh route env = %q, want LOG_LEVEL=debug + CARLOS_PURGE=header", fresh) |
| } |
| if len(restarted) != 1 || restarted[0] != "fresh.example.com" { |
| t.Fatalf("restarted = %v, want exactly [fresh.example.com]", restarted) |
| @@ -778,8 +782,8 @@ func TestSyncEnvironmentOnlyAppKeepsConvergingAfterItsOwnFirstWrite(t *testing.T |
| t.Fatalf("Sync (pass 1): %v", err) |
| } |
| path := filepath.Join(envDir, "fresh.example.com.env") |
| - if got, err := os.ReadFile(path); err != nil || string(got) != "LOG_LEVEL=debug\n" { |
| - t.Fatalf("pass 1: env = %q, err = %v; want LOG_LEVEL=debug", got, err) |
| + if got, err := os.ReadFile(path); err != nil || string(got) != "LOG_LEVEL=debug\nCARLOS_PURGE=header\n" { |
| + t.Fatalf("pass 1: env = %q, err = %v; want LOG_LEVEL=debug + CARLOS_PURGE=header", got, err) |
| } |
| |
| // The operator edits the bundle: one value changed, one key added. |
| @@ -797,7 +801,7 @@ func TestSyncEnvironmentOnlyAppKeepsConvergingAfterItsOwnFirstWrite(t *testing.T |
| if err != nil { |
| t.Fatalf("pass 2: read env: %v", err) |
| } |
| - if string(got) != "LOG_LEVEL=trace\nNEW=1\n" { |
| + if string(got) != "LOG_LEVEL=trace\nNEW=1\nCARLOS_PURGE=header\n" { |
| t.Fatalf("pass 2: env = %q, want the edited bundle to converge", got) |
| } |
| if len(restarted) != 1 || restarted[0] != "fresh.example.com" { |
| @@ -897,7 +901,7 @@ func TestSyncEnvOnlyKeyRemovalIsRefusedUntilADefaultBundleExists(t *testing.T) { |
| if err := Sync(ctx, reg, s, privKey, envDir, func(string) error { return nil }); err != nil { |
| t.Fatalf("Sync (pass 2): %v", err) |
| } |
| - if got, _ := os.ReadFile(path); string(got) != "GONE=2\nKEEP=1\n" { |
| + if got, _ := os.ReadFile(path); string(got) != "GONE=2\nKEEP=1\nCARLOS_PURGE=header\n" { |
| t.Fatalf("env = %q, want the removal refused while the app has no default bundle", got) |
| } |
| // The moment a default bundle exists the app is bucket-managed and the |
| @@ -909,7 +913,7 @@ func TestSyncEnvOnlyKeyRemovalIsRefusedUntilADefaultBundleExists(t *testing.T) { |
| if err := Sync(ctx, reg, s, privKey, envDir, func(string) error { return nil }); err != nil { |
| t.Fatalf("Sync (pass 3): %v", err) |
| } |
| - if got, _ := os.ReadFile(path); string(got) != "KEEP=1\n" { |
| + if got, _ := os.ReadFile(path); string(got) != "KEEP=1\nCARLOS_PURGE=header\n" { |
| t.Fatalf("env = %q, want the removal to land once a default bundle exists", got) |
| } |
| } |
| @@ -965,7 +969,7 @@ func TestSyncMergedRenderIsAuthoritativeOnceADefaultBundleExists(t *testing.T) { |
| if err != nil { |
| t.Fatalf("read env: %v", err) |
| } |
| - if string(got) != "LOG_LEVEL=debug\n" { |
| + if string(got) != "LOG_LEVEL=debug\nCARLOS_PURGE=header\n" { |
| t.Fatalf("env = %q, want the merged render to win once a default bundle exists", got) |
| } |
| if len(restarted) != 1 || restarted[0] != "prod.example.com" { |
| @@ -1015,12 +1019,79 @@ func TestSyncEnvironmentOnlyAppWithNoDefaultBundle(t *testing.T) { |
| if err != nil { |
| t.Fatalf("%s: read rendered env: %v", host, err) |
| } |
| - if string(got) != "ONLY=here\n" { |
| - t.Fatalf("%s: rendered env = %q, want ONLY=here", host, got) |
| + if string(got) != "ONLY=here\nCARLOS_PURGE=header\n" { |
| + t.Fatalf("%s: rendered env = %q, want ONLY=here + CARLOS_PURGE=header", host, got) |
| } |
| } |
| } |
| |
| +func TestLostKeysIgnoresAgentOwnedVars(t *testing.T) { |
| + // The sync itself appends CARLOS_PURGE=header to every file it writes |
| + // (applyAgentVars). If lostKeys counts it, every defaultMissing route |
| + // the sync previously converged reads as "about to lose a key" and is |
| + // refused forever — the false-refusal loop. |
| + cur := []byte("A=1\nCARLOS_PURGE=header\n") |
| + m := &Manifest{Schema: 1, App: "keymail", Vars: map[string]Var{"A": {Kind: "env", Value: "1"}}} |
| + lost, parsed := lostKeys(cur, m) |
| + if !parsed || len(lost) != 0 { |
| + t.Fatalf("lost=%v parsed=%v, want none/true", lost, parsed) |
| + } |
| +} |
| + |
| +// TestSyncHostConvergesAgentOwnedVarWithoutRewrite pins the convergence |
| +// property applyAgentVars must not break: syncHost's bytes.Equal |
| +// short-circuit (sync.go) has to still hold once every render carries an |
| +// appended CARLOS_PURGE=header line. The first pass writes the file |
| +// (containing the var) and restarts; an identical second pass must leave the |
| +// file's mtime and content alone and must NOT call restart again — proof |
| +// that applyAgentVars is deterministic and idempotent, not just that its |
| +// output happens to look right once. |
| +func TestSyncHostConvergesAgentOwnedVarWithoutRewrite(t *testing.T) { |
| + envDir := t.TempDir() |
| + m := &Manifest{Schema: 1, App: "keymail", Vars: map[string]Var{"A": {Kind: "env", Value: "1"}}} |
| + rt := registry.Route{Host: "steady.example.com", Kind: "instance", App: "keymail", Account: "bab", Status: "ready", Backing: registry.BackingUnit} |
| + |
| + var restarted int |
| + restart := func(string) error { restarted++; return nil } |
| + |
| + syncHost(rt, m, false, nil, envDir, restart) |
| + path := filepath.Join(envDir, "steady.example.com.env") |
| + got, err := os.ReadFile(path) |
| + if err != nil { |
| + t.Fatalf("read env after first sync: %v", err) |
| + } |
| + if string(got) != "A=1\nCARLOS_PURGE=header\n" { |
| + t.Fatalf("env = %q, want A=1 + CARLOS_PURGE=header", got) |
| + } |
| + if restarted != 1 { |
| + t.Fatalf("restarted = %d after first sync, want 1", restarted) |
| + } |
| + fi, err := os.Stat(path) |
| + if err != nil { |
| + t.Fatalf("stat after first sync: %v", err) |
| + } |
| + |
| + // Second, identical pass. |
| + syncHost(rt, m, false, nil, envDir, restart) |
| + if restarted != 1 { |
| + t.Fatalf("restarted = %d after second identical sync, want still 1 (no re-restart)", restarted) |
| + } |
| + now, err := os.Stat(path) |
| + if err != nil { |
| + t.Fatalf("stat after second sync: %v", err) |
| + } |
| + if !now.ModTime().Equal(fi.ModTime()) { |
| + t.Fatalf("env file was rewritten on an unchanged pass (mtime moved)") |
| + } |
| + got2, err := os.ReadFile(path) |
| + if err != nil { |
| + t.Fatalf("read env after second sync: %v", err) |
| + } |
| + if string(got2) != string(got) { |
| + t.Fatalf("second pass content = %q, want unchanged (%q)", got2, got) |
| + } |
| +} |
| + |
| func TestSyncMaterializesEveryRouteOfAMultiRouteApp(t *testing.T) { |
| // One app, two simultaneous routes (e.g. a stable route and a canary |
| // route are two different Hosts) — the shared config must land in |
| @@ -1056,8 +1127,8 @@ func TestSyncMaterializesEveryRouteOfAMultiRouteApp(t *testing.T) { |
| if err != nil { |
| t.Fatalf("%s: read rendered env: %v", host, err) |
| } |
| - if string(got) != "LOG_LEVEL=debug\n" { |
| - t.Fatalf("%s: rendered env = %q, want LOG_LEVEL=debug", host, got) |
| + if string(got) != "LOG_LEVEL=debug\nCARLOS_PURGE=header\n" { |
| + t.Fatalf("%s: rendered env = %q, want LOG_LEVEL=debug + CARLOS_PURGE=header", host, got) |
| } |
| } |
| } |