package service import ( "context" "errors" "testing" "sourcecraft.dev/bigbes/sr-ht-spec/core" ) // openProposalFor opens a proposal against the current approved head and returns // its result. It is the common setup for the merge tests, which then act on the // proposal the design's review plane would. func openProposalFor(t *testing.T, svc *Service, sp *Space, path, id string, content []byte) ProposeResult { t.Helper() ctx := context.Background() 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: "proposal for " + path, IfMatch: base.String(), Message: "write " + path, Writes: []DocumentWrite{{Path: path, Content: content}}, }) if err != nil { t.Fatalf("Propose(%s): %v", path, err) } return res } // TestMergeLandsProposal proves the human approve path: an open proposal that no // policy auto-merges is merged on request, recorded as human-approved, and its // document is then readable at the approved head. func TestMergeLandsProposal(t *testing.T) { svc, _ := newTestService(t) ctx := context.Background() sp, err := svc.CreateSpace(ctx, fxSpace) if err != nil { t.Fatalf("CreateSpace: %v", err) } res := openProposalFor(t, svc, sp, "specs/a.md", "S-1", mdDoc("S-1", "A", "body")) if res.Merged { t.Fatalf("specs/ proposal auto-merged without a policy") } merged, err := svc.Merge(ctx, fxSpace, res.Proposal.ID, core.ApprovalHuman) if err != nil { t.Fatalf("Merge: %v", err) } if merged.State != core.StateMerged || merged.Approval != core.ApprovalHuman { t.Fatalf("merged = %+v, want state=merged approval=human", merged) } // The document is now on the approved head. doc, err := svc.ReadDocument(ctx, sp, ApprovedRev, "specs/a.md") if err != nil { t.Fatalf("ReadDocument after merge: %v", err) } if len(doc.Data) == 0 { t.Fatalf("merged document reads empty") } // A merged proposal is no longer open, so a second resolution is refused. if _, err := svc.Merge(ctx, fxSpace, res.Proposal.ID, core.ApprovalHuman); !errors.Is(err, ErrProposalNotOpen) { t.Fatalf("re-merge: err = %v, want ErrProposalNotOpen", err) } } // TestRejectResolvesProposal proves reject moves an open proposal to rejected // and leaves it listable there, its URL still resolving. func TestRejectResolvesProposal(t *testing.T) { svc, _ := newTestService(t) ctx := context.Background() sp, err := svc.CreateSpace(ctx, fxSpace) if err != nil { t.Fatalf("CreateSpace: %v", err) } res := openProposalFor(t, svc, sp, "specs/a.md", "S-1", mdDoc("S-1", "A", "body")) rejected, err := svc.Reject(ctx, fxSpace, res.Proposal.ID) if err != nil { t.Fatalf("Reject: %v", err) } if rejected.State != core.StateRejected { t.Fatalf("state = %s, want rejected", rejected.State) } got, err := svc.GetProposal(ctx, res.Proposal.ID) if err != nil { t.Fatalf("GetProposal after reject: %v", err) } if got.State != core.StateRejected { t.Fatalf("GetProposal state = %s, want rejected", got.State) } } // TestMergeStaleWhenDocumentChangedUnderIt proves the 409: a document the // proposal edits, changed on the approved branch since the proposal's base, // cannot merge — the loser refetches and re-proposes. func TestMergeStaleWhenDocumentChangedUnderIt(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 v1 of the document the proposal will edit. commitFiles(t, sp, sp.ApprovedBranch(), 1, map[string][]byte{ "specs/a.md": mdDoc("S-1", "A", "v1"), }) res := openProposalFor(t, svc, sp, "specs/a.md", "S-1", mdDoc("S-1", "A", "v2-from-agent")) // A human pushes v3 of the same document onto the approved head, after the // proposal's base. commitFiles(t, sp, sp.ApprovedBranch(), 2, map[string][]byte{ "specs/a.md": mdDoc("S-1", "A", "v3-from-human"), }) _, err = svc.Merge(ctx, fxSpace, res.Proposal.ID, core.ApprovalHuman) if !errors.Is(err, ErrStale) { t.Fatalf("Merge of a proposal whose document moved under it: err = %v, want ErrStale", err) } } // TestProposeAddToExistingThenMerge proves the X-Proposal path: a second write // against a proposal's fixed base adds to it, and merging then lands both // documents at once. func TestProposeAddToExistingThenMerge(t *testing.T) { svc, _ := newTestService(t) ctx := context.Background() sp, err := svc.CreateSpace(ctx, fxSpace) if err != nil { t.Fatalf("CreateSpace: %v", err) } first := openProposalFor(t, svc, sp, "specs/a.md", "S-1", mdDoc("S-1", "A", "one")) // Add a second document to the same proposal, sending the same base. second, err := svc.Propose(ctx, ProposeRequest{ Space: fxSpace, Principal: agentPrincipal(), ProposalID: first.Proposal.ID, IfMatch: first.Proposal.BaseRev, Message: "add specs/b.md", Writes: []DocumentWrite{{Path: "specs/b.md", Content: mdDoc("S-2", "B", "two")}}, }) if err != nil { t.Fatalf("Propose add-to-existing: %v", err) } if second.Proposal.ID != first.Proposal.ID { t.Fatalf("add opened a new proposal %d, want %d", second.Proposal.ID, first.Proposal.ID) } if _, err := svc.Merge(ctx, fxSpace, first.Proposal.ID, core.ApprovalHuman); err != nil { t.Fatalf("Merge: %v", err) } for _, path := range []string{"specs/a.md", "specs/b.md"} { if _, err := svc.ReadDocument(ctx, sp, ApprovedRev, path); err != nil { t.Fatalf("ReadDocument(%s) after merge: %v", path, err) } } } // TestAddToExistingRejectsDriftedBase proves an add whose If-Match no longer // names the proposal's base is a 409 rather than a silent write against the old // base. func TestAddToExistingRejectsDriftedBase(t *testing.T) { svc, _ := newTestService(t) ctx := context.Background() sp, err := svc.CreateSpace(ctx, fxSpace) if err != nil { t.Fatalf("CreateSpace: %v", err) } first := openProposalFor(t, svc, sp, "specs/a.md", "S-1", mdDoc("S-1", "A", "one")) // The approved head moves; the agent mistakenly sends the new head as its // base for the add. newHead := commitFiles(t, sp, sp.ApprovedBranch(), 2, map[string][]byte{ "specs/c.md": mdDoc("S-3", "C", "unrelated"), }) _, err = svc.Propose(ctx, ProposeRequest{ Space: fxSpace, Principal: agentPrincipal(), ProposalID: first.Proposal.ID, IfMatch: newHead.String(), Message: "add specs/b.md", Writes: []DocumentWrite{{Path: "specs/b.md", Content: mdDoc("S-2", "B", "two")}}, }) if !errors.Is(err, ErrStale) { t.Fatalf("add with a drifted base: err = %v, want ErrStale", err) } }