Files
sow-tools/internal/depot/integration_test.go
T
archvillainetteandClaude Fable 5 f57c3d2c32 feat(depot): status/push/verify/get/pull commands with exit-code contract
Adds internal/depot.Run, the stdlib-flag command surface for the five
depot subcommands (0/1/2/64/70 exit contract), plus a dispatch special
case that will route "crucible depot ..." and crucible-depot to it once
Task 6 flips Wired:true (inert for now since Wired stays false).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 23:53:58 +02:00

185 lines
5.9 KiB
Go

package depot
import (
"bytes"
"fmt"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
// TestIntegrationPushStatusPull drives status/push/pull end-to-end against a
// fake Bunny backend (real bunny HTTP path, in-memory blob store) since the
// command surface has no local->local push (push --target must be bunny).
func TestIntegrationPushStatusPull(t *testing.T) {
manifestsDir := t.TempDir()
sourceDir := t.TempDir()
blobs := map[string]string{
shaOf("blob-one"): "blob-one",
shaOf("blob-two"): "blob-two",
shaOf("blob-three"): "blob-three",
}
var manifest strings.Builder
manifest.WriteString("assets:\n")
for sha, content := range blobs {
manifest.WriteString(fmt.Sprintf(" - path: %s\n sha256: %s\n size: %d\n", sha, sha, len(content)))
blobPath := filepath.Join(sourceDir, BlobKey(sha))
if err := os.MkdirAll(filepath.Dir(blobPath), 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(blobPath, []byte(content), 0644); err != nil {
t.Fatal(err)
}
}
if err := os.WriteFile(filepath.Join(manifestsDir, "manifest.yml"), []byte(manifest.String()), 0644); err != nil {
t.Fatal(err)
}
f := newFakeBunny()
// Pre-seed one of the three blobs on the target so status starts with drift=2.
var oneSHA string
for sha := range blobs {
oneSHA = sha
break
}
f.blobs[oneSHA] = []byte(blobs[oneSHA])
srv := httptest.NewServer(f.handler())
defer srv.Close()
env := map[string]string{
"BUNNY_STORAGE_HOST": hostPort(srv),
"BUNNY_STORAGE_READ_PASSWORD": "readkey",
"BUNNY_STORAGE_PASSWORD": "writekey",
"CDN_UNREACHABLE": "1", // unused; CDNBase defaults to unreachable in test cfg path via storageURL fallback
}
getenv := testGetenv(env)
// status: expect exit 1, missing=2
var out, errb bytes.Buffer
code := Run([]string{"status", "--manifests", manifestsDir, "--target", "bunny"}, &out, &errb, getenv)
if code != 1 {
t.Fatalf("status: expected 1, got %d (stdout=%s stderr=%s)", code, out.String(), errb.String())
}
if !bytesContains(out.String(), "missing=2") {
t.Fatalf("status: expected missing=2, got %s", out.String())
}
// push: uploads the 2 missing blobs
out.Reset()
errb.Reset()
code = Run([]string{"push", "--manifests", manifestsDir, "--source", sourceDir, "--target", "bunny"}, &out, &errb, getenv)
if code != 0 {
t.Fatalf("push: expected 0, got %d (stdout=%s stderr=%s)", code, out.String(), errb.String())
}
if !bytesContains(out.String(), "uploaded=2 failed=0") {
t.Fatalf("push: expected uploaded=2 failed=0, got %s", out.String())
}
putCountAfterFirstPush := 0
for _, r := range f.requestsSnapshot() {
if r.Method == "PUT" {
putCountAfterFirstPush++
}
}
if putCountAfterFirstPush != 2 {
t.Fatalf("expected 2 PUTs after first push, got %d", putCountAfterFirstPush)
}
// status: now clean
out.Reset()
errb.Reset()
code = Run([]string{"status", "--manifests", manifestsDir, "--target", "bunny"}, &out, &errb, getenv)
if code != 0 {
t.Fatalf("status after push: expected 0, got %d (stdout=%s stderr=%s)", code, out.String(), errb.String())
}
// push again: idempotent, no new PUTs, uploaded=0
out.Reset()
errb.Reset()
code = Run([]string{"push", "--manifests", manifestsDir, "--source", sourceDir, "--target", "bunny"}, &out, &errb, getenv)
if code != 0 {
t.Fatalf("second push: expected 0, got %d (stdout=%s stderr=%s)", code, out.String(), errb.String())
}
if !bytesContains(out.String(), "uploaded=0 failed=0") {
t.Fatalf("second push: expected uploaded=0 failed=0, got %s", out.String())
}
putCountAfterSecondPush := 0
for _, r := range f.requestsSnapshot() {
if r.Method == "PUT" {
putCountAfterSecondPush++
}
}
if putCountAfterSecondPush != putCountAfterFirstPush {
t.Fatalf("expected no additional PUTs on idempotent push, before=%d after=%d", putCountAfterFirstPush, putCountAfterSecondPush)
}
// pull into a dest dir: one pre-populated correct file (skipped, no GET),
// one pre-populated corrupt file (re-downloaded), one absent (downloaded).
destDir := t.TempDir()
shas := make([]string, 0, len(blobs))
for sha := range blobs {
shas = append(shas, sha)
}
correctSHA := shas[0]
corruptSHA := shas[1]
// absentSHA := shas[2] // left absent on purpose
correctPath := filepath.Join(destDir, BlobKey(correctSHA))
if err := os.MkdirAll(filepath.Dir(correctPath), 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(correctPath, []byte(blobs[correctSHA]), 0644); err != nil {
t.Fatal(err)
}
corruptPath := filepath.Join(destDir, BlobKey(corruptSHA))
if err := os.MkdirAll(filepath.Dir(corruptPath), 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(corruptPath, []byte("corrupted-content"), 0644); err != nil {
t.Fatal(err)
}
getCountBeforePull := 0
for _, r := range f.requestsSnapshot() {
if r.Method == "GET" && !strings.Contains(r.Range, "0-0") {
getCountBeforePull++
}
}
out.Reset()
errb.Reset()
code = Run([]string{"pull", "--manifests", manifestsDir, "--dest", destDir, "--target", "bunny"}, &out, &errb, getenv)
if code != 0 {
t.Fatalf("pull: expected 0, got %d (stdout=%s stderr=%s)", code, out.String(), errb.String())
}
if !bytesContains(out.String(), "pulled=2 present=1") {
t.Fatalf("pull: expected pulled=2 present=1, got %s", out.String())
}
// verify the correct pre-existing file was never re-fetched with a full GET.
fullGETsForCorrect := 0
for _, r := range f.requestsSnapshot() {
if r.Method == "GET" && strings.HasSuffix(r.Path, correctSHA) && r.Range != "bytes=0-0" {
fullGETsForCorrect++
}
}
if fullGETsForCorrect != 0 {
t.Fatalf("expected no full GET for already-correct blob, got %d", fullGETsForCorrect)
}
// all three blobs should now be present and correct in destDir.
for sha, content := range blobs {
got, err := os.ReadFile(filepath.Join(destDir, BlobKey(sha)))
if err != nil {
t.Fatalf("dest blob %s: %v", sha, err)
}
if string(got) != content {
t.Fatalf("dest blob %s: expected %q, got %q", sha, content, string(got))
}
}
}