fix(coordinator,intent): nil AdvisorBrief panic guard + interrogative gate for ADVANCE_STAGE overmatch
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
---
|
||||
title: "阶段推进 AdvisorBrief 空指针 panic 修复"
|
||||
task_id: fix-stage-brief-nil-panic-2026-09-25-r2
|
||||
status: in_progress
|
||||
created: 2026-09-25
|
||||
owner: Cursor
|
||||
task_type: fix
|
||||
policy_revision: "AGENTS.md @ 725181de"
|
||||
branch: fix/fix-stage-brief-nil-panic-2026-09-25(实施并入 dialogue-context-injection-2026-09-25 scope 持有者 worktree)
|
||||
worktree: .worktrees/fix-stage-brief-nil-panic-2026-09-25(注册用)/ 实际代码在 .worktrees/dialogue-context-injection-2026-09-25
|
||||
base_dev_sha: "725181de4aa4480f77c52e40eeaef040c6563c3f"
|
||||
write_scope:
|
||||
- internal/coordinator/engine_stage.go
|
||||
- internal/coordinator/engine_test.go
|
||||
- internal/intent/recognizer_matchers_task.go
|
||||
- .doc/test-reports/2026-09-25-stage-brief-nil-panic-fix-test-report.md
|
||||
mutex: coordinator-nil-panic-fix
|
||||
review_state: accepted_with_evidence
|
||||
integration_state: pending
|
||||
deployment: not_required
|
||||
cleanup: pending
|
||||
remote_sync: not_requested
|
||||
---
|
||||
|
||||
# 阶段推进 AdvisorBrief 空指针 panic 修复
|
||||
|
||||
## 用户目标与授权
|
||||
|
||||
- 总控 `/goal`(2026-09-25)批次 P0:消除 dev 基线存量 panic。
|
||||
- scope 冲突处置:`internal/coordinator` 被 `dialogue-context-injection-2026-09-25-r2` 声明(hard same_scope),按 catalog 规则「合并到持有者任务」——修复作为独立提交挂入持有者 worktree 分支。
|
||||
|
||||
## 修复内容
|
||||
|
||||
1. **panic**:`engine_stage.go` `stageRes.AdvisorBrief` nil 保护 + 降级文案。
|
||||
2. **意图误判**:`matchAdvanceStage` 前置 `isInterrogative` 疑问句排除(问句不构成推进指令)。
|
||||
3. **回归测试**:`TestCoordinatorEngine_AdvanceStageNilBriefNoPanic`。
|
||||
|
||||
## 验证
|
||||
|
||||
见 [测试报告](../../test-reports/2026-09-25-stage-brief-nil-panic-fix-test-report.md):PASS。
|
||||
|
||||
## 残差
|
||||
|
||||
- `internal/chat` 的 `TestChatSession_SemanticMemoryRecallAndAsyncTurnRecord` 存量失败(dev 基线同款,非本批引入),登记后续批次。
|
||||
@@ -0,0 +1,95 @@
|
||||
# 阶段推进 AdvisorBrief 空指针 panic 修复测试报告
|
||||
|
||||
- task: `fix-stage-brief-nil-panic-2026-09-25-r2`(合入 `dialogue-context-injection-2026-09-25-r2` scope;独立分支 `fix/fix-stage-brief-nil-panic-2026-09-25` 因 scope 硬重叠并入持有者任务执行)
|
||||
- date: 2026-09-25
|
||||
- tested_worktree: `.worktrees/dialogue-context-injection-2026-09-25`
|
||||
- base: `725181de`(dev tip;本 worktree 分支 tip `dc29b84c` 之上)
|
||||
- verdict: PASS
|
||||
|
||||
## 一、 测试背景
|
||||
|
||||
总控批次规划(`planned-tasks-closeout-plan-2026-09-25`)P0 项:dev 基线 `TestBizDev_CaseRecommendation_FullLink_E2E` 稳定 panic,阻塞所有后续批次的测试可信度。
|
||||
|
||||
## 二、 测试目标
|
||||
|
||||
1. 确认 panic 根因并修复;
|
||||
2. 意图误判层根因(问句被 `做.*报价` 误判为 ADVANCE_STAGE)一并修复;
|
||||
3. 回归无新增失败。
|
||||
|
||||
## 三、 测试方案与环境
|
||||
|
||||
- Linux 7.0.0-31-generic;Go 1.27.1(linuxbrew)
|
||||
- 层级:工程工具/单元(非产品 PI;验证契约工程修复口径)
|
||||
- 用例:新增回归 `TestCoordinatorEngine_AdvanceStageNilBriefNoPanic` + 既有 E2E 复跑 + 关联包全量
|
||||
|
||||
## 四、 预期结果
|
||||
|
||||
- nil brief 推进返回 accepted + 降级文案,不 panic;
|
||||
- 问句「我们之前做过类似的…吗?型检和报价大概怎么定的?」路由到 QUERY_SIMILAR_CASES;
|
||||
- E2E `TestBizDev_CaseRecommendation_FullLink_E2E` 全绿。
|
||||
|
||||
## 五、 实际测试结果与判定
|
||||
|
||||
### 5.1 panic 复现(修复前,dev 基线 725181de 同样复现)
|
||||
|
||||
**输入**:`go test -run 'TestBizDev_CaseRecommendation_FullLink_E2E' ./internal/bizdev/`
|
||||
|
||||
**实际输出**(节选):
|
||||
|
||||
```
|
||||
panic: runtime error: invalid memory address or nil pointer dereference
|
||||
internal/coordinator/engine_stage.go:306 +0x240c
|
||||
internal/coordinator/engine.go:281 +0x1fb4
|
||||
internal/chat/session.go:467 +0x21b0
|
||||
internal/bizdev/case_recommend_e2e_test.go:133 +0x2425
|
||||
```
|
||||
|
||||
**评价判定**:FAIL(存量缺陷,非本批引入)
|
||||
|
||||
### 5.2 根因
|
||||
|
||||
1. **panic 层**:`engine_stage.go:306` 裸解引用 `stageRes.AdvisorBrief.MaturityHeadline`;空 `executedArtifacts` 时 `primaryArtifact=nil → stageBrief=nil`。
|
||||
2. **意图层**(修复 1 后暴露):问句「…报价大概怎么定的?」被 `matchAdvanceStage` 的 `做.*报价` 命中(「定」∈`做.*`)误判 ADVANCE_STAGE,进入无产物阶段推进路径。
|
||||
|
||||
### 5.3 修复
|
||||
|
||||
1. `engine_stage.go`:`stageRes.AdvisorBrief != nil` 分支保护 + 降级文案「未生成阶段级参谋简报」。
|
||||
2. `intent/recognizer_matchers_task.go`:新增 `isInterrogative`(句尾 ?/吗/呢 + 疑问词表),`matchAdvanceStage` 疑问句直接不匹配。
|
||||
|
||||
### 5.4 回归测试(新增)
|
||||
|
||||
**输入**:`go test -run 'TestCoordinatorEngine_AdvanceStageNilBriefNoPanic' -v ./internal/coordinator/`
|
||||
|
||||
**实际输出**:
|
||||
|
||||
```
|
||||
=== RUN TestCoordinatorEngine_AdvanceStageNilBriefNoPanic
|
||||
2026/09/25 01:50:24 INFO coordinator_route intent=ADVANCE_STAGE ... channel=SQUAD_FIXED
|
||||
--- PASS: TestCoordinatorEngine_AdvanceStageNilBriefNoPanic (0.00s)
|
||||
PASS
|
||||
```
|
||||
|
||||
**评价判定**:PASS
|
||||
|
||||
### 5.5 原 E2E 复跑
|
||||
|
||||
**输入**:`go test -run 'TestBizDev_CaseRecommendation_FullLink_E2E' ./internal/bizdev/`
|
||||
|
||||
**实际输出**:`ok chat-agent-system/internal/bizdev 0.063s`
|
||||
|
||||
**评价判定**:PASS(案例推荐 + AdvisorBrief 注入断言全过)
|
||||
|
||||
## 六、 自动化测试套件汇总
|
||||
|
||||
| 套件 | 结果 |
|
||||
|---|---|
|
||||
| `go test ./internal/intent/` | PASS |
|
||||
| `go test ./internal/coordinator/` | PASS |
|
||||
| `go test ./internal/bizdev/` | PASS(含原 panic E2E) |
|
||||
| `go test ./internal/squad/ ./internal/dialoguectx/ ./internal/conflictresolve/ ./internal/ctxmgr/` | PASS |
|
||||
| `go test ./internal/chat/` | FAIL:`TestChatSession_SemanticMemoryRecallAndAsyncTurnRecord` —— **dev 基线 725181de 同款失败(存量,与本修复无关,已对照确认)** |
|
||||
|
||||
## 七、 测试结论与后续
|
||||
|
||||
- P0 达到授权交付终点:panic 消除 + 意图误判修正 + 回归绿。
|
||||
- 存量遗留:chat 语义记忆异步轮记录用例失败(独立问题,登记为后续批次候选,不冒充本批 PASS)。
|
||||
@@ -303,7 +303,13 @@ func (e *CoordinatorEngine) handleAdvanceStage(ctx context.Context, cand *intent
|
||||
sb.WriteString(fmt.Sprintf("已为您成功推进并完成了【%s】!\n", stageRes.StageTitle))
|
||||
}
|
||||
sb.WriteString("相关论证与文档已沉淀完成。\n\n")
|
||||
sb.WriteString(fmt.Sprintf("💡 最新进展:%s\n", stageRes.AdvisorBrief.MaturityHeadline))
|
||||
// AdvisorBrief 可为 nil:阶段全部子任务 halt/失败时 executedArtifacts 为空,stageBrief 未赋值。
|
||||
// 裸解引用会在无产物推进场景 panic(见 2026-09-25 nil-panic 修复)。
|
||||
if stageRes.AdvisorBrief != nil {
|
||||
sb.WriteString(fmt.Sprintf("💡 最新进展:%s\n", stageRes.AdvisorBrief.MaturityHeadline))
|
||||
} else {
|
||||
sb.WriteString("💡 最新进展:阶段推进完成,但未生成阶段级参谋简报。\n")
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("建议您审阅交付文档(%s),或按下方建议推进后续洽谈。\n\n", artifactPath))
|
||||
sb.WriteString("可选下一步:\n")
|
||||
for idx, opt := range nextExp.Options {
|
||||
|
||||
@@ -2,15 +2,71 @@ package coordinator_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"chat-agent-system/internal/app/stageflow"
|
||||
"chat-agent-system/internal/coordinator"
|
||||
"chat-agent-system/internal/intent"
|
||||
"chat-agent-system/internal/maturity"
|
||||
"chat-agent-system/internal/squad"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// nilBriefStageRunner 模拟阶段任务全部 halt / 无已执行产物:AdvisorBrief 为 nil。
|
||||
type nilBriefStageRunner struct{}
|
||||
|
||||
func (nilBriefStageRunner) ExecuteTask(ctx context.Context, req stageflow.TaskRunRequest) (*stageflow.TaskRunResult, error) {
|
||||
return &stageflow.TaskRunResult{}, nil
|
||||
}
|
||||
|
||||
func (nilBriefStageRunner) ExecuteStage(ctx context.Context, req stageflow.StageRunRequest) (*stageflow.StageRunResult, error) {
|
||||
return &stageflow.StageRunResult{
|
||||
Stage: req.Stage,
|
||||
StageTitle: "可行性论证",
|
||||
Success: true,
|
||||
// PrimaryArtifact / AdvisorBrief 均为 nil —— 复现 2026-09-25 nil-deref panic
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 回归:空 executedArtifacts 时 stageBrief=nil,旧代码在 NaturalReply 组装处
|
||||
// 裸解引用 stageRes.AdvisorBrief 导致 panic(TestBizDev_CaseRecommendation_FullLink_E2E 亦触发)。
|
||||
func TestCoordinatorEngine_AdvanceStageNilBriefNoPanic(t *testing.T) {
|
||||
engine := coordinator.NewCoordinatorEngine(nil)
|
||||
engine.SetStageTaskRunner(nilBriefStageRunner{})
|
||||
|
||||
userCtx := &coordinator.UserProjectContext{
|
||||
Project: coordinator.ProjectInfo{
|
||||
Name: "超声测试项目",
|
||||
CurrentStage: maturity.StageV01_Triage,
|
||||
},
|
||||
User: coordinator.UserInfo{ProjectRole: "composer"},
|
||||
BlackboardFacts: make(map[string]string),
|
||||
}
|
||||
|
||||
recRes := &intent.IntentRecognitionResult{
|
||||
TopIntent: &intent.CandidateIntent{
|
||||
Intent: intent.IntentAdvanceStage,
|
||||
FilledSlots: map[string]string{
|
||||
intent.SlotTargetStage: string(maturity.StageV05_Feasibility),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
res, err := engine.ProcessIntent(context.Background(), recRes, userCtx)
|
||||
if err != nil {
|
||||
t.Fatalf("process intent failed: %v", err)
|
||||
}
|
||||
if res.Status != coordinator.StatusAccepted {
|
||||
t.Fatalf("expected status ACCEPTED, got %s", res.Status)
|
||||
}
|
||||
if !strings.Contains(res.NaturalReply, "未生成阶段级参谋简报") {
|
||||
t.Errorf("expected graceful nil-brief fallback in reply, got: %s", res.NaturalReply)
|
||||
}
|
||||
if userCtx.LatestAdvisorBrief != nil {
|
||||
t.Errorf("expected LatestAdvisorBrief to stay nil, got %+v", userCtx.LatestAdvisorBrief)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoordinatorEngine_QueryProgress(t *testing.T) {
|
||||
engine := coordinator.NewCoordinatorEngine(nil)
|
||||
|
||||
|
||||
@@ -11,7 +11,35 @@ import (
|
||||
"chat-agent-system/internal/squad"
|
||||
)
|
||||
|
||||
// isInterrogative 判断输入是否疑问句:句尾问号/「吗」「呢」,或句中出现
|
||||
// 「怎么/如何/什么/多少/哪/是否/大概/多久/为什么」等疑问词。
|
||||
// 用于把「查询/咨询」类输入从祈使句「指令」类意图(阶段推进等)中排除。
|
||||
func isInterrogative(input string) bool {
|
||||
trimmed := strings.TrimSpace(input)
|
||||
if trimmed == "" {
|
||||
return false
|
||||
}
|
||||
lastRune, _ := utf8.DecodeLastRuneInString(trimmed)
|
||||
if lastRune == '?' || lastRune == '?' || lastRune == '吗' || lastRune == '呢' {
|
||||
return true
|
||||
}
|
||||
questionWords := []string{"怎么", "如何", "什么", "多少", "哪些", "哪一", "是否",
|
||||
"大概", "多久", "为什么", "是不是", "有没有", "何时", "什么时候", "什么样的"}
|
||||
for _, w := range questionWords {
|
||||
if strings.Contains(input, w) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func matchAdvanceStage(input string, currentStage maturity.MaturityStage) *CandidateIntent {
|
||||
// 疑问句不构成推进指令:「…做过类似的…吗?报价大概怎么定的?」属于案例/知识查询,
|
||||
// 曾被 "做.*报价" 过宽匹配误判为 ADVANCE_STAGE(见 2026-09-25 意图误判修复)。
|
||||
// 疑问标记:句尾问号/语气词「吗」「呢」,或「怎么/什么/多少/哪/是否/大概」等疑问词。
|
||||
if isInterrogative(input) {
|
||||
return nil
|
||||
}
|
||||
patterns := []string{
|
||||
"进入下一阶段", "推进到下一阶段", "推进阶段", "进入方案", "进入论证",
|
||||
"方案论证", "可行性论证", "开始方案", "进入报价", "开始报价", "商业锁定",
|
||||
|
||||
Reference in New Issue
Block a user