package service import ( "bytes" "context" "testing" "time" "sourcecraft.dev/bigbes/sr-ht-spec/core" ) // TestDigestMark proves the digest mark round-trips through the owner-keyed // store and that its absence is reported as "no mark yet" rather than an error — // the distinction the inbox turns into "every auto-merge is new on first view". func TestDigestMark(t *testing.T) { svc, _ := newTestService(t) ctx := context.Background() if _, marked, err := svc.DigestMark(ctx); err != nil || marked { t.Fatalf("DigestMark before any write: marked=%v err=%v, want (false, nil)", marked, err) } seen := time.Date(2026, 7, 20, 15, 0, 0, 0, time.UTC) if err := svc.MarkDigestSeen(ctx, seen); err != nil { t.Fatalf("MarkDigestSeen: %v", err) } got, marked, err := svc.DigestMark(ctx) if err != nil || !marked { t.Fatalf("DigestMark after write: marked=%v err=%v, want (true, nil)", marked, err) } if !got.Equal(seen) { t.Errorf("mark = %s, want %s", got, seen) } } // 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) } }