457 lines
19 KiB
Go
457 lines
19 KiB
Go
package testweknora
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"knowledge-governance/internal/governance"
|
|
"knowledge-governance/internal/jobs"
|
|
postgresstore "knowledge-governance/internal/platform/postgres"
|
|
"knowledge-governance/internal/platform/weknora"
|
|
)
|
|
|
|
func TestUploadHandlerStreamsVerifiedArtifactAndSchedulesFirstPoll(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := os.Mkdir(filepath.Join(root, "objects"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body := []byte("safe governance document\n")
|
|
if err := os.WriteFile(filepath.Join(root, "objects", "artifact.md"), body, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
digest := sha256.Sum256(body)
|
|
sha := hex.EncodeToString(digest[:])
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost || r.URL.Path != "/api/v1/knowledge-bases/kb-1/knowledge/file" {
|
|
t.Fatalf("request = %s %s", r.Method, r.URL.Path)
|
|
}
|
|
if err := r.ParseMultipartForm(1 << 20); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
file, _, err := r.FormFile("file")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer file.Close()
|
|
uploaded, err := io.ReadAll(file)
|
|
if err != nil || string(uploaded) != string(body) {
|
|
t.Fatalf("uploaded body = %q, %v", uploaded, err)
|
|
}
|
|
if r.FormValue("metadata") != `{"content_sha256":"`+sha+`","publication_id":"publication-1"}` {
|
|
t.Fatalf("metadata = %q", r.FormValue("metadata"))
|
|
}
|
|
if r.FormValue("channel") != "governance" {
|
|
t.Fatalf("channel = %q", r.FormValue("channel"))
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"success":true,"data":{"id":"knowledge-1","knowledge_base_id":"kb-1","parse_status":"pending"}}`))
|
|
}))
|
|
defer server.Close()
|
|
client, err := weknora.NewClient(weknora.ClientConfig{BaseURL: server.URL, APIKey: "test-api-key", Timeout: time.Second})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
now := time.Date(2026, time.September, 4, 3, 0, 0, 0, time.UTC)
|
|
store := &uploadStoreStub{candidate: postgresstore.PublicationForUpload{
|
|
ID: "publication-1", SourceItemVersionID: "version-1", TestKBID: "kb-1",
|
|
IngestState: governance.IngestPending, ProcessingState: governance.ProcessingPending,
|
|
ContentObjectID: "content-1", SHA256: sha, SizeBytes: int64(len(body)),
|
|
ValidatedFormat: "md", LocalSpoolRef: "objects/artifact.md", Title: "Safe title",
|
|
}}
|
|
handler, err := NewUploadHandler(UploadHandlerConfig{
|
|
Store: store, Client: client, SpoolRoot: root, MaxUploadBytes: 50 * 1024 * 1024,
|
|
FirstPollDelay: 10 * time.Second, Now: func() time.Time { return now },
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
job := jobs.Job{ID: "job-1", Type: "test.upload", Payload: []byte(`{"publication_id":"publication-1"}`)}
|
|
if err := handler.Handle(context.Background(), job); err != nil {
|
|
t.Fatalf("Handle() error = %v", err)
|
|
}
|
|
if !store.markedUploading || store.recordedKnowledgeID != "knowledge-1" || !store.nextPollAt.Equal(now.Add(10*time.Second)) {
|
|
t.Fatalf("store = %#v", store)
|
|
}
|
|
}
|
|
|
|
func TestUploadHandlerRejectsJobSnapshotWithMismatchedTarget(t *testing.T) {
|
|
root := t.TempDir()
|
|
now := time.Date(2026, time.September, 4, 3, 0, 0, 0, time.UTC)
|
|
store := &uploadStoreStub{candidate: postgresstore.PublicationForUpload{
|
|
ID: "publication-1", SourceItemVersionID: "version-1", TestKBID: "kb-1", TargetScope: governance.VisibilityInternal,
|
|
IngestState: governance.IngestPending, ProcessingState: governance.ProcessingPending,
|
|
}}
|
|
client, err := weknora.NewClient(weknora.ClientConfig{BaseURL: "http://127.0.0.1:1", APIKey: "***", Timeout: time.Second})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
handler, err := NewUploadHandler(UploadHandlerConfig{Store: store, Client: client, SpoolRoot: root, MaxUploadBytes: 1 << 20, FirstPollDelay: time.Second, Now: func() time.Time { return now }})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
job := jobs.Job{ID: "job-1", Type: "test.upload", Payload: []byte(`{"publication_id":"publication-1","target_scope":"external","target_kb_id":"kb-1"}`)}
|
|
if err := handler.Handle(context.Background(), job); !errors.Is(err, ErrUploadStateConflict) {
|
|
t.Fatalf("scope mismatch error = %v", err)
|
|
}
|
|
job = jobs.Job{ID: "job-2", Type: "test.upload", Payload: []byte(`{"publication_id":"publication-1","target_scope":"internal","target_kb_id":"kb-other"}`)}
|
|
if err := handler.Handle(context.Background(), job); !errors.Is(err, ErrUploadStateConflict) {
|
|
t.Fatalf("kb mismatch error = %v", err)
|
|
}
|
|
if store.markedUploading {
|
|
t.Fatal("mismatched job must not mark uploading")
|
|
}
|
|
}
|
|
|
|
func TestUploadHandlerRecoversUniqueRemoteKnowledgeAfterResponseLoss(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := os.Mkdir(filepath.Join(root, "objects"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body := []byte("safe ambiguous upload\n")
|
|
if err := os.WriteFile(filepath.Join(root, "objects", "artifact.md"), body, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
digest := sha256.Sum256(body)
|
|
sha := hex.EncodeToString(digest[:])
|
|
var uploads atomic.Int32
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case r.Method == http.MethodPost:
|
|
uploads.Add(1)
|
|
if err := r.ParseMultipartForm(1 << 20); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
file, _, err := r.FormFile("file")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, _ = io.Copy(io.Discard, file)
|
|
_ = file.Close()
|
|
connection, _, err := w.(http.Hijacker).Hijack()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = connection.Close()
|
|
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/knowledge-bases/kb-1/knowledge":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"success":true,"data":[{"id":"knowledge-recovered","knowledge_base_id":"kb-1","metadata":{"publication_id":"publication-1","content_sha256":"` + sha + `"}}],"total":1,"page":1,"page_size":100}`))
|
|
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/knowledge/knowledge-recovered":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"success":true,"data":{"id":"knowledge-recovered","knowledge_base_id":"kb-1","metadata":{"publication_id":"publication-1","content_sha256":"` + sha + `"}}}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
client, err := weknora.NewClient(weknora.ClientConfig{BaseURL: server.URL, APIKey: "test-api-key", Timeout: time.Second})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
now := time.Date(2026, time.September, 4, 3, 10, 0, 0, time.UTC)
|
|
store := &uploadStoreStub{candidate: postgresstore.PublicationForUpload{
|
|
ID: "publication-1", TestKBID: "kb-1", IngestState: governance.IngestPending,
|
|
ProcessingState: governance.ProcessingPending, SHA256: sha, SizeBytes: int64(len(body)),
|
|
ValidatedFormat: "md", LocalSpoolRef: "objects/artifact.md", Title: "Safe title",
|
|
}}
|
|
handler, err := NewUploadHandler(UploadHandlerConfig{
|
|
Store: store, Client: client, SpoolRoot: root, MaxUploadBytes: 50 * 1024 * 1024,
|
|
FirstPollDelay: time.Second, Now: func() time.Time { return now },
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := handler.Handle(context.Background(), jobs.Job{Payload: []byte(`{"publication_id":"publication-1"}`)}); err != nil {
|
|
t.Fatalf("Handle() error = %v", err)
|
|
}
|
|
if uploads.Load() != 1 || store.recordedKnowledgeID != "knowledge-recovered" {
|
|
t.Fatalf("uploads=%d knowledge=%q", uploads.Load(), store.recordedKnowledgeID)
|
|
}
|
|
}
|
|
|
|
func TestUploadHandlerRecoversUploadingStateBeforeAnotherPost(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := os.Mkdir(filepath.Join(root, "objects"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body := []byte("safe retry recovery\n")
|
|
if err := os.WriteFile(filepath.Join(root, "objects", "artifact.md"), body, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
digest := sha256.Sum256(body)
|
|
sha := hex.EncodeToString(digest[:])
|
|
var uploads atomic.Int32
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case r.Method == http.MethodPost:
|
|
uploads.Add(1)
|
|
http.Error(w, "must not upload", http.StatusInternalServerError)
|
|
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/knowledge-bases/kb-1/knowledge":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"success":true,"data":[{"id":"knowledge-existing","knowledge_base_id":"kb-1","metadata":{"publication_id":"publication-1","content_sha256":"` + sha + `"}}],"total":1,"page":1,"page_size":100}`))
|
|
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/knowledge/knowledge-existing":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"success":true,"data":{"id":"knowledge-existing","knowledge_base_id":"kb-1","metadata":{"publication_id":"publication-1","content_sha256":"` + sha + `"}}}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
client, err := weknora.NewClient(weknora.ClientConfig{BaseURL: server.URL, APIKey: "test-api-key", Timeout: time.Second})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
now := time.Date(2026, time.September, 4, 3, 20, 0, 0, time.UTC)
|
|
store := &uploadStoreStub{candidate: postgresstore.PublicationForUpload{
|
|
ID: "publication-1", TestKBID: "kb-1", IngestState: governance.IngestUploading,
|
|
ProcessingState: governance.ProcessingPending, SHA256: sha, SizeBytes: int64(len(body)),
|
|
ValidatedFormat: "md", LocalSpoolRef: "objects/artifact.md", Title: "Safe title",
|
|
}}
|
|
handler, err := NewUploadHandler(UploadHandlerConfig{
|
|
Store: store, Client: client, SpoolRoot: root, MaxUploadBytes: 50 * 1024 * 1024,
|
|
FirstPollDelay: time.Second, Now: func() time.Time { return now },
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := handler.Handle(context.Background(), jobs.Job{Payload: []byte(`{"publication_id":"publication-1"}`)}); err != nil {
|
|
t.Fatalf("Handle() error = %v", err)
|
|
}
|
|
if uploads.Load() != 0 || store.recordedKnowledgeID != "knowledge-existing" {
|
|
t.Fatalf("uploads=%d knowledge=%q", uploads.Load(), store.recordedKnowledgeID)
|
|
}
|
|
}
|
|
|
|
func TestUploadHandlerFailsClosedWhenAmbiguousRecoveryHasMultipleMatches(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := os.Mkdir(filepath.Join(root, "objects"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body := []byte("safe ambiguous duplicate\n")
|
|
if err := os.WriteFile(filepath.Join(root, "objects", "artifact.md"), body, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
digest := sha256.Sum256(body)
|
|
sha := hex.EncodeToString(digest[:])
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case r.Method == http.MethodPost:
|
|
_, _ = io.Copy(io.Discard, r.Body)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusConflict)
|
|
_, _ = w.Write([]byte(`{"success":false,"data":{}}`))
|
|
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/knowledge-bases/kb-1/knowledge":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"success":true,"data":[` +
|
|
`{"id":"knowledge-a","knowledge_base_id":"kb-1","metadata":{"publication_id":"publication-1","content_sha256":"` + sha + `"}},` +
|
|
`{"id":"knowledge-b","knowledge_base_id":"kb-1","metadata":{"publication_id":"publication-1","content_sha256":"` + sha + `"}}` +
|
|
`],"total":2,"page":1,"page_size":100}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
client, err := weknora.NewClient(weknora.ClientConfig{BaseURL: server.URL, APIKey: "test-api-key", Timeout: time.Second})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
now := time.Date(2026, time.September, 4, 3, 30, 0, 0, time.UTC)
|
|
store := &uploadStoreStub{candidate: postgresstore.PublicationForUpload{
|
|
ID: "publication-1", TestKBID: "kb-1", IngestState: governance.IngestPending,
|
|
ProcessingState: governance.ProcessingPending, SHA256: sha, SizeBytes: int64(len(body)),
|
|
ValidatedFormat: "md", LocalSpoolRef: "objects/artifact.md", Title: "Safe title",
|
|
}}
|
|
handler, err := NewUploadHandler(UploadHandlerConfig{
|
|
Store: store, Client: client, SpoolRoot: root, MaxUploadBytes: 50 * 1024 * 1024,
|
|
FirstPollDelay: time.Second, Now: func() time.Time { return now },
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := handler.Handle(context.Background(), jobs.Job{Payload: []byte(`{"publication_id":"publication-1"}`)}); err != nil {
|
|
t.Fatalf("Handle() error = %v", err)
|
|
}
|
|
if store.uploadFailureCode != "upload_identity_ambiguous" || store.recordedKnowledgeID != "" {
|
|
t.Fatalf("failure=%q knowledge=%q", store.uploadFailureCode, store.recordedKnowledgeID)
|
|
}
|
|
}
|
|
|
|
func TestUploadHandlerRecordsInvalidArtifactForManualRetry(t *testing.T) {
|
|
root := t.TempDir()
|
|
store := &uploadStoreStub{candidate: postgresstore.PublicationForUpload{
|
|
ID: "publication-1", TestKBID: "kb-1", IngestState: governance.IngestPending,
|
|
ProcessingState: governance.ProcessingPending, SHA256: strings.Repeat("a", 64), SizeBytes: 128,
|
|
ValidatedFormat: "md", LocalSpoolRef: "missing/artifact.md", Title: "Safe title",
|
|
}}
|
|
handler, err := NewUploadHandler(UploadHandlerConfig{
|
|
Store: store, Client: uploadClientStub{}, SpoolRoot: root, MaxUploadBytes: 1024,
|
|
FirstPollDelay: time.Second,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := handler.Handle(context.Background(), jobs.Job{Payload: []byte(`{"publication_id":"publication-1"}`)}); err != nil {
|
|
t.Fatalf("Handle() error = %v, want terminal failure recorded", err)
|
|
}
|
|
if store.uploadFailureCode != "upload_artifact_invalid" || store.markedUploading {
|
|
t.Fatalf("failure=%q markedUploading=%v", store.uploadFailureCode, store.markedUploading)
|
|
}
|
|
}
|
|
|
|
type uploadStoreStub struct {
|
|
candidate postgresstore.PublicationForUpload
|
|
markedUploading bool
|
|
recordedKnowledgeID string
|
|
nextPollAt time.Time
|
|
uploadFailureCode string
|
|
}
|
|
|
|
func (store *uploadStoreStub) LoadPublicationForUpload(context.Context, string) (postgresstore.PublicationForUpload, error) {
|
|
return store.candidate, nil
|
|
}
|
|
|
|
func (store *uploadStoreStub) MarkUploading(context.Context, string, time.Time) error {
|
|
store.markedUploading = true
|
|
return nil
|
|
}
|
|
|
|
func (store *uploadStoreStub) RecordUploadAndSchedulePoll(_ context.Context, _ string, knowledgeID string, _ time.Time, nextAt time.Time) (int, error) {
|
|
store.recordedKnowledgeID = knowledgeID
|
|
store.nextPollAt = nextAt
|
|
return 1, nil
|
|
}
|
|
|
|
func (store *uploadStoreStub) RecordUploadFailure(_ context.Context, _ string, code string, _ time.Time) error {
|
|
store.uploadFailureCode = code
|
|
return nil
|
|
}
|
|
|
|
type uploadClientStub struct{}
|
|
|
|
func (uploadClientStub) UploadFile(context.Context, weknora.UploadRequest) (weknora.Knowledge, error) {
|
|
return weknora.Knowledge{}, errors.New("unexpected upload")
|
|
}
|
|
|
|
func (uploadClientStub) ListKnowledge(context.Context, string, int, int) ([]weknora.Knowledge, int64, error) {
|
|
return nil, 0, errors.New("unexpected list")
|
|
}
|
|
|
|
func (uploadClientStub) GetKnowledge(context.Context, string) (weknora.Knowledge, error) {
|
|
return weknora.Knowledge{}, errors.New("unexpected get")
|
|
}
|
|
|
|
func TestUploadHandlerAdoptsSameSHAItemOnDuplicateConflict(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := os.Mkdir(filepath.Join(root, "objects"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body := []byte("duplicated across two folders\n")
|
|
if err := os.WriteFile(filepath.Join(root, "objects", "artifact.md"), body, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
digest := sha256.Sum256(body)
|
|
sha := hex.EncodeToString(digest[:])
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case r.Method == http.MethodPost:
|
|
_, _ = io.Copy(io.Discard, r.Body)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusConflict)
|
|
_, _ = w.Write([]byte(`{"success":false,"code":"duplicate_file","message":"file exists","data":{"id":"knowledge-sibling","knowledge_base_id":"kb-1","file_hash":"` + sha + `"}}`))
|
|
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/knowledge/knowledge-sibling":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"success":true,"data":{"id":"knowledge-sibling","knowledge_base_id":"kb-1","file_hash":"` + sha + `","metadata":{"publication_id":"publication-other","content_sha256":"` + sha + `"}}}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
client, err := weknora.NewClient(weknora.ClientConfig{BaseURL: server.URL, APIKey: "test-key", Timeout: time.Second})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
store := &uploadStoreStub{candidate: postgresstore.PublicationForUpload{
|
|
ID: "publication-1", TestKBID: "kb-1", IngestState: governance.IngestPending,
|
|
ProcessingState: governance.ProcessingPending, SHA256: sha, SizeBytes: int64(len(body)),
|
|
ValidatedFormat: "md", LocalSpoolRef: "objects/artifact.md", Title: "Safe title",
|
|
}}
|
|
handler, err := NewUploadHandler(UploadHandlerConfig{
|
|
Store: store, Client: client, SpoolRoot: root, MaxUploadBytes: 50 * 1024 * 1024,
|
|
FirstPollDelay: time.Second, Now: func() time.Time { return time.Now() },
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := handler.Handle(context.Background(), jobs.Job{Payload: []byte(`{"publication_id":"publication-1"}`)}); err != nil {
|
|
t.Fatalf("Handle() error = %v, want duplicate adoption", err)
|
|
}
|
|
if store.recordedKnowledgeID != "knowledge-sibling" {
|
|
t.Fatalf("knowledge=%q", store.recordedKnowledgeID)
|
|
}
|
|
}
|
|
|
|
func TestUploadHandlerAdoptsUniqueSameSHAItemWhenOwnRecoveryFails(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := os.Mkdir(filepath.Join(root, "objects"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body := []byte("uploading-state duplicate\n")
|
|
if err := os.WriteFile(filepath.Join(root, "objects", "artifact.md"), body, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
digest := sha256.Sum256(body)
|
|
sha := hex.EncodeToString(digest[:])
|
|
var uploads atomic.Int32
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case r.Method == http.MethodPost:
|
|
uploads.Add(1)
|
|
http.Error(w, "must not upload while recovering", http.StatusInternalServerError)
|
|
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/knowledge-bases/kb-1/knowledge":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"success":true,"data":[{"id":"knowledge-sibling","knowledge_base_id":"kb-1","metadata":{"publication_id":"publication-sibling","content_sha256":"` + sha + `"}}],"total":1,"page":1,"page_size":100}`))
|
|
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/knowledge/knowledge-sibling":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"success":true,"data":{"id":"knowledge-sibling","knowledge_base_id":"kb-1","metadata":{"publication_id":"publication-sibling","content_sha256":"` + sha + `"}}}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
client, err := weknora.NewClient(weknora.ClientConfig{BaseURL: server.URL, APIKey: "test-key", Timeout: time.Second})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
store := &uploadStoreStub{candidate: postgresstore.PublicationForUpload{
|
|
ID: "publication-1", TestKBID: "kb-1", IngestState: governance.IngestUploading,
|
|
ProcessingState: governance.ProcessingPending, SHA256: sha, SizeBytes: int64(len(body)),
|
|
ValidatedFormat: "md", LocalSpoolRef: "objects/artifact.md", Title: "Safe title",
|
|
}}
|
|
handler, err := NewUploadHandler(UploadHandlerConfig{
|
|
Store: store, Client: client, SpoolRoot: root, MaxUploadBytes: 50 * 1024 * 1024,
|
|
FirstPollDelay: time.Second, Now: func() time.Time { return time.Now() },
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := handler.Handle(context.Background(), jobs.Job{Payload: []byte(`{"publication_id":"publication-1"}`)}); err != nil {
|
|
t.Fatalf("Handle() error = %v, want sibling adoption", err)
|
|
}
|
|
if uploads.Load() != 0 || store.recordedKnowledgeID != "knowledge-sibling" {
|
|
t.Fatalf("uploads=%d knowledge=%q", uploads.Load(), store.recordedKnowledgeID)
|
|
}
|
|
}
|