Files
knowledge-governance/internal/httpapi/library_capture_test.go
T
姚章浩 bb64c32654
build / build (push) Failing after 16s
feat(release): 发布管理整库视图 + R4/R5/R6/R7 修复
- R4:封存子进程 SIGTERM 宽限停、libraryctl 信号转 ctx;失败/超时后 agent 再做
  有界 recover,恢复失败即降级阻断封存并给出原因;启动先恢复、关闭等恢复完成
- R5:封存闸口以已存 payload 为准,同作业号换范围 409 且不入新队;重放回显真实状态
- R6:作业列表按类型查询(ListByType),不再先取全局再过滤
- R7:根目录 libraryagent 二进制移出版本库,保留运行产物并精确忽略
- UI:发布管理只保留对内/对外两目标,整合快照/完整性/作业/存储;链路未接线项禁用
  并标原因;整库版本独立导航并入;文档级旧流程折叠保留
2026-10-28 12:28:04 +08:00

160 lines
6.1 KiB
Go

package httpapi
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"knowledge-governance/internal/jobs"
)
func captureRequest(body string) *http.Request {
request := httptest.NewRequest(http.MethodPost, "/api/v1/library/captures", strings.NewReader(body))
request.Header.Set("Content-Type", "application/json")
return request
}
// An unwired agent must refuse clearly instead of queueing a job nobody can run.
func TestLibraryCaptureRefusedWhenAgentUnwired(t *testing.T) {
store := &fakeJobStore{}
recorder := httptest.NewRecorder()
libraryCaptureEnqueue(recorder, captureRequest(`{"scope":"internal"}`), Config{Store: store})
if recorder.Code != http.StatusServiceUnavailable {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
var body map[string]any
if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil {
t.Fatal(err)
}
if body["code"] != "library_capture_unwired" || body["reason"] == "" {
t.Fatalf("unexpected body %v", body)
}
if store.enqueued.Type != "" {
t.Fatal("unwired agent still enqueued a job")
}
}
func TestLibraryCaptureEnqueuesFixedJobShape(t *testing.T) {
store := &fakeJobStore{enqueueEchoRequestedID: true}
recorder := httptest.NewRecorder()
libraryCaptureEnqueue(recorder, captureRequest(`{"scope":"external","job":"lib-external-review"}`),
Config{Store: store, LibraryAgentSocket: "/run/library-agent/agent.sock"})
if recorder.Code != http.StatusAccepted {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
enqueued := store.enqueued
if enqueued.Type != libraryCaptureJobType {
t.Fatalf("job type %q", enqueued.Type)
}
if enqueued.IdempotencyKey != "library.capture:lib-external-review" {
t.Fatalf("idempotency key %q", enqueued.IdempotencyKey)
}
var payload map[string]string
if err := json.Unmarshal(enqueued.Payload, &payload); err != nil {
t.Fatal(err)
}
if payload["scope"] != "external" || payload["capture_job"] != "lib-external-review" {
t.Fatalf("payload %v", payload)
}
var body map[string]any
if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil {
t.Fatal(err)
}
if body["capture_job"] != "lib-external-review" || body["scope"] != "external" {
t.Fatalf("response %v", body)
}
}
func TestLibraryCaptureRejectsInvalidRequest(t *testing.T) {
for _, body := range []string{
`{"scope":"production"}`,
`{"scope":"internal","job":"bad job; rm -rf /"}`,
`not json`,
} {
store := &fakeJobStore{}
recorder := httptest.NewRecorder()
libraryCaptureEnqueue(recorder, captureRequest(body), Config{Store: store, LibraryAgentSocket: "/tmp/agent.sock"})
if recorder.Code != http.StatusBadRequest {
t.Fatalf("body %q status=%d", body, recorder.Code)
}
if store.enqueued.Type != "" {
t.Fatalf("invalid request %q still enqueued", body)
}
}
}
func TestLibraryCaptureJobsAsksStoreForItsOwnType(t *testing.T) {
// The global queue is busy with unrelated traffic; the capture job must still
// be visible because the query is type-scoped, not filtered after the fact.
store := &fakeJobStore{
listResult: []jobs.Job{{ID: "busy", Type: "artifact.fetch", State: "running"}},
listByTypeResult: []jobs.Job{
{ID: "a", Type: libraryCaptureJobType, State: "completed",
Payload: json.RawMessage(`{"scope":"internal","capture_job":"lib-internal-1","requested_by":"yaozhanghao"}`),
CreatedAt: time.Date(2026, 10, 28, 3, 0, 0, 0, time.UTC)},
},
}
recorder := httptest.NewRecorder()
libraryCaptureJobs(recorder, httptest.NewRequest(http.MethodGet, "/api/v1/library/captures", nil), Config{Store: store})
if recorder.Code != http.StatusOK {
t.Fatalf("status=%d", recorder.Code)
}
if store.listType != libraryCaptureJobType {
t.Fatalf("store queried with type %q", store.listType)
}
var body struct {
Available bool `json:"available"`
Jobs []map[string]any `json:"jobs"`
}
if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil {
t.Fatal(err)
}
if !body.Available || len(body.Jobs) != 1 || body.Jobs[0]["capture_job"] != "lib-internal-1" {
t.Fatalf("unexpected body %+v", body)
}
}
// Reusing a capture job id with another scope must be refused, and the response
// must report the scope that was actually stored.
func TestLibraryCaptureRejectsScopeConflictForReusedJob(t *testing.T) {
store := &fakeJobStore{enqueueResult: &jobs.Job{ID: "existing-job", Type: libraryCaptureJobType,
State: jobs.StateCompleted,
Payload: json.RawMessage(`{"scope":"internal","capture_job":"lib-shared","requested_by":"yaozhanghao"}`)}}
recorder := httptest.NewRecorder()
libraryCaptureEnqueue(recorder, captureRequest(`{"scope":"external","job":"lib-shared"}`),
Config{Store: store, LibraryAgentSocket: "/run/library-agent/agent.sock"})
if recorder.Code != http.StatusConflict {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
var body map[string]any
if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil {
t.Fatal(err)
}
if body["code"] != "library_capture_scope_conflict" || body["existing_scope"] != "internal" || body["requested_scope"] != "external" {
t.Fatalf("unexpected conflict body %v", body)
}
}
// A reused job id with an identical request is answered from the stored row.
func TestLibraryCaptureReplayReportsStoredJob(t *testing.T) {
store := &fakeJobStore{enqueueResult: &jobs.Job{ID: "existing-job", Type: libraryCaptureJobType,
State: jobs.StateCompleted,
Payload: json.RawMessage(`{"scope":"internal","capture_job":"lib-shared","requested_by":"yaozhanghao"}`)}}
recorder := httptest.NewRecorder()
libraryCaptureEnqueue(recorder, captureRequest(`{"scope":"internal","job":"lib-shared"}`),
Config{Store: store, LibraryAgentSocket: "/run/library-agent/agent.sock"})
if recorder.Code != http.StatusAccepted {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
var body map[string]any
if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil {
t.Fatal(err)
}
if body["scope"] != "internal" || body["capture_job"] != "lib-shared" || body["reused"] != true || body["job_id"] != "existing-job" {
t.Fatalf("unexpected replay body %v", body)
}
}