You've already forked agentic-coding-workflow
149 lines
5.0 KiB
Go
149 lines
5.0 KiB
Go
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")
|
|
}
|