Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions shadow-docs/changes/20260918-fix-cross-platform-ci/brief.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
{
"schema": "shadow-dev/v1",
"name": "20260918-fix-cross-platform-ci",
"type": "fix",
"scope": null,
"status": "branched",
"baseBranch": "main",
"branch": "fix/20260918-fix-cross-platform-ci",
"files": [],
"github": {
"repository": "stack-wuh/shadow-dev-cli",
"issue": 25,
"issueUrl": "https://github.com/stack-wuh/shadow-dev-cli/issues/25",
"pullRequest": null,
"pullRequestUrl": null
},
"review": {
"conclusion": "pending",
"verifiedCommit": null,
"verifiedAt": null
},
"workflow": {
"operation": null,
"checkpoint": "issue:25",
"planHash": "4c33e17d13906ff50b9ef77b99c83e4f21491bc1d7ba48c1088a641850138e22",
"updatedAt": null,
"lastError": null,
"issuePlan": {
"title": "[fix] 跨平台 CI 绿灯:钉死测试的 locale 与 tar 可执行依赖",
"titleRaw": null,
"supplement": "",
"body": "## 动机\nmain 的 CI 矩阵在 macos/windows 仍有两簇恒红(ubuntu 已由 #23 转绿):①`cli.test.mjs` 用例 20(TTY)断言 stderr 含中文「命令一览」,但语言链 `locale 探测 > zh` 在 en-US runner 上渲染英文——PR #10 引入的 locale 依赖,本地中文机掩盖;已用 `SHADOW_DEV_LANG=en` 本地复现同款断言失败。②`install.test.mjs` 全部用例在 windows runner 死于 `makeTarball`:node 直接 spawn 的 `tar` 绑到 System32 bsdtar,读不了 toUnix 产出的 MSYS `/tmp` 路径(38b1364 起即存在)。两簇都是**测试侧**缺陷,运行时行为正确。红 CI 继续让真实回归裸奔。\n\n## 引用规范\n- shadow-docs/knowledge/cli-output-contract.md\n - 当前结论: 语言链 `--lang` > `SHADOW_DEV_LANG` > locale 探测 > 默认 zh,只影响 stderr 文案\n - 适用 scope: 测试必须显式固定语言(--lang / SHADOW_DEV_LANG),不得依赖 runner locale\n- shadow-docs/knowledge/install-distribution.md\n - 当前结论: shim/产物断言是平台分支的;跨平台命令要在设计上可移植\n - 适用 scope: test/install.test.mjs\n- norms/tdd-verification.md\n - 当前结论: 先复现红(本地 SHADOW_DEV_LANG=en 已复现 ①;②的权威红源是 windows CI 矩阵)再修\n\n## 决策\n- **选型:** ①用例 20 的 `runTty(['--help'])` 显式传 `SHADOW_DEV_LANG=zh`(断言中文通道);②`makeTarball` 的 tar 改在 bash 内执行(`bash -c \"tar ...\"`),与安装器本体同源(install-cli.sh 里 tar 天然由 bash 解析,Windows 下命中 git 的 GNU tar),测试与实现走同一条解析路径。\n- **对比方案:** ①改成\"按 locale 断言中英文任一\"被否——非确定性断言是反模式;②toUnix 产 Windows 原生路径给 bsdtar 被否——`-C` 参数与包内 POSIX 路径又需要 MSYS 形态,两头不讨好;根治是统一交给 bash 解析。\n- **理由:** 测试向契约要确定性:语言显式声明、工具链与生产路径一致;均不动运行时代码。\n\n## 任务\n### Phase 1\n\n- [ ] task-1 — `test/cli.test.mjs` — 用例 20 的 plain(无 --json 的 TTY help)runTty 显式 `SHADOW_DEV_LANG=zh`;本地 `SHADOW_DEV_LANG=en` 强制下先跑红(已复现)后转绿\n- [ ] task-2 — `test/install.test.mjs` — `makeTarball` 的 `spawnSync('tar',...)` 改为 `spawnSync('bash', ['-c', ...])`;本地 8 用例保持绿\n\n### Phase 2\n\n- [ ] task-3 — PR 上 CI 9 格矩阵(ubuntu/macos/windows × 20/22/24)全绿作为权威验证;失败则在同一调查上下文内修到绿\n\n完整 brief:shadow-docs/changes/20260918-fix-cross-platform-ci/brief.md\n\n<!-- shadow-dev:issue-metadata {\"name\":\"20260918-fix-cross-platform-ci\",\"type\":\"fix\",\"scope\":null,\"status\":\"proposed\",\"branch\":null,\"baseBranch\":\"main\",\"briefPath\":\"shadow-docs/changes/20260918-fix-cross-platform-ci/brief.md\",\"cliVersion\":\"1.2.0\",\"prUrl\":null,\"issueNumber\":null} -->\n",
"labels": [
"fix"
]
}
}
}
---

