c2402947a9
build / build (push) Successful in 1m26s
上游 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 项全绿。
115 lines
4.7 KiB
Go
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)
|
|
}
|
|
}
|