package pipeline import ( "context" "encoding/json" "strings" "testing" "gitea.maximumdirect.net/eric/notarius/internal/core/source" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" ) func TestCleanDebugPathPreservesRawComponents(t *testing.T) { for _, test := range []struct { value string want string }{ {value: "", want: "%"}, {value: ".", want: "%2E"}, {value: "..", want: "%2E%2E"}, {value: "a//b", want: "a/%/b"}, {value: "/a/", want: "%/a/%"}, {value: " a ", want: "%20a%20"}, } { if got := cleanDebugPath(test.value); got != test.want { t.Errorf("cleanDebugPath(%q) = %q, want %q", test.value, got, test.want) } } } func TestDebugLLMPathsKeepDotIdentitiesDistinct(t *testing.T) { recorder := newCapturedDebugRecorder() client := WithDebugLLMRecording(attemptDebugLLM{}, recorder) for _, test := range []struct { stageName string path string }{ {stageName: ".", path: "%2E/response-0001.json"}, {stageName: "..", path: "%2E%2E/response-0002.json"}, } { if _, err := client.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{StageName: test.stageName}, nil); err != nil { t.Fatalf("CompleteStructured(%q): %v", test.stageName, err) } if !recorder.has(test.path) { t.Errorf("debug artifact %q was not written; names = %#v", test.path, recorder.names()) } } } func TestDebugSourceDocumentPreservesUnitReferences(t *testing.T) { doc := validSourceDocument() envelope := debugSourceDocumentEnvelope(doc) encoded, err := json.Marshal(envelope) if err != nil { t.Fatalf("Marshal(debug source document) error = %v, want nil", err) } var decoded debugSourceDocument if err := json.Unmarshal(encoded, &decoded); err != nil { t.Fatalf("Unmarshal(debug source document) error = %v, want nil", err) } if got, want := decoded.Units[0].Ref, (source.SourceRef{SourceID: "source-1", StartUnitID: 1, EndUnitID: 1}); got != want { t.Fatalf("debug source unit ref = %#v, want %#v", got, want) } envelope.Units[0].Ref.SourceID = "mutated" if got := doc.Units[0].Ref.SourceID; got != "source-1" { t.Fatalf("source document ref = %q after debug mutation, want source-1", got) } } func TestDebugSourceUnitsRedactSecrets(t *testing.T) { units := []source.SourceUnit{{ ID: 1, Kind: "paragraph", Text: "application text Bearer secretvalue sk-secretvalue", Ref: source.SourceRef{SourceID: "source-1", StartUnitID: 1, EndUnitID: 1}, Metadata: map[string]any{"api_key": "sk-secretvalue"}, }} got := cloneSourceUnitsForDebug(units) if len(got) != 1 { t.Fatalf("debug unit count = %d, want 1", len(got)) } if !strings.Contains(got[0].Text, "application text") { t.Fatalf("debug unit text = %q, want application content retained", got[0].Text) } for _, forbidden := range []string{"secretvalue", "sk-secretvalue"} { if strings.Contains(got[0].Text, forbidden) { t.Fatalf("debug unit text contains %q: %q", forbidden, got[0].Text) } } if got, want := got[0].Metadata["api_key"], "[REDACTED]"; got != want { t.Fatalf("debug unit metadata api_key = %#v, want %q", got, want) } if units[0].Text != "application text Bearer secretvalue sk-secretvalue" { t.Fatalf("source unit text was mutated: %q", units[0].Text) } } func TestDebugSourceChunkPreservesReference(t *testing.T) { doc := validSourceDocument() chunk := source.Chunk{ ID: "chunk-1", SourceID: doc.ID, Index: 0, Ref: source.SourceRef{SourceID: doc.ID, StartUnitID: 1, EndUnitID: 1}, Content: []byte("chunk content"), MediaType: "text/plain", Units: doc.Units[:1], } envelope := debugSourceChunkEnvelope(chunk) encoded, err := json.Marshal(envelope) if err != nil { t.Fatalf("Marshal(debug source chunk) error = %v, want nil", err) } var decoded debugSourceChunk if err := json.Unmarshal(encoded, &decoded); err != nil { t.Fatalf("Unmarshal(debug source chunk) error = %v, want nil", err) } if decoded.Ref != chunk.Ref { t.Fatalf("debug chunk ref = %#v, want %#v", decoded.Ref, chunk.Ref) } }