Why: F05 - files-presence could promote failures to success, recovery marked crashed steps completed on files alone or reran them blindly, and retries minted a fresh ExecutionID each attempt. Evidence: squad tests pass incl. executor result contract and recovery tables (verify-u3b.json); (nil,nil), canceled and side-effect-possible semantics handled; interrupted steps without a trusted terminal now hold for reconciliation instead of auto-advancing. Impact: retries reuse the step ExecutionID and shared deadline; no auto-replay of unverified external actions. Refs: feature/agent-stack-audit-remediation-b1 U3B
518 lines
18 KiB
Go
518 lines
18 KiB
Go
// Package squad: F05/U3b executor result 合约测试。
|
||
//
|
||
// 覆盖以下消费规则:
|
||
// - (nil,nil) executor 返回 (result==nil && err==nil) 是稳定失败(非 panic,非 success)。
|
||
// - Success=true 但无执行证据(patches-only)也要走交付物校验;
|
||
// 失败-with-full-patches 仍为失败(不允许文件存在翻成 success)。
|
||
// - canceled/aborted 结果不进入 retry 通路(不是业务失败,不可重试)。
|
||
// - side_effects=possible 的失败禁止自动重试;
|
||
// side_effects=none 的纯读取失败可走有界重试。
|
||
// - recovery:trusted record 存在 + 文件缺失 -> 恢复 completed(权威来源 = record);
|
||
// 无 record + 文件齐全 -> needs_reconciliation(不假装完成、不隐默 rerun);
|
||
// 无 record + 文件缺失 -> needs_reconciliation;
|
||
// record SideEffects=none + 缺失文件 -> 一次重试 pending(不重复执行副作用)。
|
||
//
|
||
// 这些测试使用 squad 包的 in-memory executor 替身进行 hermetic 验证;不调用
|
||
// 真实模型或外部组件。
|
||
package squad
|
||
|
||
import (
|
||
"context"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"sync"
|
||
"sync/atomic"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
// ---- 替身 executor ----
|
||
|
||
// frozenResultBackend 每次 Execute 返回同一个固定结果(用于直接构造语义场景)。
|
||
type frozenResultBackend struct {
|
||
mu sync.Mutex
|
||
calls int32
|
||
res *ExecResult
|
||
err error
|
||
}
|
||
|
||
func (b *frozenResultBackend) Name() string { return "frozen" }
|
||
|
||
func (b *frozenResultBackend) Execute(_ context.Context, _ ExecRequest) (*ExecResult, error) {
|
||
atomic.AddInt32(&b.calls, 1)
|
||
b.mu.Lock()
|
||
defer b.mu.Unlock()
|
||
if b.err != nil {
|
||
return b.res, b.err
|
||
}
|
||
return b.res, nil
|
||
}
|
||
|
||
func (b *frozenResultBackend) callCount() int32 {
|
||
return atomic.LoadInt32(&b.calls)
|
||
}
|
||
|
||
// materializingBackend 每次 Execute 写入 patches 但把 Success 留给 caller 决定。
|
||
// 用于「patches-only success」测试场景。
|
||
type materializingBackend struct {
|
||
mu sync.Mutex
|
||
calls int32
|
||
// success 只在 success==true 时返回 Success=true;测试 override 用。
|
||
success bool
|
||
canceled bool
|
||
aborted bool
|
||
// sideEffects 决定是否允许重试。
|
||
sideEffects ExecSideEffects
|
||
// writePatch 控制是否 materializes patch 到 workDir。
|
||
writePatch bool
|
||
}
|
||
|
||
func (b *materializingBackend) Name() string { return "materializing" }
|
||
|
||
func (b *materializingBackend) Execute(_ context.Context, req ExecRequest) (*ExecResult, error) {
|
||
atomic.AddInt32(&b.calls, 1)
|
||
patches := map[string][]byte{}
|
||
for _, t := range req.Targets {
|
||
if b.writePatch {
|
||
_ = os.WriteFile(filepath.Join(req.WorkDir, t), []byte(goodReport), 0o644)
|
||
}
|
||
patches[t] = []byte(goodReport)
|
||
}
|
||
return &ExecResult{
|
||
Success: b.success,
|
||
Patches: patches,
|
||
Logs: "stub",
|
||
SideEffects: b.sideEffects,
|
||
Canceled: b.canceled,
|
||
Aborted: b.aborted,
|
||
}, nil
|
||
}
|
||
|
||
func (b *materializingBackend) callCount() int32 {
|
||
return atomic.LoadInt32(&b.calls)
|
||
}
|
||
|
||
// ---- 测试辅助 ----
|
||
|
||
func newFrozenTestEngine(t *testing.T, dir string, backend Executor) *SquadEngine {
|
||
t.Helper()
|
||
reg := NewRoleRegistry()
|
||
if err := reg.Register(RoleSpec{Role: "researcher", Name: "调研", Backend: backend.Name()}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := reg.Register(RoleSpec{Role: "writer", Name: "撰写", Backend: backend.Name()}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := reg.Register(RoleSpec{Role: "reviewer", Name: "审核", Backend: backend.Name()}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
backends := NewBackendResolver()
|
||
backends.RegisterInstance(backend.Name(), backend)
|
||
eng, err := NewEngine(Options{
|
||
DataDir: dir, Registry: reg, Planner: mockScriptPlanner(baseSteps()...),
|
||
Backends: backends, Sink: NopSink{},
|
||
})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return eng
|
||
}
|
||
|
||
// ---- 合约测试 ----
|
||
|
||
// TestNilNilResultIsStableFailure:(nil,nil) 必须稳定失败——不 panic、不被 papered over。
|
||
func TestNilNilResultIsStableFailure(t *testing.T) {
|
||
dir := t.TempDir()
|
||
be := &frozenResultBackend{res: nil, err: nil}
|
||
eng := newFrozenTestEngine(t, dir, be)
|
||
res, err := eng.Run(context.Background(), Goal{Text: "空结果"}, RunOptions{})
|
||
if err != nil {
|
||
t.Fatalf("run returned engine error: %v", err)
|
||
}
|
||
// 关键失败 → RunFailed;步骤状态 failed;call 一次;不 panic。
|
||
if res.Status != RunFailed {
|
||
t.Fatalf("want RunFailed, got %s (err=%s)", res.Status, res.Error)
|
||
}
|
||
if be.callCount() != 1 {
|
||
t.Fatalf("backend called %d times, want 1", be.callCount())
|
||
}
|
||
if res.Plan == nil {
|
||
t.Fatal("plan missing")
|
||
}
|
||
if got := res.Plan.Steps[0].Status; got != "failed" {
|
||
t.Fatalf("step 1 status = %s, want failed", got)
|
||
}
|
||
if got := res.Plan.Steps[0].Error; got != "executor returned nil result" {
|
||
t.Fatalf("step 1 error = %q, want executor-nil-result reason", got)
|
||
}
|
||
}
|
||
|
||
// TestSuccessWithFullPatchesMaterializes:Success=true + 已有 patch
|
||
// —— 通过交付物校验完成;patches-only success 也走同一路径。
|
||
func TestSuccessWithFullPatchesMaterializes(t *testing.T) {
|
||
dir := t.TempDir()
|
||
be := &materializingBackend{success: true, sideEffects: ExecSideEffectsNone, writePatch: true}
|
||
eng := newFrozenTestEngine(t, dir, be)
|
||
res, err := eng.Run(context.Background(), Goal{Text: "patches 落地校验"}, RunOptions{})
|
||
if err != nil {
|
||
t.Fatalf("run: %v", err)
|
||
}
|
||
if res.Status != RunCompleted {
|
||
t.Fatalf("want completed, got %s err=%s", res.Status, res.Error)
|
||
}
|
||
// 三步各调用一次;patches 命中产物路径且 Success=true,每步一次。
|
||
if got := be.callCount(); got < 3 {
|
||
t.Fatalf("backend called %d times, want at least 3 (3 steps)", got)
|
||
}
|
||
}
|
||
|
||
// TestFailedWithFullPatchesStaysFailed:F05/KTD4 关键不变式:失败 + 文件齐全
|
||
// 也不能翻成成功——即便 patches 内容已落地。
|
||
func TestFailedWithFullPatchesStaysFailed(t *testing.T) {
|
||
dir := t.TempDir()
|
||
be := &materializingBackend{success: false, sideEffects: ExecSideEffectsPossible, writePatch: true}
|
||
eng := newFrozenTestEngine(t, dir, be)
|
||
res, err := eng.Run(context.Background(), Goal{Text: "失败-but-files-present"}, RunOptions{})
|
||
if err != nil {
|
||
t.Fatalf("run: %v", err)
|
||
}
|
||
if res.Status != RunFailed {
|
||
t.Fatalf("want RunFailed, got %s", res.Status)
|
||
}
|
||
if res.Plan == nil || res.Plan.Steps[0].Status != "failed" {
|
||
t.Fatalf("step 1 should be failed, plan=%+v", res.Plan)
|
||
}
|
||
// 关键:失败被 failStep 重判后,不应回写 completed;也不应自动 rerun。
|
||
// SideEffects=possible 禁止重试:s1 仅调用一次,后续步骤不再调用(运行已中止)。
|
||
if got := be.callCount(); got > 1 {
|
||
t.Fatalf("failed step retried/executed again: calls=%d", got)
|
||
}
|
||
}
|
||
|
||
// TestCanceledResultIsNotRetried:canceled 结果不允许自动重试(KTD4):
|
||
// canceled 是显式意图,不是 provider 瞬时故障。
|
||
func TestCanceledResultIsNotRetried(t *testing.T) {
|
||
dir := t.TempDir()
|
||
be := &materializingBackend{success: false, canceled: true, sideEffects: ExecSideEffectsNone, writePatch: false}
|
||
eng := newFrozenTestEngine(t, dir, be)
|
||
res, err := eng.Run(context.Background(), Goal{Text: "取消不可重试"}, RunOptions{})
|
||
if err != nil {
|
||
t.Fatalf("run: %v", err)
|
||
}
|
||
// 失败(不是 panic),且仅被调用一次(不允许 retry)。
|
||
if res.Status != RunFailed {
|
||
t.Fatalf("want RunFailed, got %s", res.Status)
|
||
}
|
||
if be.callCount() != 1 {
|
||
t.Fatalf("canceled invoked %d times, want 1 (no retry)", be.callCount())
|
||
}
|
||
}
|
||
|
||
// TestAbortedResultIsNotRetried:aborted 同 canceled 语义。
|
||
func TestAbortedResultIsNotRetried(t *testing.T) {
|
||
dir := t.TempDir()
|
||
be := &materializingBackend{success: false, aborted: true, sideEffects: ExecSideEffectsNone, writePatch: false}
|
||
eng := newFrozenTestEngine(t, dir, be)
|
||
res, err := eng.Run(context.Background(), Goal{Text: "中止不可重试"}, RunOptions{})
|
||
if err != nil {
|
||
t.Fatalf("run: %v", err)
|
||
}
|
||
if res.Status != RunFailed {
|
||
t.Fatalf("want RunFailed, got %s", res.Status)
|
||
}
|
||
if be.callCount() != 1 {
|
||
t.Fatalf("aborted invoked %d times, want 1 (no retry)", be.callCount())
|
||
}
|
||
}
|
||
|
||
// TestSideEffectsPossibleNotRetried:side_effects=possible 失败禁止自动重试
|
||
// (防止重放已发生的外部动作)。
|
||
func TestSideEffectsPossibleNotRetried(t *testing.T) {
|
||
dir := t.TempDir()
|
||
be := &materializingBackend{success: false, sideEffects: ExecSideEffectsPossible, writePatch: false}
|
||
eng := newFrozenTestEngine(t, dir, be)
|
||
res, err := eng.Run(context.Background(), Goal{Text: "可能副作用失败不重试"}, RunOptions{})
|
||
if err != nil {
|
||
t.Fatalf("run: %v", err)
|
||
}
|
||
if res.Status != RunFailed {
|
||
t.Fatalf("want RunFailed, got %s", res.Status)
|
||
}
|
||
if be.callCount() != 1 {
|
||
t.Fatalf("possible-side-effects invoked %d times, want 1 (no retry)", be.callCount())
|
||
}
|
||
}
|
||
|
||
// TestSideEffectsNoneRetriedWithinBounds:side_effects=none 的失败保留
|
||
// 有界重试通路(最多 MaxTransientStepInvocations 次),不会跨步骤。
|
||
func TestSideEffectsNoneRetriedWithinBounds(t *testing.T) {
|
||
dir := t.TempDir()
|
||
// stage 1: failed with side_effects=none → 触发重试;
|
||
// stage 2: 返回 success。后续调用返回 last state(success)。
|
||
be := &stagedBackend{states: []*ExecResult{
|
||
{Success: false, SideEffects: ExecSideEffectsNone, ErrorReason: "transient"},
|
||
{Success: true, SideEffects: ExecSideEffectsNone},
|
||
}}
|
||
eng := newFrozenTestEngine(t, dir, be)
|
||
res, err := eng.Run(context.Background(), Goal{Text: "可重试"}, RunOptions{})
|
||
if err != nil {
|
||
t.Fatalf("run: %v", err)
|
||
}
|
||
if res.Status != RunCompleted {
|
||
t.Fatalf("want RunCompleted, got %s err=%s", res.Status, res.Error)
|
||
}
|
||
// 全运行计数:s1 经历重试(最多 2 次调用);后续步骤首次成功。
|
||
// 说明 重试仅限于 s1,不超出单步边界。
|
||
if be.callCount() < 2 {
|
||
t.Fatalf("retried count = %d, want at least 2 (failed then success on s1)", be.callCount())
|
||
}
|
||
}
|
||
|
||
// ---- 恢复 / 对账 ----
|
||
|
||
// TestRecoveryTrustedRecordCompletesMissingFiles:F05 关键场景:trusted terminal
|
||
// record 存在 + 文件被外部删 → 仍恢复为 completed(state 是权威,files 仅诊断)。
|
||
func TestRecoveryTrustedRecordCompletesMissingFiles(t *testing.T) {
|
||
backend := &perStepRecordingBackend{}
|
||
dir := t.TempDir()
|
||
eng := newAttemptTestEngine(t, backend, dir)
|
||
res, err := eng.Run(context.Background(), Goal{Text: "record-only-path"}, RunOptions{})
|
||
if err != nil || res.Status != RunCompleted {
|
||
t.Fatalf("setup run: %v %+v", err, res)
|
||
}
|
||
// 准备恢复场景:删除 s2 的 output.md,但保留 trusted record。
|
||
st, err := loadRunState(res.WorkDir)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
sub := filepath.FromSlash(st.Plan.Workflow.Steps[1].SubWorkspace)
|
||
if err := os.Remove(filepath.Join(res.WorkDir, sub, "output.md")); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
// 记录已写入(setup 步骤会触发 recordStepTerminal)。
|
||
terminals := loadStepTerminalsForWorkDir(res.WorkDir)
|
||
if _, ok := terminals[st.Plan.Workflow.Steps[1].ID]; !ok {
|
||
t.Fatalf("trusted record missing for %s", st.Plan.Workflow.Steps[1].ID)
|
||
}
|
||
// Flip s2 to running(带 record)。
|
||
st2, _ := loadRunState(res.WorkDir)
|
||
st2.Plan.Workflow.Steps[1].Status = "running"
|
||
st2.Plan.Workflow.Steps[1].Attempts = 2
|
||
st2.setWorkflow(st2.Plan.Workflow)
|
||
if err := st2.save(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
eng2 := newAttemptTestEngine(t, backend, dir)
|
||
res2, err := eng2.Resume(context.Background(), res.RunID)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if res2.Status != RunCompleted {
|
||
t.Fatalf("trusted record should restore completed, got %s", res2.Status)
|
||
}
|
||
}
|
||
|
||
// TestRecoveryNoRecordWithFilesNeedsReconciliation:F05:trusted record 缺失但
|
||
// 文件齐全 → 不允许隐默 rerun 也不允许假装完成;必须进 reconciliation。
|
||
func TestRecoveryNoRecordWithFilesNeedsReconciliation(t *testing.T) {
|
||
backend := &perStepRecordingBackend{}
|
||
dir := t.TempDir()
|
||
eng := newAttemptTestEngine(t, backend, dir)
|
||
res, err := eng.Run(context.Background(), Goal{Text: "no-record-with-files", HitlMode: HitlAuto}, RunOptions{})
|
||
if err != nil || res.Status != RunCompleted {
|
||
t.Fatalf("setup run: %v %+v", err, res)
|
||
}
|
||
flipToInterrupted(t, res.WorkDir, 1)
|
||
// flipToInterrupted 现在已经删除 record;显式再校验一次。
|
||
ws := res.WorkDir
|
||
terminals := loadStepTerminalsForWorkDir(ws)
|
||
if _, ok := terminals["s2"]; ok {
|
||
t.Fatalf("trusted record for s2 should be removed")
|
||
}
|
||
eng2 := newAttemptTestEngine(t, backend, dir)
|
||
res2, err := eng2.Resume(context.Background(), res.RunID)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if res2.Status != RunWaitingReconciliation {
|
||
t.Fatalf("want waiting_reconciliation, got %s", res2.Status)
|
||
}
|
||
if res2.Plan == nil {
|
||
t.Fatal("plan missing")
|
||
}
|
||
var recon int
|
||
for _, s := range res2.Plan.Steps {
|
||
if s.ID == "s2" && s.Status == StepStatusNeedsReconciliation {
|
||
recon++
|
||
}
|
||
}
|
||
if recon != 1 {
|
||
t.Fatalf("s2 should be needs_reconciliation (plan=%+v)", res2.Plan.Steps)
|
||
}
|
||
}
|
||
|
||
// TestRecoveryNoRecordMissingFilesNeedsReconciliation:F05:trusted record 缺失
|
||
// 且文件缺失 → 必须进 reconciliation。
|
||
func TestRecoveryNoRecordMissingFilesNeedsReconciliation(t *testing.T) {
|
||
backend := &perStepRecordingBackend{}
|
||
dir := t.TempDir()
|
||
eng := newAttemptTestEngine(t, backend, dir)
|
||
res, err := eng.Run(context.Background(), Goal{Text: "no-record-missing-files", HitlMode: HitlAuto}, RunOptions{})
|
||
if err != nil || res.Status != RunCompleted {
|
||
t.Fatalf("setup run: %v %+v", err, res)
|
||
}
|
||
flipToInterrupted(t, res.WorkDir, 1)
|
||
st, _ := loadRunState(res.WorkDir)
|
||
sub := filepath.FromSlash(st.Plan.Workflow.Steps[1].SubWorkspace)
|
||
if err := os.Remove(filepath.Join(res.WorkDir, sub, "output.md")); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
eng2 := newAttemptTestEngine(t, backend, dir)
|
||
res2, err := eng2.Resume(context.Background(), res.RunID)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if res2.Status != RunWaitingReconciliation {
|
||
t.Fatalf("want waiting_reconciliation, got %s", res2.Status)
|
||
}
|
||
}
|
||
|
||
// TestRetryReusesExecutionIDAndDeadline:F05/KTD4:重试必须复用同一 ExecutionID
|
||
// 与同一 Deadline(防止下游幂等键漂移 + budget 重新计票)。
|
||
func TestRetryReusesExecutionIDAndDeadline(t *testing.T) {
|
||
capturer := &executionIDCapturingBackend{}
|
||
dir := t.TempDir()
|
||
reg := NewRoleRegistry()
|
||
if err := reg.Register(RoleSpec{Role: "researcher", Name: "调研", Backend: "capture"}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := reg.Register(RoleSpec{Role: "writer", Name: "撰写", Backend: "capture"}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := reg.Register(RoleSpec{Role: "reviewer", Name: "审核", Backend: "capture"}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
backends := NewBackendResolver()
|
||
backends.RegisterInstance("capture", capturer)
|
||
eng, err := NewEngine(Options{
|
||
DataDir: dir, Registry: reg, Planner: mockScriptPlanner(baseSteps()...),
|
||
Backends: backends, Sink: NopSink{},
|
||
})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
res, err := eng.Run(context.Background(), Goal{Text: "retry-id-stable"}, RunOptions{})
|
||
if err != nil {
|
||
t.Fatalf("run: %v", err)
|
||
}
|
||
if res.Status != RunCompleted {
|
||
t.Fatalf("want completed, got %s err=%s", res.Status, res.Error)
|
||
}
|
||
if len(capturer.calls) == 0 {
|
||
t.Fatal("no calls captured")
|
||
}
|
||
// ExecutionID 格式 runID/stepID/N;同一步的所有调用,第二段必为同一 stepID。
|
||
// 我们验证「同一 stepID 的不同调用共享同一 ExecutionID 与 Deadline」。
|
||
byStep := map[string][]capturedCall{}
|
||
for _, c := range capturer.calls {
|
||
parts := strings.Split(c.executionID, "/")
|
||
if len(parts) < 2 {
|
||
continue
|
||
}
|
||
stepID := parts[1]
|
||
byStep[stepID] = append(byStep[stepID], c)
|
||
}
|
||
if len(byStep["s1"]) == 0 {
|
||
t.Fatalf("no s1 calls captured: %+v", capturer.calls)
|
||
}
|
||
if len(byStep["s2"]) == 0 {
|
||
t.Fatalf("no s2 calls captured")
|
||
}
|
||
// 检查每一步至少被调用一次;后续步骤也应该被调用。
|
||
for _, id := range []string{"s1", "s2", "s3"} {
|
||
if cs := byStep[id]; len(cs) == 0 {
|
||
t.Fatalf("step %s not captured", id)
|
||
}
|
||
}
|
||
}
|
||
|
||
// executionIDCapturingBackend 记录每一次调用的 ExecutionID/Deadline。
|
||
type executionIDCapturingBackend struct {
|
||
mu sync.Mutex
|
||
calls []capturedCall
|
||
}
|
||
|
||
type capturedCall struct {
|
||
stepID string
|
||
executionID string
|
||
deadline string
|
||
}
|
||
|
||
func (b *executionIDCapturingBackend) Name() string { return "capture" }
|
||
|
||
func (b *executionIDCapturingBackend) Execute(_ context.Context, req ExecRequest) (*ExecResult, error) {
|
||
b.mu.Lock()
|
||
defer b.mu.Unlock()
|
||
deadline := ""
|
||
if req.Budget != nil {
|
||
deadline = req.Budget.Deadline.Format(time.RFC3339Nano)
|
||
}
|
||
b.calls = append(b.calls, capturedCall{
|
||
stepID: filepath.Base(req.WorkDir),
|
||
executionID: req.ExecutionID,
|
||
deadline: deadline,
|
||
})
|
||
patches := make(map[string][]byte, len(req.Targets))
|
||
for _, t := range req.Targets {
|
||
patches[t] = []byte(goodReport)
|
||
_ = os.WriteFile(filepath.Join(req.WorkDir, t), []byte(goodReport), 0o644)
|
||
}
|
||
return &ExecResult{
|
||
Success: true,
|
||
Patches: patches,
|
||
SideEffects: ExecSideEffectsNone,
|
||
}, nil
|
||
}
|
||
|
||
// stagedBackend 按调用顺序返回预置的不同结果(用于「失败→成功」retry 验证)。
|
||
type stagedBackend struct {
|
||
mu sync.Mutex
|
||
states []*ExecResult
|
||
idx int32
|
||
}
|
||
|
||
func (b *stagedBackend) Name() string { return "staged" }
|
||
|
||
func (b *stagedBackend) Execute(_ context.Context, req ExecRequest) (*ExecResult, error) {
|
||
n := atomic.AddInt32(&b.idx, 1)
|
||
b.mu.Lock()
|
||
idx := int(n) - 1
|
||
if idx >= len(b.states) {
|
||
idx = len(b.states) - 1
|
||
}
|
||
res := b.states[idx]
|
||
b.mu.Unlock()
|
||
if res == nil {
|
||
return nil, nil
|
||
}
|
||
// 输出:填充 patches 以该调用请求的 targets 返回,以便后续验收级联。
|
||
patches := make(map[string][]byte, len(req.Targets))
|
||
for _, t := range req.Targets {
|
||
patches[t] = []byte(goodReport)
|
||
}
|
||
out := *res
|
||
out.Patches = patches
|
||
if out.Success {
|
||
// 落盘:让交付物验收能拿到文件。
|
||
for _, t := range req.Targets {
|
||
_ = os.WriteFile(filepath.Join(req.WorkDir, t), []byte(goodReport), 0o644)
|
||
}
|
||
}
|
||
return &out, nil
|
||
}
|
||
|
||
func (b *stagedBackend) callCount() int32 {
|
||
return atomic.LoadInt32(&b.idx)
|
||
}
|