You've already forked agentic-coding-workflow
bugfix
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
package acp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/yan1h/agent-coding-workflow/internal/audit"
|
||||
"github.com/yan1h/agent-coding-workflow/internal/brief"
|
||||
"github.com/yan1h/agent-coding-workflow/internal/project"
|
||||
)
|
||||
|
||||
// recordingAudit 捕获 Record 调用,供断言 brief_composed 是否写入。
|
||||
type recordingAudit struct {
|
||||
mu sync.Mutex
|
||||
entries []audit.Entry
|
||||
}
|
||||
|
||||
func (r *recordingAudit) Record(_ context.Context, e audit.Entry) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.entries = append(r.entries, e)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *recordingAudit) actions() []string {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
out := make([]string, 0, len(r.entries))
|
||||
for _, e := range r.entries {
|
||||
out = append(out, e.Action)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// fakeComposer 是 brief.Composer 的测试替身:返回固定文本并记录是否被调用。
|
||||
type fakeComposer struct {
|
||||
res brief.BriefResult
|
||||
err error
|
||||
called bool
|
||||
gotIn brief.BriefInput
|
||||
}
|
||||
|
||||
func (f *fakeComposer) ComposeTaskBrief(_ context.Context, in brief.BriefInput) (brief.BriefResult, error) {
|
||||
f.called = true
|
||||
f.gotIn = in
|
||||
return f.res, f.err
|
||||
}
|
||||
|
||||
// fakeArtifactAccess 是 ArtifactAccess 的测试替身。
|
||||
type fakeArtifactAccess struct {
|
||||
arts []*project.Artifact
|
||||
}
|
||||
|
||||
func (f fakeArtifactAccess) ListLatestArtifacts(_ context.Context, _ uuid.UUID) ([]*project.Artifact, error) {
|
||||
return f.arts, nil
|
||||
}
|
||||
|
||||
func testSession() *Session {
|
||||
return &Session{
|
||||
ID: uuid.New(),
|
||||
ProjectID: uuid.New(),
|
||||
Branch: "req/demo-7",
|
||||
CwdPath: "/tmp/wt",
|
||||
}
|
||||
}
|
||||
|
||||
func TestComposeInitialPrompt_RequirementContextComposed(t *testing.T) {
|
||||
t.Parallel()
|
||||
rec := &recordingAudit{}
|
||||
fc := &fakeComposer{res: brief.BriefResult{
|
||||
Text: "你是一名资深产品规划助手。\n\n## 当前需求\n需求 #7:导出报表",
|
||||
Tokens: 20,
|
||||
Sections: []string{"role", "requirement_core"},
|
||||
}}
|
||||
s := &sessionService{
|
||||
audit: rec,
|
||||
composer: fc,
|
||||
aa: fakeArtifactAccess{arts: []*project.Artifact{{Phase: project.PhasePlanning, Version: 2}}},
|
||||
cfg: SessionServiceConfig{BriefTokenBudget: 6000},
|
||||
}
|
||||
|
||||
req := &project.Requirement{ID: uuid.New(), Number: 7, Title: "导出报表", Phase: project.PhasePlanning}
|
||||
c := Caller{UserID: uuid.New()}
|
||||
out := s.composeInitialPrompt(context.Background(), c, testSession(), nil, req, nil, "请开始")
|
||||
|
||||
require.True(t, fc.called)
|
||||
assert.Contains(t, out, "需求 #7:导出报表")
|
||||
assert.Contains(t, out, "资深产品规划助手")
|
||||
// 组装入参带上了 requirement + artifacts + cwd + 用户文本 + budget。
|
||||
assert.Equal(t, req, fc.gotIn.Requirement)
|
||||
assert.Equal(t, "请开始", fc.gotIn.UserPrompt)
|
||||
assert.Equal(t, "/tmp/wt", fc.gotIn.RepoCwd)
|
||||
assert.Equal(t, 6000, fc.gotIn.TokenBudget)
|
||||
assert.Equal(t, brief.KindACPSession, fc.gotIn.Kind)
|
||||
assert.Len(t, fc.gotIn.Artifacts, 1)
|
||||
// 写了 brief_composed audit。
|
||||
assert.Contains(t, rec.actions(), "acp.session.brief_composed")
|
||||
}
|
||||
|
||||
func TestComposeInitialPrompt_IssueContextComposed(t *testing.T) {
|
||||
t.Parallel()
|
||||
rec := &recordingAudit{}
|
||||
fc := &fakeComposer{res: brief.BriefResult{Text: "## 当前 Issue\nIssue #3:bug", Tokens: 8}}
|
||||
s := &sessionService{audit: rec, composer: fc}
|
||||
|
||||
iss := &project.Issue{ID: uuid.New(), Number: 3, Title: "bug"}
|
||||
out := s.composeInitialPrompt(context.Background(), Caller{UserID: uuid.New()}, testSession(), nil, nil, iss, "fix it")
|
||||
|
||||
require.True(t, fc.called)
|
||||
assert.Contains(t, out, "Issue #3:bug")
|
||||
assert.Equal(t, iss, fc.gotIn.Issue)
|
||||
// issue-only 时不调用 ArtifactAccess(aa 为 nil 也不 panic)。
|
||||
}
|
||||
|
||||
func TestComposeInitialPrompt_NoPMContextFallsBackToRaw(t *testing.T) {
|
||||
t.Parallel()
|
||||
fc := &fakeComposer{res: brief.BriefResult{Text: "SHOULD NOT BE USED"}}
|
||||
s := &sessionService{audit: &recordingAudit{}, composer: fc}
|
||||
|
||||
out := s.composeInitialPrompt(context.Background(), Caller{UserID: uuid.New()}, testSession(), nil, nil, nil, "raw text only")
|
||||
assert.Equal(t, "raw text only", out)
|
||||
assert.False(t, fc.called, "composer must not be called without PM context")
|
||||
}
|
||||
|
||||
func TestComposeInitialPrompt_NilComposerFallsBackToRaw(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := &sessionService{audit: &recordingAudit{}, composer: nil}
|
||||
req := &project.Requirement{ID: uuid.New(), Number: 7, Title: "x", Phase: project.PhasePlanning}
|
||||
out := s.composeInitialPrompt(context.Background(), Caller{UserID: uuid.New()}, testSession(), nil, req, nil, "raw")
|
||||
assert.Equal(t, "raw", out)
|
||||
}
|
||||
|
||||
func TestComposeInitialPrompt_EmptyComposedTextFallsBackToRaw(t *testing.T) {
|
||||
t.Parallel()
|
||||
rec := &recordingAudit{}
|
||||
fc := &fakeComposer{res: brief.BriefResult{Text: ""}}
|
||||
s := &sessionService{audit: rec, composer: fc}
|
||||
req := &project.Requirement{ID: uuid.New(), Number: 1, Title: "t", Phase: project.PhasePlanning}
|
||||
out := s.composeInitialPrompt(context.Background(), Caller{UserID: uuid.New()}, testSession(), nil, req, nil, "fallback raw")
|
||||
assert.Equal(t, "fallback raw", out)
|
||||
// 空简报不写 brief_composed audit。
|
||||
assert.NotContains(t, rec.actions(), "acp.session.brief_composed")
|
||||
}
|
||||
Reference in New Issue
Block a user