package gitx import ( "context" "errors" "fmt" "strings" "testing" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/filemode" "github.com/go-git/go-git/v5/plumbing/object" "sourcecraft.dev/bigbes/sr-ht-spec/core" ) func TestWalkDocumentsYieldsOnlyMarkdown(t *testing.T) { repo, _ := newSpace(t) ctx := context.Background() pushApproved(t, repo, ownerMeta("seed", 1), Write{Path: "specs/0007-storage.md", Content: doc("SPEC-0007", "Storage", "alpha")}, Write{Path: "notes/daily.md", Content: doc("NOTE-0001", "Daily", "beta")}, Write{Path: core.PolicyFile, Content: []byte("review:\n auto_merge: [notes/**]\n")}, Write{Path: "specs/diagram.png", Content: []byte{0x89, 'P', 'N', 'G'}}, ) docs, err := repo.ListDocuments(ctx, DefaultApprovedBranch) if err != nil { t.Fatalf("ListDocuments: %v", err) } var paths []string for _, d := range docs { paths = append(paths, d.Path) if d.Blob.IsZero() { t.Fatalf("%q has no blob sha; it is the render cache key", d.Path) } if len(d.Data) == 0 { t.Fatalf("%q has no data", d.Path) } } want := []string{"notes/daily.md", "specs/0007-storage.md"} if strings.Join(paths, ",") != strings.Join(want, ",") { t.Fatalf("documents = %v, want %v (attachments and %s are not documents)", paths, want, core.PolicyFile) } // .spec.yml is reachable as a blob even though it is not a document. data, _, err := repo.ReadBlob(ctx, DefaultApprovedBranch, core.PolicyFile) if err != nil { t.Fatalf("ReadBlob(%s): %v", core.PolicyFile, err) } if _, err := core.ParsePolicy(data); err != nil { t.Fatalf("ParsePolicy: %v", err) } } func TestReadIsTheSamePathForEveryRevision(t *testing.T) { repo, _ := newSpace(t) ctx := context.Background() first := pushApproved(t, repo, ownerMeta("v1", 1), Write{Path: "specs/0007.md", Content: doc("SPEC-0007", "Storage", "first")}) pushApproved(t, repo, ownerMeta("v2", 2), Write{Path: "specs/0007.md", Content: doc("SPEC-0007", "Storage", "second")}) // A pinned ?rev= of a superseded revision renders that revision, not the head. if got := mustRead(t, repo, first.String(), "specs/0007.md"); !strings.Contains(got, "first") { t.Fatalf("pinned read = %q, want the superseded revision", got) } if got := mustRead(t, repo, DefaultApprovedBranch, "specs/0007.md"); !strings.Contains(got, "second") { t.Fatalf("branch read = %q, want the head revision", got) } // A proposal branch reads through the very same call. base, err := repo.ApprovedHead(ctx) if err != nil { t.Fatal(err) } openProposal(t, repo, "proposals/1", base.String(), meta("propose", 3), Write{Path: "specs/0007.md", Content: doc("SPEC-0007", "Storage", "draft")}) if got := mustRead(t, repo, "proposals/1", "specs/0007.md"); !strings.Contains(got, "draft") { t.Fatalf("proposal read = %q, want the draft", got) } } func TestReadMissingAndMalformedPaths(t *testing.T) { repo, _ := newSpace(t) ctx := context.Background() pushApproved(t, repo, ownerMeta("seed", 1), Write{Path: "specs/0007.md", Content: doc("SPEC-0007", "Storage", "body")}) if _, err := repo.ReadDocument(ctx, DefaultApprovedBranch, "specs/nope.md"); !errors.Is(err, ErrNotFound) { t.Fatalf("missing document = %v, want ErrNotFound", err) } // A directory is refused as a distinct class from "absent". if _, _, err := repo.ReadBlob(ctx, DefaultApprovedBranch, "specs"); !errors.Is(err, ErrUnsupportedEntry) { t.Fatalf("reading a directory = %v, want ErrUnsupportedEntry", err) } // core owns path validation; gitx does not re-derive it. for _, p := range []string{"../escape.md", "/abs.md", "specs/.git/x.md", "specs/0007.txt"} { if _, err := repo.ReadDocument(ctx, DefaultApprovedBranch, p); !errors.Is(err, core.ErrInvalidPath) { t.Fatalf("ReadDocument(%q) = %v, want core.ErrInvalidPath", p, err) } } } func TestOversizedBlobIsRefusedNotTruncated(t *testing.T) { repo, _ := newSpace(t) ctx := context.Background() body := strings.Repeat("x", 4096) pushApproved(t, repo, ownerMeta("big", 1), Write{Path: "specs/0007.md", Content: doc("SPEC-0007", "Storage", body)}, Write{Path: "specs/small.md", Content: doc("SPEC-0008", "Small", "tiny")}, ) // Squeeze the per-blob cap below the big document. A truncated markdown // document would be indexed and served as though it were whole, so the read // has to fail instead of returning a prefix. repo.docLimit = 512 _, err := repo.ReadDocument(ctx, DefaultApprovedBranch, "specs/0007.md") if !errors.Is(err, ErrTooLarge) { t.Fatalf("oversized ReadDocument = %v, want ErrTooLarge", err) } if _, _, err := repo.ReadBlob(ctx, DefaultApprovedBranch, "specs/0007.md"); !errors.Is(err, ErrTooLarge) { t.Fatalf("oversized ReadBlob = %v, want ErrTooLarge", err) } if _, err := repo.ListDocuments(ctx, DefaultApprovedBranch); !errors.Is(err, ErrTooLarge) { t.Fatalf("walk over an oversized document = %v, want ErrTooLarge", err) } // The small document is unaffected. if got := mustRead(t, repo, DefaultApprovedBranch, "specs/small.md"); !strings.Contains(got, "tiny") { t.Fatalf("small document = %q", got) } // The write path refuses the same content rather than storing something the // read path could never return. base, err := repo.ApprovedHead(ctx) if err != nil { t.Fatal(err) } if _, err := repo.CreateProposalBranch(ctx, "proposals/1", base.String()); err != nil { t.Fatal(err) } _, err = repo.CommitProposal(ctx, "proposals/1", []Write{{Path: "specs/0009.md", Content: doc("SPEC-0009", "Huge", body)}}, meta("too big", 2)) if !errors.Is(err, ErrTooLarge) { t.Fatalf("oversized CommitProposal = %v, want ErrTooLarge", err) } } func TestWalkBudgetsBoundTotalBytesAndEntries(t *testing.T) { repo, _ := newSpace(t) ctx := context.Background() var writes []Write for i := 1; i <= 6; i++ { writes = append(writes, Write{ Path: fmt.Sprintf("notes/%d.md", i), Content: doc(fmt.Sprintf("NOTE-%04d", i), "Note", strings.Repeat("y", 200)), }) } pushApproved(t, repo, ownerMeta("many", 1), writes...) repo.walkLimit = 300 if _, err := repo.ListDocuments(ctx, DefaultApprovedBranch); !errors.Is(err, ErrTooLarge) { t.Fatalf("walk over the byte budget = %v, want ErrTooLarge", err) } repo.walkLimit = 0 repo.entryLimit = 3 if _, err := repo.ListDocuments(ctx, DefaultApprovedBranch); !errors.Is(err, ErrTooLarge) { t.Fatalf("walk over the entry budget = %v, want ErrTooLarge", err) } } func TestWalkRefusesADocumentThatIsNotABlob(t *testing.T) { repo, _ := newSpace(t) ctx := context.Background() // Build a tree carrying a symlink at a document path. The update hook is // what keeps these out, but --push-option=skip-validation means one can // exist, and quietly skipping it would drop a document out of the index // with nothing recording that it was ever there. target, err := repo.writeBlob("link", []byte("specs/0007.md")) if err != nil { t.Fatal(err) } head, err := repo.ApprovedHead(ctx) if err != nil { t.Fatal(err) } tree, err := repo.treeOf(head) if err != nil { t.Fatal(err) } node, err := repo.loadTree(tree, 0) if err != nil { t.Fatal(err) } node.files["alias.md"] = object.TreeEntry{Name: "alias.md", Mode: filemode.Symlink, Hash: target} treeHash, err := node.write(repo.repo.Storer) if err != nil { t.Fatal(err) } commit, err := repo.writeCommit(ownerMeta("symlink", 1), treeHash, []plumbing.Hash{head}) if err != nil { t.Fatal(err) } ref := plumbing.NewBranchReferenceName(repo.ApprovedBranch()) if err := repo.repo.Storer.SetReference(plumbing.NewHashReference(ref, commit)); err != nil { t.Fatal(err) } if _, err := repo.ListDocuments(ctx, DefaultApprovedBranch); !errors.Is(err, ErrUnsupportedEntry) { t.Fatalf("walk over a symlinked document = %v, want ErrUnsupportedEntry", err) } } func TestReadHonoursContextCancellation(t *testing.T) { repo, _ := newSpace(t) pushApproved(t, repo, ownerMeta("seed", 1), Write{Path: "specs/0007.md", Content: doc("SPEC-0007", "Storage", "body")}) ctx, cancel := context.WithCancel(context.Background()) cancel() if _, err := repo.ListDocuments(ctx, DefaultApprovedBranch); !errors.Is(err, context.Canceled) { t.Fatalf("walk with a cancelled context = %v, want context.Canceled", err) } }