get-shit-done 缺陷修复实录:3593 让 `config-set <key>` 缺值调用在写入前干净失败
导读
.planning/config.json 是 get-shit-done 规格驱动开发工作流的配置中枢,而 config-set 命令负责把键值写入其中。变更集 3593-cli-negative-matrix-harness.md 记录了一个隐蔽而危险的缺陷:当用户只输入 config-set model_profile(仅有 key、缺少 value)时,旧版命令竟然返回 { updated: true } 与退出码 0——仿佛成功,实际上 undefined 值被 JSON.stringify 在写盘时静默丢弃,构成配置静默损坏。本文以该变更集为骨架,结合 CJS 处理器、SDK 查询层与配套的 CLI 对抗性输入矩阵测试,完整还原缺陷成因、双端修复方案以及驱动这次修复的 #3593 负向测试体系,读者可据此理解 get-shit-done 如何用"类型化失败"取代"静默成功"来防御配置类 CLI 的边界输入。
缺陷全貌:一条"成功"但什么都没写进去的命令
变更集首先精确定义了缺陷的触发路径与表现:
- 触发方式:调用
config-set model_profile——只传 key,省略 value。 - 旧行为:命令返回
{ updated: true }且退出码为 0,误导调用方(尤其是以脚本/Agent 方式调用时)以为写入成功。 - 根因:缺失的 value 以
undefined形式穿透参数链,而JSON.stringify在序列化对象时会静默丢弃值为undefined的键,最终落盘的配置中该 key 要么整体消失,要么留下一个不符合预期的残缺条目。 - 新行为:CJS 处理器与 SDK 的
configSet查询在任何写盘动作发生之前抛出类型化的Usage错误,从源头杜绝配置静默损坏。
值得注意的是,get-shit-done 存在双入口架构——面向交互的 CJS 命令处理器与面向 SDK/机器查询的 TypeScript 查询层。变更集明确指出两处都必须修复,这正对应本仓库的 SDK/CJS 奇偶一致性设计(例如 config-mutation.ts 的注释即声明与 get-shit-done/bin/lib/config.cjs 保持奇偶,并有 CI 奇偶测试保障)。
双端修复实现:在任何写入发生前抛错
CJS 侧:get-shit-done/bin/lib/config.cjs
CJS 命令入口 cmdConfigSet 中新增了紧跟在 key 存在性检查之后的守卫。核心逻辑是:value === undefined 时直接调用 error('Usage: config-set <key.path> <value>', ERROR_REASON.USAGE) 终止进程。注释给出了精确的防御动机:缺少该守卫时,值会以 undefined 穿过数字/布尔/JSON 的各个解析分支,写入时要么被 JSON.stringify 静默剥除键,要么落盘为损坏条目;而选择类型化 reason(而非仅靠文案提示)是为了让负向矩阵测试可以断言结构化字段而不是去 grep 输出文案。
SDK 侧:sdk/src/query/config-mutation.ts
SDK 查询处理函数 configSet(见 config-mutation.ts)同步修复。其参数契约是 args[0]=key、args[1]=value:
const keyPath = args[0];
const rawValue = args[1];
if (!keyPath) {
throw new GSDError('Usage: config-set <key.path> <value>', ErrorClassification.Validation);
}
// #3593: parity with CJS cmdConfigSet — reject `config-set <key>` invocations
// that omit the value. Without this guard parsedValue stays undefined and the
// write either silently strips the key (JSON.stringify drops undefined) or
// persists a corrupt entry.
if (rawValue === undefined) {
throw new GSDError('Usage: config-set <key.path> <value>', ErrorClassification.Validation);
}
两处守卫的代码注释都明确标注了 #3593 追溯号,并给出同一根因说明,形成可追踪的双端契约。从该文件可进一步看到 configSet 后续的完整校验链:key 白名单校验(VALID_CONFIG_KEYS/RUNTIME_STATE_KEYS/动态 key 模式)、类型化值解析 parseConfigValue(true/false/数字/JSON 尝试解析)、各类枚举值校验(如 context、workflow.drift_action、statusline.context_position),最后经 state 锁与"临时文件 + rename"的原子写落盘——守卫被放置在整条校验链最前端,确保任何校验与 IO 副作用都不会发生。
驱动修复的测试工程:#3593 CLI 对抗性输入矩阵
变更集明确指出该缺陷由 #3593 CLI adversarial-input matrix 暴露。这套测试体系不是一次性补丁,而是仓库针对 CLI 命令族的系统性负向测试基座,共包含三个文件与一个共享 harness:
| 文件 | 职责 |
|---|---|
| tests/helpers/cli-negative.cjs | 共享 harness:包装 spawnSync,把子进程输出规整为类型化 IR |
| tests/feat-3593-cli-negative-config.test.cjs | config 命令族全量 12 类对抗用例(模板) |
| tests/feat-3593-cli-negative-harness.test.cjs | harness 自身的 IR 契约元测试 |
| tests/feat-3593-cli-negative-universal.test.cjs | 七大命令族通用最小契约扫描 |
Harness 设计:从不拼接 shell 字符串,断言结构化 IR
cli-negative.cjs 的核心设计原则写在文件头注释中:恶意值(shell 元字符、空字节、Unicode、超长串)一律作为单个 argv 元素通过 spawnSync 传入,绝不拼接进 shell 字符串——这样测试框架本身不可能制造 shell 注入的假阳性。每个子进程带 --json-errors 运行,stdout/stderr 被解析成:
status/signal:退出码与终止信号;ok/reason/message:从 JSON 错误载荷中解析出的类型化字段(reason来自ERROR_REASON枚举,例如usage、config_invalid_key);hasStackTrace:stderr 中是否泄漏了 V8 栈帧(匹配\n at前缀行)。
assertSafeFailure 辅助函数将"干净失败"固化为通用不变量:非零退出、非信号终止、无栈泄漏、ok=false、reason 非空字符串。
12 类对抗输入:config 命令族矩阵
feat-3593-cli-negative-config.test.cjs 依据 CONTRIBUTING.md 的 QA Matrix 章节,对 config-get/config-set 逐一覆盖 12 类攻击面,其中与本文缺陷直接相关的用例是:
test('config-set with key but no value fails with a typed reason', (t) => {
const projectDir = createTempProject('cli-neg-config-3-');
t.after(() => cleanup(projectDir));
const result = runCli(['config-set', 'model_profile'], { cwd: projectDir });
assertSafeFailure(result, 'config-set missing value');
});
这正是变更集描述缺陷的回归测试:在修复前它必然失败(命令会以退出码 0 返回 { updated: true }),修复后则通过 assertSafeFailure 验证其按类型化方式干净失败。该文件还覆盖了缺 key(config-set 裸调用)、空串 key、纯空白 key、重复 --cwd、未知子命令、值形如 --flag、损坏的 config.json(必须保持原样不被"好心"覆写)、50KB 超长 key、Unicode key、emoji 值 JSON 往返,以及一组 shell 元字符载荷——每个载荷都会尝试在项目目录旁创建 INJ-* 哨兵文件,测试断言运行后哨兵不存在,从而证明载荷从未被 shell 解释执行。
通用契约扫描:七大命令族一视同仁
feat-3593-cli-negative-universal.test.cjs 将更窄的三条契约(裸命令不崩溃无栈泄漏;命令深度上的未知子命令在 --json-errors 下输出类型化 reason;作为 argv 元素的 shell 元字符不得被执行)推广到 phase、roadmap、state、config、workstream、init、validate 七个命令族,config 族在此充当全量矩阵的模板。
规范支撑:为什么仓库要求这种负向测试
这次修复并非孤立实践,而是 get-shit-done 明确的质量门槛。在 CONTRIBUTING.md 的 "QA Matrix Requirements" 与 "CLI and command routing" 小节中规定:凡改动 CLI 解析、命令分发、查询分发、命令路由器、gsd-tools 或 gsd-sdk,必须为受影响的命令族提供负向输入矩阵;且对缺陷修复,回归测试必须先展示原始失败再验证修复生效,bug 涉及 CLI 输入、解析器、文件系统写入或 SDK/运行时奇偶时必须使用相应 QA 矩阵并包含"坏行为不再发生"的负向证明。对于 config-set 这种"接受用户输入 + 写盘"的高风险面,纯 happy-path 测试远不足以证明安全。
从缺陷中可迁移的实践结论
- 写盘前校验缺值:任何"键值写"型命令都应在 IO 前拒绝缺值形式,不要依赖
JSON.stringify对undefined的静默剥除语义——"成功响应 + 静默丢失"比显式报错危害大得多。 - 双入口双修复:同一命令同时暴露 CJS 与 SDK 两条路径时,缺值守卫必须奇偶一致(本仓库的 CJS/SDK 奇偶测试正是为拦截这种不一致而生)。
- 类型化错误胜过文案:让失败路径携带可机器断言的 reason 码(本仓库为
ERROR_REASON/GSDError分类),负向测试即可断言结构化字段而非易碎的输出文案。 - 负向矩阵是命令族的契约:参考 feat-3593-cli-negative-config.test.cjs 的 12 类清单(缺参、空/空白、重复 flag、冲突 flag、未知子命令、flag 状值、损坏输入文件、超长、Unicode、shell 元字符),可系统化提升任何 CLI 的健壮性验收。
- 真实用法对齐:正常的写入形式为
node gsd-tools.cjs config-set <key> <value>或gsd-sdk query config-set <key> <value>(见 docs/CLI-TOOLS.md),当通过/gsd-config等交互入口维护review.models.<cli>时,同样遵循"必须有值"的契约。
修复后的 config-set 让"失败要失败得干净、可诊断"成为 get-shit-done CLI 家族的默认纪律——这正是 #3593 负向矩阵为整个命令体系带来的长期价值。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00