gateway tests: a Claude Code run a test keeps for the next turn ends with the test, so -count no longer gathers 11 fake Claude Code processes a round (#1060)

Six tests have their server keep a Claude Code run for a conversation's
next turn, which the bridge holds for idleLongest (an hour):
TestClaudeRunKeptForTheNextTurn, TestClaudeOneOffAskNotKept,
TestClaudeRunLetGoWhenTheConversationMovedOn and
TestClaudeRunKeptAcrossEffort (claude_subscription_test.go),
TestClaudeRunKeptAfterTheClientsOwnToolCall and
TestCodexThreadKeepsItsClaudeRunAcrossAMemoryCall. Nothing ended those
runs, so each test's fake Claude Code ran on until the test binary
exited, 11 more each round: under -race -count=50, gateway.test had 77
of them after about 8 rounds.

Each now ends its server's runs as the gateway does when it stops,
t.Cleanup(s.subscription.abortAll), like the tests that already keep
runs. fakeClaude, and TestClaudeRunKeptAcrossEffort's own fake, register
runsEndWithTest before New(), so it runs after the test's cleanups: it
fails the test when a run of that fake is still in its server's bridge
and not ended, and ends it. Like the park sweep, it counts a run killed
or exited, on its way out of the bridge, as ended.

Without the six abortAll cleanups the six tests fail:

    claude_subscription_foreign_test.go:19: Claude Code runs the test started, still going after it: 3
    --- FAIL: TestClaudeRunKeptAfterTheClientsOwnToolCall
    claude_subscription_test.go:148: Claude Code runs the test started, still going after it: 3
    --- FAIL: TestClaudeRunKeptForTheNextTurn
    claude_subscription_test.go:192: Claude Code runs the test started, still going after it: 1
    --- FAIL: TestClaudeOneOffAskNotKept
    claude_subscription_test.go:349: Claude Code runs the test started, still going after it: 2
    --- FAIL: TestClaudeRunLetGoWhenTheConversationMovedOn
    claude_subscription_test.go:571: Claude Code runs the test started, still going after it: 1
    --- FAIL: TestClaudeRunKeptAcrossEffort
    codex_developer_cache_test.go:73: Claude Code runs the test started, still going after it: 1
    --- FAIL: TestCodexThreadKeepsItsClaudeRunAcrossAMemoryCall

With them they pass.

The test binary's processes after each round of -count=3, each test run
alone with a probe that lists gateway.test's descendants after it: before,
3, 6, 9 (TestClaudeRunKeptForTheNextTurn,
TestClaudeRunKeptAfterTheClientsOwnToolCall), 2, 4, 6
(TestClaudeRunLetGoWhenTheConversationMovedOn) and 1, 2, 3 (the other
three); the six in one run 11, 22, 33. After, none for each. The
package's other tests, run the same way one by one, left none running
into their next round: in a first round the probe saw only the stand-ins
TestMain runs once (testenv.StandIns), and once a fake of
TestClaudeBridgeSafeguardContextUpdates that abortAll had just killed.
The 21 that need bun, which end their plugin host with plugin.Settle, and
the live Codex test were skipped here.

-race -count=20 of the six passes. gofmt -l, go vet ./..., go build ./...,
GOOS=linux go build -tags nogui ./..., GOOS=windows go build ./... and
go test -tags nogui ./... pass under a temp HOME with Go's caches pinned.
Tests only.
This commit is contained in:
forever authored and GitHub committed 2026-10-07 05:59:51 +08:00
1 parent a24458ca4b
commit ee7ab461c5
3 files changed
+47

No files matched your search

@@ -18,6 +18,7 @@ import (
func TestClaudeRunKeptAfterTheClientsOwnToolCall(t *testing.T) {
fakeClaude(t)
s := New()
t.Cleanup(s.subscription.abortAll)
p := provider.Provider{ID: "claude", Account: &provider.Account{Agent: "claude", User: "u"}}
ask := func(msgs string) string {
t.Helper()
@@ -80,6 +80,7 @@ func TestCleanClaudeEnvRemovesGatewayOverrides(t *testing.T) {
// fakeClaude answers each line it is given with the process it runs in and
// how many turns that process has had, as Claude Code's stream-json does.
// A test calls it before New(), for runsEndWithTest.
func fakeClaude(t *testing.T) {
t.Helper()
if runtime.GOOS == "windows" {
@@ -99,6 +100,45 @@ done
`
testenv.Program(t, filepath.Join(dir, "claude"), script)
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
runsEndWithTest(t, dir)
}
// runsEndWithTest fails the test, and ends the runs, when a Claude Code run
// of the fake in dir is still going after the test's cleanups. A run kept
// for the next turn waits idleLongest, so the test ends its server's runs
// with t.Cleanup(s.subscription.abortAll), registered after this call, as
// cleanups run last first.
func runsEndWithTest(t *testing.T, dir string) {
t.Helper()
dir = filepath.Clean(dir) // as exec.LookPath's filepath.Join gives it
t.Cleanup(func() {
t.Helper()
var left []*subscriptionRun
bridges.Range(func(key, _ any) bool {
b := key.(*subscriptionBridge)
b.mu.Lock()
for _, run := range b.runs {
if run.cmd == nil || filepath.Dir(run.cmd.Path) != dir {
continue
}
// a run killed, or exited, may not have left b.runs yet
run.mu.Lock()
closed := run.closed
run.mu.Unlock()
if !closed {
left = append(left, run)
}
}
b.mu.Unlock()
return true
})
if len(left) > 0 {
t.Errorf("Claude Code runs the test started, still going after it: %d", len(left))
}
for _, run := range left {
run.abort()
}
})
}
// A conversation's next turn goes to the Claude Code that had its last,
@@ -107,6 +147,7 @@ done
func TestClaudeRunKeptForTheNextTurn(t *testing.T) {
fakeClaude(t)
s := New()
t.Cleanup(s.subscription.abortAll)
p := provider.Provider{ID: "claude", Account: &provider.Account{Agent: "claude", User: "u"}}
ask := func(msgs string) string {
t.Helper()
@@ -151,6 +192,7 @@ func TestClaudeRunKeptForTheNextTurn(t *testing.T) {
func TestClaudeOneOffAskNotKept(t *testing.T) {
fakeClaude(t)
s := New()
t.Cleanup(s.subscription.abortAll)
p := provider.Provider{ID: "claude", Account: &provider.Account{Agent: "claude", User: "u"}}
ask := func(msgs string) string {
t.Helper()
@@ -308,6 +350,7 @@ func TestClaudeLimits(t *testing.T) {
func TestClaudeRunLetGoWhenTheConversationMovedOn(t *testing.T) {
fakeClaude(t)
s := New()
t.Cleanup(s.subscription.abortAll)
p := provider.Provider{ID: "claude", Account: &provider.Account{Agent: "claude", User: "u"}}
ask := func(model, msgs string) string {
t.Helper()
@@ -528,7 +571,9 @@ done
`
testenv.Program(t, filepath.Join(dir, "claude"), script)
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
runsEndWithTest(t, dir)
s := New()
t.Cleanup(s.subscription.abortAll)
p := provider.Provider{ID: "claude", Account: &provider.Account{Agent: "claude", User: "u"}}
ask := func(effort, msgs string) string {
t.Helper()
@@ -72,6 +72,7 @@ func TestCodexLaterDeveloperMessageKeepsTheSystem(t *testing.T) {
func TestCodexThreadKeepsItsClaudeRunAcrossAMemoryCall(t *testing.T) {
fakeClaude(t)
s := New()
t.Cleanup(s.subscription.abortAll)
p := provider.Provider{ID: "claude", Account: &provider.Account{Agent: "claude", User: "u"}}
said := regexp.MustCompile(`pid \d+ turn \d+`)
ask := func(body string) string {