test(serve): retire shared catalogs with temporary runtime fixtures

Problem: the no-index history search test could inherit a process catalog from
a temporary runtime fixture, so its asserted precondition was not established.

Root cause: shared catalogs deliberately outlive controllers. Several actual
boot fixtures and the shared unread harness omitted their existing fixture
cleanup, allowing the process pointer to outlive the temporary home.

Fix: use the existing shared cleanup in those owners, establish no-index state
explicitly, and preserve coverage for real active-catalog failures. Production
code, workflow gates, timeouts, defaults and baseline remain unchanged.

Verification: preceding fixtures compile and fail the lifecycle regression;
the provider-editor sibling control passes. Complete serve1798pass/5existing
skip and complete disjoint race inventory1798pass/5skip; root239 package
inventory23696pass/215existing skip, required builtin/boot, rootvet, full lint,
makecheck, formatting and Windows/Linux amd64 compile pass. Original Linux500
and the local fullrace5m alarm remain separate evidence, not claimed fixed by
an external rerun. Refs #14025, #14133; followup to #14505.
This commit is contained in:
SivanCola committed 2026-10-26 11:46:27 +08:00
1 parent c65f8be1b4
commit 2279f841db
12 files changed
+98 -3

No files matched your search

@@ -37,6 +37,7 @@ func TestNewSessionMakesStillTrueBuildNoticesVisibleAgain(t *testing.T) {
t.Setenv("REASONIX_HOME", home)
t.Setenv("REASONIX_STATE_HOME", home)
t.Setenv("REASONIX_CACHE_HOME", filepath.Join(home, "cache"))
closeSharedCatalogsOnCleanup(t)
t.Chdir(root)
kind := fmt.Sprintf("build-notices-session-%d", switchSkillProviderSeq.Add(1))
rec := testutil.NewMock(kind, testutil.Turn{Text: "ok"})
@@ -0,0 +1,53 @@
package serve
import (
"context"
"path/filepath"
"testing"
"time"
"reasonix/internal/base/testenv"
"reasonix/internal/state/history"
)
func waitFixtureCatalog(t *testing.T, searcher *history.IndexedSearcher) {
t.Helper()
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
defer cancel()
tick := time.NewTicker(time.Millisecond)
defer tick.Stop()
for searcher.IndexStatus().Path == "" {
select {
case <-ctx.Done():
t.Fatal("fixture catalog did not open")
case <-tick.C:
}
}
}
func TestTemporaryServeHomesRetireTheirSharedCatalog(t *testing.T) {
for _, fixture := range []string{"unread", "provider_editor"} {
t.Run(fixture, func(t *testing.T) {
if err := history.CloseSharedCatalog(t.Context()); err != nil {
t.Fatal(err)
}
closeSharedCatalogsOnCleanup(t)
var searcher *history.IndexedSearcher
t.Run("temporary_home", func(t *testing.T) {
t.Setenv("REASONIX_CACHE_HOME", filepath.Join(testenv.TempDir(t), "cache"))
root := ""
if fixture == "unread" {
root = newUnreadHarness(t, "plain").root
} else {
_ = newProviderEditServer(t)
root = testenv.TempDir(t)
}
searcher = history.NewIndexedSearcher(history.Options{SessionDir: root})
waitFixtureCatalog(t, searcher)
})
if searcher != nil && searcher.IndexStatus().Path != "" {
t.Error("shared catalog retained a temporary home's database after fixture cleanup")
}
})
}
}
@@ -20,6 +20,7 @@ func TestMCPPreviewSeparatesDisplayFromInstallValues(t *testing.T) {
root := testenv.TempDir(t)
home := testenv.TempDir(t)
t.Setenv("REASONIX_HOME", home)
closeSharedCatalogsOnCleanup(t)
t.Chdir(root)
resolver := &provider.StaticResolver{Descriptors: []provider.Descriptor{{Ref: "neutral/fixture", Model: "fixture", Default: true}}, Providers: map[string]provider.Provider{"neutral/fixture": credentialPreviewProvider{}}}
ctrl, err := boot.Build(t.Context(), boot.Options{Home: home, WorkspaceRoot: root, Model: "neutral/fixture", ProviderResolver: resolver, SessionDir: filepath.Join(root, "sessions"), TokenMode: boot.TokenModeFull, Sink: event.Discard})
@@ -27,6 +27,7 @@ func TestRevisionParseRefusalMasksCredentials(t *testing.T) {
func TestRevisionInstallRoundTripThroughBoot(t *testing.T) {
root, home := testenv.TempDir(t), testenv.TempDir(t)
t.Setenv("REASONIX_HOME", home)
closeSharedCatalogsOnCleanup(t)
t.Chdir(root)
resolver := &provider.StaticResolver{Descriptors: []provider.Descriptor{{Ref: "neutral/fixture", Model: "fixture", Default: true}}, Providers: map[string]provider.Provider{"neutral/fixture": credentialPreviewProvider{}}}
ctrl, err := boot.Build(t.Context(), boot.Options{Home: home, WorkspaceRoot: root, Model: "neutral/fixture", ProviderResolver: resolver, SessionDir: filepath.Join(root, "sessions"), TokenMode: boot.TokenModeFull, Sink: event.Discard})
+35 -3
View File
@@ -1,10 +1,15 @@
package serve
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"reasonix/internal/base/testenv"
"reasonix/internal/state/history"
)
func TestHistorySearchRefusesAnEmptyQueryWithItsCode(t *testing.T) {
@@ -20,13 +25,40 @@ func TestHistorySearchRefusesAnEmptyQueryWithItsCode(t *testing.T) {
}
}
func TestHistorySearchAnswersAnArrayEvenWithNoIndexYet(t *testing.T) {
func TestHistorySearchPreservesCatalogFailure(t *testing.T) {
if err := history.CloseSharedCatalog(t.Context()); err != nil {
t.Fatal(err)
}
t.Setenv("REASONIX_CACHE_HOME", filepath.Join(testenv.TempDir(t), "cache"))
closeSharedCatalogsOnCleanup(t)
searcher := history.NewIndexedSearcher(history.Options{SessionDir: testenv.TempDir(t)})
waitFixtureCatalog(t, searcher)
ctx, cancel := context.WithCancel(t.Context())
cancel()
rec := httptest.NewRecorder()
historySearch(rec, httptest.NewRequest(http.MethodGet, "/history/search?q=zebra", nil).WithContext(ctx))
var reason Reason
if err := json.Unmarshal(rec.Body.Bytes(), &reason); err != nil {
t.Fatal(err)
}
if rec.Code != http.StatusInternalServerError || reason.Code != "history.search_failed" {
t.Fatalf("status %d code %q, want 500 history.search_failed", rec.Code, reason.Code)
}
}
func TestHistorySearchAnswersAnArrayEvenWithNoIndexYet(t *testing.T) {
if err := history.CloseSharedCatalog(t.Context()); err != nil {
t.Fatal(err)
}
mux := http.NewServeMux()
mux.HandleFunc("GET /history/search", historySearch)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/history/search?q=zebra", nil))
if rec.Code != http.StatusOK || rec.Body.String() == "null\n" {
t.Fatalf("status %d body %q, want 200 and a JSON array", rec.Code, rec.Body.String())
var hits []historySearchHit
if err := json.Unmarshal(rec.Body.Bytes(), &hits); err != nil {
t.Fatal(err)
}
if rec.Code != http.StatusOK || hits == nil || len(hits) != 0 {
t.Fatalf("status %d body %q, want 200 and an empty JSON array", rec.Code, rec.Body.String())
}
}
@@ -32,6 +32,7 @@ func TestInstalledUnicodeAgentsMatchDisplayedInvocations(t *testing.T) {
t.Setenv("REASONIX_HOME", home)
t.Setenv("REASONIX_STATE_HOME", home)
t.Setenv("REASONIX_CACHE_HOME", filepath.Join(home, "cache"))
closeSharedCatalogsOnCleanup(t)
t.Chdir(root)
kind := fmt.Sprintf("unicode-plugin-agents-%d", unicodeAgentProviderSeq.Add(1))
rec := testutil.NewMock(kind)
@@ -39,6 +39,7 @@ func TestInstalledUnicodeSkillsMatchDisplayedInvocations(t *testing.T) {
t.Setenv("REASONIX_HOME", home)
t.Setenv("REASONIX_STATE_HOME", home)
t.Setenv("REASONIX_CACHE_HOME", filepath.Join(home, "cache"))
closeSharedCatalogsOnCleanup(t)
t.Chdir(root)
kind := fmt.Sprintf("unicode-plugin-skills-%d", unicodeSkillProviderSeq.Add(1))
rec := testutil.NewMock(kind, testutil.Turn{Text: "ok"}, testutil.Turn{Text: "ok"}, testutil.Turn{Text: "ok"})
@@ -54,6 +54,7 @@ func (e *refreshEndpoint) snapshot() (auth []string, bodies []map[string]any) {
func TestEffectProviderRefreshThroughBootBuild(t *testing.T) {
home := testenv.TempDir(t)
t.Setenv("REASONIX_HOME", home)
closeSharedCatalogsOnCleanup(t)
root := testenv.TempDir(t)
t.Chdir(root)
@@ -65,6 +65,7 @@ func newUnreadHarness(t *testing.T, mode string) *unreadHarness {
home := testenv.TempDir(t)
t.Setenv("REASONIX_HOME", home)
t.Setenv("REASONIX_STATE_HOME", home)
closeSharedCatalogsOnCleanup(t)
root := testenv.TempDir(t)
model := &unreadModel{mode: mode, started: make(chan struct{})}
kind := fmt.Sprintf("unread-%s-%d", t.Name(), unreadKinds.Add(1))
@@ -24,6 +24,7 @@ func TestModelSwitchPreservesLiveSkillDiscovery(t *testing.T) {
t.Setenv("REASONIX_HOME", home)
t.Setenv("REASONIX_STATE_HOME", home)
t.Setenv("REASONIX_CACHE_HOME", filepath.Join(home, "cache"))
closeSharedCatalogsOnCleanup(t)
t.Chdir(root)
kind := fmt.Sprintf("switch-skill-store-%d", switchSkillProviderSeq.Add(1))
rec := testutil.NewMock(kind, testutil.Turn{Text: "ok"}, testutil.Turn{Text: "ok"}, testutil.Turn{Text: "ok"}, testutil.Turn{Text: "ok"})
@@ -76,6 +76,7 @@ type traceCase struct {
func runTrace(t *testing.T, tc traceCase) traceReading {
t.Helper()
dir := testenv.TempDir(t)
closeSharedCatalogsOnCleanup(t)
kind := "trace-script-" + t.Name()
provider.Register(kind, func(provider.Config) (provider.Provider, error) {
return &traceScript{script: tc.executor}, nil
@@ -23,6 +23,7 @@ func TestUsageDateRangeThroughBootBuild(t *testing.T) {
home := testenv.TempDir(t)
workspace := testenv.TempDir(t)
t.Setenv("REASONIX_HOME", home)
closeSharedCatalogsOnCleanup(t)
seed := `default_model = "boot/openai-compatible"
[[providers]]