From 8c8ae974004e8d318e5dec1dc36e9eafc172f96d Mon Sep 17 00:00:00 2001 From: BaaRai <130491490+BaaRai@users.noreply.github.com> Date: Sat, 14 Nov 2026 18:21:52 +0100 Subject: [PATCH] test(server): join every live test server before its TempDir cleanup New restores every unfinished task in the shared database and runs its planner, worker and deadline loops on the server context. Tests that passed context.Background() never stopped or joined them, so they kept writing under the manager TempDir after the test returned and the TempDir cleanup failed with directory not empty. Add stopTestServer, which cancels the context, waits for each task to go quiescent, joins its loops, the archive worker and the side-question writer, and defer it right after New in every test that started a live server. --- server/chat_llm_resolve_test.go | 8 ++++++-- server/core_test.go | 4 +++- server/inheritance_api_test.go | 4 +++- server/lessons_test.go | 8 ++++++-- server/mgmt_test.go | 4 +++- server/skill_gaps_test.go | 4 +++- server/task_categories_test.go | 4 +++- server/task_control_routes_test.go | 4 +++- server/task_detail_metrics_test.go | 19 +------------------ server/task_metadata_test.go | 8 ++++++-- server/task_outcome_test.go | 4 +++- server/task_templates_test.go | 8 ++++++-- server/testserver_test.go | 27 +++++++++++++++++++++++++++ 13 files changed, 73 insertions(+), 33 deletions(-) create mode 100644 server/testserver_test.go diff --git a/server/chat_llm_resolve_test.go b/server/chat_llm_resolve_test.go index 35431ce..b4851b8 100644 --- a/server/chat_llm_resolve_test.go +++ b/server/chat_llm_resolve_test.go @@ -18,7 +18,9 @@ func TestChatUnavailableReasonDistinguishesStates(t *testing.T) { } defer m.Close() td := t.TempDir() - s := New(context.Background(), m, td, td, td) + ctx, cancel := context.WithCancel(context.Background()) + s := New(ctx, m, td, td, td) + defer stopTestServer(t, s, cancel) // Start from a clean profile table; other tests in the shared DB may have left rows. existing, _ := m.pg.ListProfiles() @@ -68,7 +70,9 @@ func TestResolveChatAgentHonoursConversationProfile(t *testing.T) { } defer m.Close() td := t.TempDir() - s := New(context.Background(), m, td, td, td) + ctx, cancel := context.WithCancel(context.Background()) + s := New(ctx, m, td, td, td) + defer stopTestServer(t, s, cancel) // Force the global fallback to nil so a non-nil result can ONLY come from the // conversation's own profile — this is exactly the situation the user hit diff --git a/server/core_test.go b/server/core_test.go index a8cb1b5..0fc0da8 100644 --- a/server/core_test.go +++ b/server/core_test.go @@ -24,7 +24,9 @@ func TestCoreTaskLifecyclePG(t *testing.T) { } defer m.Close() td := t.TempDir() - s := New(context.Background(), m, td, td, td) + ctx, cancel := context.WithCancel(context.Background()) + s := New(ctx, m, td, td, td) + defer stopTestServer(t, s, cancel) h := s.Handler() tok, err := signJWT(s.jwtKey) if err != nil { diff --git a/server/inheritance_api_test.go b/server/inheritance_api_test.go index 5c4cc5d..627d6f7 100644 --- a/server/inheritance_api_test.go +++ b/server/inheritance_api_test.go @@ -67,7 +67,9 @@ func TestInheritedActivityDetailAndRelationDeletion(t *testing.T) { t.Fatal(err) } - s := New(context.Background(), m, t.TempDir(), t.TempDir(), t.TempDir()) + ctx, cancel := context.WithCancel(context.Background()) + s := New(ctx, m, t.TempDir(), t.TempDir(), t.TempDir()) + defer stopTestServer(t, s, cancel) token, err := signJWT(s.jwtKey) if err != nil { t.Fatal(err) diff --git a/server/lessons_test.go b/server/lessons_test.go index c56f418..8da1f68 100644 --- a/server/lessons_test.go +++ b/server/lessons_test.go @@ -50,7 +50,9 @@ SELECT id,'completed','fixed','{}'::jsonb FROM findings WHERE task_id=$1 AND nam t.Fatal(err) } - s := New(context.Background(), m, t.TempDir(), t.TempDir(), t.TempDir()) + ctx, cancel := context.WithCancel(context.Background()) + s := New(ctx, m, t.TempDir(), t.TempDir(), t.TempDir()) + defer stopTestServer(t, s, cancel) token, err := signJWT(s.jwtKey) if err != nil { t.Fatal(err) @@ -144,7 +146,9 @@ VALUES ($1,'xss','Reflected XSS','reflected in search','high','confirmed')`, tas t.Fatal(err) } - s := New(context.Background(), m, t.TempDir(), t.TempDir(), t.TempDir()) + ctx, cancel := context.WithCancel(context.Background()) + s := New(ctx, m, t.TempDir(), t.TempDir(), t.TempDir()) + defer stopTestServer(t, s, cancel) token, err := signJWT(s.jwtKey) if err != nil { t.Fatal(err) diff --git a/server/mgmt_test.go b/server/mgmt_test.go index 76c3428..ea50f47 100644 --- a/server/mgmt_test.go +++ b/server/mgmt_test.go @@ -23,7 +23,9 @@ func TestMgmtAPI(t *testing.T) { // and archive jobs. defer m.Close() td := t.TempDir() - s := New(context.Background(), m, td, td, td) + ctx, cancel := context.WithCancel(context.Background()) + s := New(ctx, m, td, td, td) + defer stopTestServer(t, s, cancel) h := s.Handler() tok, err := signJWT(s.jwtKey) if err != nil { diff --git a/server/skill_gaps_test.go b/server/skill_gaps_test.go index bd84efe..846632e 100644 --- a/server/skill_gaps_test.go +++ b/server/skill_gaps_test.go @@ -33,7 +33,9 @@ INSERT INTO skill_usage(skill,agent_key,task_id,found) VALUES t.Fatal(err) } - s := New(context.Background(), m, t.TempDir(), t.TempDir(), t.TempDir()) + ctx, cancel := context.WithCancel(context.Background()) + s := New(ctx, m, t.TempDir(), t.TempDir(), t.TempDir()) + defer stopTestServer(t, s, cancel) token, err := signJWT(s.jwtKey) if err != nil { t.Fatal(err) diff --git a/server/task_categories_test.go b/server/task_categories_test.go index 9e96fcc..29deb4e 100644 --- a/server/task_categories_test.go +++ b/server/task_categories_test.go @@ -21,7 +21,9 @@ func TestTaskCategoryBatchRoute(t *testing.T) { defer m.Close() td := t.TempDir() - s := New(context.Background(), m, td, td, td) + ctx, cancel := context.WithCancel(context.Background()) + s := New(ctx, m, td, td, td) + defer stopTestServer(t, s, cancel) h := s.Handler() token, err := signJWT(s.jwtKey) if err != nil { diff --git a/server/task_control_routes_test.go b/server/task_control_routes_test.go index 26e2ad6..33cdae1 100644 --- a/server/task_control_routes_test.go +++ b/server/task_control_routes_test.go @@ -16,7 +16,9 @@ func TestWorkerControlRoutes(t *testing.T) { defer m.Close() td := t.TempDir() - s := New(context.Background(), m, td, td, td) + ctx, cancel := context.WithCancel(context.Background()) + s := New(ctx, m, td, td, td) + defer stopTestServer(t, s, cancel) h := s.Handler() token, err := signJWT(s.jwtKey) if err != nil { diff --git a/server/task_detail_metrics_test.go b/server/task_detail_metrics_test.go index ded6e02..933943d 100644 --- a/server/task_detail_metrics_test.go +++ b/server/task_detail_metrics_test.go @@ -7,7 +7,6 @@ import ( "net/http/httptest" "strconv" "testing" - "time" ) func TestTaskDetailReturnsListMetrics(t *testing.T) { @@ -41,23 +40,7 @@ VALUES ($1,'goal','{}','met'), ($1,'goal','{}','open')`, task.ExpID); err != nil ctx, cancel := context.WithCancel(context.Background()) s := New(ctx, m, t.TempDir(), t.TempDir(), t.TempDir()) - // New restores every unfinished task in the shared database and runs its - // planner and worker loops, which write transcripts under m.dir. Stop and - // join them before m.Close and the TempDir cleanup run. - defer func() { - cancel() - for _, lt := range s.m.List() { - s.engine.BeginDelete(lt.ID) - waitCtx, waitCancel := context.WithTimeout(context.Background(), 30*time.Second) - if err := s.waitTaskQuiescent(waitCtx, lt.ID); err != nil { - t.Errorf("task %s still running at teardown: %v", lt.ID, err) - } - waitCancel() - s.engine.StopTask(lt.ID) - } - s.archiveWG.Wait() - <-s.side.done - }() + defer stopTestServer(t, s, cancel) token, err := signJWT(s.jwtKey) if err != nil { t.Fatal(err) diff --git a/server/task_metadata_test.go b/server/task_metadata_test.go index 1082f03..b36a59a 100644 --- a/server/task_metadata_test.go +++ b/server/task_metadata_test.go @@ -24,7 +24,9 @@ func TestTaskMetadataPatchReturnsRenameAndPin(t *testing.T) { } taskID, _ := strconv.ParseInt(task.ID, 10, 64) defer func() { _ = m.pg.DeleteTask(taskID) }() - s := New(context.Background(), m, t.TempDir(), t.TempDir(), t.TempDir()) + ctx, cancel := context.WithCancel(context.Background()) + s := New(ctx, m, t.TempDir(), t.TempDir(), t.TempDir()) + defer stopTestServer(t, s, cancel) token, err := signJWT(s.jwtKey) if err != nil { t.Fatal(err) @@ -66,7 +68,9 @@ func TestConversationBatchDeleteReportsMissing(t *testing.T) { t.Skipf("postgres unavailable (%v) — skipping", err) } defer m.Close() - s := New(context.Background(), m, t.TempDir(), t.TempDir(), t.TempDir()) + ctx, cancel := context.WithCancel(context.Background()) + s := New(ctx, m, t.TempDir(), t.TempDir(), t.TempDir()) + defer stopTestServer(t, s, cancel) first, err := m.pg.CreateConversation("mainagent", "batch-http-first", nil) if err != nil { t.Fatal(err) diff --git a/server/task_outcome_test.go b/server/task_outcome_test.go index 833294b..c321560 100644 --- a/server/task_outcome_test.go +++ b/server/task_outcome_test.go @@ -60,7 +60,9 @@ INSERT INTO skill_usage(skill,task_id,found) VALUES ('api-recon',$1,true),('grap t.Fatal(err) } - s := New(context.Background(), m, t.TempDir(), t.TempDir(), t.TempDir()) + ctx, cancel := context.WithCancel(context.Background()) + s := New(ctx, m, t.TempDir(), t.TempDir(), t.TempDir()) + defer stopTestServer(t, s, cancel) token, err := signJWT(s.jwtKey) if err != nil { t.Fatal(err) diff --git a/server/task_templates_test.go b/server/task_templates_test.go index 529519f..82a18cd 100644 --- a/server/task_templates_test.go +++ b/server/task_templates_test.go @@ -20,7 +20,9 @@ func TestTaskTemplateHTTPCRUD(t *testing.T) { t.Skipf("postgres unavailable (%v) — skipping", err) } defer m.Close() - s := New(context.Background(), m, t.TempDir(), t.TempDir(), t.TempDir()) + ctx, cancel := context.WithCancel(context.Background()) + s := New(ctx, m, t.TempDir(), t.TempDir(), t.TempDir()) + defer stopTestServer(t, s, cancel) h := s.Handler() token, err := signJWT(s.jwtKey) if err != nil { @@ -90,7 +92,9 @@ func TestConversationPatchReturnsPinState(t *testing.T) { t.Skipf("postgres unavailable (%v) — skipping", err) } defer m.Close() - s := New(context.Background(), m, t.TempDir(), t.TempDir(), t.TempDir()) + ctx, cancel := context.WithCancel(context.Background()) + s := New(ctx, m, t.TempDir(), t.TempDir(), t.TempDir()) + defer stopTestServer(t, s, cancel) conversation, err := m.pg.CreateConversation("mainagent", "pin through http", nil) if err != nil { t.Fatal(err) diff --git a/server/testserver_test.go b/server/testserver_test.go new file mode 100644 index 0000000..4ed032f --- /dev/null +++ b/server/testserver_test.go @@ -0,0 +1,27 @@ +package server + +import ( + "context" + "testing" + "time" +) + +// stopTestServer cancels a test server's context and joins what New started. +// New restores every unfinished task in the shared database and runs its +// planner, worker and deadline loops, which write under m.dir. Call it with +// defer right after New, so it runs before m.Close and the TempDir cleanups. +func stopTestServer(t *testing.T, s *Server, cancel context.CancelFunc) { + t.Helper() + cancel() + for _, task := range s.m.List() { + s.engine.BeginDelete(task.ID) + waitCtx, waitCancel := context.WithTimeout(context.Background(), 30*time.Second) + if err := s.waitTaskQuiescent(waitCtx, task.ID); err != nil { + t.Errorf("task %s still running at teardown: %v", task.ID, err) + } + waitCancel() + s.engine.StopTask(task.ID) + } + s.archiveWG.Wait() + <-s.side.done +}