# 跨平台 CI 绿灯:钉死测试的 locale 与 tar 可执行依赖

## 动机

main 的 CI 矩阵在 macos/windows 仍有两簇恒红(ubuntu 已由 #23 转绿):①`cli.test.mjs` 用例 20(TTY)断言 stderr 含中文「命令一览」,但语言链 `locale 探测 > zh` 在 en-US runner 上渲染英文——PR #10 引入的 locale 依赖,本地中文机掩盖;已用 `SHADOW_DEV_LANG=en` 本地复现同款断言失败。②`install.test.mjs` 全部用例在 windows runner 死于 `makeTarball`:node 直接 spawn 的 `tar` 绑到 System32 bsdtar,读不了 toUnix 产出的 MSYS `/tmp` 路径(38b1364 起即存在)。两簇都是**测试侧**缺陷,运行时行为正确。红 CI 继续让真实回归裸奔。

## 引用规范

- shadow-docs/knowledge/cli-output-contract.md
- 当前结论: 语言链 `--lang` > `SHADOW_DEV_LANG` > locale 探测 > 默认 zh,只影响 stderr 文案
- 适用 scope: 测试必须显式固定语言(--lang / SHADOW_DEV_LANG),不得依赖 runner locale
- shadow-docs/knowledge/install-distribution.md
- 当前结论: shim/产物断言是平台分支的;跨平台命令要在设计上可移植
- 适用 scope: test/install.test.mjs
- norms/tdd-verification.md
- 当前结论: 先复现红(本地 SHADOW_DEV_LANG=en 已复现 ①;②的权威红源是 windows CI 矩阵)再修

## 决策

- **选型:** ①用例 20 的 `runTty(['--help'])` 显式传 `SHADOW_DEV_LANG=zh`(断言中文通道);②`makeTarball` 的 tar 改在 bash 内执行(`bash -c "tar ..."`),与安装器本体同源(install-cli.sh 里 tar 天然由 bash 解析,Windows 下命中 git 的 GNU tar),测试与实现走同一条解析路径。
- **对比方案:** ①改成"按 locale 断言中英文任一"被否——非确定性断言是反模式;②toUnix 产 Windows 原生路径给 bsdtar 被否——`-C` 参数与包内 POSIX 路径又需要 MSYS 形态,两头不讨好;根治是统一交给 bash 解析。
- **理由:** 测试向契约要确定性:语言显式声明、工具链与生产路径一致;均不动运行时代码。

## 任务

### Phase 1

- [x] task-1 — `test/cli.test.mjs` — 用例 20 的 plain(无 --json 的 TTY help)runTty 显式 `SHADOW_DEV_LANG=zh`;本地 `SHADOW_DEV_LANG=en` 强制下先跑红(已复现)后转绿
- [x] task-2 — `test/install.test.mjs` — `makeTarball` 的 `spawnSync('tar',...)` 改为 `spawnSync('bash', ['-c', ...])`;本地 8 用例保持绿

### Phase 2

- [ ] task-3 — PR 上 CI 9 格矩阵(ubuntu/macos/windows × 20/22/24)全绿作为权威验证;失败则在同一调查上下文内修到绿

## 结果

- 实际耗时: —
- 验证: —

## 知识评估

- **预期影响:** 更新
- **候选卡片:** shadow-docs/knowledge/cli-output-contract.md
- **理由:** 语言链约束需追加"测试必须显式固定语言,禁止依赖 runner locale"的执行约束;安装卡片验证方式补"tar 经 bash 解析"的可移植性约束
2 changes: 1 addition & 1 deletion shadow-docs/knowledge/cli-output-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ verified: 2026-09-18
- 缺必填参数报错时,stderr 必须从 `COMMANDS` 逐行列出缺失参数的目录描述(`flag * desc`,经 `human.argLine` 与 help 详情共用同一渲染),示例行占位符与目录 example 一致(变更名为 `<change-name>`);`HINTS` 只保留 code 级短句兜底,不得重复目录中的参数说明。
- `shadow-dev change list` 是变更名发现入口:stdout 契约 `data.changes:[{name,type,status,branch,archived}]`(按 name 排序)。默认只列 `shadow-docs/changes/` 活动目录(条目 `archived:false`);`--all` 合并归档条目(`archived:true`),`--archived` 只列归档,两参同传按超集 `--all`;解析失败目录静默跳过(与 indexer 同规则,双目录扫描复用 `brief(r, name, archived)` 第三参)。新增无值布尔 flag 必须在 `lib/args.mjs` 白名单登记,否则会被当作取值 flag 吞掉后随参数。
- 抑制 stdout 的分支必须仍然设置退出码;plan 的人用收场行必须透出 `planHash`(PTY 环境下的 agent 兜底)。`--json` 是跨环境逃生门,不得复用为其他语义。
- 错误 code 与 `data.nextStep` 模板永不本地化;语言链固定为 `--lang` > `SHADOW_DEV_LANG` > locale 探测 > 默认 zh,且只影响 stderr 文案。
- 错误 code 与 `data.nextStep` 模板永不本地化;语言链固定为 `--lang` > `SHADOW_DEV_LANG` > locale 探测 > 默认 zh,且只影响 stderr 文案。**测试对 stderr 文案的断言必须显式钉 `--lang` 或 `SHADOW_DEV_LANG`**——CI runner 的 locale(macos/windows 为 en)会经探测改变渲染语言,隐式依赖机器环境的断言在非中文 runner 上恒假。
- `nextStep` 为 additive 字段,写入发生在 planHash 持久化与计算之后,不得参与 hash 输入。
- plan 域可导出 `present(x)` 声明 stdout data 的**最小投影**(`cli.mjs planDomain` 应用,缺省恒等):重字段(全文正文、整段 brief/repo 回显)不进机器契约,语义输入仍在完整 planData 内参与 planHash。首个用户是 `issue plan`(摘要三件套 bodyBytes/bodySha256/sections,见 [issue 正文双通道结构契约](issue-body-contract.md));新域裁剪须有 token 依据并测试钉住。
- help 的 `data.help` 恒为字符串(概览默认唯一字段,最小面 <1KB);结构化目录 `data.commands` 经 `--full` opt-in;`help <命令>` 组详情恒定返回该组 `commands`。
Expand Down
2 changes: 2 additions & 0 deletions shadow-docs/knowledge/install-distribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ source:
- changes/20260917-fix-installer-url-taint/brief.md
- changes/20260917-feature-install-link-mode/brief.md
- changes/20260918-fix-installer-ci-cmd-assert/brief.md
- changes/20260918-fix-cross-platform-ci/brief.md
verified: 2026-09-18
---

Expand All @@ -26,6 +27,7 @@ verified: 2026-09-18
- `LINK` 与 `CURRENT` 语义互斥不复用:`install` 永不写/删 `LINK`,`unlink` 只删 `LINK`;status 的 `linked` 为 additive 字段。JSON 输出中的 Windows 路径必须转义反斜杠(`sed 's/\\/\\\\/g'`)。
- Windows 上 `LINK` 落盘存 `cygpath -w` 的 Windows 形态(`.cmd` shim 用 `set /p` 直读,POSIX 形态 node 打不开);shim `.cmd` 用 goto 两段分支而非括号块(括号块内 `%errorlevel%` 提前展开会吞掉真实退出码)。
- 信任边界分轨表述:release 轨 HTTPS + GitHub 仓库(无独立校验和),link 轨目标是用户显式给出的本机目录——引入 link 不扩大下载面,也不得把 link 目标喂给任何网络请求。
- 测试需要打包 tar 时**必须经 `bash -c 'tar ...'` 执行**(与安装器本体同一解析路径):node 直接 `spawnSync('tar')` 在 Windows runner 绑到 System32 bsdtar,读不了 `toUnix()` 产出的 MSYS `/tmp` 路径,导致全部安装器用例在 windows CI 恒红。

## 适用边界

Expand Down
3 changes: 2 additions & 1 deletion test/cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,8 @@ test('jsonEnabled routes the JSON surface by environment and explicit flags', as
})

