ci / vet-and-test (pull_request) Successful in 40s
Why: User rulings 4/5 plus the P2-P4 live evidence. Keyword scoring dimensions (readability, actionability) kept failing valid documents - normal Chinese section headings flagged as column titles, loops of 4 corrections - because they grade writing preferences, not verifiable facts. Unverified numbers (~10-13 per run) remained the largest quality leak through every phase. And the verifier runs on the same model as the worker (lightweight falls back to chat), so "independent" verification was self-review. Separately, svc seeding after boot never registered the tavily tools until a restart because keys were snapshotted at startup. Changes: quality.HardFacts (lint actually ran + zero errors) replaces the full ScoreBytes projection in CheckArtifacts - keyword dimensions remain in ScoreBytes for eval visibility only. New fact-consistency check (runtime.factCheck, default on): extract percentages/dates/large numbers from deliverable text, trace each against the material corpus (attachment text + tool evidence); misses get exactly one correction round (fix, drop, or mark as target/estimate), never a loop. runVerifier skips same-source model verification unless runtime.verifierSameModel is set (hard facts + fact check still run). Search tools now register lazily, reading tavily keys per call (settings/seeding take effect immediately). Evidence: SELF - TestHardFactsIgnoresKeywordDimensions, TestHardFactsKeepsLintUnavailableBlocking, TestUnverifiedFactsAndNotice, TestFactCheckContinuationOnce (fact note fires, corrected run passes); go vet + go test ./... pass; live before/after on the golden task set recorded on the phase05 task card per the policy clause. Impact: Gate stops rejecting valid documents on stylistic grounds; invented figures get one forced correction round; same-model verification no longer pretends to be independent; tavily config is hot. No contract or storage changes.
181 lines
6.3 KiB
Go
181 lines
6.3 KiB
Go
package squadadapter
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"strings"
|
||
"testing"
|
||
|
||
"agent-native/runtime/subagent"
|
||
"agent-squad/squad"
|
||
core "agent-system-core"
|
||
)
|
||
|
||
func TestMapPlanAndStatus(t *testing.T) {
|
||
p := &squad.WorkflowPlan{Title: "示例编排", Steps: []squad.StepSpec{
|
||
{ID: "s1", Title: "梳理", Status: "completed"},
|
||
{ID: "s2", Title: "成文", Status: "running"},
|
||
{ID: "s3", Title: "终审", Status: "skipped"},
|
||
{ID: "s4", Title: "待办", Status: "pending"},
|
||
}}
|
||
snap := mapPlan(p)
|
||
if snap.Title != "示例编排" || len(snap.Steps) != 4 {
|
||
t.Fatalf("snapshot: %+v", snap)
|
||
}
|
||
want := []string{"done", "in_progress", "skipped", "pending"}
|
||
for i, w := range want {
|
||
if snap.Steps[i].Status != w {
|
||
t.Errorf("step %d: want %s got %s", i, w, snap.Steps[i].Status)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestBuiltinRolesValid(t *testing.T) {
|
||
reg := squad.NewRoleRegistry()
|
||
for _, r := range builtinRoles() {
|
||
if err := reg.Register(r); err != nil {
|
||
t.Errorf("role %s invalid: %v", r.Role, err)
|
||
}
|
||
if r.Backend != "agent-native" {
|
||
t.Errorf("role %s backend: %s", r.Backend, r.Backend)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestIsHoldStatus(t *testing.T) {
|
||
for _, s := range []string{"waiting_human", "awaiting_confirmation", "awaiting_clarification", "quality_review"} {
|
||
if !isHoldStatus(s) {
|
||
t.Errorf("%s should be hold", s)
|
||
}
|
||
}
|
||
for _, s := range []string{"completed", "executing", "failed"} {
|
||
if isHoldStatus(s) {
|
||
t.Errorf("%s should not be hold", s)
|
||
}
|
||
}
|
||
}
|
||
|
||
// subagentFakeRc 只实现 SpawnSubagent 的最小 Ctx 桩(其余方法本测试不触达)。
|
||
type subagentFakeRc struct {
|
||
gotName string
|
||
gotTask string
|
||
payload core.AgentResultPayload
|
||
err error
|
||
}
|
||
|
||
func (f *subagentFakeRc) Context() context.Context { return context.Background() }
|
||
func (f *subagentFakeRc) Run() core.Run { return core.Run{} }
|
||
func (f *subagentFakeRc) Settings() core.EffectiveSettings { return nil }
|
||
func (f *subagentFakeRc) Emit(typ core.StreamEventType, payload any) error { return nil }
|
||
func (f *subagentFakeRc) AskHITL(card core.HITLCard) (core.HITLResponse, error) {
|
||
return core.HITLResponse{}, nil
|
||
}
|
||
func (f *subagentFakeRc) UpdatePlan(p core.PlanSnapshot) error { return nil }
|
||
func (f *subagentFakeRc) WriteArtifact(name, kind, mime string, content []byte) (core.Artifact, error) {
|
||
return core.Artifact{}, nil
|
||
}
|
||
func (f *subagentFakeRc) SpawnSubagent(name, task, hint string) (core.AgentResultPayload, error) {
|
||
f.gotName, f.gotTask = name, task
|
||
return f.payload, f.err
|
||
}
|
||
|
||
func TestSubagentFactoryBridgesSpawnSubagent(t *testing.T) {
|
||
rc := &subagentFakeRc{payload: core.AgentResultPayload{Status: core.RunCompleted, Text: "子代理结论"}}
|
||
res, err := SubagentFactory(rc)(context.Background(), subagent.Request{
|
||
Goal: "查证数据", Inputs: "docs/v2", WriteScope: "research/",
|
||
NonGoals: "不做实现", DoneCriteria: "三来源对照",
|
||
Deliverables: []string{"notes.md"},
|
||
OutputFormat: "Markdown:结论先行,附对照表",
|
||
Tools: []string{"web_search", "read_file"},
|
||
})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if !res.Success || res.Text != "子代理结论" {
|
||
t.Fatalf("bridge result wrong: %+v", res)
|
||
}
|
||
for _, want := range []string{"查证数据", "docs/v2", "research/", "不做实现", "三来源对照", "notes.md",
|
||
"输出格式:Markdown:结论先行,附对照表", "工具面(可用工具,不得越界):web_search, read_file"} {
|
||
if !strings.Contains(rc.gotTask, want) {
|
||
t.Fatalf("task brief missing %q:\n%s", want, rc.gotTask)
|
||
}
|
||
}
|
||
|
||
// 未完成状态:Success=false 且 Err 带结束状态。
|
||
rc2 := &subagentFakeRc{payload: core.AgentResultPayload{Status: core.RunFailed, Summary: "预算耗尽"}}
|
||
res2, err := SubagentFactory(rc2)(context.Background(), subagent.Request{Goal: "g", DoneCriteria: "d"})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if res2.Success || !strings.Contains(res2.Err, "failed") || res2.Text != "预算耗尽" {
|
||
t.Fatalf("failure mapping wrong: %+v", res2)
|
||
}
|
||
|
||
// SpawnSubagent 报错透传。
|
||
rc3 := &subagentFakeRc{err: errors.New("spawn refused")}
|
||
if _, err := SubagentFactory(rc3)(context.Background(), subagent.Request{Goal: "g", DoneCriteria: "d"}); err == nil {
|
||
t.Fatal("spawn error must propagate")
|
||
}
|
||
}
|
||
|
||
func TestBuiltinRolesExplicitExecConfig(t *testing.T) {
|
||
roles := builtinRoles()
|
||
byKey := map[string]squad.RoleSpec{}
|
||
for _, r := range roles {
|
||
byKey[r.Role] = r
|
||
}
|
||
if len(roles) != 4 {
|
||
t.Fatalf("want 4 builtin roles, got %d", len(roles))
|
||
}
|
||
// 全员 BashPermission 显式声明。
|
||
for _, r := range roles {
|
||
switch r.BashPermission {
|
||
case squad.BashPermissionDeny, squad.BashPermissionAsk, squad.BashPermissionAllow:
|
||
default:
|
||
t.Fatalf("role %s bash_permission not explicit: %q", r.Role, r.BashPermission)
|
||
}
|
||
}
|
||
// 调研员:search + fetch;审校员:grep/glob;撰稿员:ask + 三套技能 + 生成工具。
|
||
for _, want := range []string{"web_search", "fetch_url"} {
|
||
if !containsStr(byKey["researcher"].Tools, want) {
|
||
t.Fatalf("researcher tools missing %s: %v", want, byKey["researcher"].Tools)
|
||
}
|
||
}
|
||
for _, want := range []string{"grep", "glob"} {
|
||
if !containsStr(byKey["reviewer"].Tools, want) {
|
||
t.Fatalf("reviewer tools missing %s: %v", want, byKey["reviewer"].Tools)
|
||
}
|
||
}
|
||
w := byKey["writer"]
|
||
if w.BashPermission != squad.BashPermissionAsk {
|
||
t.Fatalf("writer needs ask (run_python for docs), got %q", w.BashPermission)
|
||
}
|
||
if !containsStr(w.Tools, "run_python") || !containsStr(w.Tools, "load_skill") {
|
||
t.Fatalf("writer tools missing generation path: %v", w.Tools)
|
||
}
|
||
if len(w.Skills) != 3 || !containsStr(w.Skills, "docx-author") || !containsStr(w.Skills, "pptx-author") {
|
||
t.Fatalf("writer skills wrong: %v", w.Skills)
|
||
}
|
||
}
|
||
|
||
func containsStr(list []string, want string) bool {
|
||
for _, v := range list {
|
||
if v == want {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// Q3 WP2:agent-system 退役后,未注入 AgentNativeOptions 时解析器为空——
|
||
// 步骤后端解析 fail-closed,不再静默走 SpawnSubagent 压缩路径。
|
||
func TestBuildBackendsWithoutNativeOptionsIsEmpty(t *testing.T) {
|
||
saved := AgentNativeOptions
|
||
AgentNativeOptions = nil
|
||
defer func() { AgentNativeOptions = saved }()
|
||
r := BuildBackends(&subagentFakeRc{})
|
||
if _, _, err := r.Resolve(squad.RoleSpec{Role: "writer", Backend: "agent-native"}, "agent-native"); err == nil {
|
||
t.Fatal("无内核工厂时解析必须 fail-closed")
|
||
}
|
||
}
|