Files
knowledge-governance/internal/httpapi/library_deadletters_test.go
姚章浩 c2402947a9
build / build (push) Successful in 1m26s
feat(library): 死信按精确指纹解除,保留上游记录
上游 task_dead_letters 没有解决标记,purge/DELETE 会销毁"失败过"的唯一证据。
治理侧新增解除记录(迁移 073):绑定实时行逐字节指纹、重放引用、源侧观测摘要与
操作人;行被改过、重放后又出现新死信、源侧未结清都不命中,封存照旧阻断。
指纹先压紧空白再摘要,避免 PG to_jsonb::text 与 json.Marshal 的空白差异。

试过:go test ./internal/{librarydeadletter,libraryagent,httpapi,releaseadapter/v071,
platform/postgres},make verify 15 项全绿。
2026-10-29 06:55:13 +08:00

115 lines
4.7 KiB
Go

package httpapi
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"knowledge-governance/internal/governance"
"knowledge-governance/internal/librarydeadletter"
)
type deadLetterStoreFake struct {
fakeJobStore
releases []governance.LibraryDeadLetterRelease
recorded int
}
func (f *deadLetterStoreFake) RecordDeadLetterRelease(_ context.Context, release governance.LibraryDeadLetterRelease) (governance.LibraryDeadLetterRelease, bool, error) {
f.recorded++
f.releases = append(f.releases, release)
return release, false, nil
}
func (f *deadLetterStoreFake) DeadLetterReleases(context.Context, string, int) ([]governance.LibraryDeadLetterRelease, error) {
return f.releases, nil
}
func deadLetterRequest(t *testing.T, role, action string, body string) *http.Request {
t.Helper()
request := httptest.NewRequest(http.MethodPost, "/api/v1/library/dead-letter-releases", strings.NewReader(body))
request.Header.Set("Content-Type", "application/json")
if action != "" {
request.Header.Set("X-Governance-Action", action)
}
return request.WithContext(context.WithValue(request.Context(), sessionKey{}, sessionIdentity{Username: "tester", Role: role}))
}
const deadLetterRow = `{"id": 7, "payload": {}, "scope": "knowledge", "scope_id": "k1", "failed_at": "2026-10-28T10:00:00+00:00", "last_error": "boom", "task_type": "wiki:ingest", "tenant_id": 10001, "fail_count": 3, "related_id": "k1"}`
func deadLetterBody(t *testing.T, replayAt time.Time) string {
t.Helper()
observation := librarydeadletter.Observation{
Project: "kgmaint-weknora", TenantID: 10001, KnowledgeBaseID: "kb-1",
CapturedAt: time.Now().UTC(), Rows: []json.RawMessage{json.RawMessage(deadLetterRow)},
}
raw, err := json.Marshal(observation)
if err != nil {
t.Fatal(err)
}
body, err := json.Marshal(map[string]any{
"dead_letter_id": 7, "observation": json.RawMessage(raw),
"observation_digest": librarydeadletter.Fingerprint(raw),
"replay": map[string]any{"queue": "default", "task_id": "task-1",
"at": replayAt.Format(time.RFC3339), "evidence": "run_now 200"},
})
if err != nil {
t.Fatal(err)
}
return string(body)
}
func TestDeadLetterReleaseRequiresIntentAndRole(t *testing.T) {
store := &deadLetterStoreFake{}
config := Config{Store: store, LibraryTargetInstance: "kgmaint-weknora"}
recorder := httptest.NewRecorder()
deadLetterReleaseHandlers(recorder, deadLetterRequest(t, roleViewer, "dead-letter-release", deadLetterBody(t, time.Now().Add(-time.Hour))), config)
if recorder.Code != http.StatusForbidden || store.recorded != 0 {
t.Fatalf("viewer recorded a release: %d %s", recorder.Code, recorder.Body.String())
}
recorder = httptest.NewRecorder()
deadLetterReleaseHandlers(recorder, deadLetterRequest(t, roleEngineer, "", deadLetterBody(t, time.Now().Add(-time.Hour))), config)
if recorder.Code != http.StatusForbidden || store.recorded != 0 {
t.Fatalf("missing write intent recorded a release: %d", recorder.Code)
}
}
func TestDeadLetterReleaseRefusesUnwiredInstanceAndBadEvidence(t *testing.T) {
store := &deadLetterStoreFake{}
recorder := httptest.NewRecorder()
deadLetterReleaseHandlers(recorder, deadLetterRequest(t, roleEngineer, "dead-letter-release",
deadLetterBody(t, time.Now().Add(-time.Hour))), Config{Store: store})
if recorder.Code != http.StatusServiceUnavailable {
t.Fatalf("unwired instance accepted: %d", recorder.Code)
}
// 摘要被改写:必须拒绝,而不是"看着像就放过"。
config := Config{Store: store, LibraryTargetInstance: "kgmaint-weknora"}
broken := strings.Replace(deadLetterBody(t, time.Now().Add(-time.Hour)), "sha256:", "sha256:0", 1)
recorder = httptest.NewRecorder()
deadLetterReleaseHandlers(recorder, deadLetterRequest(t, roleEngineer, "dead-letter-release", broken), config)
if recorder.Code != http.StatusConflict || store.recorded != 0 {
t.Fatalf("tampered observation accepted: %d", recorder.Code)
}
}
func TestDeadLetterReleaseRecordsExactFingerprint(t *testing.T) {
store := &deadLetterStoreFake{}
config := Config{Store: store, LibraryTargetInstance: "kgmaint-weknora"}
recorder := httptest.NewRecorder()
deadLetterReleaseHandlers(recorder, deadLetterRequest(t, roleEngineer, "dead-letter-release",
deadLetterBody(t, time.Now().Add(-30*time.Minute))), config)
if recorder.Code != http.StatusOK || store.recorded != 1 {
t.Fatalf("release not recorded: %d %s", recorder.Code, recorder.Body.String())
}
recorded := store.releases[0]
if recorded.Actor != "tester" || recorded.Instance != "kgmaint-weknora" ||
recorded.Fingerprint != librarydeadletter.Fingerprint([]byte(deadLetterRow)) ||
recorded.DeadLetterID != 7 || recorded.ReplayTaskID != "task-1" {
t.Fatalf("release lost its audit binding: %+v", recorded)
}
}