package httpapi import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" "knowledge-governance/internal/libraryexecutor" ) // fakeJobStore 只是"存储"替身;执行器状态同样是它的一条记录。 func (s *fakeJobStore) LibraryExecutorStatus(_ context.Context, instance string) (*libraryexecutor.Observation, error) { if s.executorStatus == nil || s.executorStatus.Instance != instance { return nil, nil } return s.executorStatus, nil } // healthyExecutor 返回一份"worker 刚观测到、指向本实例"的记录。 func healthyExecutor(now time.Time) *libraryexecutor.Observation { return &libraryexecutor.Observation{ Instance: "kgmaint-weknora", Reachable: true, ObservedAt: now, Identity: libraryexecutor.Identity{Project: "kgmaint-weknora", SourceApp: "kgmaint-app", ArchiveRoot: "/tmp/kg-e2e/archive", Writers: "app"}, } } // captureReadyConfig 是"执行器真的可用"时的配置:实例、期望项目与观测三者一致。 func captureReadyConfig(store *fakeJobStore) Config { store.executorStatus = healthyExecutor(time.Now().UTC()) return Config{ Store: store, LibraryAgentSocket: "/run/library-agent/agent.sock", LibrarySourceInstance: "kgmaint-weknora", LibrarySourceProject: "kgmaint-weknora", LibraryArchiveRoot: "/var/lib/knowledge-governance/library", } } func decodeCaptureRefusal(t *testing.T, recorder *httptest.ResponseRecorder) (string, string) { t.Helper() var body struct { Code string `json:"code"` Reason string `json:"reason"` } if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil { t.Fatalf("body %s: %v", recorder.Body.String(), err) } return body.Code, body.Reason } // 页面上的"可发起"必须等于接口真的会接受:配置了 socket 但执行器不可达、 // 指向别的实例、降级或过期时,都必须带着原因拒绝,而不是先排队再失败。 func TestLibraryCaptureRefusesWhenExecutorIsNotVerified(t *testing.T) { now := time.Now().UTC() tests := []struct { name string config func(*Config) text string }{ {name: "no instance identity", config: func(c *Config) { c.LibrarySourceInstance = "" }, text: "LIBRARY_SOURCE_INSTANCE"}, {name: "nothing reported yet", config: func(c *Config) { c.Store.(*fakeJobStore).executorStatus = nil }, text: "还没有上报状态"}, {name: "unreachable", config: func(c *Config) { c.Store.(*fakeJobStore).executorStatus.Reachable = false c.Store.(*fakeJobStore).executorStatus.Error = "library agent is unreachable" }, text: "不可达"}, {name: "stale observation", config: func(c *Config) { c.Store.(*fakeJobStore).executorStatus.ObservedAt = now.Add(-10 * time.Minute) }, text: "已过期"}, {name: "points at another instance", config: func(c *Config) { c.Store.(*fakeJobStore).executorStatus.Identity.Project = "weknora071full" }, text: "指向的实例不匹配"}, {name: "degraded", config: func(c *Config) { c.Store.(*fakeJobStore).executorStatus.Degraded = "写入者恢复失败" }, text: "降级"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { store := &fakeJobStore{} config := captureReadyConfig(store) test.config(&config) recorder := httptest.NewRecorder() libraryCaptureEnqueue(recorder, captureRequest(`{"scope":"internal"}`), config) if recorder.Code != http.StatusServiceUnavailable { t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String()) } code, reason := decodeCaptureRefusal(t, recorder) if code != "library_capture_unavailable" { t.Fatalf("code=%q body=%s", code, recorder.Body.String()) } if reason == "" || !strings.Contains(reason, test.text) { t.Fatalf("reason=%q want it to mention %q", reason, test.text) } if store.enqueued.Type != "" { t.Fatal("refused capture still enqueued a job") } }) } } // The overview must report the same reason the capture endpoint would use. func TestLibraryOverviewReportsExecutorIdentityAndReason(t *testing.T) { store := &fakeLibraryStore{} config := captureReadyConfig(&store.fakeJobStore) // libraryJobStore 的方法在 fakeLibraryStore 上,执行器状态在嵌入的 fakeJobStore 上。 config.Store = store recorder := httptest.NewRecorder() libraryOverviewHandler(recorder, httptest.NewRequest(http.MethodGet, "/api/v1/library/overview?scope=internal", nil), config) if recorder.Code != http.StatusOK { t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String()) } var body struct { CaptureAvailable bool `json:"capture_available"` Executor struct { Available bool `json:"available"` Instance string `json:"instance"` Project string `json:"project"` SourceApp string `json:"source_app"` ArchiveRoot string `json:"archive_root"` } `json:"executor"` } if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil { t.Fatal(err) } if !body.CaptureAvailable || !body.Executor.Available { t.Fatalf("overview did not report an available executor: %s", recorder.Body.String()) } if body.Executor.Project != "kgmaint-weknora" || body.Executor.SourceApp != "kgmaint-app" || body.Executor.ArchiveRoot != "/tmp/kg-e2e/archive" || body.Executor.Instance != "kgmaint-weknora" { t.Fatalf("executor view = %+v", body.Executor) } store.fakeJobStore.executorStatus.Identity.Project = "weknora071full" recorder = httptest.NewRecorder() libraryOverviewHandler(recorder, httptest.NewRequest(http.MethodGet, "/api/v1/library/overview?scope=internal", nil), config) if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil { t.Fatal(err) } if body.CaptureAvailable { t.Fatalf("overview still says available after the executor moved to another instance: %s", recorder.Body.String()) } } // "恢复至该版本"必须由真实条件决定:没有 restore 授权就明确说出来。 func TestLibraryRestoreAvailabilityExplainsMissingAuthorization(t *testing.T) { store := &fakeJobStore{} config := Config{Store: store, ProductionReleaseRequestsEnabled: true, ProductionBackupRestoreVerified: true} recorder := httptest.NewRecorder() libraryRestoreAvailability(recorder, httptest.NewRequest(http.MethodGet, "/api/v1/library/restore-availability?scope=internal&snapshot_id=snap-1", nil), config) if recorder.Code != http.StatusOK { t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String()) } var body struct { Allowed bool `json:"allowed"` Reasons []string `json:"reasons"` Authorization struct { State string `json:"state"` } `json:"authorization"` } if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil { t.Fatal(err) } if body.Allowed || body.Authorization.State != "none" || len(body.Reasons) == 0 { t.Fatalf("body=%s", recorder.Body.String()) } found := false for _, reason := range body.Reasons { if strings.Contains(reason, "恢复授权") { found = true } } if !found { t.Fatalf("reasons = %v", body.Reasons) } }