Merge remote-tracking branch 'origin/main' into feat/native-start-meeting
2 files changed,
+79
−5
AGENTS.md+39 −0internal/calls/credits_inventory_test.go+40 −5
CI failed — run details
diff --git a/AGENTS.md b/AGENTS.md| index d658e1c..5420d63 100644 |
| --- a/AGENTS.md |
| +++ b/AGENTS.md |
| @@ -122,3 +122,42 @@ Edit any line. **This file is the source of truth, not the profile name.** |
| The profile only seeded this list at scaffold time; nothing re-reads it, so |
| changing a line here changes what the app does. A convention deleted from |
| this file is not a convention to reinstate. |
| + |
| +## Work in a worktree, and catch main up first |
| + |
| +Take a worktree for the work. Do not work in the shared checkout. |
| + |
| + git -C <repo> fetch origin |
| + git -C <repo> worktree add ../<repo>-<task> -b <branch> origin/main |
| + |
| +Both halves matter, for different reasons. |
| + |
| +**The worktree**, because several sessions share this machine and some |
| +share this repository, and a checkout has one HEAD, one index and one |
| +working tree between all of them. A branch switch under somebody else's |
| +uncommitted work destroys it with nothing on screen to say so, and two |
| +sessions editing the same paths disagree quietly instead of conflicting |
| +loudly. A worktree gives the work its own HEAD and its own files, and |
| +costs a directory. |
| + |
| +**From `origin/main`, after a fetch** — not from whatever the checkout |
| +happens to be sitting on. A branch cut from a stale main is a merge |
| +conflict scheduled for later, against files somebody else has renamed in |
| +the meantime. A branch cut from another task's branch is worse: it |
| +carries that task's commits into yours, where they are reviewed as |
| +though you wrote them, and the two can only be separated by hand. |
| + |
| +Remove it when the work has landed — `git worktree remove` — so that |
| +`git worktree list` keeps saying something true. |
| + |
| +Written down because of 2026-09-17, in this repository, while it was |
| +still called `meet`. The shared checkout was |
| +sitting on one task's branch while holding a *different* task's feature |
| +uncommitted on disk — some four hundred lines, a new migration among |
| +them — put there by a session nobody else knew was working in that tree. |
| +Two other sessions were meanwhile coordinating over the same checkout by |
| +asking each other not to touch it, which is the arrangement a worktree |
| +makes unnecessary. Nothing was lost. Nothing about the arrangement would |
| +have said so if it had been, and by then the same feature existed twice, |
| +once on each side of a rename, because the two sessions had branched |
| +from different starting points. |
diff --git a/internal/calls/credits_inventory_test.go b/internal/calls/credits_inventory_test.go| index d1cfff2..458deac 100644 |
| --- a/internal/calls/credits_inventory_test.go |
| +++ b/internal/calls/credits_inventory_test.go |
| @@ -20,6 +20,32 @@ import ( |
| // Check the production package graph, not the test binary's dependencies. |
| // A dependency or notice change must update the embedded attribution too. |
| func TestCreditsMatchProductionDependencies(t *testing.T) { |
| + // Read the embedded inventory first, and check the toolchain against |
| + // it before anything else runs. Everything below depends on this |
| + // binary being the Go the notices were generated with: |
| + // runtime.Version() goes into the expected inventory, and |
| + // runtime.GOROOT()/LICENSE is read out of it. |
| + // |
| + // Under any other toolchain the failure is real and says nothing |
| + // about credits, and it surfaces two different misleading ways. A |
| + // Homebrew keg without LICENSE at GOROOT fails the read with a bare |
| + // file error that never mentions credits at all. A keg with it gets |
| + // past that and fails the comparison instead, whose message sends |
| + // you to `make credits` — which would rewrite notices.json to name |
| + // whatever toolchain you happen to be on, burying the dependency |
| + // inventory the test exists to protect. Both cost a session a while |
| + // before this guard was here. |
| + var embedded creditInventory |
| + if err := json.Unmarshal(creditNotices, &embedded); err != nil { |
| + t.Fatal(err) |
| + } |
| + if want := embeddedGoVersion(embedded); want != "" && want != runtime.Version() { |
| + t.Fatalf("credits were generated with %s, this test is running under %s.\n"+ |
| + "Run `GOTOOLCHAIN=%s make ci`. Do not run `make credits` to fix this: it would\n"+ |
| + "rewrite notices.json to name this toolchain instead of checking the inventory.", |
| + want, runtime.Version(), want) |
| + } |
| + |
| ctx, cancel := context.WithTimeout(t.Context(), 45*time.Second) |
| defer cancel() |
| command := exec.CommandContext(ctx, "go", "list", "-deps", "-json", "./cmd/calls") |
| @@ -44,13 +70,22 @@ func TestCreditsMatchProductionDependencies(t *testing.T) { |
| } |
| inventory.Components = append(inventory.Components, credits.Lucide(), credits.Component{Name: "Go", Version: runtime.Version(), SourceURL: "https://go.dev/LICENSE", Notices: []credits.Notice{{Name: "LICENSE", Text: string(goLicense)}}}) |
| sort.Slice(inventory.Components, func(i, j int) bool { return inventory.Components[i].Name < inventory.Components[j].Name }) |
| - var embedded creditInventory |
| - if err := json.Unmarshal(creditNotices, &embedded); err != nil { |
| - t.Fatal(err) |
| - } |
| expected, _ := json.Marshal(creditInventory{Components: inventory.Components, MissingNotices: inventory.MissingNotices}) |
| actual, _ := json.Marshal(embedded) |
| if !bytes.Equal(expected, actual) { |
| - t.Fatal("embedded credits differ from production dependencies; run make credits") |
| + t.Fatal("embedded credits differ from production dependencies; run make credits " + |
| + "(the toolchain is already ruled out by the check at the top of this test)") |
| + } |
| +} |
| + |
| +// embeddedGoVersion is the Go toolchain the embedded notices name, or "" |
| +// when they name none — in which case there is nothing to compare against |
| +// and the test proceeds exactly as it did before the guard existed. |
| +func embeddedGoVersion(inventory creditInventory) string { |
| + for _, component := range inventory.Components { |
| + if component.Name == "Go" { |
| + return component.Version |
| + } |
| } |
| + return "" |
| } |