package service
import (
"context"
"errors"
"strings"
"testing"
"sourcecraft.dev/bigbes/sr-ht-spec/core"
)
func TestReadDocumentResolvesTheApprovedHeadByDefault(t *testing.T) {
svc, root := newService(t)
sp := newSpace(t, root, 1)
ctx := context.Background()
first := commitFiles(t, sp, sp.ApprovedBranch(), 1, map[string][]byte{
"specs/0007-storage.md": doc("SPEC-0007", "Storage model", "first"),
})
second := commitFiles(t, sp, sp.ApprovedBranch(), 2, map[string][]byte{
"specs/0007-storage.md": doc("SPEC-0007", "Storage model", "second"),
})
got, err := svc.ReadDocument(ctx, sp, ApprovedRev, "specs/0007-storage.md")
if err != nil {
t.Fatalf("ReadDocument: %v", err)
}
if !contains(got.Data, "second") {
t.Errorf("approved read returned %q, want the newest revision", got.Data)
}
if got.Rev != second.String() {
t.Errorf("Rev = %s, want the resolved approved head %s", got.Rev, second)
}
if got.Blob == "" {
t.Error("Blob is empty; it is the render cache key")
}
// The same call with a pinned revision is the same code path with a
// different revision — one storage tier, no checkout.
pinned, err := svc.ReadDocument(ctx, sp, first.String(), "specs/0007-storage.md")
if err != nil {
t.Fatalf("pinned ReadDocument: %v", err)
}
if !contains(pinned.Data, "first") {
t.Errorf("pinned read returned %q, want the pinned revision", pinned.Data)
}
if pinned.Blob == got.Blob {
t.Error("two revisions of one document share a blob sha")
}
}
// A draft must never be served by default: doing so would poison every
// downstream agent context with unreviewed text.
func TestReadDocumentDoesNotServeAProposalByDefault(t *testing.T) {
svc, root := newService(t)
sp := newSpace(t, root, 1)
ctx := context.Background()
commitFiles(t, sp, sp.ApprovedBranch(), 1, map[string][]byte{
"specs/0007-storage.md": doc("SPEC-0007", "Storage model", "approved"),
})
cutBranch(t, sp, "proposals/1", sp.ApprovedBranch())
commitFiles(t, sp, "proposals/1", 2, map[string][]byte{
"specs/0007-storage.md": doc("SPEC-0007", "Storage model", "draft"),
})
approved, err := svc.ReadDocument(ctx, sp, ApprovedRev, "specs/0007-storage.md")
if err != nil {
t.Fatalf("ReadDocument: %v", err)
}
if !contains(approved.Data, "approved") {
t.Fatalf("default read returned the draft: %q", approved.Data)
}
draft, err := svc.ReadDocument(ctx, sp, "proposals/1", "specs/0007-storage.md")
if err != nil {
t.Fatalf("ReadDocument at the proposal branch: %v", err)
}
if !contains(draft.Data, "draft") {
t.Errorf("proposal read returned %q", draft.Data)
}
}
func TestReadDocumentMapsAbsenceOntoErrNotFound(t *testing.T) {
svc, root := newService(t)
sp := newSpace(t, root, 1)
ctx := context.Background()
tests := []struct {
name string
rev string
path string
}{
{"missing document", ApprovedRev, "specs/nope.md"},
{"unknown revision", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "specs/nope.md"},
// Revision arithmetic is not a usable revision; it must not be
// distinguishable from an absent one by probing.
{"revision arithmetic", "main^2", "specs/nope.md"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if _, err := svc.ReadDocument(ctx, sp, tc.rev, tc.path); !errors.Is(err, ErrNotFound) {
t.Fatalf("err = %v, want ErrNotFound", err)
}
})
}
}
func TestListDocumentsReturnsEveryDocumentAtARevision(t *testing.T) {
svc, root := newService(t)
sp := newSpace(t, root, 1)
ctx := context.Background()
commitFiles(t, sp, sp.ApprovedBranch(), 1, map[string][]byte{
"specs/0007-storage.md": doc("SPEC-0007", "Storage", "body"),
"notes/scratch.md": doc("NOTE-0001", "Scratch", "body"),
"attachment.png": []byte("\x89PNG not a document"),
core.PolicyFile: []byte("review:\n auto_merge: [notes/**]\n"),
})
docs, err := svc.ListDocuments(ctx, sp, ApprovedRev)
if err != nil {
t.Fatalf("ListDocuments: %v", err)
}
var paths []string
for _, d := range docs {
paths = append(paths, d.Path)
}
want := []string{"notes/scratch.md", "specs/0007-storage.md"}
if len(paths) != len(want) {
t.Fatalf("paths = %v, want %v (documents only)", paths, want)
}
for i := range want {
if paths[i] != want[i] {
t.Fatalf("paths = %v, want %v", paths, want)
}
}
}
func TestPolicyReadsTheVersionedSpecYml(t *testing.T) {
svc, root := newService(t)
sp := newSpace(t, root, 1)
ctx := context.Background()
// A space with no .spec.yml gets the house contract and nothing
// auto-merged: the fail-closed direction.
pol, err := svc.Policy(ctx, sp, ApprovedRev)
if err != nil {
t.Fatalf("Policy: %v", err)
}
if pol.AutoMerges("notes/x.md") {
t.Error("a space with no policy auto-merged a path")
}
if len(pol.Schema.Required) == 0 {
t.Error("default policy carries no required keys")
}
before := commitFiles(t, sp, sp.ApprovedBranch(), 1, map[string][]byte{
core.PolicyFile: []byte("review:\n auto_merge: [notes/**]\n"),
})
pol, err = svc.Policy(ctx, sp, ApprovedRev)
if err != nil {
t.Fatalf("Policy: %v", err)
}
if !pol.AutoMerges("notes/x.md") || pol.AutoMerges("specs/x.md") {
t.Errorf("auto_merge = %v", pol.Review.AutoMerge)
}
// Policy is read at a revision, which is what makes a policy change
// reviewable like any other change.
commitFiles(t, sp, sp.ApprovedBranch(), 2, map[string][]byte{
core.PolicyFile: []byte("review:\n auto_merge: []\n"),
})
pinned, err := svc.Policy(ctx, sp, before.String())
if err != nil {
t.Fatalf("pinned Policy: %v", err)
}
if !pinned.AutoMerges("notes/x.md") {
t.Error("a pinned policy read returned the newer policy")
}
}
func TestPolicyFailsOnAnUnparseableSpecYml(t *testing.T) {
svc, root := newService(t)
sp := newSpace(t, root, 1)
commitFiles(t, sp, sp.ApprovedBranch(), 1, map[string][]byte{
core.PolicyFile: []byte("review:\n auto-merge: [notes/**]\n"),
})
_, err := svc.Policy(context.Background(), sp, ApprovedRev)
if !errors.Is(err, core.ErrInvalidPolicy) {
t.Fatalf("err = %v, want core.ErrInvalidPolicy — a typo that silently does nothing is worse", err)
}
}
func TestResolveRevPinsTheApprovedHead(t *testing.T) {
svc, root := newService(t)
sp := newSpace(t, root, 1)
head := commitFiles(t, sp, sp.ApprovedBranch(), 1, map[string][]byte{
"specs/0007-storage.md": doc("SPEC-0007", "Storage", "body"),
})
got, err := svc.ResolveRev(context.Background(), sp, ApprovedRev)
if err != nil {
t.Fatalf("ResolveRev: %v", err)
}
if got != head.String() {
t.Errorf("ResolveRev = %s, want %s", got, head)
}
}
func contains(data []byte, want string) bool {
return strings.Contains(string(data), want)
}