package artifact import ( "bytes" "context" "errors" "os" "path/filepath" "strings" "testing" "gitea.maximumdirect.net/eric/promptkit/internal/domain" ) func TestCompositeReaderRejectsUnsupportedReferences(t *testing.T) { _, err := NewCompositeReader().Read(context.Background(), domain.ArtifactRef{ Type: domain.ArtifactRefType("unsupported"), URI: "unsupported://bucket/key", }) if !errors.Is(err, ErrUnsupportedRefType) { t.Fatalf("expected ErrUnsupportedRefType, got %v", err) } } func TestCompositeReaderSourceParityAndOpaqueHashes(t *testing.T) { reader := NewCompositeReader() hashes := make(map[string]string) tests := []struct { name string content string }{ {name: "empty", content: ""}, {name: "ordinary", content: "same content"}, {name: "changed", content: "changed content"}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { filePath := filepath.Join(t.TempDir(), "artifact.txt") if err := os.WriteFile(filePath, []byte(tc.content), 0o600); err != nil { t.Fatal(err) } sources := []struct { name string ref domain.ArtifactRef wantURI string }{ { name: "inline", ref: domain.ArtifactRef{Type: domain.ArtifactRefInline, Body: tc.content}, }, { name: "inline with uri", ref: domain.ArtifactRef{Type: domain.ArtifactRefInline, URI: "memory://input", Body: tc.content}, wantURI: "memory://input", }, { name: "file", ref: domain.ArtifactRef{Type: domain.ArtifactRefFile, URI: filePath}, wantURI: filePath, }, } var sourceHash string for _, source := range sources { t.Run(source.name, func(t *testing.T) { first, err := reader.Read(context.Background(), source.ref) if err != nil { t.Fatalf("first read: %v", err) } second, err := reader.Read(context.Background(), source.ref) if err != nil { t.Fatalf("second read: %v", err) } if string(first.Body) != tc.content || first.Size != int64(len(tc.content)) { t.Fatalf("body=%q size=%d, want %q/%d", first.Body, first.Size, tc.content, len(tc.content)) } if first.URI != source.wantURI { t.Fatalf("URI = %q, want %q", first.URI, source.wantURI) } if first.Hash == "" || first.Hash != second.Hash { t.Fatalf("hashes are not non-empty and stable: %q/%q", first.Hash, second.Hash) } if sourceHash == "" { sourceHash = first.Hash } else if first.Hash != sourceHash { t.Fatalf("equal content hashes differ: %q/%q", sourceHash, first.Hash) } if source.ref.Type == domain.ArtifactRefFile { if first.Name != filepath.Base(filePath) || !strings.HasPrefix(first.ContentType, "text/plain") { t.Fatalf("unexpected file metadata: %+v", first) } } else if first.ContentType != "text/plain" { t.Fatalf("inline content type = %q", first.ContentType) } }) } hashes[tc.name] = sourceHash }) } if hashes["empty"] == hashes["ordinary"] || hashes["ordinary"] == hashes["changed"] { t.Fatalf("changed content did not change opaque hash: %#v", hashes) } } func TestCompositeReaderCopiesInlineData(t *testing.T) { reader := NewCompositeReader() ref := domain.ArtifactRef{ Type: domain.ArtifactRefInline, Body: "hello", URI: "inline:greeting", } first, err := reader.Read(context.Background(), ref) if err != nil { t.Fatalf("read first artifact: %v", err) } first.Body[0] = 'j' second, err := reader.Read(context.Background(), ref) if err != nil { t.Fatalf("read second artifact: %v", err) } if got := string(second.Body); got != ref.Body { t.Fatalf("expected an independent body %q, got %q", ref.Body, got) } if second.URI != ref.URI { t.Fatalf("expected URI %q, got %q", ref.URI, second.URI) } } func TestCompositeReaderHonorsPreCancellation(t *testing.T) { filePath := filepath.Join(t.TempDir(), "artifact.txt") if err := os.WriteFile(filePath, []byte("ignored"), 0o600); err != nil { t.Fatal(err) } tests := []struct { name string ref domain.ArtifactRef }{ {name: "inline", ref: domain.ArtifactRef{Type: domain.ArtifactRefInline, Body: "ignored"}}, {name: "file", ref: domain.ArtifactRef{Type: domain.ArtifactRefFile, URI: filePath}}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() artifact, err := NewCompositeReader().Read(ctx, tc.ref) if artifact != nil || !errors.Is(err, context.Canceled) { t.Fatalf("artifact=%#v err=%v, want nil/context.Canceled", artifact, err) } }) } } func TestFileReaderFailuresAndMetadata(t *testing.T) { reader := NewCompositeReader() t.Run("missing file path", func(t *testing.T) { _, err := reader.Read(context.Background(), domain.ArtifactRef{Type: domain.ArtifactRefFile}) if !errors.Is(err, ErrMissingFilePath) { t.Fatalf("expected ErrMissingFilePath, got %v", err) } }) t.Run("missing file", func(t *testing.T) { _, err := reader.Read(context.Background(), domain.ArtifactRef{ Type: domain.ArtifactRefFile, URI: filepath.Join(t.TempDir(), "missing.txt"), }) if err == nil { t.Fatal("expected missing file error") } }) t.Run("directory rejected before open", func(t *testing.T) { artifact, err := reader.Read(context.Background(), domain.ArtifactRef{ Type: domain.ArtifactRefFile, URI: t.TempDir(), }) if artifact != nil || !errors.Is(err, ErrUnsupportedFile) { t.Fatalf("artifact=%#v err=%v, want nil/ErrUnsupportedFile", artifact, err) } }) t.Run("non-regular opened target rejected", func(t *testing.T) { filePath := filepath.Join(t.TempDir(), "artifact.txt") if err := os.WriteFile(filePath, []byte("content"), 0o600); err != nil { t.Fatal(err) } directoryInfo, err := os.Stat(t.TempDir()) if err != nil { t.Fatal(err) } fileReader := &fileReader{open: func(path string) (artifactFile, error) { file, err := os.Open(path) if err != nil { return nil, err } return &reportedInfoFile{artifactFile: file, info: directoryInfo}, nil }} artifact, err := fileReader.Read(context.Background(), domain.ArtifactRef{ Type: domain.ArtifactRefFile, URI: filePath, }) if artifact != nil || !errors.Is(err, ErrUnsupportedFile) { t.Fatalf("artifact=%#v err=%v, want nil/ErrUnsupportedFile", artifact, err) } }) t.Run("unknown extension uses text fallback", func(t *testing.T) { filePath := filepath.Join(t.TempDir(), "artifact.unknownextension") if err := os.WriteFile(filePath, []byte("content"), 0o600); err != nil { t.Fatal(err) } artifact, err := reader.Read(context.Background(), domain.ArtifactRef{ Type: domain.ArtifactRefFile, URI: filePath, }) if err != nil { t.Fatalf("read artifact: %v", err) } if artifact.ContentType != "text/plain" { t.Fatalf("content type = %q", artifact.ContentType) } }) } func TestFileReaderCancelsAfterReadProgress(t *testing.T) { filePath := filepath.Join(t.TempDir(), "artifact.bin") content := bytes.Repeat([]byte("x"), fileReadChunkSize*2) if err := os.WriteFile(filePath, content, 0o600); err != nil { t.Fatal(err) } ctx, cancel := context.WithCancel(context.Background()) var opened *cancelAfterProgressFile reader := &fileReader{open: func(path string) (artifactFile, error) { file, err := os.Open(path) if err != nil { return nil, err } opened = &cancelAfterProgressFile{artifactFile: file, cancel: cancel} return opened, nil }} artifact, err := reader.Read(ctx, domain.ArtifactRef{Type: domain.ArtifactRefFile, URI: filePath}) if artifact != nil || !errors.Is(err, context.Canceled) { t.Fatalf("artifact=%#v err=%v, want nil/context.Canceled", artifact, err) } if opened == nil || opened.reads != 1 { t.Fatalf("read count = %v, want one progressing read", opened) } } type reportedInfoFile struct { artifactFile info os.FileInfo } func (f *reportedInfoFile) Stat() (os.FileInfo, error) { return f.info, nil } type cancelAfterProgressFile struct { artifactFile cancel context.CancelFunc reads int } func (f *cancelAfterProgressFile) Read(buffer []byte) (int, error) { n, err := f.artifactFile.Read(buffer) if n > 0 { f.reads++ f.cancel() } return n, err }