package docs import ( "context" "encoding/json" "errors" "testing" "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/yan1h/agent-coding-workflow/internal/infra/git" "github.com/yan1h/agent-coding-workflow/internal/jobs" ) type fakeDocsRepo struct { summaries map[string]*Summary // keyed by commitSHA|kind upserts int } func key(commit, kind string) string { return commit + "|" + kind } func newFakeDocsRepo() *fakeDocsRepo { return &fakeDocsRepo{summaries: map[string]*Summary{}} } func (r *fakeDocsRepo) Get(_ context.Context, _ uuid.UUID, commit, kind string) (*Summary, error) { return r.summaries[key(commit, kind)], nil } func (r *fakeDocsRepo) Upsert(_ context.Context, in UpsertInput) (*Summary, error) { r.upserts++ s := &Summary{ ID: uuid.New(), WorkspaceID: in.WorkspaceID, WorktreeID: in.WorktreeID, Branch: in.Branch, CommitSHA: in.CommitSHA, Kind: in.Kind, Title: in.Title, BodyMD: in.BodyMD, Model: in.Model, Status: in.Status, Error: in.Error, } r.summaries[key(in.CommitSHA, in.Kind)] = s return s, nil } type fakeLocator struct{} func (fakeLocator) LocateWorkspace(context.Context, uuid.UUID) (string, error) { return "/tmp/x", nil } func (fakeLocator) LocateWorktree(context.Context, uuid.UUID) (string, error) { return "/tmp/x", nil } // gitShim embeds git.Runner (nil) and overrides only DiffStat, the single method // the SummaryRunner invokes. Any other call would panic, which never happens here. type gitShim struct { git.Runner diff string } func (g gitShim) DiffStat(context.Context, string, string) (string, error) { return g.diff, nil } type fakeSummarizer struct { title, body, model string err error } func (s fakeSummarizer) Summarize(context.Context, string, string, string) (string, string, string, error) { return s.title, s.body, s.model, s.err } func newRunner(repo Repository, summ Summarizer, diff string) *SummaryRunner { return NewSummaryRunner(repo, fakeLocator{}, gitShim{diff: diff}, summ, nil) } func TestSummaryRunner_Success(t *testing.T) { repo := newFakeDocsRepo() wsID := uuid.New() r := newRunner(repo, fakeSummarizer{title: "Add feature X", body: "# Add feature X\n\n- did stuff", model: "m1"}, "f.go | 2 +-") payload, _ := json.Marshal(summaryPayload{WorkspaceID: wsID, Branch: "main", CommitSHA: "abc"}) require.NoError(t, r.Handle(context.Background(), jobs.Job{Payload: payload})) s := repo.summaries[key("abc", KindCommit)] require.NotNil(t, s) require.Equal(t, StatusCompleted, s.Status) require.Equal(t, "# Add feature X\n\n- did stuff", s.BodyMD) require.NotNil(t, s.Title) require.Equal(t, "Add feature X", *s.Title) require.NotNil(t, s.Model) require.Equal(t, "m1", *s.Model) } func TestSummaryRunner_IdempotentWhenCompleted(t *testing.T) { repo := newFakeDocsRepo() wsID := uuid.New() repo.summaries[key("abc", KindCommit)] = &Summary{CommitSHA: "abc", Kind: KindCommit, Status: StatusCompleted} r := newRunner(repo, fakeSummarizer{title: "x", body: "y", model: "m"}, "stat") payload, _ := json.Marshal(summaryPayload{WorkspaceID: wsID, Branch: "main", CommitSHA: "abc"}) require.NoError(t, r.Handle(context.Background(), jobs.Job{Payload: payload})) require.Equal(t, 0, repo.upserts, "completed summary must not be regenerated") } func TestSummaryRunner_FailurePersistsFailedRow(t *testing.T) { repo := newFakeDocsRepo() wsID := uuid.New() r := newRunner(repo, fakeSummarizer{err: errors.New("llm down")}, "stat") payload, _ := json.Marshal(summaryPayload{WorkspaceID: wsID, Branch: "main", CommitSHA: "abc"}) err := r.Handle(context.Background(), jobs.Job{Payload: payload}) require.Error(t, err) s := repo.summaries[key("abc", KindCommit)] require.NotNil(t, s) require.Equal(t, StatusFailed, s.Status) require.NotNil(t, s.Error) } var _ Repository = (*fakeDocsRepo)(nil)