docs: nearest-region plan rev 3, after Astra's plan round 2, proved in scratch
All ten new findings (6 P1, 4 P2) and the four partly-resolved round-1 findings verified and accepted. The code blocks they touched were extracted verbatim into a throwaway worktree at e8406ce6: whole-tree build and vet clean, every prescribed test FAIL before its implementation and PASS after, mutation checks confirmed for the snapshot-completeness, eligible-only location, R9 scope, canary ordering and guess invariants, and the five cross-compile targets built. The proof surfaced five more fixes (an import, a test package, keyed literals, a role-pin entry, the e2e harness), all folded in. The scratch worktree and branch were deleted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 file changed,
+632
−237
docs/superpowers/plans/2026-10-02-nearest-region-default.md+632 −237
diff --git a/docs/superpowers/plans/2026-10-02-nearest-region-default.md b/docs/superpowers/plans/2026-10-02-nearest-region-default.md| index b2936fe30..ecbd495e4 100644 |
| --- a/docs/superpowers/plans/2026-10-02-nearest-region-default.md |
| +++ b/docs/superpowers/plans/2026-10-02-nearest-region-default.md |
| @@ -2,14 +2,18 @@ |
| > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. |
| -**Revision:** 2. |
| +**Revision:** 3. |
| - Rev 1 (9513d52c) was reviewed by Astra |
| (`CODEX-PLAN-NEAREST-REGION-R1.md`, beside this file): not ready, with |
| 8 P1 and 10 P2. Every finding was checked against the code and accepted. |
| -- Rev 2 also folds in spec rev 6's ruling R9: placed instances need the |
| - deployment's secrets key. |
| -- The dispositions table is at the end of this plan. |
| +- Rev 2 (fcf15ae9) folded in spec rev 6's ruling R9: placed instances need |
| + the deployment's secrets key. |
| +- Rev 3 answers Astra's round 2 (`CODEX-PLAN-NEAREST-REGION-R2.md`). |
| +- The code blocks the findings touched were **proved**: extracted verbatim |
| + into a throwaway worktree at the branch base, built and vetted, and each |
| + prescribed test was run before and after its implementation step. |
| +- Both dispositions tables, with the proof results, are at the end. |
| **Goal:** Give every new app a region at creation, preselected from a timezone-then-IP guess, and have new instances default to that region. Never place an instance automatically on a box that cannot open the deployment's secrets. Existing apps, older CLIs and DNS stay unchanged. |
| @@ -89,7 +93,10 @@ beside this plan. Section numbers below (§N) are the spec's. |
| - **Cross-compile gate.** The five CI targets, exactly as |
| `.amadan/ci.d/20-cross@5` runs them: |
| `CGO_ENABLED=0 GOOS=<os> GOARCH=<arch> go build ./...` for darwin/arm64, |
| - darwin/amd64, linux/arm64, windows/amd64 and windows/arm64. |
| + darwin/amd64, linux/arm64, windows/amd64 and windows/arm64. Every loop |
| + below records each failure and ends with `[ "$fail" = 0 ] || exit 1`. Run |
| + each such block as one script (`bash -c` or a tool call), so that a failed |
| + target stops it before the commit lines that follow (codex plan-R2 10). |
| - **Test environment.** |
| - Use the default Go build cache (`~/.cache/go-build`, writable inside the |
| sandbox). Do not set `GOCACHE`, `GOMODCACHE` or `GOTMPDIR`. |
| @@ -233,6 +240,14 @@ plan and marked "plan". |
| | O8 (plan) | `internal/console/server.go` | `console: app region %s/%s: %s (%s)` | |
| | O9 (plan) | `internal/console/regiondrift.go` | `console: placement discrepancy check %s/%s: only the first %d records were checked` | |
| | O10 (plan) | `internal/host/edgesfile.go` | `agent: CARLOS_SECRETS_KEY does not parse, so no secrets_key_fp is published: %v` | |
| +| O11 (plan) | `internal/console/apiinstances.go` | `console: api instances create %s/%s: read app region: %v` | |
| +| O12 (plan) | same | `console: api instances create %s/%s: read edges: %v` | |
| +| O13 (plan) | `internal/console/regioncandidates.go` | `console: region policy %s/%s: read manifest: %v` | |
| +| O14 (plan) | same | `console: region policy %s/%s: open registry: %v` | |
| +| O15 (plan) | same | `console: region policy %s/%s: placement: %v` | |
| +| O16 (plan) | `internal/console/apicli.go` | `console: claim %s/%s: read back region: %v` | |
| +| O17 (plan) | `apimode.go` | `regions: unreadable response: %w` | |
| +| O18 (plan) | `internal/console/regiondrift.go` | `console: placement discrepancy check %s/%s: %v` | |
| | C9 | `region_copy.go` | `Region: %s (from your timezone). Use --region to choose another.` | |
| | C10 | same | `Region: %s (from your network location). Use --region to choose another.` | |
| | C11 | same | `Region: %s (the platform default). Use --region to choose another.` | |
| @@ -242,6 +257,7 @@ plan and marked "plan". |
| | C14 | same | `channel %s, %s` | |
| | C15 | same | `%s already runs in %s. --region only applies when deploy creates an app. To place one instance elsewhere, use carlos instances create --region.` | |
| | C15b (plan) | same | `could not read %s's region (%v), so deploy stopped before shipping anything. Try again, or run without --region.` | |
| +| C15c (plan) | same | `the console could not read %s's region, so deploy stopped before shipping anything. Try again, or run without --region.` | |
| | C25 | same | `canary %s already runs in %s. --region applies only when canary creates the host. Run carlos canary remove first to start it somewhere else.` **Note for Paul:** the verb is `carlos canary rm` (or `delete`); there is no `remove`. | |
| | C30 | same | `%s runs on your fleet %s, which decides where every instance runs, so --region does not apply. Run the command without --region.` | |
| | C30b (plan) | same | `this console can't say where %s's instances run, so --region can't be checked. Update the console, or run the command without --region.` | |
| @@ -514,9 +530,11 @@ because the schema really did grow. |
| Run: |
| ```bash |
| +fail=0 |
| for t in darwin/arm64 darwin/amd64 linux/arm64 windows/amd64 windows/arm64; do |
| - CGO_ENABLED=0 GOOS=${t%/*} GOARCH=${t#*/} go build ./... || { echo "FAILED $t"; break; } |
| + CGO_ENABLED=0 GOOS=${t%/*} GOARCH=${t#*/} go build ./... || { echo "FAILED $t"; fail=1; } |
| done |
| +[ "$fail" = 0 ] || exit 1 # stops this block before any commit below it |
| ``` |
| Expected: no `FAILED` line. `internal/host` is linked into the CLI. |
| @@ -745,6 +763,9 @@ func edgesFileSecretsFP(getenv func(string) string) string { |
| The log text is O10 from the copy record. `ParsePrivateKey`'s errors name |
| the variable and the parse failure, never the value. |
| +Add `"github.com/carlosframework/platform/internal/appconfig"` to |
| +`edgesfile.go`'s imports. |
| + |
| In `edgesFileLoop`'s ident literal, add: |
| ```go |
| @@ -757,9 +778,11 @@ Run: |
| ```bash |
| mkdir -p /tmp/g && TMPDIR=/tmp/g go test ./internal/host ./internal/appconfig -count=1 |
| +fail=0 |
| for t in darwin/arm64 darwin/amd64 linux/arm64 windows/amd64 windows/arm64; do |
| - CGO_ENABLED=0 GOOS=${t%/*} GOARCH=${t#*/} go build ./... || { echo "FAILED $t"; break; } |
| + CGO_ENABLED=0 GOOS=${t%/*} GOARCH=${t#*/} go build ./... || { echo "FAILED $t"; fail=1; } |
| done |
| +[ "$fail" = 0 ] || exit 1 # stops this block before any commit below it |
| ``` |
| Expected: PASS, and no `FAILED` line. |
| @@ -1805,9 +1828,11 @@ and its `golang.org/x/sys` requirement only. |
| - [ ] **Step 6: Cross-compile, then commit** |
| ```bash |
| +fail=0 |
| for t in darwin/arm64 darwin/amd64 linux/arm64 windows/amd64 windows/arm64; do |
| - CGO_ENABLED=0 GOOS=${t%/*} GOARCH=${t#*/} go build ./... || { echo "FAILED $t"; break; } |
| + CGO_ENABLED=0 GOOS=${t%/*} GOARCH=${t#*/} go build ./... || { echo "FAILED $t"; fail=1; } |
| done |
| +[ "$fail" = 0 ] || exit 1 # stops this block before any commit below it |
| git add go.mod go.sum internal/geo/mmdb.go internal/geo/mmdb_test.go internal/geo/testdata |
| git commit -m "geo: optional GeoIP locator; lookup errors logged as a fixed category (nearest-region §6.2, §6.5) |
| @@ -2202,6 +2227,12 @@ INSERT INTO apps (app, created_by, created_at, region, region_source) VALUES ('l |
| t.Fatal(err) |
| } |
| defer s.Close() |
| + // Open migrates but does not backfill accounts (NewServer does), and |
| + // rename and move both resolve the app's account row: seed the ops |
| + // account the legacy row belongs to, and the move's destination. |
| + if _, err := s.db.Exec(`INSERT OR IGNORE INTO accounts (id, sqid, name, created_at) VALUES (?, 'bab', 'Ops', 0)`, OpsAccountID); err != nil { |
| + t.Fatal(err) |
| + } |
| if err := s.RenameAppIn(OpsAccountID, "legacy", "renamed"); err != nil { |
| t.Fatal(err) |
| } |
| @@ -2353,7 +2384,7 @@ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>" |
| - the `Server` struct; |
| - `NewServer`, whose last line before `return` calls `logRegionConfig`. |
| - Modify: `cmd/console/main.go:278` |
| -- Test: `cmd/console/main_test.go` (append) |
| +- Test: `cmd/console/region_test.go` (create, `package main`) |
| **Interfaces:** |
| - Consumes: `geo.Locator` and `geo.OpenMMDB` (Task 5), |
| @@ -2441,6 +2472,13 @@ func TestLogRegionConfig(t *testing.T) { |
| if !strings.Contains(out, "CONSOLE_HOME_REGION is unset") || !strings.Contains(out, "CARLOS_SECRETS_PUBKEY is unset") { |
| t.Fatalf("missing startup lines: %s", out) |
| } |
| + // Switch off: the guess endpoint still answers, so a missing secrets |
| + // key is still worth saying (codex plan-R2 8); an unnamed home is not. |
| + buf.Reset() |
| + logRegionConfig(Config{}) |
| + if out := buf.String(); !strings.Contains(out, "CARLOS_SECRETS_PUBKEY is unset") || strings.Contains(out, "CONSOLE_HOME_REGION") { |
| + t.Fatalf("switch off logged %q, want only the secrets-key line", out) |
| + } |
| buf.Reset() |
| pub, _ := mustTestSecretsKey() |
| logRegionConfig(Config{AppRegions: true, HomeRegion: "ie", SecretsPub: pub}) |
| @@ -2453,10 +2491,18 @@ func TestLogRegionConfig(t *testing.T) { |
| The two `strings.Contains` fragments are substrings of O1 and O2. Use |
| fragments of the approved text if review changed them. |
| -Append to `cmd/console/main_test.go`, adding `"path/filepath"` to its imports |
| -if absent: |
| +Create `cmd/console/region_test.go`. It must be `package main`: |
| +`main_test.go` is the external `package main_test` and cannot reach the |
| +unexported helpers. |
| ```go |
| +package main |
| + |
| +import ( |
| + "path/filepath" |
| + "testing" |
| +) |
| + |
| func TestOpenGeoIPIsOptionalAndNeverFatal(t *testing.T) { |
| if openGeoIP("") != nil { |
| t.Fatal("unset CONSOLE_GEOIP_DB must mean no locator") |
| @@ -2545,12 +2591,10 @@ func (s *Server) appRegion(accountID int64, app string) (string, string, error) |
| } |
| // logRegionConfig names, once at startup, each configuration that leaves |
| -// part of the feature off. |
| +// part of the feature off. The secrets-key line does not depend on the |
| +// switch: the guess endpoint answers with the switch off too (R9). |
| func logRegionConfig(cfg Config) { |
| - if !cfg.AppRegions { |
| - return |
| - } |
| - if cfg.HomeRegion == "" { |
| + if cfg.AppRegions && cfg.HomeRegion == "" { |
| log.Print("console: CONSOLE_APP_REGIONS=on but CONSOLE_HOME_REGION is unset, so app regions stay off until the home is named") |
| } |
| if cfg.SecretsPub == "" { |
| @@ -2606,7 +2650,7 @@ Expected: PASS. |
| - [ ] **Step 7: Commit** |
| ```bash |
| -git add internal/console/server.go internal/console/regionconfig.go internal/console/regionconfig_test.go cmd/console/main.go cmd/console/main_test.go |
| +git add internal/console/server.go internal/console/regionconfig.go internal/console/regionconfig_test.go cmd/console/main.go cmd/console/region_test.go |
| git commit -m "console: app-regions switch, expected secrets fingerprint, GeoIP, test seams (nearest-region §5.6, §6.2, R9) |
| Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>" |
| @@ -2634,7 +2678,11 @@ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>" |
| - Produces: |
| ```go |
| -func readEdgesFilesStrict(ctx context.Context, s release.Store) ([]edgesFilePlacementView, error) |
| +type edgesSnapshot struct { |
| + Files []edgesFilePlacementView |
| + Complete bool // false when any listed file could not be read or parsed |
| +} |
| +func readEdgesSnapshot(ctx context.Context, s release.Store) (edgesSnapshot, error) // error only when the LISTING fails |
| type regionCandidate struct { |
| Code string // record vocabulary; "" is the home |
| Home bool |
| @@ -2642,8 +2690,9 @@ type regionCandidate struct { |
| Reachable, Hosting, KeyOK, Eligible bool |
| NoKeyProven bool // R9: current writers only, and none holds the key |
| } |
| -func regionCandidates(files []edgesFilePlacementView, homeName, expectedFP string) []regionCandidate |
| -func (s *Server) regionCands(files []edgesFilePlacementView) []regionCandidate |
| +func regionCandidates(snap edgesSnapshot, homeName, expectedFP string) []regionCandidate |
| +func (s *Server) regionCands(snap edgesSnapshot) []regionCandidate |
| +func (s *Server) candsOrHome(ctx context.Context) []regionCandidate // a fresh snapshot, or the home alone |
| func homeOnlyCandidates() []regionCandidate |
| func (c regionCandidate) unavailableReason() string // "", absent, not-hosting, no-secrets-key, not-eligible |
| func findCandidate(cands []regionCandidate, code string) (regionCandidate, bool) |
| @@ -2765,17 +2814,18 @@ func captureLog(w io.Writer) (restore func()) { |
| return func() { log.SetOutput(prev); log.SetFlags(flags) } |
| } |
| -func views(t *testing.T, bodies ...string) []edgesFilePlacementView { |
| +// views is a COMPLETE snapshot of the given edges-file bodies. |
| +func views(t *testing.T, bodies ...string) edgesSnapshot { |
| t.Helper() |
| - var out []edgesFilePlacementView |
| + snap := edgesSnapshot{Complete: true} |
| for _, b := range bodies { |
| var f edgesFilePlacementView |
| if err := json.Unmarshal([]byte(b), &f); err != nil { |
| t.Fatal(err) |
| } |
| - out = append(out, f) |
| + snap.Files = append(snap.Files, f) |
| } |
| - return out |
| + return snap |
| } |
| func TestRegionCandidatesNeedPositiveEligibility(t *testing.T) { |
| @@ -2831,6 +2881,72 @@ func TestRegionCandidatesSecretsKey(t *testing.T) { |
| } |
| } |
| +// codex plan-R2 1: a negative proof needs EVERY box. With one file |
| +// unreadable or malformed, a readable keyless box proves nothing. |
| +func TestNoKeyProvenNeedsACompleteRead(t *testing.T) { |
| + incomplete := views(t, homeBox, ukNoKey) |
| + incomplete.Complete = false |
| + if c, _ := findCandidate(regionCandidates(incomplete, "ie", testKeyFP), "uk"); c.NoKeyProven { |
| + t.Fatal("an incomplete read proved a region keyless") |
| + } |
| + ctx := context.Background() |
| + for name, extra := range map[string]string{"malformed": "{not json", "unversioned": `{"box":"uk-box-2"}`} { |
| + s := release.NewDirStore(t.TempDir()) |
| + for key, body := range map[string]string{"ie-box": homeBox, "uk-box": ukNoKey, "uk-box-2": extra} { |
| + if _, err := s.Put(ctx, "control/edges/"+key+".json", []byte(body), ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + snap, err := readEdgesSnapshot(ctx, s) |
| + if err != nil || snap.Complete { |
| + t.Fatalf("%s: snapshot complete=%v err=%v, want incomplete", name, snap.Complete, err) |
| + } |
| + if c, _ := findCandidate(regionCandidates(snap, "ie", testKeyFP), "uk"); c.NoKeyProven { |
| + t.Fatalf("%s: proved keyless with a box unassessed", name) |
| + } |
| + } |
| + s := release.NewDirStore(t.TempDir()) |
| + for key, body := range map[string]string{"ie-box": homeBox, "uk-box": ukNoKey, "uk-box-2": ukEligible} { |
| + if _, err := s.Put(ctx, "control/edges/"+key+".json", []byte(body), ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + snap, err := readEdgesSnapshot(ctx, failingGet{Store: s, key: "control/edges/uk-box-2.json"}) |
| + if err != nil || snap.Complete { |
| + t.Fatalf("failed GET: complete=%v err=%v, want incomplete", snap.Complete, err) |
| + } |
| + if c, _ := findCandidate(regionCandidates(snap, "ie", testKeyFP), "uk"); c.NoKeyProven { |
| + t.Fatal("a failed GET let the readable keyless box prove the region keyless") |
| + } |
| +} |
| + |
| +type failingGet struct { |
| + release.Store |
| + key string |
| +} |
| + |
| +func (f failingGet) Get(ctx context.Context, key string) ([]byte, string, error) { |
| + if key == f.key { |
| + return nil, "", errors.New("read timeout") |
| + } |
| + return f.Store.Get(ctx, key) |
| +} |
| + |
| +// codex plan-R2 2: a candidate's coordinates come from its ELIGIBLE boxes |
| +// only; ties among them go to the lexically smallest AWS region. |
| +func TestCandidateLocationComesFromEligibleBoxes(t *testing.T) { |
| + syd1 := edgeJSON("uk-syd-1", "2.2.2.1", "ap-southeast-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true)}) |
| + syd2 := edgeJSON("uk-syd-2", "2.2.2.2", "ap-southeast-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(false), fp: testKeyFP}) |
| + c, _ := findCandidate(regionCandidates(views(t, homeBox, ukEligible, syd1, syd2), "ie", testKeyFP), "uk") |
| + if !c.Eligible || c.AWSRegion != "eu-west-2" { |
| + t.Fatalf("mixed eligibility located uk at %q, want the eligible box's eu-west-2", c.AWSRegion) |
| + } |
| + dub := edgeJSON("uk-dub", "2.2.2.3", "eu-west-1", "uk", eligibleBox) |
| + if c, _ := findCandidate(regionCandidates(views(t, homeBox, ukEligible, dub), "ie", testKeyFP), "uk"); c.AWSRegion != "eu-west-1" { |
| + t.Fatalf("eligible tie = %q, want the lexically smallest eu-west-1", c.AWSRegion) |
| + } |
| +} |
| + |
| func TestRegionCandidatesFoldTheHomesName(t *testing.T) { |
| coded := edgeJSON("ie2", "5.6.7.9", "eu-west-1", "ie", eligibleBox) |
| cands := regionCandidates(views(t, homeBox, coded), "ie", testKeyFP) |
| @@ -2853,13 +2969,13 @@ func TestRegionCandidatesLogAMissingPointOnce(t *testing.T) { |
| } |
| } |
| -func TestReadEdgesFilesStrictReportsListFailure(t *testing.T) { |
| - if _, err := readEdgesFilesStrict(context.Background(), failingList{}); err == nil { |
| +func TestReadEdgesSnapshotReportsListFailure(t *testing.T) { |
| + if _, err := readEdgesSnapshot(context.Background(), failingList{}); err == nil { |
| t.Fatal("a failed listing must be an error, not an empty answer (codex R2-2)") |
| } |
| - files, err := readEdgesFilesStrict(context.Background(), release.NewDirStore(t.TempDir())) |
| - if err != nil || len(files) != 0 { |
| - t.Fatalf("an empty prefix = %v %v", files, err) |
| + snap, err := readEdgesSnapshot(context.Background(), release.NewDirStore(t.TempDir())) |
| + if err != nil || len(snap.Files) != 0 || !snap.Complete { |
| + t.Fatalf("an empty prefix = %+v %v, want complete and empty", snap, err) |
| } |
| } |
| @@ -2905,7 +3021,7 @@ the current etag (`provision.go:775`), so calling it twice is safe. |
| - [ ] **Step 2: Run them to verify they fail** |
| -Run: `go test ./internal/console -run 'TestRegionCandidates|TestReadEdgesFilesStrict|TestAppPolicyStates' -count=1` |
| +Run: `go test ./internal/console -run 'TestRegionCandidates|TestReadEdgesSnapshot|TestAppPolicyStates|TestNoKeyProven|TestCandidateLocation' -count=1` |
| Expected: FAIL, undefined. |
| - [ ] **Step 3: Extend the view and add the strict read** |
| @@ -2929,23 +3045,33 @@ func readEdgesFiles(ctx context.Context, s release.Store) []edgesFilePlacementVi |
| if s == nil { |
| return nil |
| } |
| - out, err := readEdgesFilesStrict(ctx, s) |
| + snap, err := readEdgesSnapshot(ctx, s) |
| if err != nil { |
| log.Printf("console: api instances: could not list %s for the placement join: %v", edgesFilePrefix, err) |
| return nil |
| } |
| - return out |
| + return snap.Files |
| } |
| -// readEdgesFilesStrict REPORTS a failed listing instead of answering |
| -// empty: the door must tell "could not read" (503, C28) from "no box |
| -// publishes this code" (codex R2-2). Single bad files are still skipped. |
| -func readEdgesFilesStrict(ctx context.Context, s release.Store) ([]edgesFilePlacementView, error) { |
| +// edgesSnapshot is one read of control/edges/. Complete is false when any |
| +// listed file could not be read or parsed: positive facts about the boxes |
| +// that WERE read still hold, but nothing may be concluded about "every |
| +// box" (R9's NoKeyProven, codex plan-R2 1). |
| +type edgesSnapshot struct { |
| + Files []edgesFilePlacementView |
| + Complete bool |
| +} |
| + |
| +// readEdgesSnapshot REPORTS a failed listing instead of answering empty: |
| +// the door must tell "could not read" (503, C28) from "no box publishes |
| +// this code" (codex R2-2). A single unreadable or malformed file is |
| +// skipped, as before, and marks the snapshot incomplete. |
| +func readEdgesSnapshot(ctx context.Context, s release.Store) (edgesSnapshot, error) { |
| keys, err := s.List(ctx, edgesFilePrefix) |
| if err != nil { |
| - return nil, err |
| + return edgesSnapshot{}, err |
| } |
| - var out []edgesFilePlacementView |
| + snap := edgesSnapshot{Complete: true} |
| for _, key := range keys { |
| name, ok := strings.CutPrefix(key, edgesFilePrefix) |
| if !ok || !strings.HasSuffix(name, ".json") || strings.Contains(name, "/") { |
| @@ -2954,16 +3080,18 @@ func readEdgesFilesStrict(ctx context.Context, s release.Store) ([]edgesFilePlac |
| body, _, err := s.Get(ctx, key) |
| if err != nil { |
| log.Printf("console: api instances: could not read %s for the placement join: %v", key, err) |
| + snap.Complete = false |
| continue |
| } |
| var f edgesFilePlacementView |
| if err := json.Unmarshal(body, &f); err != nil || f.V < 1 || f.Box == "" { |
| log.Printf("console: api instances: skipping %s in the placement join: not a versioned edges file", key) |
| + snap.Complete = false |
| continue |
| } |
| - out = append(out, f) |
| + snap.Files = append(snap.Files, f) |
| } |
| - return out, nil |
| + return snap, nil |
| } |
| ``` |
| @@ -3018,10 +3146,12 @@ func (c regionCandidate) unavailableReason() string { |
| func isTrue(b *bool) bool { return b != nil && *b } |
| -// regionCandidates folds the edges files into candidates. expectedFP is |
| +// regionCandidates folds an edges snapshot into candidates. expectedFP is |
| // the console's own secrets fingerprint; "" means it cannot judge, and then |
| // no coded region is KeyOK. The home is exempt from every eligibility fact. |
| -func regionCandidates(files []edgesFilePlacementView, homeName, expectedFP string) []regionCandidate { |
| +// A coded candidate's AWS region comes only from its ELIGIBLE boxes (codex |
| +// plan-R2 2); NoKeyProven needs a complete snapshot (codex plan-R2 1). |
| +func regionCandidates(snap edgesSnapshot, homeName, expectedFP string) []regionCandidate { |
| type acc struct { |
| c regionCandidate |
| regions map[string]int |
| @@ -3039,7 +3169,7 @@ func regionCandidates(files []edgesFilePlacementView, homeName, expectedFP strin |
| } |
| h := get("", true) |
| h.c.Reachable, h.c.Hosting, h.c.KeyOK, h.c.Eligible = true, true, true, true |
| - for _, f := range files { |
| + for _, f := range snap.Files { |
| home := f.Placement == "" || (homeName != "" && f.Placement == homeName) |
| if !home && !provision.ValidRegion(f.Placement) { |
| continue // fleet/<name> and malformed codes are never candidates |
| @@ -3052,10 +3182,10 @@ func regionCandidates(files []edgesFilePlacementView, homeName, expectedFP strin |
| if f.EIP == "" { |
| continue |
| } |
| - if f.Region != "" { |
| - a.regions[f.Region]++ |
| - } |
| if home { |
| + if f.Region != "" { |
| + a.regions[f.Region]++ |
| + } |
| continue |
| } |
| hosting := isTrue(f.HibernationConfigured) || f.HibernationConfigured == nil |
| @@ -3070,12 +3200,15 @@ func regionCandidates(files []edgesFilePlacementView, homeName, expectedFP strin |
| a.unknownWriter = a.unknownWriter || f.AcceptsInbound == nil |
| } |
| a.c.Eligible = a.c.Eligible || eligible |
| + if eligible && f.Region != "" { |
| + a.regions[f.Region]++ |
| + } |
| } |
| out := make([]regionCandidate, 0, len(by)) |
| for _, a := range by { |
| a.c.AWSRegion = majority(a.c.Code, a.regions) |
| - a.c.NoKeyProven = !a.c.Home && expectedFP != "" && a.hostingBoxes > 0 && !a.unknownWriter && !a.keyFound |
| - if a.c.Reachable { |
| + a.c.NoKeyProven = !a.c.Home && snap.Complete && expectedFP != "" && a.hostingBoxes > 0 && !a.unknownWriter && !a.keyFound |
| + if a.c.Eligible { |
| if _, ok := geo.LookupAWSRegion(a.c.AWSRegion); !ok { |
| if _, said := noPointSaid.LoadOrStore(a.c.Code, true); !said { |
| log.Printf("console: region %q has no point in the shipped AWS region table (AWS region %q), so region guesses leave it out", a.c.Code, a.c.AWSRegion) |
| @@ -3090,8 +3223,18 @@ func regionCandidates(files []edgesFilePlacementView, homeName, expectedFP strin |
| // regionCands is regionCandidates with this console's home and expected |
| // secrets fingerprint. |
| -func (s *Server) regionCands(files []edgesFilePlacementView) []regionCandidate { |
| - return regionCandidates(files, s.cfg.HomeRegion, s.expectedSecretsFP()) |
| +func (s *Server) regionCands(snap edgesSnapshot) []regionCandidate { |
| + return regionCandidates(snap, s.cfg.HomeRegion, s.expectedSecretsFP()) |
| +} |
| + |
| +// candsOrHome is the candidates from a fresh snapshot, or the home alone |
| +// when the listing fails: for displays and guesses that must not fail. |
| +func (s *Server) candsOrHome(ctx context.Context) []regionCandidate { |
| + snap, err := readEdgesSnapshot(ctx, s.cfg.Releases) |
| + if err != nil { |
| + return homeOnlyCandidates() |
| + } |
| + return s.regionCands(snap) |
| } |
| // majority picks the most common AWS region, ties to the lexically |
| @@ -3269,7 +3412,7 @@ var ( |
| ) |
| func TestGuessRegionOrder(t *testing.T) { |
| - loc := fakeLocator{"81.2.69.142": {-33.9, 151.2}} |
| + loc := fakeLocator{"81.2.69.142": {Lat: -33.9, Lon: 151.2}} |
| cands := []regionCandidate{cHome, cUK, cAU} |
| cases := []struct { |
| name string |
| @@ -3450,13 +3593,20 @@ func guessRegion(cands []regionCandidate, allows func(code string) bool, in gues |
| Run: `go test ./internal/console -run TestGuessRegion -count=1` |
| Expected: PASS. |
| -- [ ] **Step 5: Mutation check** |
| +- [ ] **Step 5: Mutation checks, one at a time (codex plan-R2 9)** |
| + |
| +Each mutation must fail the property test on its own, against a passing |
| +baseline. So restore and re-run between them: |
| -Temporarily change `guessRegion`'s first line to `return guessResult{}, false`. |
| -Run `go test ./internal/console -run TestGuessRegionInvariantProperty -count=1`: |
| -it must FAIL. Then change the final `if homeIn {` to `if true {` and run |
| -again: it must FAIL. Revert both changes. Run the step 4 command again, and |
| -confirm with `git diff --stat` that only the intended files changed. |
| +1. Run `go test ./internal/console -run TestGuessRegionInvariantProperty -count=1`. |
| + It must PASS; this is the baseline. |
| +2. **Mutation A:** make `guessRegion`'s first line `return guessResult{}, false`. |
| + Run the same command. It must FAIL with "guess existence false, want |
| + true". Revert, and re-run: PASS. |
| +3. **Mutation B:** change the final `if homeIn {` to `if true {`. This is a |
| + home fallback the policy does not allow. Run the same command. It must |
| + FAIL with "guess existence true, want false". Revert, and re-run: PASS. |
| +4. `git diff --stat` shows only this task's two files. |
| - [ ] **Step 6: Commit** |
| @@ -3519,7 +3669,6 @@ import ( |
| "bytes" |
| "context" |
| "encoding/json" |
| - "net" |
| "net/http" |
| "strings" |
| "testing" |
| @@ -3584,7 +3733,7 @@ func TestAPICreateAppResolvesRegion(t *testing.T) { |
| // is located; a service principal's never is (spec §6.2). |
| func TestAPICreateAppLocatesOnlyAPerson(t *testing.T) { |
| fx, token := regionFixture(t) |
| - fx.srv.cfg.GeoIP = fakeLocator{"81.2.69.142": {51.5, -0.12}} |
| + fx.srv.cfg.GeoIP = fakeLocator{"81.2.69.142": {Lat: 51.5, Lon: -0.12}} |
| if status, res := postJSONFrom(t, newAppURL(fx), map[string]string{"name": "person"}, token, "81.2.69.142"); status != http.StatusOK || |
| res["region"] != "uk" || res["region_source"] != "ip" { |
| t.Fatalf("person = %d %v", status, res) |
| @@ -3748,7 +3897,7 @@ func (s *Server) validateExplicitRegion(ctx context.Context, raw string) (string |
| if published && !(caps[code].Reachable && caps[code].Hosting) { |
| return "", &doorRefusal{http.StatusBadRequest, msgRegionNotArmed(code, s.namedCodes(hostingCaps(caps)))} |
| } |
| - if files, err := readEdgesFilesStrict(ctx, s.cfg.Releases); err == nil { |
| + if files, err := readEdgesSnapshot(ctx, s.cfg.Releases); err == nil { |
| if c, ok := findCandidate(s.regionCands(files), code); ok && c.NoKeyProven { |
| return "", &doorRefusal{http.StatusBadRequest, msgNoSecretsKey(code)} |
| } |
| @@ -3796,7 +3945,7 @@ func (s *Server) resolveNewAppRegion(ctx context.Context, rq regionReq, adopting |
| if adopting { |
| return "", "default" |
| } |
| - files, err := readEdgesFilesStrict(ctx, s.cfg.Releases) |
| + files, err := readEdgesSnapshot(ctx, s.cfg.Releases) |
| if err != nil { |
| return "", "default" |
| } |
| @@ -3888,7 +4037,7 @@ Pass `rq` to `claimApp`. In `claimOK`: |
| if err != nil { |
| log.Printf("console: claim %s/%s: read back region: %v", acct.Sqid, name, err) |
| } |
| - files, ferr := readEdgesFilesStrict(r.Context(), s.cfg.Releases) |
| + files, ferr := readEdgesSnapshot(r.Context(), s.cfg.Releases) |
| cands := homeOnlyCandidates() |
| if ferr == nil { |
| cands = s.regionCands(files) |
| @@ -4093,7 +4242,7 @@ func (s *Server) newAppPicker(ctx context.Context, ip net.IP) *regionPickerView |
| if !s.appRegionsOn() { |
| return nil |
| } |
| - files, err := readEdgesFilesStrict(ctx, s.cfg.Releases) |
| + files, err := readEdgesSnapshot(ctx, s.cfg.Releases) |
| if err != nil { |
| return nil |
| } |
| @@ -4122,7 +4271,7 @@ func (s *Server) pickedRegion(ctx context.Context, r *http.Request) (regionReq, |
| if v == "" || v == autoRegion { |
| return rq, true |
| } |
| - files, err := readEdgesFilesStrict(ctx, s.cfg.Releases) |
| + files, err := readEdgesSnapshot(ctx, s.cfg.Releases) |
| if err != nil { |
| return rq, false |
| } |
| @@ -4146,7 +4295,7 @@ func (s *Server) handleNewAppRegion(w http.ResponseWriter, r *http.Request) { |
| jsonError(w, http.StatusForbidden, "Cross-site request blocked.") |
| return |
| } |
| - files, err := readEdgesFilesStrict(r.Context(), s.cfg.Releases) |
| + files, err := readEdgesSnapshot(r.Context(), s.cfg.Releases) |
| cands := homeOnlyCandidates() |
| if err == nil { |
| cands = s.regionCands(files) |
| @@ -4196,7 +4345,7 @@ In `claimOK`, replace the audit line: |
| detail := "created app " + name |
| if s.appRegionsOn() { |
| region, _, _ := s.store.AppRegion(acct.ID, name) |
| - files, _ := readEdgesFilesStrict(r.Context(), s.cfg.Releases) |
| + files, _ := readEdgesSnapshot(r.Context(), s.cfg.Releases) |
| detail = fmt.Sprintf(regionCopy.AuditCreated, name, s.placeOf(region, s.regionCands(files))) |
| } |
| s.audit(acct, name, addr, "create", detail) |
| @@ -4449,6 +4598,9 @@ func TestExplicitCreateIntoAKeylessRegion(t *testing.T) { |
| t.Fatalf("keyless replica = %d %s, want C31", status, out) |
| } |
| mustCreate(t, u, token, `{"host":"s.go.tito.io","region":"uk","kind":"static"}`) |
| + // An addr-backed service proxies to a process the platform never starts, |
| + // so it needs no config on the box either (codex plan-R2 7). |
| + mustCreate(t, u, token, `{"host":"v.go.tito.io","region":"uk","kind":"service","addrs":["10.0.0.4:8080"]}`) |
| setEdges(t, fx, map[string]string{"uk-box": ukOldWriter}) |
| mustCreate(t, u, token, `{"host":"o.go.tito.io","region":"uk"}`) // unknown keeps today's behaviour |
| setEdges(t, fx, map[string]string{"uk-box": ukNoKey}) |
| @@ -4501,7 +4653,7 @@ Expected: FAIL. |
| // An APP-DERIVED region needs positive eligibility — the secrets key |
| // included (R9) — and never rides the dormant gate (codex R2-2). |
| if regionFrom == "app" { |
| - files, err := readEdgesFilesStrict(ctx, s.cfg.Releases) |
| + files, err := readEdgesSnapshot(ctx, s.cfg.Releases) |
| if err != nil { |
| log.Printf("console: api instances create %s/%s: read edges: %v", acct.Sqid, app, err) |
| jsonError(w, http.StatusServiceUnavailable, msgAppRegionUnconfirmed(s.homeRegionOut(region))) |
| @@ -4547,7 +4699,7 @@ of its final `switch` (`:852`): |
| // CURRENT writer proves the region keyless; unknown keeps today's |
| // behaviour, so roll order never refuses a working region. |
| if probe.DerivesPaths() || probe.Role == "replica" { |
| - if files, err := readEdgesFilesStrict(ctx, s.cfg.Releases); err == nil { |
| + if files, err := readEdgesSnapshot(ctx, s.cfg.Releases); err == nil { |
| if c, ok := findCandidate(s.regionCands(files), region); ok && c.NoKeyProven { |
| jsonError(w, http.StatusBadRequest, msgNoSecretsKey(s.homeRegionOut(region))) |
| return |
| @@ -4573,7 +4725,7 @@ At `:1066`, after `out := recordView(rec)`: |
| ```go |
| out.RegionFrom = regionFrom |
| if rec.Role == "" && placed == "" { |
| - out.RegionName = s.placeOf(rec.Region, s.regionCands(readEdgesFiles(ctx, s.cfg.Releases))) |
| + out.RegionName = s.placeOf(rec.Region, s.candsOrHome(ctx)) |
| } |
| ``` |
| @@ -4583,6 +4735,11 @@ Run: `go test ./internal/console -count=1` |
| Expected: PASS, including `TestAPIInstanceCreate*` and the |
| placement-door tests. |
| +Then a mutation check on the R9 gate's scope. Change |
| +`if probe.DerivesPaths() || probe.Role == "replica" {` to `if true {`, and |
| +run `go test ./internal/console -run TestExplicitCreateIntoAKeylessRegion -count=1`. |
| +It must FAIL on the static or addr-backed create. Revert, and re-run: PASS. |
| + |
| - [ ] **Step 7: Commit** |
| ```bash |
| @@ -4608,12 +4765,14 @@ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>" |
| ```go |
| type appDefaultView struct { |
| - Region string `json:"region"` // the STORED region, under its name |
| - Name string `json:"name"` |
| - Source string `json:"source"` |
| - Effective string `json:"effective"` // where a no-region create lands |
| - Available *bool `json:"available,omitempty"` |
| - Reason string `json:"reason,omitempty"` |
| + Region string `json:"region"` // the STORED region, under its name; "" when unknown |
| + Name string `json:"name"` |
| + Source string `json:"source"` |
| + Effective string `json:"effective"` // where a no-region create lands |
| + Available *bool `json:"available,omitempty"` |
| + Reason string `json:"reason,omitempty"` |
| + RegionKnown bool `json:"region_known"` // the stored region was read |
| + PlacementKnown bool `json:"placement_known"` // the manifest and fleet placement were read |
| } |
| func (s *Server) appDefault(ctx context.Context, acct *Account, app string) appDefaultView |
| func (s *Server) handleAPIRegionGuess(w http.ResponseWriter, r *http.Request) |
| @@ -4622,6 +4781,12 @@ func (s *Server) handleAPIRegionGuess(w http.ResponseWriter, r *http.Request) |
| `GET …/regions` entries gain `aws_region` and `name`. The response gains |
| `app_default`. |
| +`region_known` and `placement_known` are additive fields this plan adds to |
| +spec §9.3's block (codex plan-R2 3). A response is always HTTP 200, so the |
| +CLI needs them to tell a confirmed fact from one the console could not read. |
| +`deploy --region` needs the stored region, and `canary --region` needs the |
| +fleet placement. Each refuses when its fact is unknown (Tasks 21, 22). |
| + |
| - [ ] **Step 1: Write the failing tests** |
| ```go |
| @@ -4631,6 +4796,7 @@ package console |
| import ( |
| "context" |
| "encoding/json" |
| + "errors" |
| "io" |
| "net/http" |
| "strings" |
| @@ -4718,6 +4884,25 @@ func TestAppDefaultReasons(t *testing.T) { |
| } |
| } |
| +// codex plan-R2 3: a fact the console could not read is never presented as |
| +// known: no home-valued region, no non-fleet effective. |
| +func TestAppDefaultKnownFlags(t *testing.T) { |
| + acct := &Account{ID: OpsAccountID, Sqid: "bab"} |
| + fx, _, _ := fillFixture(t, "uk") |
| + if d := fx.srv.appDefault(t.Context(), acct, "titogo"); !d.RegionKnown || !d.PlacementKnown || d.Region != "uk" { |
| + t.Fatalf("readable = %+v", d) |
| + } |
| + fx.srv.appRegionOf = func(int64, string) (string, string, error) { return "", "", errors.New("db gone") } |
| + if d := fx.srv.appDefault(t.Context(), acct, "titogo"); d.RegionKnown || !d.PlacementKnown || d.Region != "" || d.Effective != "" || d.Available != nil { |
| + t.Fatalf("unreadable region = %+v, want region unknown and nothing presented as fact", d) |
| + } |
| + fx2, _, _ := fillFixture(t, "uk") |
| + corruptManifest(t, fx2) |
| + if d := fx2.srv.appDefault(t.Context(), acct, "titogo"); !d.RegionKnown || d.PlacementKnown || d.Reason != "policy-unreadable" { |
| + t.Fatalf("unreadable policy = %+v, want placement unknown", d) |
| + } |
| +} |
| + |
| func TestRegionGuessEndpoint(t *testing.T) { |
| fx, token, _ := fillFixture(t, "") |
| status, _, out := rawReq(t, http.MethodPost, guessURL(fx), token, `{"tz":"Europe/London"}`) |
| @@ -4818,31 +5003,34 @@ import ( |
| // appDefaultView is §9.3's app_default. region is the STORED region (what |
| // deploy --region compares against); effective is where a no-region create |
| // lands — the fleet itself for a placed app, never the platform home |
| -// (codex R2-4). |
| +// (codex R2-4). RegionKnown and PlacementKnown say which facts were READ: |
| +// an unread fact is left empty, never filled with the home (codex |
| +// plan-R2 3). |
| type appDefaultView struct { |
| - Region string `json:"region"` |
| - Name string `json:"name"` |
| - Source string `json:"source"` |
| - Effective string `json:"effective"` |
| - Available *bool `json:"available,omitempty"` |
| - Reason string `json:"reason,omitempty"` |
| + Region string `json:"region"` |
| + Name string `json:"name"` |
| + Source string `json:"source"` |
| + Effective string `json:"effective"` |
| + Available *bool `json:"available,omitempty"` |
| + Reason string `json:"reason,omitempty"` |
| + RegionKnown bool `json:"region_known"` |
| + PlacementKnown bool `json:"placement_known"` |
| } |
| func boolp(b bool) *bool { return &b } |
| func (s *Server) appDefault(ctx context.Context, acct *Account, app string) appDefaultView { |
| - stored, source, err := s.appRegion(acct.ID, app) |
| - files, ferr := readEdgesFilesStrict(ctx, s.cfg.Releases) |
| + stored, source, rerr := s.appRegion(acct.ID, app) |
| + snap, ferr := readEdgesSnapshot(ctx, s.cfg.Releases) |
| cands := homeOnlyCandidates() |
| if ferr == nil { |
| - cands = s.regionCands(files) |
| - } |
| - v := appDefaultView{Region: s.homeRegionOut(stored), Name: s.placeOf(stored, cands), Source: source, Effective: s.homeRegionOut(stored)} |
| - if err != nil { |
| - v.Reason = "policy-unreadable" |
| - return v |
| + cands = s.regionCands(snap) |
| } |
| pol := s.appPolicy(ctx, acct, app) |
| + v := appDefaultView{RegionKnown: rerr == nil, PlacementKnown: pol.State != policyUnreadable} |
| + if rerr == nil { |
| + v.Region, v.Name, v.Source, v.Effective = s.homeRegionOut(stored), s.placeOf(stored, cands), source, s.homeRegionOut(stored) |
| + } |
| switch { |
| case pol.State == policyUnreadable: |
| v.Reason = "policy-unreadable" |
| @@ -4853,6 +5041,10 @@ func (s *Server) appDefault(ctx context.Context, acct *Account, app string) appD |
| case pol.pooled(): |
| v.Effective, v.Reason, v.Available = s.homeRegionOut(""), "pooled", boolp(true) |
| return v |
| + case rerr != nil: |
| + // The stored region is unknown, so where a create lands is too. |
| + v.Reason = "policy-unreadable" |
| + return v |
| case ferr != nil: |
| v.Reason = "edges-unreadable" |
| return v |
| @@ -4895,7 +5087,7 @@ func (s *Server) handleAPIRegionGuess(w http.ResponseWriter, r *http.Request) { |
| if ip == nil { |
| ip = s.personIP(r) |
| } |
| - files, err := readEdgesFilesStrict(r.Context(), s.cfg.Releases) |
| + files, err := readEdgesSnapshot(r.Context(), s.cfg.Releases) |
| cands := homeOnlyCandidates() |
| if err == nil { |
| cands = s.regionCands(files) |
| @@ -4924,7 +5116,7 @@ In `apiregions.go`, change the gate line to |
| Name string `json:"name,omitempty"` |
| } |
| set, _ := armedPlacements(r.Context(), s.cfg.Releases) |
| - cands := s.regionCands(readEdgesFiles(r.Context(), s.cfg.Releases)) |
| + cands := s.candsOrHome(r.Context()) |
| named := func(code string) region { |
| c, _ := findCandidate(cands, s.homeRegionIn(code)) |
| return region{Code: code, AWSRegion: c.AWSRegion, Name: s.placeOf(s.homeRegionIn(code), cands)} |
| @@ -4956,16 +5148,30 @@ In `authcli.go`: |
| s.cliRoute("POST /api/cli/apps/{account}/{app}/regions/guess", svcrole.Allow(svcrole.Instance), s.handleAPIRegionGuess) |
| ``` |
| +Then add the new route to `exemptFromLegacyPin` in |
| +`internal/console/svcroutes_test.go`, beside `GET …/regions`. |
| +`TestInstanceRoleMatchesTodaysAppTokenReach` pins every `instance`-role route |
| +to what an app token reached before service principals, and the guess route |
| +did not exist then: |
| + |
| +```go |
| + // The region guess (nearest-region spec §9.4) stands on GET .../regions' |
| + // footing: the same deployment topology, filtered by the app's own |
| + // policy, for a hint the caller supplies. It did not exist before |
| + // service principals, and titogo's per-instance picker is its consumer. |
| + "POST /api/cli/apps/{account}/{app}/regions/guess": true, |
| +``` |
| + |
| - [ ] **Step 5: Run the tests to verify they pass** |
| Run: `go test ./internal/console -count=1` |
| Expected: PASS, including the existing `/regions` tests, whose `regions` and |
| -`default` must be unchanged. |
| +`default` must be unchanged, and `TestInstanceRoleMatchesTodaysAppTokenReach`. |
| - [ ] **Step 6: Commit** |
| ```bash |
| -git add internal/console/regiondefault.go internal/console/regiondefault_test.go internal/console/apiregions.go internal/console/authcli.go |
| +git add internal/console/regiondefault.go internal/console/regiondefault_test.go internal/console/apiregions.go internal/console/authcli.go internal/console/svcroutes_test.go |
| git commit -m "console: app_default with every reason, place names, POST regions/guess (nearest-region §9.3, §9.4) |
| Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>" |
| @@ -5061,7 +5267,7 @@ In `handleAppInstances`, after the placement section: |
| case d.Region != s.cfg.HomeRegion: |
| p.RegionLine = fmt.Sprintf(regionCopy.StartsIn, d.Name) |
| default: |
| - if files, err := readEdgesFilesStrict(r.Context(), s.cfg.Releases); err == nil && eligibleCount(s.regionCands(files)) >= 2 { |
| + if files, err := readEdgesSnapshot(r.Context(), s.cfg.Releases); err == nil && eligibleCount(s.regionCands(files)) >= 2 { |
| p.RegionLine = fmt.Sprintf(regionCopy.StartsIn, d.Name) |
| } |
| } |
| @@ -5242,14 +5448,14 @@ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>" |
| --- |
| -### Task 18: CLI copy (C9 to C15, C13a, C13b, C15b, C22, C25, C30, C30b, from the record) |
| +### Task 18: CLI copy (C9 to C15, C13a, C13b, C15b, C15c, C22, C25, C30, C30b, from the record) |
| **Files:** |
| - Create: `region_copy.go` (root, package `main`) |
| - Test: `region_copy_test.go` |
| **Interfaces:** |
| -- Consumes: the record's C9 to C15, C13a, C13b, C15b, C22a to C22d, C25, C30 |
| +- Consumes: the record's C9 to C15, C13a, C13b, C15b, C15c, C22a to C22d, C25, C30 |
| and C30b (Task 0). |
| - Produces: |
| @@ -5260,6 +5466,7 @@ func regionNotAppliedSwitchOff(place string) string // C13b |
| func instanceCreatedSuffix(channel, place string) string // C14 |
| func deployRegionMismatch(app, place string) string // C15 |
| func deployRegionUnreadable(app string, err error) string // C15b |
| +func deployRegionUnknown(app string) string // C15c |
| func canaryRegionMismatch(host, place string) string // C25 |
| func canaryRegionOnFleet(app, fleet string) string // C30 |
| func canaryFleetUnknown(app string) string // C30b |
| @@ -5292,7 +5499,7 @@ func TestRegionCLICopy(t *testing.T) { |
| all := []string{ |
| regionNotRecordedOldConsole, regionNotAppliedSwitchOff("P"), |
| instanceCreatedSuffix("stable", "P"), deployRegionMismatch("blog", "P"), |
| - deployRegionUnreadable("blog", errors.New("timeout")), |
| + deployRegionUnreadable("blog", errors.New("timeout")), deployRegionUnknown("blog"), |
| canaryRegionMismatch("c.go.tito.io", "P"), canaryRegionOnFleet("blog", "acme"), |
| canaryFleetUnknown("blog"), |
| flagRegionAppsCreate, flagRegionDeploy, flagRegionCanary, flagRegionInstancesCreate, |
| @@ -5359,6 +5566,11 @@ func deployRegionUnreadable(app string, err error) string { |
| return fmt.Sprintf("could not read %s's region (%v), so deploy stopped before shipping anything. Try again, or run without --region.", app, err) |
| } |
| +// C15c: the console answered, but could not read the app's region. |
| +func deployRegionUnknown(app string) string { |
| + return fmt.Sprintf("the console could not read %s's region, so deploy stopped before shipping anything. Try again, or run without --region.", app) |
| +} |
| + |
| // C25 |
| func canaryRegionMismatch(host, place string) string { |
| return fmt.Sprintf("canary %s already runs in %s. --region applies only when canary creates the host. Run carlos canary remove first to start it somewhere else.", host, place) |
| @@ -5525,9 +5737,11 @@ Run: |
| ```bash |
| go test . -run TestLocalTimezone -count=1 |
| +fail=0 |
| for t in darwin/arm64 darwin/amd64 linux/arm64 windows/amd64 windows/arm64; do |
| - CGO_ENABLED=0 GOOS=${t%/*} GOARCH=${t#*/} go build ./... || { echo "FAILED $t"; break; } |
| + CGO_ENABLED=0 GOOS=${t%/*} GOARCH=${t#*/} go build ./... || { echo "FAILED $t"; fail=1; } |
| done |
| +[ "$fail" = 0 ] || exit 1 # stops this block before any commit below it |
| ``` |
| Expected: PASS, and no `FAILED` line. |
| @@ -5653,8 +5867,24 @@ func (s *apiSession) createApp(ctx context.Context, a apiAcct, app, place string |
| }{app, place, controlPlane, buildProbe, region, timezone} |
| ``` |
| -Leave the rest of the function unchanged. Update every other call site with |
| -`, "", ""` (tests) or the real values (steps 4 and 5). |
| +Leave the rest of the function unchanged. Update every other call site |
| +(codex plan-R2 5). The callers are: |
| + |
| +- the tests: `apimode_test.go:3041`, `:3052`, `:3060`, `:3066`, `:3099`, |
| + `:3106` and `apps_test.go:311`, each gaining `, "", ""`; |
| +- `apps.go:497` and `deploy.go:536`, which get the real values in steps 4 |
| + and 5. |
| + |
| +`offerCreateApp` also changes signature in step 5, so update its one test |
| +caller, `deploy_wizard_test.go:104`, to pass `""` as the new last argument: |
| + |
| +```go |
| + app := offerCreateApp(context.Background(), s, apiAcct{Seg: "bdk", Exact: true}, |
| + &noAppsError{Account: "tom@keymail.dev"}, "") |
| +``` |
| + |
| +Run `grep -n 'createApp(\|offerCreateApp(' *.go` afterwards. Every match |
| +must carry the new arity. |
| - [ ] **Step 4: Add `printAppRegion` and wire `apps create`** |
| @@ -5737,7 +5967,7 @@ reaches a real console. |
| - [ ] **Step 7: Commit** |
| ```bash |
| -git add apimode.go apps.go apps_region.go apps_region_test.go deploy.go apps_test.go |
| +git add apimode.go apps.go apps_region.go apps_region_test.go deploy.go apps_test.go apimode_test.go deploy_wizard_test.go |
| git commit -m "cli: apps create and deploy onboarding send a timezone, take --region, print the region line (nearest-region §11) |
| Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>" |
| @@ -5753,11 +5983,16 @@ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>" |
| - Test: `deploy_region_test.go`, `deploy_region_e2e_test.go` |
| **Interfaces:** |
| -- Consumes: `deployRegionMismatch` and `deployRegionUnreadable` (Task 18). |
| +- Consumes: `deployRegionMismatch`, `deployRegionUnreadable` and |
| + `deployRegionUnknown` (Task 18). |
| - Produces: |
| ```go |
| -type apiAppDefault struct{ Region, Name, Source, Effective, Reason string; Available *bool } |
| +type apiAppDefault struct { |
| + Region, Name, Source, Effective, Reason string |
| + Available *bool |
| + RegionKnown, PlacementKnown bool |
| +} |
| type apiRegionEntry struct{ Code, Name string } |
| type apiRegionsResult struct { |
| Regions []apiRegionEntry `json:"regions"` |
| @@ -5785,8 +6020,8 @@ import ( |
| // codex plan-R1 2: a READ failure stops deploy; only a successful response |
| // without app_default (an older console) skips the check. |
| func TestDeployRegionPreflight(t *testing.T) { |
| - uk := &apiAppDefault{Region: "uk", Name: "P-uk", Effective: "uk"} |
| - fleet := &apiAppDefault{Region: "uk", Name: "P-uk", Effective: "fleet/acme", Reason: "fleet"} |
| + uk := &apiAppDefault{Region: "uk", Name: "P-uk", Effective: "uk", RegionKnown: true, PlacementKnown: true} |
| + fleet := &apiAppDefault{Region: "uk", Name: "P-uk", Effective: "fleet/acme", Reason: "fleet", RegionKnown: true, PlacementKnown: true} |
| boom := errors.New("500 from console") |
| cases := []struct { |
| name string |
| @@ -5802,6 +6037,8 @@ func TestDeployRegionPreflight(t *testing.T) { |
| {"read failed", apiRegionsResult{}, boom, "ie", deployRegionUnreadable("blog", boom)}, |
| {"fleet, stored equal", apiRegionsResult{AppDefault: fleet}, nil, "uk", ""}, |
| {"fleet, stored different", apiRegionsResult{AppDefault: fleet}, nil, "ie", deployRegionMismatch("blog", "P-uk")}, |
| + // codex plan-R2 3: a 200 whose stored region could not be read. |
| + {"region unknown", apiRegionsResult{AppDefault: &apiAppDefault{RegionKnown: false, PlacementKnown: true}}, nil, "ie", deployRegionUnknown("blog")}, |
| } |
| for _, c := range cases { |
| err := deployRegionPreflight(c.res, c.err, "blog", c.asked) |
| @@ -5830,12 +6067,14 @@ In `apimode.go`: |
| // GET …/regions (nearest-region spec §9.3). AppDefault is nil from a |
| // console that predates it. |
| type apiAppDefault struct { |
| - Region string `json:"region"` |
| - Name string `json:"name"` |
| - Source string `json:"source"` |
| - Effective string `json:"effective"` |
| - Reason string `json:"reason"` |
| - Available *bool `json:"available"` |
| + Region string `json:"region"` |
| + Name string `json:"name"` |
| + Source string `json:"source"` |
| + Effective string `json:"effective"` |
| + Reason string `json:"reason"` |
| + Available *bool `json:"available"` |
| + RegionKnown bool `json:"region_known"` |
| + PlacementKnown bool `json:"placement_known"` |
| } |
| type apiRegionEntry struct { |
| @@ -5880,9 +6119,11 @@ In `deploy.go`: |
| ```go |
| // deployRegionPreflight is spec §11: --region on an EXISTING app is |
| // compared with the app's STORED region before anything ships. A failed |
| -// read STOPS the deploy (C15b): skipping it would ship to whatever the app |
| -// already is. Only a successful response without app_default — an older |
| -// console — skips the check. |
| +// read STOPS the deploy (C15b), and so does a successful answer whose |
| +// stored region the console could not read (C15c, codex plan-R2 3): |
| +// either way the region is unknown, and shipping would go to whatever the |
| +// app already is. Only a successful response without app_default — an |
| +// older console — skips the check. |
| func deployRegionPreflight(res apiRegionsResult, readErr error, app, asked string) error { |
| if asked == "" { |
| return nil |
| @@ -5890,7 +6131,13 @@ func deployRegionPreflight(res apiRegionsResult, readErr error, app, asked strin |
| if readErr != nil { |
| return errors.New(deployRegionUnreadable(app, readErr)) |
| } |
| - if res.AppDefault == nil || res.AppDefault.Region == asked { |
| + if res.AppDefault == nil { |
| + return nil |
| + } |
| + if !res.AppDefault.RegionKnown { |
| + return errors.New(deployRegionUnknown(app)) |
| + } |
| + if res.AppDefault.Region == asked { |
| return nil |
| } |
| return errors.New(deployRegionMismatch(app, res.AppDefault.Name)) |
| @@ -5934,7 +6181,6 @@ console, and no release object lands in the store. |
| package main |
| import ( |
| - "bytes" |
| "context" |
| "encoding/json" |
| "net/http" |
| @@ -5955,20 +6201,23 @@ import ( |
| // signed-in CLI credentials saved, and a front that can fail or strip the |
| // /regions answer and counts every write reaching the console. |
| type regionE2E struct { |
| + bin string // built before useTempCredentials changes directory |
| + creds string |
| url string |
| store release.Store |
| - mode atomic.Value // "", "fail", "legacy" |
| + mode atomic.Value // "", "fail", "legacy", "region-unknown", "placement-unknown" |
| writes atomic.Int64 |
| cst *console.Store |
| } |
| func regionE2EConsole(t *testing.T, appRegion string) *regionE2E { |
| t.Helper() |
| - useTempCredentials(t) |
| + bin := buildCarlos(t) // first: useTempCredentials chdirs out of the module |
| + creds := useTempCredentials(t) |
| clearDeploymentEnv(t) |
| const addr, app = "paul@keymail.dev", "titogo" |
| tmp := t.TempDir() |
| - e := ®ionE2E{store: release.NewDirStore(filepath.Join(tmp, "store"))} |
| + e := ®ionE2E{bin: bin, creds: creds, store: release.NewDirStore(filepath.Join(tmp, "store"))} |
| e.mode.Store("") |
| seed, pubB64, err := release.GenerateKey() |
| if err != nil { |
| @@ -5997,12 +6246,22 @@ func regionE2EConsole(t *testing.T, appRegion string) *regionE2E { |
| case "fail": |
| http.Error(w, `{"error":"boom"}`, http.StatusInternalServerError) |
| return |
| - case "legacy": |
| + case "legacy", "region-unknown", "placement-unknown": |
| + // A successful 200 whose body an older console, or a |
| + // console that could not read one fact, would send. |
| rec := httptest.NewRecorder() |
| srv.ServeHTTP(rec, r) |
| var m map[string]any |
| json.Unmarshal(rec.Body.Bytes(), &m) |
| - delete(m, "app_default") |
| + d, _ := m["app_default"].(map[string]any) |
| + switch e.mode.Load().(string) { |
| + case "legacy": |
| + delete(m, "app_default") |
| + case "region-unknown": |
| + d["region_known"], d["region"], d["effective"] = false, "", "" |
| + case "placement-unknown": |
| + d["placement_known"], d["reason"] = false, "policy-unreadable" |
| + } |
| w.Header().Set("Content-Type", "application/json") |
| json.NewEncoder(w).Encode(m) |
| return |
| @@ -6044,8 +6303,8 @@ func regionE2EConsole(t *testing.T, appRegion string) *regionE2E { |
| // config in reach. |
| func (e *regionE2E) run(t *testing.T, args ...string) (string, error) { |
| t.Helper() |
| - cmd := exec.Command(buildCarlos(t), args...) |
| - cmd.Env = carlosTestEnv(t, "CARLOS_CREDENTIALS="+os.Getenv("CARLOS_CREDENTIALS")) |
| + cmd := exec.Command(e.bin, args...) |
| + cmd.Env = carlosTestEnv(t, "CARLOS_CREDENTIALS="+e.creds) |
| cmd.Dir = t.TempDir() |
| out, err := cmd.CombinedOutput() |
| return string(out), err |
| @@ -6077,22 +6336,27 @@ func artifact(t *testing.T) string { |
| func TestDeployRegionRefusalsShipNothing(t *testing.T) { |
| for _, c := range []struct{ mode, want string }{ |
| - {"", "already runs in"}, // the mismatch (C15); the stored region is uk |
| - {"fail", "deploy stopped before shipping anything"}, // C15b |
| + {"", deployRegionMismatch("titogo", "")[:20]}, // C15: the stored region is uk |
| + {"fail", "deploy stopped before shipping anything"}, // C15b |
| + {"region-unknown", deployRegionUnknown("titogo")[:30]}, // C15c: a 200 that does not know |
| } { |
| - e := regionE2EConsole(t, "uk") |
| - e.mode.Store(c.mode) |
| - out, err := e.run(t, "deploy", "--app", "titogo", "--region", "ie", "--console", e.url, |
| - "--version", "abc1234", "--timeout", "1s", artifact(t)) |
| - if err == nil || !bytes.Contains([]byte(out), []byte(c.want)) { |
| - t.Fatalf("mode %q: err=%v out=%s, want a refusal containing %q", c.mode, err, out, c.want) |
| - } |
| - if n := e.writes.Load(); n != 0 { |
| - t.Fatalf("mode %q: %d write(s) reached the console before the refusal", c.mode, n) |
| - } |
| - if n := e.releases(t); n != 0 { |
| - t.Fatalf("mode %q: %d release object(s) were shipped", c.mode, n) |
| - } |
| + // A subtest each: useTempCredentials chdirs, and only a subtest |
| + // restores the directory before the next case builds the binary. |
| + t.Run("mode="+c.mode, func(t *testing.T) { |
| + e := regionE2EConsole(t, "uk") |
| + e.mode.Store(c.mode) |
| + out, err := e.run(t, "deploy", "--app", "titogo", "--region", "ie", "--console", e.url, |
| + "--channel", "canary/e2e", "--version", "abc1234", "--timeout", "1s", artifact(t)) |
| + if err == nil || !strings.Contains(out, c.want) { |
| + t.Fatalf("err=%v out=%s, want a refusal containing %q", err, out, c.want) |
| + } |
| + if n := e.writes.Load(); n != 0 { |
| + t.Fatalf("%d write(s) reached the console before the refusal", n) |
| + } |
| + if n := e.releases(t); n != 0 { |
| + t.Fatalf("%d release object(s) were shipped", n) |
| + } |
| + }) |
| } |
| } |
| @@ -6102,22 +6366,24 @@ func TestDeployRegionSkipsAgainstAnOlderConsole(t *testing.T) { |
| e := regionE2EConsole(t, "uk") |
| e.mode.Store("legacy") |
| out, _ := e.run(t, "deploy", "--app", "titogo", "--region", "ie", "--console", e.url, |
| - "--version", "abc1234", "--timeout", "1s", artifact(t)) |
| - if strings.Contains(out, "already runs in") || strings.Contains(out, "deploy stopped before shipping") { |
| + "--channel", "canary/e2e", "--version", "abc1234", "--timeout", "1s", artifact(t)) |
| + if strings.Contains(out, deployRegionMismatch("titogo", "")[:20]) || strings.Contains(out, "deploy stopped before shipping") { |
| t.Fatalf("an older console refused: %s", out) |
| } |
| - if e.writes.Load() == 0 { |
| - t.Fatalf("deploy never reached the ship: %s", out) |
| + if e.writes.Load() == 0 || e.releases(t) == 0 { |
| + t.Fatalf("deploy never shipped: %s", out) |
| } |
| } |
| ``` |
| -The two `want` fragments are substrings of the draft C15 and C15b. If the |
| -approved text differs, use substrings of the approved text. |
| +The `want` fragments come from the copy functions themselves, so they follow |
| +the approved text. The C15b fragment is a literal substring of its draft; if |
| +review changed it, use a substring of the approved text. |
| `useTempCredentials`, `clearDeploymentEnv`, `e2eDeviceLogin`, |
| `loadCredentials`, `credential` and `buildCarlos` are the existing root test |
| helpers used by `TestE2EDeployThroughConsole` |
| -(`deploy_e2e_test.go:266-330`). |
| +(`deploy_e2e_test.go:266-330`). `useTempCredentials` calls `t.Chdir` |
| +(`auth_test.go:26`), which is why the binary is built first. |
| - [ ] **Step 6: Run the command-level tests** |
| @@ -6149,11 +6415,19 @@ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>" |
| - Consumes: |
| - Task 18: `canaryRegionOnFleet`, `canaryFleetUnknown`, |
| `canaryRegionMismatch` and `instanceCreatedSuffix`; |
| - - Task 21: `appRegions`, `apiRegionsResult.nameOf` and `regionE2EConsole`. |
| + - Task 21: `appRegions`, `apiRegionsResult.nameOf`, `apiAppDefault` |
| + with `PlacementKnown`, and `regionE2EConsole`. |
| - Produces: |
| - - `func canaryRegionCheck(res apiRegionsResult, readErr error, recs []apiInstanceRecord, app, host, asked string) error`; |
| + - `func canaryRegionCheck(res apiRegionsResult, readErr error, recs []apiInstanceRecord, app, hostPrefix, asked string) error`; |
| - `func instanceCreatedLine(rec apiInstanceRecord, envSuffix string) string`. |
| +The check runs once, before `instancesEnable` (`canary.go:381`), which is a |
| +PUT. It uses only reads: `GET …/regions`, and `GET …/instances`, which lists |
| +records with or without a manifest (`apiinstances.go:409-420`). An existing |
| +canary is found by its host prefix, `<name>.<app>.` (`canaryHost`, |
| +`canary.go:210`), so the domain the PUT would return is not needed (codex |
| +plan-R2 4). |
| + |
| - [ ] **Step 1: Write the failing unit tests** |
| ```go |
| @@ -6167,12 +6441,16 @@ import ( |
| func TestCanaryRegionCheck(t *testing.T) { |
| names := []apiRegionEntry{{Code: "ie", Name: "P-ie"}, {Code: "uk", Name: "P-uk"}} |
| - plain := apiRegionsResult{Regions: names, Default: "ie", AppDefault: &apiAppDefault{Region: "uk", Effective: "uk"}} |
| - fleet := apiRegionsResult{Regions: names, Default: "ie", AppDefault: &apiAppDefault{Region: "uk", Effective: "fleet/acme", Reason: "fleet"}} |
| + known := func(d apiAppDefault) *apiAppDefault { d.RegionKnown, d.PlacementKnown = true, true; return &d } |
| + plain := apiRegionsResult{Regions: names, Default: "ie", AppDefault: known(apiAppDefault{Region: "uk", Effective: "uk"})} |
| + fleet := apiRegionsResult{Regions: names, Default: "ie", AppDefault: known(apiAppDefault{Region: "uk", Effective: "fleet/acme", Reason: "fleet"})} |
| older := apiRegionsResult{Regions: names, Default: "ie"} // no app_default |
| + unread := apiRegionsResult{Regions: names, Default: "ie", AppDefault: &apiAppDefault{Region: "uk", Effective: "uk", RegionKnown: true, Reason: "policy-unreadable"}} |
| boom := errors.New("timeout") |
| - existingUK := []apiInstanceRecord{{Host: "c.go.tito.io", Region: "uk"}} |
| - existingHome := []apiInstanceRecord{{Host: "c.go.tito.io", Region: ""}} |
| + const prefix = "c.blog." |
| + existingUK := []apiInstanceRecord{{Host: "c.blog.go.tito.io", Region: "uk"}} |
| + existingHome := []apiInstanceRecord{{Host: "c.blog.go.tito.io", Region: ""}} |
| + other := []apiInstanceRecord{{Host: "d.blog.go.tito.io", Region: "uk"}} |
| cases := []struct { |
| name string |
| res apiRegionsResult |
| @@ -6183,21 +6461,20 @@ func TestCanaryRegionCheck(t *testing.T) { |
| }{ |
| {"no flag", fleet, boom, existingUK, "", ""}, |
| {"new canary", plain, nil, nil, "uk", ""}, |
| + {"another canary is not this one", plain, nil, other, "ie", ""}, |
| {"existing, same", plain, nil, existingUK, "uk", ""}, |
| {"existing home read as its name", plain, nil, existingHome, "ie", ""}, |
| - // codex plan-R1 18: the place name, not the bare code. |
| - {"existing, different", plain, nil, existingUK, "ie", canaryRegionMismatch("c.go.tito.io", "P-uk")}, |
| - // codex R4-2: refused identically before and after the canary exists. |
| + {"existing, different, named by place", plain, nil, existingUK, "ie", canaryRegionMismatch("c.blog.go.tito.io", "P-uk")}, |
| {"fleet, first run", fleet, nil, nil, "uk", canaryRegionOnFleet("blog", "acme")}, |
| {"fleet, repeat", fleet, nil, existingUK, "uk", canaryRegionOnFleet("blog", "acme")}, |
| - // codex plan-R1 3: an older console cannot rule a fleet out, so the |
| - // flag is refused on every run, never honoured on the first. |
| {"older console, first run", older, nil, nil, "uk", canaryFleetUnknown("blog")}, |
| {"older console, repeat", older, nil, existingUK, "uk", canaryFleetUnknown("blog")}, |
| {"read failed", plain, boom, nil, "uk", canaryFleetUnknown("blog")}, |
| + // codex plan-R2 3: a 200 whose placement the console could not read. |
| + {"placement unknown", unread, nil, nil, "uk", canaryFleetUnknown("blog")}, |
| } |
| for _, c := range cases { |
| - err := canaryRegionCheck(c.res, c.err, c.recs, "blog", "c.go.tito.io", c.asked) |
| + err := canaryRegionCheck(c.res, c.err, c.recs, "blog", prefix, c.asked) |
| got := "" |
| if err != nil { |
| got = err.Error() |
| @@ -6224,9 +6501,6 @@ func TestInstanceCreatedLine(t *testing.T) { |
| } |
| ``` |
| -`apiInstanceRecord`'s `Host`, `Channel` and `Region` fields are the ones at |
| -`apimode.go:2840-2870`. |
| - |
| - [ ] **Step 2: Run them to verify they fail** |
| Run: `go test . -run 'TestCanaryRegionCheck|TestInstanceCreatedLine' -count=1` |
| @@ -6244,27 +6518,28 @@ Add to `apiInstanceRecord`: |
| In `canary.go`: |
| ```go |
| -// canaryRegionCheck is spec §11's canary --region rule, resolved BEFORE |
| -// the create/rechannel/ready branch, so the answer never depends on |
| -// whether the canary exists yet (codex R4-2): |
| +// canaryRegionCheck is spec §11's canary --region rule, run before ANY |
| +// write, so the answer never depends on whether the canary exists yet |
| +// (codex R4-2, plan-R2 4): |
| // - the flag needs a console that can say whether a fleet governs; a |
| -// failed read, or an older console with no app_default, refuses it on |
| -// every run (C30b), because the create door would silently replace it |
| -// with the fleet; |
| +// failed read, an older console with no app_default, or one that |
| +// could not read the placement, refuses it on every run (C30b), |
| +// because the create door would silently replace it with the fleet; |
| // - a fleet-placed app refuses the flag every time (C30); |
| -// - an existing canary in another region refuses (C25), named by place. |
| -func canaryRegionCheck(res apiRegionsResult, readErr error, recs []apiInstanceRecord, app, host, asked string) error { |
| +// - an existing canary (a record under hostPrefix, "<name>.<app>.") in |
| +// another region refuses (C25), named by place. |
| +func canaryRegionCheck(res apiRegionsResult, readErr error, recs []apiInstanceRecord, app, hostPrefix, asked string) error { |
| if asked == "" { |
| return nil |
| } |
| - if readErr != nil || res.AppDefault == nil { |
| + if readErr != nil || res.AppDefault == nil || !res.AppDefault.PlacementKnown { |
| return errors.New(canaryFleetUnknown(app)) |
| } |
| if strings.HasPrefix(res.AppDefault.Effective, "fleet/") { |
| return errors.New(canaryRegionOnFleet(app, strings.TrimPrefix(res.AppDefault.Effective, "fleet/"))) |
| } |
| for _, r := range recs { |
| - if r.Host != host { |
| + if !strings.HasPrefix(r.Host, hostPrefix) { |
| continue |
| } |
| have := r.Region |
| @@ -6272,7 +6547,7 @@ func canaryRegionCheck(res apiRegionsResult, readErr error, recs []apiInstanceRe |
| have = res.Default |
| } |
| if have != asked { |
| - return errors.New(canaryRegionMismatch(host, res.nameOf(have))) |
| + return errors.New(canaryRegionMismatch(r.Host, res.nameOf(have))) |
| } |
| } |
| return nil |
| @@ -6293,43 +6568,34 @@ func instanceCreatedLine(rec apiInstanceRecord, envSuffix string) string { |
| } |
| ``` |
| -- [ ] **Step 4: Wire the flags and the output** |
| +- [ ] **Step 4: Wire the flags, the check and the output** |
| -In `runCanaryDeploy`: |
| +In `runCanaryDeploy`'s flags: |
| ```go |
| regionFlag := fs.String("region", "", flagRegionCanary) |
| ``` |
| -The check runs twice, so that every refusal precedes every write. |
| -`instancesEnable` (`canary.go:381`) is a PUT, so the fleet and console rules |
| -must run before it. |
| - |
| -Directly after `app2 := resolvedApp(...)`: |
| +After `ctx := context.Background()` and `channel := canaryChannel(canary)` |
| +(`canary.go:375-377`), and **before** the `instancesEnable` call: |
| ```go |
| - asked := strings.TrimSpace(*regionFlag) |
| - var regionRes apiRegionsResult |
| - if asked != "" { |
| - res, err := sess.appRegions(ctx, acct, app2) |
| - // No records yet: this pass applies the console and fleet rules only. |
| - if err := canaryRegionCheck(res, err, nil, app2, "", asked); err != nil { |
| + // --region is checked with reads only, before the first write (the |
| + // instancesEnable PUT below): nothing changes when it is refused. |
| + if asked := strings.TrimSpace(*regionFlag); asked != "" { |
| + res, rerr := sess.appRegions(ctx, acct, app2) |
| + existing, _, _, lerr := sess.instanceList(ctx, acct, app2, "", "") |
| + if lerr != nil { |
| + log.Fatalf("canary: list instances for %s: %v", app2, lerr) |
| + } |
| + if err := canaryRegionCheck(res, rerr, existing, app2, canary+"."+app2+".", asked); err != nil { |
| log.Fatalf("canary: %v", err) |
| } |
| - regionRes = res |
| } |
| ``` |
| -After `recs` is listed and before `canaryRecordPlan`, add the existing-canary |
| -rule: |
| - |
| -```go |
| - if asked != "" { |
| - if err := canaryRegionCheck(regionRes, nil, recs, app2, host, asked); err != nil { |
| - log.Fatalf("canary: %v", err) |
| - } |
| - } |
| -``` |
| +`ctx` is declared at `canary.go:375`, so the block must come after it (codex |
| +plan-R2 5). |
| In the create branch, pass the flag: |
| `sess.instanceCreate(ctx, acct, app2, host, strings.TrimSpace(*regionFlag), channel, recKind, env, nil, nil, nil)`. |
| @@ -6348,7 +6614,7 @@ the output to: |
| instanceCreatedLine(rec, environmentSuffix(rec.Environment))) |
| ``` |
| -- [ ] **Step 5: Write the command-level test (codex plan-R1 3, 12)** |
| +- [ ] **Step 5: Write the command-level test (codex plan-R1 3, 12; plan-R2 3, 4)** |
| ```go |
| // canary_region_e2e_test.go |
| @@ -6356,50 +6622,80 @@ package main |
| import ( |
| "context" |
| + "errors" |
| + "os" |
| "strings" |
| "testing" |
| + "time" |
| "github.com/carlosframework/platform/internal/console" |
| "github.com/carlosframework/platform/internal/provision" |
| "github.com/carlosframework/platform/internal/release" |
| ) |
| -// Two identical runs, against a fleet-placed app on the current console and |
| -// against an older console's /regions: the same refusal both times, and no |
| -// write, record or upload in between. |
| -func TestCanaryRegionRefusesIdenticallyAndWritesNothing(t *testing.T) { |
| - for _, c := range []struct{ name, mode, want string }{ |
| - {"fleet-placed app", "", canaryRegionOnFleet("titogo", "acme")}, |
| - {"older console", "legacy", canaryFleetUnknown("titogo")}, |
| - } { |
| - e := regionE2EConsole(t, "") |
| - if err := e.cst.SetAppPlacement(console.OpsAccountID, "titogo", "fleet/acme"); err != nil { |
| - t.Fatal(err) |
| - } |
| - e.mode.Store(c.mode) |
| - var outs []string |
| - for run := 0; run < 2; run++ { |
| - out, err := e.run(t, "canary", "deploy", "--app", "titogo", "--name", "probe", "--region", "uk", |
| - "--console", e.url, "--version", "abc1234", "--timeout", "1s", artifact(t)) |
| - if err == nil || !strings.Contains(out, c.want) { |
| - t.Fatalf("%s run %d: err=%v out=%s, want %q", c.name, run, err, out, c.want) |
| +// Two identical runs per case: the same refusal both times, and no write, |
| +// no new record, no manifest and no upload, before or after. |
| +func TestCanaryRegionRefusesBeforeAnyWrite(t *testing.T) { |
| + const existing = "probe.titogo.bab.oncarlos.test" |
| + cases := []struct { |
| + name, mode, region string |
| + fleet, canary bool |
| + want string |
| + }{ |
| + {"fleet-placed app", "", "uk", true, false, canaryRegionOnFleet("titogo", "acme")}, |
| + {"older console", "legacy", "uk", true, false, canaryFleetUnknown("titogo")}, |
| + {"placement unknown", "placement-unknown", "uk", false, false, canaryFleetUnknown("titogo")}, |
| + {"existing canary elsewhere", "", "ie", false, true, canaryRegionMismatch(existing, "uk")}, |
| + } |
| + for _, c := range cases { |
| + t.Run(c.name, func(t *testing.T) { |
| + e := regionE2EConsole(t, "") |
| + ctx := context.Background() |
| + if c.fleet { |
| + if err := e.cst.SetAppPlacement(console.OpsAccountID, "titogo", "fleet/acme"); err != nil { |
| + t.Fatal(err) |
| + } |
| } |
| - outs = append(outs, out) |
| - } |
| - recs, _, err := provision.ListRecords(context.Background(), e.store, release.OpsAccount, "titogo", 1000) |
| - if err != nil { |
| - t.Fatal(err) |
| - } |
| - if len(recs) != 0 || e.writes.Load() != 0 || e.releases(t) != 0 { |
| - t.Fatalf("%s: records %d, writes %d, releases %d; want none", c.name, len(recs), e.writes.Load(), e.releases(t)) |
| - } |
| + if c.canary { |
| + rec := provision.Record{Schema: 1, Host: existing, App: "titogo", Account: release.OpsAccount, |
| + Region: "uk", Channel: "canary/probe", CreatedAt: time.Now().UTC()} |
| + if err := provision.CreateRecord(ctx, e.store, rec); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + e.mode.Store(c.mode) |
| + for run := 0; run < 2; run++ { |
| + out, err := e.run(t, "canary", "deploy", "--app", "titogo", "--name", "probe", "--region", c.region, |
| + "--console", e.url, "--version", "abc1234", "--timeout", "1s", artifact(t)) |
| + if err == nil || !strings.Contains(out, c.want) { |
| + t.Fatalf("run %d: err=%v out=%s, want %q", run, err, out, c.want) |
| + } |
| + } |
| + recs, _, err := provision.ListRecords(ctx, e.store, release.OpsAccount, "titogo", 1000) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + wantRecs := 0 |
| + if c.canary { |
| + wantRecs = 1 |
| + } |
| + if len(recs) != wantRecs || e.writes.Load() != 0 || e.releases(t) != 0 { |
| + t.Fatalf("records %d (want %d), writes %d, releases %d; want nothing written", len(recs), wantRecs, e.writes.Load(), e.releases(t)) |
| + } |
| + if c.canary && recs[0].Region != "uk" { |
| + t.Fatalf("the existing canary moved to %q", recs[0].Region) |
| + } |
| + if _, err := provision.GetManifest(ctx, e.store, release.OpsAccount, "titogo"); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatalf("an instances manifest was written: %v", err) |
| + } |
| + }) |
| } |
| } |
| ``` |
| -Both refusals in this test come from the first check, which runs before |
| -`instancesEnable`'s PUT. The zero-writes assertion fails if the check is |
| -moved after it. |
| +For the existing-canary case, C25 names the region `uk` by its code: the |
| +test console has no box publishing `uk`, so `/regions` gives it no place |
| +name, and `nameOf` falls back to the code. |
| - [ ] **Step 6: Run the root tests** |
| @@ -6407,20 +6703,28 @@ Run: `go test . -count=1 -timeout 30m` |
| Expected: PASS, including the byte-for-byte output assertions in |
| `canary_test.go` and `instances_environment_test.go`. |
| +Then the ordering check: move the `--region` block from step 4 to after the |
| +`instancesEnable` call, and run |
| +`go test . -run TestCanaryRegionRefusesBeforeAnyWrite -count=1`. It must |
| +FAIL on writes. Move it back, and re-run: PASS. |
| + |
| - [ ] **Step 7: Cross-compile, then commit** |
| ```bash |
| +fail=0 |
| for t in darwin/arm64 darwin/amd64 linux/arm64 windows/amd64 windows/arm64; do |
| - CGO_ENABLED=0 GOOS=${t%/*} GOARCH=${t#*/} go build ./... || { echo "FAILED $t"; break; } |
| + CGO_ENABLED=0 GOOS=${t%/*} GOARCH=${t#*/} go build ./... || { echo "FAILED $t"; fail=1; } |
| done |
| +[ "$fail" = 0 ] || exit 1 # stops this block before any commit below it |
| git add canary.go canary_region_test.go canary_region_e2e_test.go instances_cli.go apimode.go |
| -git commit -m "cli: canary --region refuses identically on fleets and older consoles; place in output (nearest-region §11) |
| +git commit -m "cli: canary --region checks with reads before any write; refuses identically on fleets and older consoles (nearest-region §11) |
| Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>" |
| ``` |
| --- |
| + |
| ### Task 23: The region service-gate script, proved against a real backhaul |
| **Files:** |
| @@ -6839,21 +7143,54 @@ func TestApprovedCopyIsApplied(t *testing.T) { |
| - [ ] **Step 2: Run the full gate** |
| -Run, from the worktree root: |
| +Run, from the worktree root, as one script: |
| ```bash |
| +set -e |
| go build ./... |
| +go vet ./... |
| +test -z "$(gofmt -l .)" |
| +fail=0 |
| for t in darwin/arm64 darwin/amd64 linux/arm64 windows/amd64 windows/arm64; do |
| - CGO_ENABLED=0 GOOS=${t%/*} GOARCH=${t#*/} go build ./... || { echo "FAILED $t"; break; } |
| + CGO_ENABLED=0 GOOS=${t%/*} GOARCH=${t#*/} go build ./... || { echo "FAILED $t"; fail=1; } |
| done |
| +[ "$fail" = 0 ] || exit 1 |
| +(cd launchctx && go vet ./... && go test ./... -count=1) |
| +# Unix-socket packages need a short path; /tmp/g is the one place this plan |
| +# uses /tmp, and only for that (it is RAM-backed tmpfs, not disk). |
| mkdir -p /tmp/g |
| -TMPDIR=/tmp/g make test |
| +TMPDIR=/tmp/g go test ./internal/host/... ./internal/edge/... -count=1 |
| +# Everything else on the on-disk TMPDIR the environment provides (inside |
| +# the sandbox, $TMPDIR is on disk): console tests need it. |
| +go test $(go list ./... | grep -v -e /internal/host -e /internal/edge) -count=1 -timeout 30m |
| ``` |
| -Expected: no `FAILED` line, every package passes, `gofmt -l .: clean`, and |
| -`TestApprovedCopyIsApplied` passes. `TMPDIR=/tmp/g` is for the Unix-socket |
| -tests in `internal/host` and `internal/edge`. `/tmp/g` is on disk, so the |
| -console tests' on-disk requirement holds too. |
| +Expected: the script exits 0, with no `FAILED` line, and |
| +`TestApprovedCopyIsApplied` among the passes. This is `make test`'s content, |
| +split so each package gets the `TMPDIR` it needs. |
| + |
| +- [ ] **Step 2b: Check for strings nobody reviewed (codex plan-R1 13, plan-R2)** |
| + |
| +Presence of the approved strings does not prove the absence of others. List |
| +every prose literal this branch adds to non-test Go code: |
| + |
| +```bash |
| +BASE=e8406ce6 # the commit this branch was cut from |
| +git diff -U0 "$BASE" -- '*.go' ':!*_test.go' ':!internal/geo/awsregions.go' \ |
| + | grep '^+' | grep -v '^+++' \ |
| + | grep -oE '"([^"\\]|\\.)*[a-z][^"]* [^"]*"' | sort -u |
| +``` |
| + |
| +Every line printed must be one of three things: |
| + |
| +- a record entry's text; |
| +- a message that already existed before this branch and was only moved or |
| + reused, such as `canary: list instances for %s: %v`; |
| +- code rather than prose, such as a fragment of a Go expression the pattern |
| + happened to catch. |
| + |
| +Anything else is unreviewed copy. Take it to a copy review and add it to the |
| +record before landing. |
| - [ ] **Step 3: Confirm nothing touches DNS steering (R8)** |
| @@ -7006,3 +7343,61 @@ Also folded in: **spec rev 6, ruling R9.** |
| A copy note for Paul is in Task 0: C25 says `carlos canary remove`, but the |
| verb is `carlos canary rm`. |
| + |
| +## Review dispositions (Astra, plan round 2, on fcf15ae9 with spec 9ccfaca6) |
| + |
| +Every code block these findings touched was proved, not just read. A |
| +throwaway worktree was cut at the branch base (e8406ce6). The plan's Go |
| +blocks for Tasks 1, 1b, 2 to 12, 14, 15, 18 to 22 were extracted from this |
| +file verbatim and applied. The prescribed tests were run before each |
| +implementation step (all failed, mostly as build failures) and after it (all |
| +passed). The scratch worktree and branch were deleted afterwards. The |
| +results are in the "Proof" column. |
| + |
| +| # | sev | finding | disposition | proof | |
| +| --- | --- | --- | --- | --- | |
| +| R1-9 | P2 (partly) | Migration preservation | **accepted**, see R2-6 | `TestRegionValuesSurviveRebuildRenameAndMove` FAIL before Task 8's code, PASS after | |
| +| R1-12 | P2 (partly) | Existing-canary zero-write proof missing | **accepted**, see R2-4 | `TestCanaryRegionRefusesBeforeAnyWrite/existing_canary_elsewhere` PASS | |
| +| R1-13 | P2 (partly) | Diagnostics outside the copy inventory | **accepted.** O11 to O18 added to Task 0, after a scan of every prose literal the scratch diff added. Task 25 step 2b repeats that scan before landing. | the scan found no other unlisted prose | |
| +| R1-17 | P2 (partly) | `/tmp/g` called disk-backed | **accepted.** Task 25 runs socket packages on `/tmp/g` (stated as tmpfs) and everything else on the on-disk `TMPDIR`. | gate script restructured | |
| +| R2-1 | P1 | Incomplete edge reads could prove a region keyless | **accepted.** `readEdgesSnapshot` returns `Complete=false` on any failed GET or malformed file, and `NoKeyProven` requires a complete snapshot. Positive eligibility is unaffected. | `TestNoKeyProvenNeedsACompleteRead` PASS; with `snap.Complete` removed it FAILs ("an incomplete read proved a region keyless") | |
| +| R2-2 | P1 | Ineligible boxes located a candidate | **accepted.** A coded candidate's AWS region is tallied only from eligible boxes; the home keeps its own tally. | `TestCandidateLocationComesFromEligibleBoxes` PASS; tallying every box FAILs ("located uk at ap-southeast-2") | |
| +| R2-3 | P1 | A 200 marked `policy-unreadable` passed the CLI checks | **accepted.** `app_default` gains `region_known` and `placement_known` (additive), and an unread region is left empty, never filled with the home. `deploy --region` refuses on an unknown region (new C15c); `canary --region` refuses on an unknown placement (C30b). | `TestAppDefaultKnownFlags` PASS; e2e `mode=region-unknown` and `placement_unknown` refuse with 0 writes | |
| +| R2-4 | P1 | Existing-canary mismatch checked after a write | **accepted.** One check, with reads only (`GET …/regions`, `GET …/instances`, matched by host prefix `<name>.<app>.`), before `instancesEnable`'s PUT. | `TestCanaryRegionRefusesBeforeAnyWrite` 4/4 PASS with 0 writes, no manifest, record unchanged; with the check moved after the PUT, 4/4 FAIL with "writes 2" | |
| +| R2-5 | P1 | Three compile failures | **accepted.** Task 12's unused `net` import removed. Task 20 names every caller, including `apimode_test.go`'s six, `apps_test.go:311` and `deploy_wizard_test.go:104`. Task 22's block sits after `ctx` is declared. | `go build ./...` and `go vet ./...` clean on the whole scratch tree | |
| +| R2-6 | P1 | The migration test's rename could not pass | **accepted.** The test seeds the ops account row before `RenameAppIn` and `MoveApp`. | PASS (see R1-9) | |
| +| R2-7 | P2 | No test for R9's addr-backed exemption | **accepted.** An explicit addr-backed create into a keyless region must return 201. A mutation step widens the gate. | PASS; with the gate widened to every kind it FAILs ("want 201") | |
| +| R2-8 | P2 | Missing-key diagnostic gated on the switch | **accepted.** O2 logs whenever `CARLOS_SECRETS_PUBKEY` is unset; O1 stays switch-gated. A switch-off case was added. | `TestLogRegionConfig` PASS | |
| +| R2-9 | P2 | Second mutation check proved nothing | **accepted.** The steps restore and re-run the baseline between mutations. | baseline PASS; mutation A FAILs ("existence false, want true"); restore PASS; mutation B FAILs ("existence true, want false"); restore PASS | |
| +| R2-10 | P2 | Cross-build loop returned 0 on failure | **accepted.** Every loop records failures and ends with `[ "$fail" = 0 ] \|\| exit 1`. | all five targets built with exit 0; a loop with a bad target (`plan9/bogus`) exits 1 before the commit line | |
| + |
| +Fixes the proof surfaced, beyond Astra's findings, all now in the tasks: |
| + |
| +- **Task 1b:** `edgesfile.go` needs the `appconfig` import. |
| +- **Task 9:** `cmd/console/main_test.go` is `package main_test`, so the new |
| + tests go in `cmd/console/region_test.go` (`package main`). |
| +- **Tasks 11 and 12:** `go vet` rejects unkeyed `geo.Point` literals across |
| + packages, so they are keyed. |
| +- **Task 15:** the guess route joins `exemptFromLegacyPin` |
| + (`svcroutes_test.go`), or `TestInstanceRoleMatchesTodaysAppTokenReach` |
| + fails. |
| +- **Task 21's e2e:** |
| + - builds the binary before `useTempCredentials` changes directory; |
| + - runs each refusal case as a subtest; |
| + - passes `--channel canary/e2e`, so the older-console case really ships. |
| + |
| +Whole-package results after all of the above: |
| + |
| +- `go test ./internal/console`: `ok` (356.9s). |
| +- `internal/geo`, `internal/appconfig`, `internal/provision` and |
| + `cmd/console`: `ok`. |
| +- The root package could not be run as a whole on this machine. The CLI |
| + walks up from the working directory and finds the operator's real |
| + `/home/paulca/.carlos/config`, so a pre-existing test exits with "not |
| + logged in". The unmodified base fails identically. Every new root test was |
| + run by name and passed. |
| +- The 211 existing tests in the changed CLI files (`canary_test.go`, |
| + `instances_*_test.go`, `apps_test.go`, `deploy_*_test.go`, |
| + `apimode_test.go`) were run one at a time on both trees. Outcomes were |
| + identical: 173 PASS on each, and the same 38 blocked on each by that |
| + config. Task 25 step 2 runs them all in CI, where no such config exists. |