a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
85 lines
2.8 KiB
Go
85 lines
2.8 KiB
Go
package daemon
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
"go.uber.org/zap/zaptest/observer"
|
|
)
|
|
|
|
// TestFanOut_AuditsAndLogsFailures asserts the read-only fan-out emits a
|
|
// structured audit line per remote-routed call AND names a failing remote
|
|
// in the logs (not only in the JSON federation block).
|
|
func TestFanOut_AuditsAndLogsFailures(t *testing.T) {
|
|
core, logs := observer.New(zap.InfoLevel)
|
|
|
|
good := fakeRemote(t, fakeRemoteOpts{indexed: true, toolJSON: `{"nodes":[],"edges":[]}`})
|
|
|
|
// A remote that negotiates fine but 500s on the tool call -> degraded.
|
|
badMux := http.NewServeMux()
|
|
badMux.HandleFunc("/v1/health", func(w http.ResponseWriter, _ *http.Request) {
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"status": "ok", "indexed": true,
|
|
"schema_version": localSchemaMajor, "api_version": 1,
|
|
})
|
|
})
|
|
badMux.HandleFunc("/v1/tools/", func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
})
|
|
bad := httptest.NewServer(badMux)
|
|
t.Cleanup(bad.Close)
|
|
|
|
fed := NewFederator(FederationConfig{
|
|
PerRemoteTimeout: 250 * time.Millisecond,
|
|
Budget: 2 * time.Second,
|
|
HealthTTL: time.Millisecond,
|
|
}, func(e ServerEntry) (*ServerClient, error) { return NewServerClient(e) }, zap.New(core))
|
|
|
|
local := envelope(`{"nodes":[],"edges":[],"total_nodes":0,"total_edges":0}`)
|
|
ctx := withAuditInfo(context.Background(), "/repo", "sess-9")
|
|
fed.Augment(ctx, "find_usages", []byte(`{}`), local,
|
|
[]ServerEntry{{Slug: "good", URL: good.URL}, {Slug: "bad", URL: bad.URL}})
|
|
|
|
// Audit: one remote-routed call line per remote carrying the full
|
|
// {session_id, cwd, tool, target_slug} tuple, tagged via=fan-out.
|
|
audit := logs.FilterMessage("federation: remote-routed call").All()
|
|
if len(audit) != 2 {
|
|
t.Fatalf("want 2 fan-out audit lines, got %d", len(audit))
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, e := range audit {
|
|
f := e.ContextMap()
|
|
if f["via"] != "fan-out" || f["tool"] != "find_usages" {
|
|
t.Errorf("audit line missing via/tool: %v", f)
|
|
}
|
|
if f["cwd"] != "/repo" || f["session_id"] != "sess-9" {
|
|
t.Errorf("audit line missing cwd/session_id tuple: %v", f)
|
|
}
|
|
seen[f["target_slug"].(string)] = true
|
|
}
|
|
if !seen["good"] || !seen["bad"] {
|
|
t.Errorf("both remotes must be audited; got %v", seen)
|
|
}
|
|
|
|
// Degraded: the failing remote is named in the logs with a reason.
|
|
degraded := logs.FilterMessage("federation: remote degraded").All()
|
|
if len(degraded) == 0 {
|
|
t.Fatal("a degraded fan-out must emit a warning naming the remote")
|
|
}
|
|
found := false
|
|
for _, e := range degraded {
|
|
f := e.ContextMap()
|
|
if f["target_slug"] == "bad" && f["reason"] != "" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("the failing remote 'bad' must be named with a reason; got %d degraded lines", len(degraded))
|
|
}
|
|
}
|