285 lines
8.1 KiB
Go
285 lines
8.1 KiB
Go
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
|
|
}
|