From 7c1379c2b225ba90b91e14992fc5b0ec3415fa4b Mon Sep 17 00:00:00 2001 From: ZeroPoint95 <329227198+ZeroPoint95@users.noreply.github.com> Date: Wed, 30 Sep 2026 19:57:39 -0400 Subject: [PATCH 01/10] changes: entry for #1703 Co-Authored-By: Claude Opus 5.5 --- .../unreleased/1703-standing-isolation-macos-root.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 docs/changes/unreleased/1703-standing-isolation-macos-root.md diff --git a/docs/changes/unreleased/1703-standing-isolation-macos-root.md b/docs/changes/unreleased/1703-standing-isolation-macos-root.md new file mode 100644 index 0000000000..a237be0dcd --- /dev/null +++ b/docs/changes/unreleased/1703-standing-isolation-macos-root.md @@ -0,0 +1,10 @@ +--- +kind: fixed +title: the standing isolation test compares the canonical repository root, so it passes on macOS +pr: 1703 +surface: [engine] +invalidates: [] +--- + +The recorded root was right all along; the test compared it with the raw temp +directory, which macOS spells through the /var to /private/var symlink. From 8655fa3761bd7079594c7ca0b23125717c75c114 Mon Sep 17 00:00:00 2001 From: ZeroPoint95 <329227198+ZeroPoint95@users.noreply.github.com> Date: Wed, 30 Sep 2026 19:46:23 -0400 Subject: [PATCH 02/10] session: the standing isolation test compares the canonical repository root TestStandingIsolationRecordsItsCopyBeforeWorkerInitialization compared the recorded Root, which repositoryRoot canonicalizes, against the raw t.TempDir(). On macOS those spell /private/var/folders and /var/folders for the same directory, so a correct record failed. The test now compares against canonicalPath(repo), as its siblings already do. Co-Authored-By: Claude Opus 5.5 --- internal/session/standing_isolation_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/session/standing_isolation_test.go b/internal/session/standing_isolation_test.go index 8a30670d37..2ad1e2c80e 100644 --- a/internal/session/standing_isolation_test.go +++ b/internal/session/standing_isolation_test.go @@ -59,8 +59,12 @@ func TestStandingIsolationRecordsItsCopyBeforeWorkerInitialization(t *testing.T) if _, err := runner.Run(context.Background(), item, runDir, ""); !errors.Is(err, want) { t.Fatalf("initialization error changed: %v", err) } + // The record keeps the repository's canonical root ([repositoryRoot]), so + // the comparison resolves symlinks too: on macOS t.TempDir() answers under + // /var/folders while the same directory's canonical spelling is under + // /private/var/folders, and a raw comparison fails a correct record. trees := loadStandingTrees(runDir) - if len(trees) != 1 || trees[0].Root != repo || trees[0].Home == "" || trees[0].HomeSha == "" { + if len(trees) != 1 || trees[0].Root != canonicalPath(repo) || trees[0].Home == "" || trees[0].HomeSha == "" { t.Fatalf("lost recovery identity: %+v", trees) } if got := currentBranch(trees[0].Dir); got != trees[0].Branch { From 437bc38aa6a546c7707ea3adcc9a3dfe3317fa35 Mon Sep 17 00:00:00 2001 From: ZeroPoint95 <329227198+ZeroPoint95@users.noreply.github.com> Date: Thu, 1 Oct 2026 11:23:04 -0400 Subject: [PATCH 03/10] tests: the suite is green on macOS, and the allocation law prices its marshals itself MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three back-to-back `make pr-ready BASE=origin/dev~5` runs on a clean dev (df7022daa) failed with the same ten tests every time on a Mac, while CI on ubuntu was green for the same commit. None of them flaked; each was a Linux assumption in a test. Five internal/session tests hold a task under a 1 TiB memory floor, and the governor reads /proc/meminfo, which a Mac does not have — an unknown reading admits everything, so the hold never came. They now go through requireHostMemoryReading, the skip two sibling tests already carried inline. Four cmd/codeaf engine tests compared the envelope's files against a raw t.TempDir(), which macOS spells through the /var -> /private/var link while the engine records the canonical root. beltRepoWorkspace and the unborn-branch test resolve the folder, as team_launch_test and update_test already do. The warm breakpoints encode law said 8 allocations; that figure belonged to encoding/json on Go 1.26 and is 11 on Go 1.27 with the memo unchanged. The test now prices the two marked marshals in-process and demands the result slice and nothing more on top of them, which is the actual promise. PERF.md says so. Co-Authored-By: Claude Fable 5.1 --- PERF.md | 2 +- cmd/codeaf/do_engine_contract_test.go | 9 +++- cmd/codeaf/do_engine_test.go | 10 ++++- .../1703-standing-isolation-macos-root.md | 10 ----- internal/provider/alloclaws_test.go | 44 ++++++++++++++++--- internal/session/run_admission_test.go | 13 +++++- .../session/task_pressure_external_test.go | 1 + internal/session/task_run_belt_test.go | 5 +-- internal/session/task_run_recover_test.go | 1 + internal/session/taskcrew_recovery_test.go | 1 + 10 files changed, 72 insertions(+), 24 deletions(-) delete mode 100644 docs/changes/unreleased/1703-standing-isolation-macos-root.md diff --git a/PERF.md b/PERF.md index e9475b803f..22e42fe4fb 100644 --- a/PERF.md +++ b/PERF.md @@ -1663,7 +1663,7 @@ Pinned by `internal/session/window_policy_test.go`, | Law | Where it is pinned | | --- | --- | | A warm tool-schema encode allocates **nothing**. The belt is append-only, so the memo hands back the slice it holds. | `internal/provider/alloclaws_test.go` | -| A warm transcript encode costs the **same** at 81 turns as at 8 — 1 allocation automatic, 8 with breakpoints. Encode runs once per call, so a per-call cost linear in the transcript is a per-run cost quadratic in the run. | `internal/provider/alloclaws_test.go` | +| A warm transcript encode costs the **same** at 81 turns as at 8 — 1 allocation automatic; with breakpoints, that slice plus the two marked marshals and **nothing else**. The marshals' own price belongs to encoding/json and the test measures it in-process rather than naming it (8 in all on Go 1.26, 11 on Go 1.27). Encode runs once per call, so a per-call cost linear in the transcript is a per-run cost quadratic in the run. | `internal/provider/alloclaws_test.go` | | The babble guard builds **one** zlib writer per stream and Resets it per window; a window costs at most 4 allocations. A writer per window is a hundred kilobytes of deflate state per five hundred bytes of reply. | `internal/provider/alloclaws_test.go` | | The hub's backlog fold is **amortized constant per delta**: ten times the deltas for less than twice the allocations. `Text += delta` is quadratic — 1.6 GB of copying over one long reply, under the hub's lock. | `internal/session/alloclaws_test.go` | | Splitting or shape-checking a **4,000-line paste** allocates at most **10 times** the bytes of 1,000 lines. A linear parser measured 5.1 (slices grow by doubling, so up to twice either side of fourfold); the parser that copied the rest of the paste at every word measured 15.3 and froze a real 4,000-line paste for twenty seconds. The gate counts bytes because that copy is one allocation per word, so an allocation count grows fourfold either way. | `internal/tui3/imagepaste_test.go` | diff --git a/cmd/codeaf/do_engine_contract_test.go b/cmd/codeaf/do_engine_contract_test.go index 1021c138d4..86e20a0580 100644 --- a/cmd/codeaf/do_engine_contract_test.go +++ b/cmd/codeaf/do_engine_contract_test.go @@ -219,7 +219,12 @@ func TestDoOnTheRunEngineSignsWorkerCommitsUnlessContributingForbids(t *testing. func TestDoOnTheRunEngineSignsFirstCommitOnUnbornBranch(t *testing.T) { beltRunEnv(t) t.Setenv("CODEAF_PLANDB_BIN", beltPlandbDoor(t)) - workspace := t.TempDir() + // Canonical for the same reason beltRepoWorkspace resolves its folder: the + // envelope names files under the engine's resolved root. + workspace, err := filepath.EvalSymlinks(t.TempDir()) + if err != nil { + t.Fatal(err) + } beltGit(t, workspace, "init") beltGit(t, workspace, "checkout", "-b", "work") seat := &beltSeat{ @@ -240,7 +245,7 @@ func TestDoOnTheRunEngineSignsFirstCommitOnUnbornBranch(t *testing.T) { }, } var stdout, stderr strings.Builder - err := doErrand(doRequest{ + err = doErrand(doRequest{ task: "add hello.txt", workspace: workspace, asJSON: true, timeout: 60 * time.Second, slots: bound(1), stdout: &stdout, stderr: &stderr, newBeltCompleter: func(string) session.Completer { return seat }, diff --git a/cmd/codeaf/do_engine_test.go b/cmd/codeaf/do_engine_test.go index df366edd00..17d55700d9 100644 --- a/cmd/codeaf/do_engine_test.go +++ b/cmd/codeaf/do_engine_test.go @@ -251,7 +251,15 @@ func beltRepoWorkspace(t *testing.T) string { if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not on PATH") } - dir := t.TempDir() + // The engine records every path under the repository's canonical root + // (internal/session repositoryRoot resolves symlinks so one spelling serves + // every later path and lock), and on macOS t.TempDir() is spelled through + // the /var -> /private/var link. Resolving it here lets a test compare the + // files the envelope names against the workspace it built, on any host. + dir, err := filepath.EvalSymlinks(t.TempDir()) + if err != nil { + t.Fatal(err) + } beltGit(t, dir, "init") beltGit(t, dir, "checkout", "-b", "work") if err := os.WriteFile(filepath.Join(dir, "README.md"), []byte("the project\n"), 0o644); err != nil { diff --git a/docs/changes/unreleased/1703-standing-isolation-macos-root.md b/docs/changes/unreleased/1703-standing-isolation-macos-root.md deleted file mode 100644 index a237be0dcd..0000000000 --- a/docs/changes/unreleased/1703-standing-isolation-macos-root.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -kind: fixed -title: the standing isolation test compares the canonical repository root, so it passes on macOS -pr: 1703 -surface: [engine] -invalidates: [] ---- - -The recorded root was right all along; the test compared it with the raw temp -directory, which macOS spells through the /var to /private/var symlink. diff --git a/internal/provider/alloclaws_test.go b/internal/provider/alloclaws_test.go index 7b9eeee43a..1131789e8b 100644 --- a/internal/provider/alloclaws_test.go +++ b/internal/provider/alloclaws_test.go @@ -65,12 +65,21 @@ func TestTheWarmToolEncodeAllocatesNothing(t *testing.T) { // // - automatic: 1 — the []json.RawMessage the caller is handed. Every element // of it is a slice the memo already holds. There is nothing else to pay. -// - breakpoints: 8 — that same slice, plus the two marked positions, which are +// - breakpoints: that same slice, plus the two marked positions, which are // re-derived per call BY DESIGN. The tail marker rolls forward every turn, so // a memo of the marked form would be a cache of the one thing that changes // (memo.go says this where it explains what is deliberately not memoized). // Two marshals, whatever they cost, and never 244. // +// THE BREAKPOINTS FIGURE IS MEASURED IN THIS PROCESS, NOT WRITTEN DOWN. What +// two marked marshals cost belongs to encoding/json, and it moved under us +// once already: 8 on Go 1.26, 11 on Go 1.27, with the memo unchanged. A +// number copied from one toolchain is a law about that toolchain, and it +// turned a Go upgrade on the laptop into a red that CI, on the go.mod +// toolchain, could not see. So the test prices the two marshals itself and +// demands the slice and nothing more on top of them — which is the actual +// promise, and a stricter one than any constant. +// // The equality across sizes is the load-bearing assertion; the constants are the // teaching. One-per-message at 81 turns would be 244. func TestTheWarmTranscriptEncodeCostsTheSameAtEightyTurnsAsAtEight(t *testing.T) { @@ -88,13 +97,35 @@ func TestTheWarmTranscriptEncodeCostsTheSameAtEightyTurnsAsAtEight(t *testing.T) }) } + // markedCost is what the two marked positions of a transcript cost to + // marshal on this toolchain: the part of a warm breakpoints encode that is + // re-derived by design, priced here rather than copied from PERF.md. + markedCost := func(t *testing.T, turns int) float64 { + t.Helper() + messages := benchTranscript(turns) + placed := breakpointsFor(messages) + total := 0.0 + for _, index := range []int{placed.system, placed.tail} { + if index < 0 { + continue + } + message := messages[index] + total += testing.AllocsPerRun(100, func() { + if _, err := marshalMarked(message); err != nil { + t.Fatalf("marked marshal: %v", err) + } + }) + } + return total + } + for _, dialect := range []struct { name string d cacheDialect want float64 }{ {"automatic", cacheDialectAutomatic, 1}, - {"breakpoints", cacheDialectBreakpoints, 8}, + {"breakpoints", cacheDialectBreakpoints, 1 + markedCost(t, 81)}, } { t.Run(dialect.name, func(t *testing.T) { // 8 and 81 turns are the ends of the range BENCHMARKS.md records for @@ -107,10 +138,11 @@ func TestTheWarmTranscriptEncodeCostsTheSameAtEightyTurnsAsAtEight(t *testing.T) "quadratically (memo.go)", small, large) } if large != dialect.want { - t.Fatalf("a warm %s encode costs %.0f allocations, and the law is %.0f. "+ - "If this is a deliberate change — a different marked-message shape, a "+ - "different result slice — move the number here and in PERF.md together; "+ - "if it is not, something on the warm path stopped being memoized.", + t.Fatalf("a warm %s encode costs %.0f allocations, and the law is %.0f: "+ + "the result slice plus whatever the marked positions cost to marshal on "+ + "this toolchain. If this is a deliberate change — a different result "+ + "slice, a third marked position — change the law here and in PERF.md "+ + "together; if it is not, something on the warm path stopped being memoized.", dialect.name, large, dialect.want) } }) diff --git a/internal/session/run_admission_test.go b/internal/session/run_admission_test.go index f5a60daef6..532044390d 100644 --- a/internal/session/run_admission_test.go +++ b/internal/session/run_admission_test.go @@ -7,10 +7,21 @@ import ( "github.com/Agent-Field/codeaf/internal/config" ) -func TestRunAdmissionUsesRealHostMemoryAndSharedLanes(t *testing.T) { +// requireHostMemoryReading skips a test whose hold depends on the governor +// seeing real memory. The reading is /proc/meminfo (hostMemoryMB), and a host +// without it — macOS — leaves the governor unknown, and an unknown governor +// admits everything (admissionGovernor.admits: silence is not pressure). A +// 1 TiB floor holds nothing there, so a test that waits for the hold waits +// forever and a test that asserts it fails on every Mac while CI stays green. +func requireHostMemoryReading(t *testing.T) { + t.Helper() if _, err := os.Stat("/proc/meminfo"); err != nil { t.Skip("host has no proc memory reading") } +} + +func TestRunAdmissionUsesRealHostMemoryAndSharedLanes(t *testing.T) { + requireHostMemoryReading(t) lanes := NewTaskLanes() gate := NewRunAdmission(0, 1<<40, lanes) if gate == nil || gate.MayStart() { diff --git a/internal/session/task_pressure_external_test.go b/internal/session/task_pressure_external_test.go index 3bf0b3a2f3..45c7bf0a6a 100644 --- a/internal/session/task_pressure_external_test.go +++ b/internal/session/task_pressure_external_test.go @@ -71,6 +71,7 @@ func TestMachineAdmissionNoticesSettingsFromAnotherProcess(t *testing.T) { } func TestNewAdmissionReadsSettingsChangedBeforeItsCreation(t *testing.T) { + requireHostMemoryReading(t) for _, profileKind := range []string{"named", "default"} { t.Run(profileKind, func(t *testing.T) { profile := t.TempDir() diff --git a/internal/session/task_run_belt_test.go b/internal/session/task_run_belt_test.go index 17bde84e13..d194630942 100644 --- a/internal/session/task_run_belt_test.go +++ b/internal/session/task_run_belt_test.go @@ -389,9 +389,7 @@ func TestStartTaskBashBeltStartsARunOnTheStore(t *testing.T) { } func TestBeltRunCarriesMachineGateAndShowsItsHold(t *testing.T) { - if _, err := os.Stat("/proc/meminfo"); err != nil { - t.Skip("host has no proc memory reading") - } + requireHostMemoryReading(t) t.Setenv("CODEAF_TASK_BELT", "bash") double := newBeltRunDouble("done") registerBeltRunEngine(t, double) @@ -449,6 +447,7 @@ func TestBeltRunCarriesMachineGateAndShowsItsHold(t *testing.T) { } func TestHeldBeltRunStopsWithoutPreparingRepository(t *testing.T) { + requireHostMemoryReading(t) t.Setenv("CODEAF_TASK_BELT", "bash") double := newBeltRunDouble("done") registerBeltRunEngine(t, double) diff --git a/internal/session/task_run_recover_test.go b/internal/session/task_run_recover_test.go index f0d5c84b51..3325495924 100644 --- a/internal/session/task_run_recover_test.go +++ b/internal/session/task_run_recover_test.go @@ -11,6 +11,7 @@ import ( ) func TestHeldRunReopensOnTheSameAdmissionWithoutPreparingFiles(t *testing.T) { + requireHostMemoryReading(t) t.Setenv("CODEAF_TASK_BELT", "bash") engine := newBeltRunDouble("done") registerBeltRunEngine(t, engine) diff --git a/internal/session/taskcrew_recovery_test.go b/internal/session/taskcrew_recovery_test.go index a7b3396117..7ebed5b99f 100644 --- a/internal/session/taskcrew_recovery_test.go +++ b/internal/session/taskcrew_recovery_test.go @@ -50,6 +50,7 @@ func (r *recoveryModelRecorder) CompleteWithMessages(_ context.Context, _ []ai.M // The profile and conversation deliberately disagree with every accepted seat. // Reopening must use the stored policy, not ask the router for another decision. func TestHeldRecoveryRetainsTheAcceptedCrew(t *testing.T) { + requireHostMemoryReading(t) t.Setenv("CODEAF_TASK_BELT", "bash") engine := newBeltRunDouble("done") registerBeltRunEngine(t, engine) From d7a8fd6b7c14a85b1cfa3ccf72c46f104fea0b10 Mon Sep 17 00:00:00 2001 From: ZeroPoint95 <329227198+ZeroPoint95@users.noreply.github.com> Date: Thu, 1 Oct 2026 11:23:22 -0400 Subject: [PATCH 04/10] changes: entry for #1722 Co-Authored-By: Claude Fable 5.1 --- .../1722-test-suite-green-on-macos.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 docs/changes/unreleased/1722-test-suite-green-on-macos.md diff --git a/docs/changes/unreleased/1722-test-suite-green-on-macos.md b/docs/changes/unreleased/1722-test-suite-green-on-macos.md new file mode 100644 index 0000000000..42f68eb5f8 --- /dev/null +++ b/docs/changes/unreleased/1722-test-suite-green-on-macos.md @@ -0,0 +1,18 @@ +--- +kind: fixed +title: the test suite is green on macOS, and the allocation law prices its marshals itself +pr: 1722 +surface: [engine, cli] +invalidates: + - "the ten tests that failed `make pr-ready` on a clean dev on a Mac were a Linux assumption in each test, never a product fault or a flake — CI on ubuntu was green for the same commit" + - "`internal/provider/alloclaws_test.go` no longer names 8 for a warm breakpoints encode; the figure belongs to encoding/json (8 on Go 1.26, 11 on Go 1.27) and the test now measures the two marked marshals in-process" +--- + +Five `internal/session` tests that hold a task under a 1 TiB memory floor skip +on a host without `/proc/meminfo`, through `requireHostMemoryReading`, because +the governor reads nothing there and an unknown reading admits everything. Four +`cmd/codeaf` engine tests and the standing isolation test compare against the +canonical temp folder, since the engine records the resolved root and macOS +spells `/var` through `/private/var`. The warm breakpoints encode law demands +the result slice plus whatever the two marked marshals cost on this toolchain, +and nothing more. From 57344ce29bd81cc5f90b212c8d2f144ece98a8ab Mon Sep 17 00:00:00 2001 From: ZeroPoint95 <329227198+ZeroPoint95@users.noreply.github.com> Date: Thu, 1 Oct 2026 11:23:59 -0400 Subject: [PATCH 05/10] changes: the entry's surface is engine and its claims end in full stops Co-Authored-By: Claude Fable 5.1 --- docs/changes/unreleased/1722-test-suite-green-on-macos.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/changes/unreleased/1722-test-suite-green-on-macos.md b/docs/changes/unreleased/1722-test-suite-green-on-macos.md index 42f68eb5f8..6cafcb2ee4 100644 --- a/docs/changes/unreleased/1722-test-suite-green-on-macos.md +++ b/docs/changes/unreleased/1722-test-suite-green-on-macos.md @@ -2,10 +2,10 @@ kind: fixed title: the test suite is green on macOS, and the allocation law prices its marshals itself pr: 1722 -surface: [engine, cli] +surface: [engine] invalidates: - - "the ten tests that failed `make pr-ready` on a clean dev on a Mac were a Linux assumption in each test, never a product fault or a flake — CI on ubuntu was green for the same commit" - - "`internal/provider/alloclaws_test.go` no longer names 8 for a warm breakpoints encode; the figure belongs to encoding/json (8 on Go 1.26, 11 on Go 1.27) and the test now measures the two marked marshals in-process" + - "the ten tests that failed `make pr-ready` on a clean dev on a Mac were a Linux assumption in each test, never a product fault or a flake — CI on ubuntu was green for the same commit." + - "`internal/provider/alloclaws_test.go` no longer names 8 for a warm breakpoints encode; the figure belongs to encoding/json (8 on Go 1.26, 11 on Go 1.27) and the test now measures the two marked marshals in-process." --- Five `internal/session` tests that hold a task under a 1 TiB memory floor skip From 23f4f36eac9c50ee907da3954ef08f825a04d037 Mon Sep 17 00:00:00 2001 From: ZeroPoint95 <329227198+ZeroPoint95@users.noreply.github.com> Date: Thu, 1 Oct 2026 11:34:52 -0400 Subject: [PATCH 06/10] session: the held-run tests state their machine and run on every host MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Skipping the four held-run tests on a host without /proc/meminfo meant nobody on a Mac ever ran them, and a regression in holding, reopening on the same admission, keeping the accepted crew or reading settings before creation would show up only on CI. hostReading was the one place both /proc files are read, so the governors are now built over a package-level readHost, and littleMemoryHost states a quiet machine with one gibibyte available for the rest of a test — the 1 TiB floor holds everywhere, deterministically, and the seam is restored when the test ends. requireHostMemoryReading stays for the one test whose claim is about this host's own reading. With the hold finally reachable on a Mac, the reopen test met the same /var -> /private/var mismatch the others did on its recorded ground, and compares against canonicalPath now. Co-Authored-By: Claude Fable 5.1 --- .../1722-test-suite-green-on-macos.md | 18 ++++++++++-------- internal/session/run_admission_test.go | 13 +++++++------ internal/session/task_pressure.go | 13 +++++++++++-- .../session/task_pressure_external_test.go | 2 +- internal/session/task_pressure_test.go | 15 +++++++++++++++ internal/session/task_run_belt_test.go | 2 +- internal/session/task_run_recover_test.go | 7 +++++-- internal/session/taskcrew_recovery_test.go | 2 +- 8 files changed, 51 insertions(+), 21 deletions(-) diff --git a/docs/changes/unreleased/1722-test-suite-green-on-macos.md b/docs/changes/unreleased/1722-test-suite-green-on-macos.md index 6cafcb2ee4..4165cd51ea 100644 --- a/docs/changes/unreleased/1722-test-suite-green-on-macos.md +++ b/docs/changes/unreleased/1722-test-suite-green-on-macos.md @@ -8,11 +8,13 @@ invalidates: - "`internal/provider/alloclaws_test.go` no longer names 8 for a warm breakpoints encode; the figure belongs to encoding/json (8 on Go 1.26, 11 on Go 1.27) and the test now measures the two marked marshals in-process." --- -Five `internal/session` tests that hold a task under a 1 TiB memory floor skip -on a host without `/proc/meminfo`, through `requireHostMemoryReading`, because -the governor reads nothing there and an unknown reading admits everything. Four -`cmd/codeaf` engine tests and the standing isolation test compare against the -canonical temp folder, since the engine records the resolved root and macOS -spells `/var` through `/private/var`. The warm breakpoints encode law demands -the result slice plus whatever the two marked marshals cost on this toolchain, -and nothing more. +Four `internal/session` tests that hold a task under a 1 TiB memory floor +state their machine through `littleMemoryHost`, a `readHost` seam the +governors are built over, because the real reading is `/proc/meminfo` and a +host without it admits everything; they now run and pass on every host instead +of holding only on Linux. Four `cmd/codeaf` engine tests, the standing +isolation test and the held-run reopen test compare against the canonical temp +folder, since the engine records the resolved root and macOS spells `/var` +through `/private/var`. The warm breakpoints encode law demands the result +slice plus whatever the two marked marshals cost on this toolchain, and nothing +more. diff --git a/internal/session/run_admission_test.go b/internal/session/run_admission_test.go index 532044390d..f76c4f13a6 100644 --- a/internal/session/run_admission_test.go +++ b/internal/session/run_admission_test.go @@ -7,12 +7,13 @@ import ( "github.com/Agent-Field/codeaf/internal/config" ) -// requireHostMemoryReading skips a test whose hold depends on the governor -// seeing real memory. The reading is /proc/meminfo (hostMemoryMB), and a host -// without it — macOS — leaves the governor unknown, and an unknown governor -// admits everything (admissionGovernor.admits: silence is not pressure). A -// 1 TiB floor holds nothing there, so a test that waits for the hold waits -// forever and a test that asserts it fails on every Mac while CI stays green. +// requireHostMemoryReading skips a test that is about the REAL reading. It is +// /proc/meminfo (hostMemoryMB), and a host without it — macOS — leaves the +// governor unknown, and an unknown governor admits everything +// (admissionGovernor.admits: silence is not pressure). A test of held-run +// behaviour that merely needs a hold states its machine with littleMemoryHost +// instead and runs everywhere; only a test whose claim is about this host's +// own numbers belongs behind this skip. func requireHostMemoryReading(t *testing.T) { t.Helper() if _, err := os.Stat("/proc/meminfo"); err != nil { diff --git a/internal/session/task_pressure.go b/internal/session/task_pressure.go index 8b46068bff..0a9fce3dee 100644 --- a/internal/session/task_pressure.go +++ b/internal/session/task_pressure.go @@ -365,7 +365,7 @@ func newAdmissionGovernor(maxLoad float64, minFreeMB int) *admissionGovernor { return &admissionGovernor{ maxLoad: maxLoad, minFreeMB: minFreeMB, - read: hostReading, + read: readHost, now: time.Now, } } @@ -378,7 +378,7 @@ func newAdmissionGovernorForProfile(maxLoad float64, minFreeMB int, profileDir s governor := newAdmissionGovernor(maxLoad, minFreeMB) // An empty profile selects the ordinary CODEAF_HOME profile too. if governor == nil { - governor = &admissionGovernor{read: hostReading, now: time.Now} + governor = &admissionGovernor{read: readHost, now: time.Now} } governor.settings = func() (float64, int) { return config.TaskAdmissionLimitsAt(profileDir, maxLoad, minFreeMB) @@ -563,6 +563,15 @@ func (g *admissionGovernor) footprintLocked() int { return max(share, g.peakShareMB) } +// readHost is the reading every governor is built over, and it is a variable +// for one reason: a test of HELD work needs the hold to be there at the first +// StartTask, before any governor exists for it to reach into through the +// `read` field, and on a host with no /proc — macOS — the real reading is +// unknown and an unknown reading holds nothing. A test states its machine +// here (littleMemoryHost in task_pressure_test.go) and restores this when it +// ends. Nothing outside a test assigns it. +var readHost = hostReading + // hostReading asks this machine what it is carrying. Every part is read // independently, and a part that cannot be read is reported as zero rather // than as a failure of the whole: a kernel with a loadavg and no meminfo still diff --git a/internal/session/task_pressure_external_test.go b/internal/session/task_pressure_external_test.go index 45c7bf0a6a..ca8f2ea631 100644 --- a/internal/session/task_pressure_external_test.go +++ b/internal/session/task_pressure_external_test.go @@ -71,7 +71,7 @@ func TestMachineAdmissionNoticesSettingsFromAnotherProcess(t *testing.T) { } func TestNewAdmissionReadsSettingsChangedBeforeItsCreation(t *testing.T) { - requireHostMemoryReading(t) + littleMemoryHost(t) for _, profileKind := range []string{"named", "default"} { t.Run(profileKind, func(t *testing.T) { profile := t.TempDir() diff --git a/internal/session/task_pressure_test.go b/internal/session/task_pressure_test.go index 07d1385899..ec53d8b8e5 100644 --- a/internal/session/task_pressure_test.go +++ b/internal/session/task_pressure_test.go @@ -60,6 +60,21 @@ func loaded() machineReading { return reading } +// littleMemoryHost makes every governor built for the rest of the test see a +// quiet machine with one gibibyte available, so a test that holds work under +// a 1 TiB floor (`TaskMinFreeMB = 1 << 40`) is held on every host. The real +// reading is /proc/meminfo, and a host without it — macOS — reports unknown, +// which admits everything; stating the machine here instead of skipping +// keeps the held-run tests running wherever the suite is run. +func littleMemoryHost(t *testing.T) { + t.Helper() + reading := roomFor(0) + reading.availableMB = 1024 + previous := readHost + readHost = func() (machineReading, bool) { return reading, true } + t.Cleanup(func() { readHost = previous }) +} + // fanRunner is a scripted runner that only records a start: every node it is // handed stays running until the test settles it, ON THE TEST'S OWN GOROUTINE, // so the frontier pass a settle causes has finished before the next line reads diff --git a/internal/session/task_run_belt_test.go b/internal/session/task_run_belt_test.go index d194630942..aa7000901b 100644 --- a/internal/session/task_run_belt_test.go +++ b/internal/session/task_run_belt_test.go @@ -447,7 +447,7 @@ func TestBeltRunCarriesMachineGateAndShowsItsHold(t *testing.T) { } func TestHeldBeltRunStopsWithoutPreparingRepository(t *testing.T) { - requireHostMemoryReading(t) + littleMemoryHost(t) t.Setenv("CODEAF_TASK_BELT", "bash") double := newBeltRunDouble("done") registerBeltRunEngine(t, double) diff --git a/internal/session/task_run_recover_test.go b/internal/session/task_run_recover_test.go index 3325495924..aa72a25229 100644 --- a/internal/session/task_run_recover_test.go +++ b/internal/session/task_run_recover_test.go @@ -11,7 +11,7 @@ import ( ) func TestHeldRunReopensOnTheSameAdmissionWithoutPreparingFiles(t *testing.T) { - requireHostMemoryReading(t) + littleMemoryHost(t) t.Setenv("CODEAF_TASK_BELT", "bash") engine := newBeltRunDouble("done") registerBeltRunEngine(t, engine) @@ -61,7 +61,10 @@ func TestHeldRunReopensOnTheSameAdmissionWithoutPreparingFiles(t *testing.T) { second.beltMu.Lock() resumed := second.beltRun second.beltMu.Unlock() - if resumed == nil || resumed.row != id || resumed.brief != "held original brief" || resumed.ground != repo { + // The run records its ground under the canonical repository root + // (repositoryRoot resolves symlinks), and on macOS t.TempDir() is spelled + // through the /var -> /private/var link, so the comparison resolves too. + if resumed == nil || resumed.row != id || resumed.brief != "held original brief" || resumed.ground != canonicalPath(repo) { t.Fatalf("reopened run = %+v", resumed) } assertHeldJoined(second) diff --git a/internal/session/taskcrew_recovery_test.go b/internal/session/taskcrew_recovery_test.go index 7ebed5b99f..0a947d90bd 100644 --- a/internal/session/taskcrew_recovery_test.go +++ b/internal/session/taskcrew_recovery_test.go @@ -50,7 +50,7 @@ func (r *recoveryModelRecorder) CompleteWithMessages(_ context.Context, _ []ai.M // The profile and conversation deliberately disagree with every accepted seat. // Reopening must use the stored policy, not ask the router for another decision. func TestHeldRecoveryRetainsTheAcceptedCrew(t *testing.T) { - requireHostMemoryReading(t) + littleMemoryHost(t) t.Setenv("CODEAF_TASK_BELT", "bash") engine := newBeltRunDouble("done") registerBeltRunEngine(t, engine) From 011f0648208d1912b15ebcd473256d8a0a484530 Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Fri, 2 Oct 2026 10:39:45 -0400 Subject: [PATCH 07/10] tests: the warm encode law prices encoding/json, not marshalMarked The breakpoints law measured marshalMarked itself and allowed the result slice on top, so an allocation added inside marshalMarked raised its own allowance and passed: a defensive strings.Clone of the tool content failed the old constant (9 against 8) and passed the new form. The law now prices only the encoding/json calls the two marked positions are designed to make, built from the package's own wire types with their inputs prebuilt, names this package's own allocations (the result slice, the parts slice of the marked system message, nothing for a tool result), and checks each marked position before the whole warm path. It stays toolchain-independent: 8 on Go 1.26 and 11 on Go 1.27, both measured. The equality between 8 and 81 turns is unchanged. PERF.md and the change entry now say exactly that, call the allocation failure a toolchain assumption rather than a Linux one, and give the held runs' memory floor in MiB. Co-Authored-By: Claude Opus 5.5 (1M context) --- PERF.md | 2 +- .../1722-test-suite-green-on-macos.md | 16 +- internal/provider/alloclaws_test.go | 155 +++++++++++++----- 3 files changed, 125 insertions(+), 48 deletions(-) diff --git a/PERF.md b/PERF.md index 3c039ca598..20596f55b9 100644 --- a/PERF.md +++ b/PERF.md @@ -1663,7 +1663,7 @@ Pinned by `internal/session/window_policy_test.go`, | Law | Where it is pinned | | --- | --- | | A warm tool-schema encode allocates **nothing**. The belt is append-only, so the memo hands back the slice it holds. | `internal/provider/alloclaws_test.go` | -| A warm transcript encode costs the **same** at 81 turns as at 8 — 1 allocation automatic; with breakpoints, that slice plus the two marked marshals and **nothing else**. The marshals' own price belongs to encoding/json and the test measures it in-process rather than naming it (8 in all on Go 1.26, 11 on Go 1.27). Encode runs once per call, so a per-call cost linear in the transcript is a per-run cost quadratic in the run. | `internal/provider/alloclaws_test.go` | +| A warm transcript encode costs the **same** at 81 turns as at 8 — 1 allocation automatic; with breakpoints, the result slice, the parts slice the marked system message is expanded into, and encoding/json's own price for the two marked marshals, measured in-process (8 in all on Go 1.26, 11 on Go 1.27 — both measured). This package's own allocations are named, and an extra allocation of its own anywhere on that path, inside `marshalMarked` included, fails. Encode runs once per call, so a per-call cost linear in the transcript is a per-run cost quadratic in the run. | `internal/provider/alloclaws_test.go` | | The babble guard builds **one** zlib writer per stream and Resets it per window; a window costs at most 4 allocations. A writer per window is a hundred kilobytes of deflate state per five hundred bytes of reply. | `internal/provider/alloclaws_test.go` | | The hub's backlog fold is **amortized constant per delta**: ten times the deltas for less than twice the allocations. `Text += delta` is quadratic — 1.6 GB of copying over one long reply, under the hub's lock. | `internal/session/alloclaws_test.go` | | Splitting or shape-checking a **4,000-line paste** allocates at most **10 times** the bytes of 1,000 lines. A linear parser measured 5.1 (slices grow by doubling, so up to twice either side of fourfold); the parser that copied the rest of the paste at every word measured 15.3 and froze a real 4,000-line paste for twenty seconds. The gate counts bytes because that copy is one allocation per word, so an allocation count grows fourfold either way. | `internal/tui3/imagepaste_test.go` | diff --git a/docs/changes/unreleased/1722-test-suite-green-on-macos.md b/docs/changes/unreleased/1722-test-suite-green-on-macos.md index 4165cd51ea..ab5a762ad8 100644 --- a/docs/changes/unreleased/1722-test-suite-green-on-macos.md +++ b/docs/changes/unreleased/1722-test-suite-green-on-macos.md @@ -4,17 +4,17 @@ title: the test suite is green on macOS, and the allocation law prices its marsh pr: 1722 surface: [engine] invalidates: - - "the ten tests that failed `make pr-ready` on a clean dev on a Mac were a Linux assumption in each test, never a product fault or a flake — CI on ubuntu was green for the same commit." - - "`internal/provider/alloclaws_test.go` no longer names 8 for a warm breakpoints encode; the figure belongs to encoding/json (8 on Go 1.26, 11 on Go 1.27) and the test now measures the two marked marshals in-process." + - "the ten tests that failed `make pr-ready` on a clean dev on a Mac were a host or toolchain assumption in each test, never a product fault or a flake — CI on ubuntu was green for the same commit." + - "`internal/provider/alloclaws_test.go` no longer names 8 for a warm breakpoints encode; encoding/json's own price is measured in-process, this package's own allocations are named, and nothing more is allowed (8 in all on Go 1.26, 11 on Go 1.27 — both measured)." --- -Four `internal/session` tests that hold a task under a 1 TiB memory floor -state their machine through `littleMemoryHost`, a `readHost` seam the -governors are built over, because the real reading is `/proc/meminfo` and a +Four `internal/session` tests that hold a task under a memory floor no machine +meets (`1 << 40` MiB) state their machine through `littleMemoryHost`, a `readHost` +seam the governors are built over, because the real reading is `/proc/meminfo` and a host without it admits everything; they now run and pass on every host instead of holding only on Linux. Four `cmd/codeaf` engine tests, the standing isolation test and the held-run reopen test compare against the canonical temp folder, since the engine records the resolved root and macOS spells `/var` -through `/private/var`. The warm breakpoints encode law demands the result -slice plus whatever the two marked marshals cost on this toolchain, and nothing -more. +through `/private/var`. The warm breakpoints encode law measures encoding/json's +own price in-process, names this package's own result and parts slice allocations, +and allows nothing more anywhere on the warm path, inside `marshalMarked` included. diff --git a/internal/provider/alloclaws_test.go b/internal/provider/alloclaws_test.go index 1131789e8b..14846e4bcc 100644 --- a/internal/provider/alloclaws_test.go +++ b/internal/provider/alloclaws_test.go @@ -1,8 +1,11 @@ package provider import ( + "encoding/json" "fmt" "testing" + + "github.com/Agent-Field/agentfield/sdk/go/ai" ) // THE LAWS THIS FILE DEFENDS ARE ABOUT WORK, NOT ABOUT TIME. @@ -52,6 +55,58 @@ func TestTheWarmToolEncodeAllocatesNothing(t *testing.T) { } } +// markedJSONPrice prices only the encoding/json calls the benchmark's two +// marked shapes are designed to make. Their inputs are built before measuring, +// so this baseline cannot absorb an allocation added inside marshalMarked or +// the memo. A new fixture shape needs its own price rather than a guess. +func markedJSONPrice(t *testing.T, message ai.Message) float64 { + t.Helper() + if len(message.Content) != 1 || message.Content[0].Type != "text" || len(message.ToolCalls) != 0 { + t.Fatalf("marked JSON price has no law for role %q with %d content parts and %d tool calls", + message.Role, len(message.Content), len(message.ToolCalls)) + } + price := func(marshal func() ([]byte, error)) float64 { + return testing.AllocsPerRun(100, func() { + if _, err := marshal(); err != nil { + t.Fatalf("pricing encoding/json: %v", err) + } + }) + } + part := message.Content[0] + switch message.Role { + case "system": + if message.ToolCallID != "" { + t.Fatalf("marked JSON price has no law for a system message with tool id %q", message.ToolCallID) + } + marshalPart := func() ([]byte, error) { + return json.Marshal(markedTextPart{ + Type: part.Type, Text: part.Text, CacheControl: ephemeralBreakpoint, + }) + } + raw, err := marshalPart() + if err != nil { + t.Fatalf("building marked JSON price inputs: %v", err) + } + parts := []json.RawMessage{raw} + return price(marshalPart) + price(func() ([]byte, error) { + return json.Marshal(markedMessage{Role: message.Role, Content: parts}) + }) + case "tool": + if message.ToolCallID == "" { + t.Fatal("marked JSON price has no law for a tool result without an id") + } + return price(func() ([]byte, error) { + return json.Marshal(markedToolMessage{ + Role: message.Role, Content: part.Text, + ToolCallID: message.ToolCallID, CacheControl: ephemeralBreakpoint, + }) + }) + default: + t.Fatalf("marked JSON price has no law for role %q", message.Role) + return 0 + } +} + // THE WARM TRANSCRIPT ENCODE COSTS THE SAME AT EIGHTY TURNS AS AT EIGHT. // // This is the law the memo exists for, stated as the only thing that can prove @@ -60,29 +115,38 @@ func TestTheWarmToolEncodeAllocatesNothing(t *testing.T) { // linear in transcript length is a per-run cost quadratic in the length of the // run — which is exactly what it was before memo.go. // -// The two constants are named rather than merely bounded, because each of them -// is a specific thing and a change to either is a change worth reading: +// This package's own allocations are named rather than merely bounded, because +// each is a specific thing and a change to one is a change worth reading: // // - automatic: 1 — the []json.RawMessage the caller is handed. Every element // of it is a slice the memo already holds. There is nothing else to pay. -// - breakpoints: that same slice, plus the two marked positions, which are -// re-derived per call BY DESIGN. The tail marker rolls forward every turn, so -// a memo of the marked form would be a cache of the one thing that changes -// (memo.go says this where it explains what is deliberately not memoized). -// Two marshals, whatever they cost, and never 244. +// - breakpoints: that same slice, the parts slice the marked system message is +// expanded into, and encoding/json's price for the two marked positions. +// The tool result has no overhead of its own. The marked forms are re-derived +// per call BY DESIGN: the tail marker rolls forward every turn, so a memo of +// it would cache the one thing that changes (memo.go explains this choice). // -// THE BREAKPOINTS FIGURE IS MEASURED IN THIS PROCESS, NOT WRITTEN DOWN. What -// two marked marshals cost belongs to encoding/json, and it moved under us -// once already: 8 on Go 1.26, 11 on Go 1.27, with the memo unchanged. A -// number copied from one toolchain is a law about that toolchain, and it -// turned a Go upgrade on the laptop into a red that CI, on the go.mod -// toolchain, could not see. So the test prices the two marshals itself and -// demands the slice and nothing more on top of them — which is the actual -// promise, and a stricter one than any constant. +// ONLY ENCODING/JSON'S PRICE IS MEASURED IN THIS PROCESS. The complete +// breakpoints path cost 8 on Go 1.26 and 11 on Go 1.27 with the memo unchanged, +// so a total copied from one toolchain turned a Go upgrade into a red that CI +// could not see. The baseline now calls encoding/json directly with this +// package's wire types and prebuilt inputs; it never calls marshalMarked or +// encodeMessages. This package's own costs stay named, and each marked position +// is checked before the whole warm path. An extra allocation of this package's +// own ANYWHERE ON THAT PATH, inside marshalMarked included, therefore fails +// instead of raising its own allowance. // // The equality across sizes is the load-bearing assertion; the constants are the // teaching. One-per-message at 81 turns would be 244. func TestTheWarmTranscriptEncodeCostsTheSameAtEightyTurnsAsAtEight(t *testing.T) { + const ( + // resultSliceCost is the slice the caller needs to hold this call's encoded transcript. + resultSliceCost = 1 + // partsSliceCost is the storage needed to expand the marked system message into array form. + partsSliceCost = 1 + // toolOverhead is zero because the marked tool result needs no intermediate storage. + toolOverhead = 0 + ) warmEncode := func(t *testing.T, turns int, dialect cacheDialect) float64 { t.Helper() messages := benchTranscript(turns) @@ -97,26 +161,36 @@ func TestTheWarmTranscriptEncodeCostsTheSameAtEightyTurnsAsAtEight(t *testing.T) }) } - // markedCost is what the two marked positions of a transcript cost to - // marshal on this toolchain: the part of a warm breakpoints encode that is - // re-derived by design, priced here rather than copied from PERF.md. - markedCost := func(t *testing.T, turns int) float64 { - t.Helper() - messages := benchTranscript(turns) - placed := breakpointsFor(messages) - total := 0.0 - for _, index := range []int{placed.system, placed.tail} { - if index < 0 { - continue + messages := benchTranscript(81) + placed := breakpointsFor(messages) + if placed.system < 0 || placed.tail < 0 { + t.Fatal("the benchmark transcript must place both a system and a tail breakpoint") + } + systemJSON := markedJSONPrice(t, messages[placed.system]) + tailJSON := markedJSONPrice(t, messages[placed.tail]) + for _, position := range []struct { + name string + index int + jsonCost float64 + ownCost float64 + }{ + {"system", placed.system, systemJSON, partsSliceCost}, + {"tail", placed.tail, tailJSON, toolOverhead}, + } { + allocations := testing.AllocsPerRun(100, func() { + if _, err := marshalMarked(messages[position.index]); err != nil { + t.Fatalf("marked %s position: %v", position.name, err) } - message := messages[index] - total += testing.AllocsPerRun(100, func() { - if _, err := marshalMarked(message); err != nil { - t.Fatalf("marked marshal: %v", err) - } - }) + }) + t.Logf("marked %s position: encoding/json=%.0f package=%.0f total=%.0f", + position.name, position.jsonCost, position.ownCost, allocations) + if want := position.jsonCost + position.ownCost; allocations != want { + t.Fatalf("marked %s position (%d) costs %.0f allocations, want %.0f: "+ + "encoding/json's price is measured in-process (%.0f), and this package's "+ + "own overhead is named (%.0f). An extra allocation inside marshalMarked "+ + "must fail rather than raise its own allowance.", + position.name, position.index, allocations, want, position.jsonCost, position.ownCost) } - return total } for _, dialect := range []struct { @@ -124,13 +198,15 @@ func TestTheWarmTranscriptEncodeCostsTheSameAtEightyTurnsAsAtEight(t *testing.T) d cacheDialect want float64 }{ - {"automatic", cacheDialectAutomatic, 1}, - {"breakpoints", cacheDialectBreakpoints, 1 + markedCost(t, 81)}, + {"automatic", cacheDialectAutomatic, resultSliceCost}, + {"breakpoints", cacheDialectBreakpoints, resultSliceCost + systemJSON + partsSliceCost + tailJSON + toolOverhead}, } { t.Run(dialect.name, func(t *testing.T) { // 8 and 81 turns are the ends of the range BENCHMARKS.md records for // real runs, and they are 25 and 244 messages long. small, large := warmEncode(t, 8, dialect.d), warmEncode(t, 81, dialect.d) + t.Logf("warm %s encode: 8 turns=%.0f 81 turns=%.0f law=%.0f", + dialect.name, small, large, dialect.want) if small != large { t.Fatalf("a warm encode costs %.0f allocations at 8 turns and %.0f at 81 — "+ "the memo's whole promise is that this number does not move with the "+ @@ -139,10 +215,11 @@ func TestTheWarmTranscriptEncodeCostsTheSameAtEightyTurnsAsAtEight(t *testing.T) } if large != dialect.want { t.Fatalf("a warm %s encode costs %.0f allocations, and the law is %.0f: "+ - "the result slice plus whatever the marked positions cost to marshal on "+ - "this toolchain. If this is a deliberate change — a different result "+ - "slice, a third marked position — change the law here and in PERF.md "+ - "together; if it is not, something on the warm path stopped being memoized.", + "encoding/json's price is measured in-process, and this package's own "+ + "allocations are named. An extra allocation of this package's own "+ + "anywhere on the warm path, inside marshalMarked included, fails. If "+ + "this is deliberate, change the law here and in PERF.md together; if "+ + "it is not, the warm path is doing work it should not.", dialect.name, large, dialect.want) } }) From 2c35c86698e6e64b1570d0606009f8b9c09303e3 Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Fri, 2 Oct 2026 10:39:45 -0400 Subject: [PATCH 08/10] tests: the held-run memory floor is an exbibyte, not a tebibyte The littleMemoryHost comment called the 1 << 40 floor 1 TiB, but TaskMinFreeMB is in MiB, so the floor is 1 EiB. Only the comment changes. Co-Authored-By: Claude Opus 5.5 (1M context) --- internal/session/task_pressure_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/session/task_pressure_test.go b/internal/session/task_pressure_test.go index ec53d8b8e5..7510d9ef05 100644 --- a/internal/session/task_pressure_test.go +++ b/internal/session/task_pressure_test.go @@ -62,7 +62,7 @@ func loaded() machineReading { // littleMemoryHost makes every governor built for the rest of the test see a // quiet machine with one gibibyte available, so a test that holds work under -// a 1 TiB floor (`TaskMinFreeMB = 1 << 40`) is held on every host. The real +// a 1 EiB floor (`TaskMinFreeMB = 1 << 40` MiB) is held on every host. The real // reading is /proc/meminfo, and a host without it — macOS — reports unknown, // which admits everything; stating the machine here instead of skipping // keeps the held-run tests running wherever the suite is run. From 52397a0765a0bc4254abf7f73e13c92a69582a40 Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Fri, 2 Oct 2026 10:39:45 -0400 Subject: [PATCH 09/10] session: the held belt stop test reads the run under its lock TestHeldBeltRunStopsWithoutPreparingRepository polled agent.beltRun without agent.beltMu while releaseBeltRun clears it under that lock, so -race reported a data race in about one run in three (on dev as well; CI does not run -race). The poll now takes the lock, as the test's other read already does; -race -count=10 is clean. Co-Authored-By: Claude Opus 5.5 (1M context) --- internal/session/task_run_belt_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/session/task_run_belt_test.go b/internal/session/task_run_belt_test.go index aa7000901b..6e6049f735 100644 --- a/internal/session/task_run_belt_test.go +++ b/internal/session/task_run_belt_test.go @@ -485,7 +485,11 @@ func TestHeldBeltRunStopsWithoutPreparingRepository(t *testing.T) { if !strings.Contains(line, "stopping") { t.Fatalf("stop line = %q, want stopping", line) } - beltRunWaitFor(t, "the stopped held run", func() bool { return agent.beltRun == nil }) + beltRunWaitFor(t, "the stopped held run", func() bool { + agent.beltMu.Lock() + defer agent.beltMu.Unlock() + return agent.beltRun == nil + }) rows := agent.graph().runRows(id) if len(rows) != 1 || rows[0].State != TaskFailed || !rows[0].Stopped { t.Fatalf("stopped held row = %+v", rows) From 8448f431cc25402d9ee20c3734d9d1ad28abca18 Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Fri, 2 Oct 2026 10:53:55 -0400 Subject: [PATCH 10/10] tests: the warm encode law refuses a wire type with its own marshaler The law prices encoding/json by marshalling the package's own wire types, so a MarshalJSON added to one of them would put this package's allocations inside the measured price and raise its own allowance: a defensive copy in such a method took the warm encode from 8 to 11 with identical wire bytes, and the law passed. The price now refuses json.Marshaler and encoding.TextMarshaler on markedTextPart, markedMessage and markedToolMessage, as values and as pointers, before measuring; a marshaler added on purpose has to be priced as named overhead here and in PERF.md together. The per-position failure now says which way the count moved, and names what to update together when the change is deliberate. Co-Authored-By: Claude Opus 5.5 (1M context) --- internal/provider/alloclaws_test.go | 32 ++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/internal/provider/alloclaws_test.go b/internal/provider/alloclaws_test.go index 14846e4bcc..815cb3d70e 100644 --- a/internal/provider/alloclaws_test.go +++ b/internal/provider/alloclaws_test.go @@ -1,8 +1,10 @@ package provider import ( + "encoding" "encoding/json" "fmt" + "reflect" "testing" "github.com/Agent-Field/agentfield/sdk/go/ai" @@ -61,6 +63,26 @@ func TestTheWarmToolEncodeAllocatesNothing(t *testing.T) { // the memo. A new fixture shape needs its own price rather than a guess. func markedJSONPrice(t *testing.T, message ai.Message) float64 { t.Helper() + // A wire type's own marshaler makes encoding/json's measured price include this package's work. + // A deliberate marshaler needs named overhead here and in PERF.md in the same change. + for _, wireType := range []reflect.Type{ + reflect.TypeFor[markedTextPart](), + reflect.TypeFor[markedMessage](), + reflect.TypeFor[markedToolMessage](), + } { + for _, candidate := range []reflect.Type{wireType, reflect.PointerTo(wireType)} { + for _, marshaler := range []reflect.Type{ + reflect.TypeFor[json.Marshaler](), + reflect.TypeFor[encoding.TextMarshaler](), + } { + if candidate.Implements(marshaler) { + t.Fatalf("%v implements %v: encoding/json's price is measured only while the wire types "+ + "have no marshaler of their own. A marshaler added on purpose is this package's work "+ + "and must be priced as a named overhead here and in PERF.md together.", candidate, marshaler) + } + } + } + } if len(message.Content) != 1 || message.Content[0].Type != "text" || len(message.ToolCalls) != 0 { t.Fatalf("marked JSON price has no law for role %q with %d content parts and %d tool calls", message.Role, len(message.Content), len(message.ToolCalls)) @@ -185,11 +207,15 @@ func TestTheWarmTranscriptEncodeCostsTheSameAtEightyTurnsAsAtEight(t *testing.T) t.Logf("marked %s position: encoding/json=%.0f package=%.0f total=%.0f", position.name, position.jsonCost, position.ownCost, allocations) if want := position.jsonCost + position.ownCost; allocations != want { + change := "Fewer allocations mean the marked shape or its named overhead has changed." + if allocations > want { + change = "An extra allocation inside marshalMarked must fail rather than raise its own allowance." + } t.Fatalf("marked %s position (%d) costs %.0f allocations, want %.0f: "+ "encoding/json's price is measured in-process (%.0f), and this package's "+ - "own overhead is named (%.0f). An extra allocation inside marshalMarked "+ - "must fail rather than raise its own allowance.", - position.name, position.index, allocations, want, position.jsonCost, position.ownCost) + "own overhead is named (%.0f). %s If this is deliberate, update markedJSONPrice's "+ + "shapes, the named overhead constants, and the PERF.md row together.", + position.name, position.index, allocations, want, position.jsonCost, position.ownCost, change) } }