crucible depot: Increment 1 core (status/push/verify/get/pull + local/cdn/bunny backends) (#30)
test / test (push) Successful in 1m31s
build-binaries / build-binaries (push) Successful in 2m27s
build-image / publish (push) Successful in 42s

Implements Increment 1 of `docs/superpowers/specs/2026-07-04-crucible-depot-core-design.md`: a new stdlib-only `internal/depot` package wired into the dispatcher.

- `crucible depot status|push|verify|get|pull` with backends `local`/`cdn`/`bunny`; exit contract `0` clean / `1` drift / `2` unconfirmed-only / `64` usage / `70` internal.
- Presence is always probed against the real target (IPv4 1-byte range GET; HEAD is banned with a regression-tripwire test). `unconfirmed` is a distinct state, never collapsed into `missing`.
- No prompting anywhere: missing `BUNNY_STORAGE_*` env fails closed (read path included), enforced by a no-stdin test.
- Uploads: probe-then-PUT with `Checksum: <UPPER-sha>`; read/write key split (`BUNNY_STORAGE_READ_PASSWORD` falls back to `BUNNY_STORAGE_PASSWORD`).
- Field-driven fix included: per-probe transient retry (curl `--retry 2` equivalent) — without it a real 1490-blob CDN sweep reported 1222 false-unconfirmed; with it, 1490/1490 present in 74s, exit 0.
- Registry: depot `Wired: true`, joins the interactive menu; stale "(SeaweedFS)" wording removed.

Tests: unit + httptest fake-Bunny (probe sequence, Checksum header, key split, 428 throttling → exit 2) + local→bunny integration (drift → push → clean → idempotent no-second-PUT; incremental pull). `make check` green.

**Merge ordering:** this merges FIRST; the companion `sow-assets-manifest#crucible-depot-cutover` PR needs its flake input bumped to include this.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: #30
Reviewed-by: xtul <mpiasecki720@protonmail.com>
Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
This commit was merged in pull request #30.
This commit is contained in:
2026-07-04 23:30:54 +00:00
committed by archvillainette
parent 448ebc74d4
commit 9357b30994
19 changed files with 3176 additions and 32 deletions
+181
View File
@@ -0,0 +1,181 @@
package depot
import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
)
func testGetenv(vals map[string]string) func(string) string {
return func(k string) string { return vals[k] }
}
func TestRunUsage(t *testing.T) {
t.Run("no args", func(t *testing.T) {
var out, errb bytes.Buffer
code := Run(nil, &out, &errb, testGetenv(nil))
if code != 64 {
t.Fatalf("expected 64, got %d (stderr=%s)", code, errb.String())
}
})
t.Run("unknown subcommand", func(t *testing.T) {
var out, errb bytes.Buffer
code := Run([]string{"bogus"}, &out, &errb, testGetenv(nil))
if code != 64 {
t.Fatalf("expected 64, got %d (stderr=%s)", code, errb.String())
}
})
t.Run("status --target local without DEPOT_DIR", func(t *testing.T) {
dir := t.TempDir()
writeManifest(t, dir, shaOf("blob-a"), 5)
var out, errb bytes.Buffer
code := Run([]string{"status", "--manifests", dir, "--target", "local"}, &out, &errb, testGetenv(nil))
if code != 64 {
t.Fatalf("expected 64, got %d (stderr=%s)", code, errb.String())
}
if !bytesContains(errb.String(), "DEPOT_DIR") {
t.Fatalf("expected stderr to mention DEPOT_DIR, got %s", errb.String())
}
})
t.Run("push --target cdn rejected", func(t *testing.T) {
dir := t.TempDir()
var out, errb bytes.Buffer
code := Run([]string{"push", "--manifests", dir, "--source", dir, "--target", "cdn"}, &out, &errb, testGetenv(nil))
if code != 64 {
t.Fatalf("expected 64, got %d (stderr=%s)", code, errb.String())
}
})
}
func TestGetInvalidSHA(t *testing.T) {
var out, errb bytes.Buffer
dir := t.TempDir()
code := Run([]string{"get", "not-a-sha", dir + "/dest", "--target", "local"}, &out, &errb, testGetenv(map[string]string{"DEPOT_DIR": dir}))
if code != 64 {
t.Fatalf("expected 64, got %d (stderr=%s)", code, errb.String())
}
}
func TestNoKeyStatusBunnyFailsClosedNoStdin(t *testing.T) {
dir := t.TempDir()
writeManifest(t, dir, shaOf("blob-a"), 5)
var out, errb bytes.Buffer
code := Run([]string{"status", "--manifests", dir, "--target", "bunny"}, &out, &errb, testGetenv(nil))
if code != 70 {
t.Fatalf("expected 70, got %d (stdout=%s stderr=%s)", code, out.String(), errb.String())
}
if !bytesContains(errb.String(), "BUNNY_STORAGE_HOST") {
t.Fatalf("expected stderr to mention BUNNY_STORAGE_HOST, got %s", errb.String())
}
}
func TestStatusExitCodes(t *testing.T) {
t.Run("drift", func(t *testing.T) {
f := newFakeBunny()
srv := httptest.NewServer(f.handler())
defer srv.Close()
sha := shaOf("missing-blob")
dir := t.TempDir()
writeManifest(t, dir, sha, 5)
var out, errb bytes.Buffer
code := Run([]string{"status", "--manifests", dir, "--target", "bunny"}, &out, &errb,
testGetenv(map[string]string{
"BUNNY_STORAGE_HOST": hostPort(srv),
"BUNNY_STORAGE_READ_PASSWORD": "readkey",
}))
if code != 1 {
t.Fatalf("expected 1, got %d (stdout=%s stderr=%s)", code, out.String(), errb.String())
}
if !bytesContains(out.String(), "missing="+"1") {
t.Fatalf("expected missing=1 in output, got %s", out.String())
}
if !bytesContains(out.String(), "missing "+sha) {
t.Fatalf("expected missing line for sha, got %s", out.String())
}
})
t.Run("unconfirmed-only", func(t *testing.T) {
stubProbeSleep(t)
f := newFakeBunny()
sha := shaOf("flaky-blob")
f.statusFor[sha] = 428
srv := httptest.NewServer(f.handler())
defer srv.Close()
dir := t.TempDir()
writeManifest(t, dir, sha, 5)
var out, errb bytes.Buffer
code := Run([]string{"status", "--manifests", dir, "--target", "bunny"}, &out, &errb,
testGetenv(map[string]string{
"BUNNY_STORAGE_HOST": hostPort(srv),
"BUNNY_STORAGE_READ_PASSWORD": "readkey",
"DEPOT_CONFIRM_RETRIES": "1",
}))
if code != 2 {
t.Fatalf("expected 2, got %d (stdout=%s stderr=%s)", code, out.String(), errb.String())
}
if !bytesContains(out.String(), "unconfirmed="+"1") {
t.Fatalf("expected unconfirmed=1 in output, got %s", out.String())
}
})
}
func TestPushTransportErrorCountsFailed(t *testing.T) {
// Probes 404 (blob absent), PUTs 500 (transport-level upload failure).
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPut {
w.WriteHeader(500)
return
}
w.WriteHeader(404)
}))
defer srv.Close()
sha := shaOf("push-fail-blob")
manifestsDir := t.TempDir()
writeManifest(t, manifestsDir, sha, 14)
sourceDir := t.TempDir()
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("push-fail-blob"), 0644); err != nil {
t.Fatal(err)
}
var out, errb bytes.Buffer
code := Run([]string{"push", "--manifests", manifestsDir, "--source", sourceDir, "--target", "bunny"}, &out, &errb,
testGetenv(map[string]string{
"BUNNY_STORAGE_HOST": hostPort(srv),
"BUNNY_STORAGE_PASSWORD": "writekey",
}))
if code != 70 {
t.Fatalf("expected 70, got %d (stdout=%s stderr=%s)", code, out.String(), errb.String())
}
if !bytesContains(out.String(), "uploaded=0 failed=1") {
t.Fatalf("expected uploaded=0 failed=1, got %s", out.String())
}
}
func bytesContains(s, substr string) bool {
return bytes.Contains([]byte(s), []byte(substr))
}
func writeManifest(t *testing.T, dir, sha string, size int64) {
t.Helper()
content := fmt.Sprintf("assets:\n - path: foo\n sha256: %s\n size: %d\n", sha, size)
if err := os.WriteFile(filepath.Join(dir, "manifest.yml"), []byte(content), 0644); err != nil {
t.Fatal(err)
}
}