Files
Agent-Native/exec/settle_parity_test.go
weichenandCursor fedf671a48
ci / vet-and-test (push) Successful in 5s
ci / docker-checkout-hostgateway (push) Successful in 3s
Import the current agent-native module as a fresh history.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-29 12:29:04 +00:00

143 lines
4.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package exec
import (
"encoding/json"
"os"
"os/exec"
"path/filepath"
"testing"
)
// settleParityCase 是 PI settle-policy.mjs 与 Go AssessStepFailure 的共享
// 行为夹具(U9b Batch1-②):同一场景两侧必须得到一致的成功/可重试判定。
// events 供 node 事件流分类(applyAgentEvent→classifyRunOutcome);
// signals 供 Go 结果信号分类(AssessStepFailure);expect 为共同期望。
type settleParityCase struct {
Name string `json:"name"`
Events []map[string]any `json:"events"`
Signals struct {
Success bool `json:"success"`
ErrorReason string `json:"error_reason"`
Retryable bool `json:"retryable"`
TargetMissing bool `json:"target_missing"`
} `json:"signals"`
Expect struct {
OK bool `json:"ok"`
Retryable bool `json:"retryable"`
} `json:"expect"`
// GoOnly 为 true 的场景(如 target-exists)没有 node 对应信号,只断言 Go。
GoOnly bool `json:"go_only"`
}
func nodeParityScript(casesAbsPath string) string {
return `
import { newSettleState, applyAgentEvent, classifyRunOutcome } from './settle-policy.mjs';
import fs from 'fs';
const cases = JSON.parse(fs.readFileSync(` + "`" + casesAbsPath + "`" + `, 'utf8'));
const out = [];
for (const c of cases) {
if (c.go_only) { out.push({name: c.name, go_only: true}); continue; }
const st = newSettleState();
for (const ev of c.events || []) { applyAgentEvent(st, ev); }
const r = classifyRunOutcome({
succeeded: st.succeeded,
retryFailed: st.retryFailed,
providerTransient: st.providerTransient,
lastErr: st.lastErr,
missing: c.signals && c.signals.target_missing ? ["output.md"] : [],
});
out.push({name: c.name, ok: r.ok, retryable: r.retryable});
}
process.stdout.write(JSON.stringify(out));
`
}
func TestSettlePolicyParityWithNode(t *testing.T) {
if _, err := exec.LookPath("node"); err != nil {
t.Skip("node not available")
}
casesPath, err := filepath.Abs(filepath.Join("testdata", "settle-parity-cases.json"))
if err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(casesPath)
if err != nil {
t.Fatalf("read fixture: %v", err)
}
var cases []settleParityCase
if err := json.Unmarshal(data, &cases); err != nil {
t.Fatalf("parse fixture: %v", err)
}
if len(cases) < 8 {
t.Fatalf("expect a meaningful scenario set, got %d", len(cases))
}
// node 侧:cwd 置于 settle-policy.mjs 所在 testdata,脚本相对导入(同 digest 交叉验证模式)。
tdDir, err := filepath.Abs(filepath.Join("..", "adapters", "pitransport", "testdata"))
if err != nil {
t.Fatal(err)
}
nodeCmd := exec.Command("node", "--input-type=module", "-e", nodeParityScript(casesPath))
nodeCmd.Dir = tdDir // './settle-policy.mjs' 相对 cwd 解析
out, err := nodeCmd.Output()
if err != nil {
t.Fatalf("node parity runner: %v", err)
}
var nodeResults []struct {
Name string `json:"name"`
OK *bool `json:"ok"`
Retryable *bool `json:"retryable"`
GoOnly bool `json:"go_only"`
}
if err := json.Unmarshal(out, &nodeResults); err != nil {
t.Fatalf("parse node output: %v: %s", err, out)
}
nodeBy := map[string]int{}
for i, r := range nodeResults {
nodeBy[r.Name] = i
}
for _, c := range cases {
c := c
t.Run(c.Name, func(t *testing.T) {
sig := StepFailureSignals{
Result: &Result{
Success: c.Signals.Success,
ErrorReason: c.Signals.ErrorReason,
Retryable: c.Signals.Retryable,
},
TargetMissing: c.Signals.TargetMissing,
}
got := AssessStepFailure(sig)
// 契约:Go 是终裁者——成功不得触发重试;mjs 判可重试的必须保持
// Transient;mjs 判永久的不得被翻成 Transient。
switch {
case c.Expect.OK:
if got.Class == FailureTransient {
t.Fatalf("Go flipped success to transient: %s", got.Reason)
}
case c.Expect.Retryable:
if got.Class != FailureTransient {
t.Fatalf("Go must stay transient: class=%v reason=%s", got.Class, got.Reason)
}
default:
if got.Class == FailureTransient {
t.Fatalf("Go flipped permanent to transient: %s", got.Reason)
}
}
if c.GoOnly {
t.Log("Go-only signal; node side n/a")
return
}
idx, ok := nodeBy[c.Name]
if !ok || nodeResults[idx].OK == nil {
t.Fatalf("node result missing for %s", c.Name)
}
if *nodeResults[idx].OK != c.Expect.OK || *nodeResults[idx].Retryable != c.Expect.Retryable {
t.Fatalf("node: ok=%v retryable=%v; want ok=%v retryable=%v",
*nodeResults[idx].OK, *nodeResults[idx].Retryable, c.Expect.OK, c.Expect.Retryable)
}
})
}
}