package service
import (
"bytes"
"context"
"testing"
"sourcecraft.dev/bigbes/sr-ht-spec/core"
)
// TestInboxAndDigest proves the instance-wide queues: an open proposal lands in
// the inbox, a policy-auto-merged one lands in the digest, and neither shows in
// the other.
func TestInboxAndDigest(t *testing.T) {
svc, _ := newTestService(t)
ctx := context.Background()
sp, err := svc.CreateSpace(ctx, fxSpace)
if err != nil {
t.Fatalf("CreateSpace: %v", err)
}
// A policy that auto-merges notes/, so one proposal lands and one waits.
commitFiles(t, sp, sp.ApprovedBranch(), 1, map[string][]byte{
".spec.yml": []byte("review:\n auto_merge: [notes/**]\n"),
})
base, err := sp.Repo.ApprovedHead(ctx)
if err != nil {
t.Fatalf("ApprovedHead: %v", err)
}
// Auto-merges (notes/).
auto, err := svc.Propose(ctx, ProposeRequest{
Space: fxSpace, Principal: agentPrincipal(), Title: "note", IfMatch: base.String(),
Message: "note", Writes: []DocumentWrite{{Path: "notes/a.md", Content: mdDoc("N-1", "A", "b")}},
})
if err != nil || !auto.Merged {
t.Fatalf("auto-merge propose: merged=%v err=%v", auto.Merged, err)
}
// Stays open (specs/).
open, err := svc.Propose(ctx, ProposeRequest{
Space: fxSpace, Principal: agentPrincipal(), Title: "spec", IfMatch: base.String(),
Message: "spec", Writes: []DocumentWrite{{Path: "specs/b.md", Content: mdDoc("S-1", "B", "c")}},
})
if err != nil || open.Merged {
t.Fatalf("open propose: merged=%v err=%v", open.Merged, err)
}
inbox, err := svc.InboxProposals(ctx)
if err != nil {
t.Fatalf("InboxProposals: %v", err)
}
if len(inbox) != 1 || inbox[0].ID != open.Proposal.ID {
t.Fatalf("inbox = %+v, want only the open proposal %d", inbox, open.Proposal.ID)
}
digest, err := svc.DigestProposals(ctx, 0)
if err != nil {
t.Fatalf("DigestProposals: %v", err)
}
if len(digest) != 1 || digest[0].ID != auto.Proposal.ID || digest[0].Approval != core.ApprovalPolicy {
t.Fatalf("digest = %+v, want only the policy-merged proposal %d", digest, auto.Proposal.ID)
}
}
// TestProposalDiffReturnsChangedDocuments proves ProposalDiff returns exactly
// the documents a proposal changes — a modified one with its base and proposed
// content, and an added one marked new — and not the documents it leaves alone.
func TestProposalDiffReturnsChangedDocuments(t *testing.T) {
svc, _ := newTestService(t)
ctx := context.Background()
sp, err := svc.CreateSpace(ctx, fxSpace)
if err != nil {
t.Fatalf("CreateSpace: %v", err)
}
// The approved head carries two documents; the proposal edits one, adds a
// third, and leaves the second untouched.
commitFiles(t, sp, sp.ApprovedBranch(), 1, map[string][]byte{
"specs/a.md": mdDoc("S-1", "A", "original body"),
"specs/keep.md": mdDoc("S-2", "Keep", "unchanged body"),
})
base, err := sp.Repo.ApprovedHead(ctx)
if err != nil {
t.Fatalf("ApprovedHead: %v", err)
}
res, err := svc.Propose(ctx, ProposeRequest{
Space: fxSpace,
Principal: agentPrincipal(),
Title: "edit and add",
IfMatch: base.String(),
Message: "two changes",
Writes: []DocumentWrite{
{Path: "specs/a.md", Content: mdDoc("S-1", "A", "revised body")},
{Path: "specs/new.md", Content: mdDoc("S-3", "New", "brand new body")},
},
})
if err != nil {
t.Fatalf("Propose: %v", err)
}
docs, err := svc.ProposalDiff(ctx, res.Proposal)
if err != nil {
t.Fatalf("ProposalDiff: %v", err)
}
byPath := make(map[string]ProposalDoc, len(docs))
for _, d := range docs {
byPath[d.Path] = d
}
if _, ok := byPath["specs/keep.md"]; ok {
t.Errorf("ProposalDiff returned the untouched specs/keep.md")
}
edited, ok := byPath["specs/a.md"]
if !ok {
t.Fatalf("ProposalDiff missing the edited document")
}
if edited.New {
t.Errorf("specs/a.md marked new, want an edit")
}
if !bytes.Contains(edited.Base, []byte("original body")) {
t.Errorf("edited doc base = %q, want the approved content", edited.Base)
}
if !bytes.Contains(edited.Proposed, []byte("revised body")) {
t.Errorf("edited doc proposed = %q, want the proposal content", edited.Proposed)
}
added, ok := byPath["specs/new.md"]
if !ok {
t.Fatalf("ProposalDiff missing the added document")
}
if !added.New || added.Base != nil {
t.Errorf("added doc = %+v, want New with a nil base", added)
}
}