test('TTY suppresses stdout JSON; --json and env restore it; planHash surfaces on stderr', () => {
const plain = runTty(['--help'])
// 中文通道断言必须显式钉语言:CI runner locale(mac/windows 为 en)经语言链探测会渲染英文,禁止隐式依赖机器环境
const plain = runTty(['--help'], process.cwd(), { SHADOW_DEV_LANG: 'zh' })
assert.equal(plain.status, 0, plain.stderr)
assert.equal(plain.stdout.trim(), '', 'interactive help must not print JSON')
assert.match(plain.stderr, /shadow-dev 命令一览/)
Expand Down
6 changes: 4 additions & 2 deletions test/install.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ function makeTarball(version) {
writeFileSync(join(app, 'package.json'), JSON.stringify({ name: 'shadow-dev-cli', version }))
writeFileSync(join(app, 'README.md'), '# fake artifact\n')
const tgz = join(dir, `shadow-dev-cli-v${version}.tar.gz`)
const r = spawnSync('tar', ['-czf', toUnix(tgz), '-C', toUnix(dir), 'shadow-dev-cli'], { encoding: 'utf8' })
// tar 必须在 bash 内执行:Windows runner 上 node 直 spawn 绑到 System32 bsdtar,读不了 MSYS 路径;
// bash -c 让测试与 install-cli.sh 本体走同一解析路径(Windows 命中 git 的 GNU tar)
const r = spawnSync('bash', ['-c', 'tar -czf "$0" -C "$1" shadow-dev-cli', toUnix(tgz), toUnix(dir)], { encoding: 'utf8' })
assert.equal(r.status, 0, r.stderr)
return tgz
}
Expand Down Expand Up @@ -100,7 +102,7 @@ test('artifact without cli.mjs fails selfcheck and leaves pointer untouched', sk
writeFileSync(join(dir, 'shadow-dev-cli', 'package.json'), '{"name":"shadow-dev-cli","version":"0.0.1"}')
writeFileSync(join(dir, 'shadow-dev-cli', 'README.md'), '# nope\n')
const tgz = join(dir, 'bad.tar.gz')
spawnSync('tar', ['-czf', toUnix(tgz), '-C', toUnix(dir), 'shadow-dev-cli'])
spawnSync('bash', ['-c', 'tar -czf "$0" -C "$1" shadow-dev-cli', toUnix(tgz), toUnix(dir)])
const r = run(['install', '--from', toUnix(tgz), '--prefix', toUnix(prefix), '--bin', toUnix(bin)])
assert.equal(r.status, 3)
assert.ok(!existsSync(join(prefix, 'CURRENT')))
Expand Down
Loading