Embed Facts in the PI narrative prompt and teach the bind-mounted runner to write assistant text into missing reasoning targets, matching pi-native.
195 lines
6.5 KiB
Go
195 lines
6.5 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"chat-agent-system/internal/agent"
|
|
"chat-agent-system/internal/config"
|
|
"chat-agent-system/internal/docrevision"
|
|
)
|
|
|
|
type recordingNarrativeBackend struct {
|
|
last agent.Request
|
|
payload []byte
|
|
body string
|
|
fail bool
|
|
}
|
|
|
|
func (r *recordingNarrativeBackend) Name() string { return "recording-narrative" }
|
|
|
|
func (r *recordingNarrativeBackend) Execute(_ context.Context, req agent.Request) (*agent.Result, error) {
|
|
r.last = req
|
|
if b, err := os.ReadFile(filepath.Join(req.WorkDir, "facts.json")); err == nil {
|
|
r.payload = b
|
|
}
|
|
if r.fail {
|
|
return &agent.Result{Success: false, ErrorReason: "TIMEOUT"}, nil
|
|
}
|
|
body := r.body
|
|
if body == "" {
|
|
body = `{"schema_version":1,"schema_kind":"change_report_narrative_v1","input_digest":"abc","from_sha":"f","to_sha":"t","summary":["叙事"]}`
|
|
}
|
|
return &agent.Result{Success: true, Patches: map[string][]byte{"narrative.md": []byte(body)}}, nil
|
|
}
|
|
|
|
func TestPiChangeReportNarrator_NoTools(t *testing.T) {
|
|
rec := &recordingNarrativeBackend{}
|
|
n := &PiChangeReportNarrator{Backend: rec, Model: "test"}
|
|
raw, err := n.Fetch(context.Background(), docrevision.NarrativeFetchSpec{
|
|
InputDigest: "abc", FromSHA: "f", ToSHA: "t",
|
|
Hunks: []docrevision.DiffHunk{{ID: "h1", Kind: "modify", BeforeText: "1.0", AfterText: "新阈值 2.5"}},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(raw, "change_report_narrative_v1") {
|
|
t.Fatalf("raw=%s", raw)
|
|
}
|
|
if !strings.Contains(strings.ToLower(rec.last.Instructions), "zero tool") {
|
|
t.Fatalf("instructions=%s", rec.last.Instructions)
|
|
}
|
|
var facts map[string]any
|
|
if err := json.Unmarshal(rec.payload, &facts); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if facts["input_digest"] != "abc" || facts["hunk_count"] != float64(1) {
|
|
t.Fatalf("facts=%v", facts)
|
|
}
|
|
hunks, _ := facts["hunks"].([]any)
|
|
if len(hunks) != 1 {
|
|
t.Fatalf("hunks=%v", facts["hunks"])
|
|
}
|
|
hunk, _ := hunks[0].(map[string]any)
|
|
if hunk["after_text"] != "新阈值 2.5" || rec.last.ToolProfile != agent.ToolProfileReasoning {
|
|
t.Fatalf("hunk=%v profile=%s", hunk, rec.last.ToolProfile)
|
|
}
|
|
}
|
|
|
|
func TestPiChangeReportNarrator_LiveMiniMax(t *testing.T) {
|
|
if os.Getenv("RUN_PI_E2E") != "1" {
|
|
t.Skip("set RUN_PI_E2E=1 for live PI narrative")
|
|
}
|
|
if testing.Short() {
|
|
t.Skip("live PI narrative is not a -short test")
|
|
}
|
|
if err := os.Chdir(filepath.Clean(filepath.Join("..", ".."))); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg, err := config.Load("")
|
|
if err != nil || cfg == nil {
|
|
t.Fatalf("config: %v", err)
|
|
}
|
|
if strings.TrimSpace(cfg.PiAgentRunnerAuthToken) == "" {
|
|
if tok := runnerTokenFromDotenv(); tok != "" {
|
|
cfg.PiAgentRunnerAuthToken = tok
|
|
}
|
|
}
|
|
if strings.TrimSpace(cfg.PiAgentRunnerAuthToken) == "" {
|
|
t.Fatal("runner auth token missing")
|
|
}
|
|
cfg.PiAgentRunnerURL = "http://127.0.0.1:18865"
|
|
// Share the bind-mounted host DATA_DIR with the running pi-agent container
|
|
// (compose mounts <repo>/data/dev/workspaces → /app/data/workspaces).
|
|
dataDir := "/home/weichen/projects/Agent-System/data/dev"
|
|
if st, err := os.Stat(filepath.Join(dataDir, "workspaces")); err != nil || !st.IsDir() {
|
|
t.Fatalf("shared data/dev/workspaces missing at %s: %v", dataDir, err)
|
|
}
|
|
|
|
fromSHA := "fromsha-closeout-20260923"
|
|
toSHA := "tosha-closeout-20260923"
|
|
ruleRisk := docrevision.RiskMedium
|
|
hunks := []docrevision.DiffHunk{{
|
|
ID: "h1", Kind: "modify", Section: "性能指标",
|
|
BeforeText: "响应时间阈值:2 秒",
|
|
AfterText: "响应时间阈值:1 秒(验收口径收紧)",
|
|
}}
|
|
impacts := []docrevision.DocImpact{{
|
|
DocID: "doc_perf_design", ImpactType: "stale_candidate",
|
|
Reason: "性能设计中的响应预算需复核",
|
|
}}
|
|
digest := docrevision.NarrativeInputDigest(fromSHA, toSHA, ruleRisk, impacts, hunks)
|
|
// Prove the minimized model input still carries clipped change text (not counts only).
|
|
factsPreview := docrevision.NarrativeModelInput(docrevision.NarrativeFetchSpec{
|
|
FromSHA: fromSHA, ToSHA: toSHA, RuleRisk: ruleRisk, InputDigest: digest,
|
|
Impacts: impacts, Hunks: hunks,
|
|
})
|
|
factsJSON, err := json.Marshal(factsPreview)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var decodedFacts map[string]any
|
|
if err := json.Unmarshal(factsJSON, &decodedFacts); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rawHunks, _ := decodedFacts["hunks"].([]any)
|
|
if len(rawHunks) != 1 {
|
|
t.Fatalf("facts preview missing clipped hunks: %s", string(factsJSON))
|
|
}
|
|
h0, _ := rawHunks[0].(map[string]any)
|
|
if !strings.Contains(fmt.Sprint(h0["after_text"]), "1 秒") {
|
|
t.Fatalf("clipped after_text missing: %#v", h0)
|
|
}
|
|
if decodedFacts["hunk_count"] != float64(1) || decodedFacts["impact_count"] != float64(1) {
|
|
t.Fatalf("counts: %v", decodedFacts)
|
|
}
|
|
|
|
n := &PiChangeReportNarrator{
|
|
Backend: agent.NewBackend("pi-agent", cfg),
|
|
Model: cfg.PiAgentModel,
|
|
Timeout: 4 * time.Minute,
|
|
DataDir: dataDir,
|
|
}
|
|
start := time.Now()
|
|
raw, err := n.Fetch(context.Background(), docrevision.NarrativeFetchSpec{
|
|
FromSHA: fromSHA, ToSHA: toSHA, RuleRisk: ruleRisk, InputDigest: digest,
|
|
Impacts: impacts, Hunks: hunks,
|
|
})
|
|
elapsed := time.Since(start).Round(time.Millisecond)
|
|
if err != nil {
|
|
t.Fatalf("live fetch: %v", err)
|
|
}
|
|
t.Logf("elapsed=%s model=%s raw=%s", elapsed, cfg.PiAgentModel, raw)
|
|
|
|
var payload map[string]any
|
|
if err := json.Unmarshal([]byte(raw), &payload); err != nil {
|
|
t.Fatalf("live narrative must be JSON: %v raw=%s", err, raw)
|
|
}
|
|
if payload["schema_kind"] != docrevision.NarrativeSchemaKind {
|
|
t.Fatalf("schema_kind=%v", payload["schema_kind"])
|
|
}
|
|
if !strings.EqualFold(fmt.Sprint(payload["input_digest"]), digest) {
|
|
t.Fatalf("input_digest must echo facts exactly: got %v want %s", payload["input_digest"], digest)
|
|
}
|
|
summary, _ := payload["summary"].([]any)
|
|
if len(summary) == 0 {
|
|
t.Fatalf("live summary empty: %s", raw)
|
|
}
|
|
|
|
docrevision.SetPIChangeReportNarrativeForTest(true)
|
|
t.Cleanup(docrevision.ResetPIChangeReportNarrativeForTest)
|
|
diff := docrevision.DiffResult{Hunks: hunks}
|
|
rep, rerr := docrevision.ResolveChangeReport(raw, fromSHA, toSHA, ruleRisk, impacts, diff)
|
|
if rerr != nil {
|
|
t.Fatalf("ResolveChangeReport: %v", rerr)
|
|
}
|
|
if rep == nil || rep.AnalysisStatus != docrevision.AnalysisComplete {
|
|
t.Fatalf("flag-on live want complete, got %+v", rep)
|
|
}
|
|
if rep.RiskLevel != ruleRisk {
|
|
t.Fatalf("PI must not override server risk: want %s got %s", ruleRisk, rep.RiskLevel)
|
|
}
|
|
if len(rep.DocumentImpacts) != 1 || rep.DocumentImpacts[0].DocID != "doc_perf_design" {
|
|
t.Fatalf("server impacts must win: %+v", rep.DocumentImpacts)
|
|
}
|
|
if rep.DeterministicFacts["narrative_applied"] != true {
|
|
t.Fatalf("facts: %+v", rep.DeterministicFacts)
|
|
}
|
|
}
|