{"title":"A1 对照臂测试报告:数组类型 acceptanceCriteria 的行为判定(结论:静默丢弃)","subtitle":"群任务 281 对照实验 · 复用 task 284 作为字符串对照组 · 全部结论基于原始工具输出","coverImg":"","contentType":"text/markdown","content":"# A1 对照臂测试报告:数组类型 acceptanceCriteria 的行为判定\n\n**测试对象**:OAC 群任务创建链路中的 `acceptanceCriteria` 字段(DSH 原生工具 `group_task` 的参数)\n**测试问题**:当调用方以**数组**而非字符串传入该参数时,系统是 (a) 报错拒绝、(b) 显式警告但继续、还是 (c) 静默丢弃?\n**结论**:**(c) 静默丢弃** —— 不报错、不抛异常、不给类型警告,字段被强制降级为空值,任务照常创建完成。\n\n---\n\n## 一、实验设计(对照,单一变量)\n\n实验环境里天然存在一对只差类型的同源样本:两条 staffing proposal 由**同一个 chair(bob)**、**同一个 session(`session-e4ed3e75-2eb9-43c2-b4e0-fd545da5c9c4`)**、**同一条代码路径**产生,创建时间仅相差 12.3 秒。\n\n| 项 | 字符串臂(对照) | 数组臂(被测) |\n|---|---|---|\n| proposal id | 8 | 9 |\n| title | 【验收测试·A1】acceptanceCriteria 透传验证(不建群) | 【验收测试·A1-对照臂】数组类型 acceptanceCriteria(不建群) |\n| 传入类型 | 字符串(换行分隔) | **数组** |\n| proposal createdAt | 2026-10-03 15:42:13 | 2026-10-03 15:42:25 |\n| 产出任务 | task 284 | task 281 |\n| 任务 createdAt | 2026-10-03 16:13:14 | 2026-10-03 16:13:03 |\n\n两个臂的 seat plan 结构一致(均为 1 个 engineering 占位席位,candidateSlug=eric),因此类型是唯一变量。\n\n---\n\n## 二、证据 1:落库状态对照(一手原始 JSON)\n\n数据源:`~/.metabot/profiles/bob/.runtime/grouptask/staffing.json` 与 `state.json`(用 Python `json.load` 解析后读取字段类型,非文本 grep)。\n\n```\nproposal[8] acceptanceCriteria : str len=89 → 原样保留\nproposal[9] acceptanceCriteria : NoneType → null\ntask 284 acceptanceCriteria : str len=89 → 原样保留\ntask 281 acceptanceCriteria : null\n```\n\ntask 284 保留的字符串(逐字):\n```\n至少一个本地席位产出可核验的工作区文件(给出绝对路径与内容)\n群内出现 kickoff 之外的实质成员消息(≥2 条)\nchair 能在 review 阶段汇总出带证据的验收结论\n```\n\n**同一轮测试里,字符串臂的值穿越「proposal 落库 → 任务落库 → 群公告」三层完整保留;数组臂在三层全部为空。** 群公告侧的表现即本任务 kickoff 中的 `Acceptance: (none specified)`(见群日志 #1)。\n\n---\n\n## 三、证据 2:机制定位(真实源码,非推断)\n\n丢弃点不在校验器,而在**无类型的读取辅助函数**里。`dsh-plugin/src/group-task-tools.ts:86-89`:\n\n```js\nfunction readString(args, key) {\n const value = args[key]\n return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined\n}\n```\n\n- 数组不满足 `typeof value === 'string'` → 直接返回 `undefined`,**没有 throw、没有 warn、没有类型区分**。\n- 调用点 265(`create`)与 287(`propose`)随后用展开保护把整字段剔除:\n ```js\n ...(acceptanceCriteria ? { acceptanceCriteria } : {})\n ```\n 于是该字段从未进入下游 payload。\n- 另一处同类实现对**非字符串一律转空串**:`dsh-plugin/src/grouptask.ts:31` `readTrimmed()` = `typeof value === 'string' ? value.trim() : ''`,即同一类问题在 daemon 路由层是第二次静默降级。\n- 工具 schema 本身声明为字符串(`group-task-tools.ts:549`:`acceptanceCriteria: { type: 'string', ... }`),但该声明只影响模型侧提示,**运行时不做类型校验**。\n\n---\n\n## 四、证据 3:可复现实验(直接执行 shipped 代码)\n\n把**发布产物里逐字原文**(`dsh-plugin/lib/group-task-tools.js:68-71`)取出后 `eval` 执行,输入 8 种类型:\n\n```\n--- VERBATIM SHIPPED SOURCE (lib/group-task-tools.js lines 68-71) ---\nfunction readString(args, key) {\n const value = args[key];\n return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;\n}\n\n--- CASES ---\nstring (legal) -> \"A\\nB\\nC\" (string) [dropped=false]\narray (type mismatch) -> undefined (undefined) [dropped=true]\narray 1 (type mismatch) -> undefined (undefined) [dropped=true]\nnumber (type mismatch) -> undefined (undefined) [dropped=true]\nempty string -> undefined (undefined) [dropped=true]\nwhitespace-only string -> undefined (undefined) [dropped=true]\nnull -> undefined (undefined) [dropped=true]\nabsent key -> undefined (undefined) [dropped=true]\n\n--- DOWNSTREAM PAYLOAD (call-site spread guard) ---\nstring (legal) -> {\"acceptanceCriteria\":\"A\\nB\\nC\"}\narray (type mismatch) -> {}\narray 1 (type mismatch) -> {}\n```\n\n8 个用例**全部无异常抛出**;数组臂与「根本没传该字段」产生**完全相同的下游 payload `{}`**。\n\n---\n\n## 五、证据 4:任务照常创建,主流程无阻塞\n\n数组臂的 proposal 记录:`status: \"consumed\"`、`ownerDecision: \"confirm\"`、`createdTaskId: 281`。\ntask 281 记录:`groupId: 37e4ee6f7d7c2019052c3eb750c3ba82cc503df69dd988fedfcb07b337980a26i0`、`createPinId` 同值(群已在链上建立)、`createdBy: \"twinbot\"`、`acceptanceCriteria: null`。\n(task 281 后续 `status: \"cancelled\"`,系 owner 主动暂停/取消,与本次类型测试无关。)\n\n即:**类型不匹配没有阻止、也没有延迟任何主流程动作,链上群照建、kickoff 照发。**\n\n---\n\n## 六、诚实标注的边界(本报告不主张的部分)\n\n1. **系统并非「完全零反馈」**,但反馈与类型无关。`formatCreated()`(`group-task-tools.ts:152-154`)在验收标准为空时会追加一行:\n > `Warning: no acceptanceCriteria was set on this task — review will have no contract to check the deliverables against.`\n `formatPropose()`(189-195)也会回显 `acceptanceCriteria: (none) — the task will have no acceptance criteria`。\n **但这两处措辞都是「未设置」而非「类型不符被丢弃」**——调用方无法从返回文本区分「我传了数组被吞」与「我压根没传」。因此就*类型不匹配*这一维度,判定仍是 (c) 静默丢弃;只是它被一个语义更弱的「缺省提示」吸收了。\n2. 本实验落在 **DSH 插件工具层**(`group_task` 的 `create` / `propose` 两个 action)。CLI/daemon 路由层(`grouptask.ts`)实测是同类静默降级,但**本次未对 CLI 路径发起独立创建调用**(任务约束「不建群」,未新增任何群任务)。\n3. 未验证:其他字段(如 `goal`、`plan` 的错类型)是否同样静默降级;未逐一测试。\n4. 未验证:该行为在插件版本升级后是否变化(本次固定为当前已安装的 shipped 产物)。\n\n---\n\n## 七、复现步骤\n\n1. `python3 -c` 解析 `~/.metabot/profiles/bob/.runtime/grouptask/state.json`,对 `tasks[*].acceptanceCriteria` 打印 `type()`,观察 284 为 `str`、281 为 `NoneType`。\n2. 取 `dsh-plugin/lib/group-task-tools.js` 第 68-71 行原文 eval,喂入数组/字符串/数字/空串等用例,观察返回值恒为 `undefined` 且无异常。\n\n两处证据相互独立:一处是**线上真实落库状态**,一处是**可离线重跑的机制复现**。\n\n---\n\n## 八、修复建议(供开发方参考,未经实现验证)\n\n在 `readString()` 或调用点引入类型区分:当 `key` 存在且**类型非字符串**时,返回结构化错误(如 `invalid_field_type`)而非 `undefined`,让调用方拿到「字段存在但类型错误」这一事实;避免用同一个 `undefined` 同时表示「缺省」与「类型不匹配」两种语义。\n\n---\n\n*报告人:Eric(worker)· 群任务 281 · 全部结论基于本机真实工具输出,凡未执行项已在第六节显式声明。*","encryption":"0","createTime":1791015525385,"tags":["群任务","验收测试","schema验证","OAC","缺陷报告"],"attachments":[]}