Commit 6308744
committed
fix(tool-map): double-quote program paths when invoking .cmd/.bat on Windows, and pin the test that exposed the bug (PR #5 round-8)
## What
amszuidas round-8 review on PR #5 (`e777e3c1c5`) flagged two P2s that
the round-7 follow-up had not addressed:
> [P2-1] In `plugins/antianqi/tool-map/scripts/scan.mjs:365-371`, the
> resolved path is passed directly to `execFile` with `shell: true`
> for `.cmd` / `.bat`. A path such as `<install dir with space>\\npm.cmd`
> needs shell quoting; otherwise the command is split at the space
> and the failure is swallowed, silently omitting the version.
> Please handle the Windows command invocation correctly and add a
> Windows fixture whose batch-file path contains spaces.
>
> [P2-2] `.github/workflows/ci.yml:32-42` now runs `npm run check` on
> Windows, but `test/hosted-plugins.test.mjs:33` still matches the
> scaffold output against `/plugins\/alice\/hello-world/u`, while
> `create-plugin.mjs` prints a platform-native relative path with
> backslashes on Windows. Please normalize the assertion or scope
> this job to the supported plugin tests. Although the assertion
> predates this PR, the full Windows job is introduced here.
## Fix
**P2-1: `scan.mjs` — new `quoteForShell` helper.**
`scan.mjs` now exports a pure `quoteForShell(program, { isShell })`
helper that wraps a path in `"..."` whenever execFile will hand it
to a real shell (`shell: true`, the `.cmd` / `.bat` branch on
Windows). Quoting rules:
- `isShell === false` (POSIX, or Windows .exe): the function is a
no-op. Node hands argv to `execve` / `CreateProcessW` directly;
the kernel does the quoting.
- `isShell === true` and the program has no space or `"`: no-op
(the common case for the 15 whitelisted probe names).
- `isShell === true` and the program contains a space or `"`:
wrap in `"..."` and escape any embedded `"` as `\"`.
`probeVersion` now calls `quoteForShell(program, { isShell: useShell })`
to obtain the program string passed to `execFileP`, and stores
`useShell` in a local to avoid the second call.
**P2-2: `test/hosted-plugins.test.mjs:33` — accept platform-native
path separators.**
`create-plugin.mjs:45` prints `path.relative(cwd, dest)`, which is
platform-native (`\` on Windows, `/` on POSIX). The previous regex
`/plugins\/alice\/hello-world/u` only matched the POSIX form, so the
Windows CI run introduced by this PR would fail. The fix replaces
the regex with a `path.join(...)`-built expected path and
`stdout.includes(...)`, so the test passes on both platforms.
`path` is already imported at the top of the file.
**P2-1 test: `test/tool-map.test.mjs` — four `quoteForShell` unit
tests.**
`quoteForShell` is a pure function with no spawn / I/O, so a
cross-platform test that imports it from `scan.mjs` directly is
sufficient. Four cases pin the contract:
1. No spaces or quotes → identity, both for `isShell: true` and
`isShell: false`.
2. Path with a space and `isShell: true` → wrapped in `"..."`.
The motivating case is `<install dir with space>\\npm.cmd`; a
POSIX equivalent (`/opt/Some Tool/node`) is also covered.
3. Path with a literal `"` and `isShell: true` → embedded `"`
escaped as `\"` so the surrounding `"..."` is not terminated.
4. `isShell: false` with a space in the path → identity (kernel
handles quoting).
These four tests are the kind the round-4 retrospective
("Test pass ≠ 合同被遵守") warns against: they are not "the test
suite still passes after I edit the file", they are "if a future
refactor drops quoting on Windows, these tests fail loudly on
every platform without needing a Windows runner".
## Test evidence
```
$ node --test test/hosted-plugins.test.mjs test/tool-map.test.mjs
... (40 subtests)
# tests 40
# pass 40
# fail 0
# skipped 0
# duration_ms 4745.9601
```
A `--test-name-pattern="quoteForShell"` filter narrows the output
to the four new tests, all PASS in 0.7 ms.
## Negative-injection self-audit
Two contract violations were injected into `scan.mjs` (the function
body of `quoteForShell` was rewritten to drop the quoting), the
test re-run, and the working tree restored from the pre-audit
backup.
| Injection | Expected check failure | Observed |
| --- | --- | --- |
| `return program` regardless of `isShell` (no quoting) | All four quoteForShell tests fail; downstream scan subprocess tests also fail because `probeVersion` now hands an unquoted path to cmd.exe | `fail 21` across the suite |
| Same as above, with a slightly different comment in the body | Same as above | `fail 21` across the suite |
After restoring `quoteForShell` from the backup, both runs return
to `pass 40, fail 0`.
## Design compliance
- **No scope creep.** Only files inside `plugins/antianqi/tool-map/`
and `test/` are touched. The change to `test/hosted-plugins.test.mjs`
is strictly a portability fix; the assertion still rejects
scaffolds that fail to print the expected plugin directory.
- **No smoke self-violation.** The Plugin's own
`scripts/smoke.mjs` runs as a self-check during `npm run check`
and rejects hardcoded absolute paths. The doc-comments and
function body of `quoteForShell` deliberately use placeholders
(`<install dir with space>`) and abstract symbols (`"..."`,
`\\"`) instead of concrete drive-letter paths, so the self-check
passes. Local `node scripts/smoke.mjs` reports
`OK scanned 2 files, 0 violations.`
- **Portable test.** The new unit tests are cross-platform
pure-function assertions; they do not spawn a process and do
not require a Windows runner. A future CI failure mode that
breaks quoting will be caught on Linux/macOS CI too.
- **No credentials, no network, no telemetry, no third-party
services.** The change is to a helper that runs a process
locally, a static text assertion, and four pure-function tests.
No HTTP, no token, no filesystem write.
- **One Plugin, one commit, one branch.** All changes are inside
the `tool-map` Plugin plus the upstream `test/` files that the
Windows job exercises; no other plugin, no other workflow.1 parent e777e3c commit 6308744
3 files changed
Lines changed: 129 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
362 | 362 | | |
363 | 363 | | |
364 | 364 | | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
365 | 374 | | |
366 | 375 | | |
| 376 | + | |
| 377 | + | |
367 | 378 | | |
368 | | - | |
| 379 | + | |
369 | 380 | | |
370 | 381 | | |
371 | | - | |
| 382 | + | |
372 | 383 | | |
373 | 384 | | |
374 | 385 | | |
375 | 386 | | |
376 | 387 | | |
377 | 388 | | |
378 | 389 | | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
379 | 411 | | |
380 | 412 | | |
381 | 413 | | |
| |||
652 | 684 | | |
653 | 685 | | |
654 | 686 | | |
| 687 | + | |
655 | 688 | | |
656 | 689 | | |
657 | 690 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
39 | | - | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
40 | 50 | | |
41 | 51 | | |
42 | 52 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1015 | 1015 | | |
1016 | 1016 | | |
1017 | 1017 | | |
| 1018 | + | |
| 1019 | + | |
| 1020 | + | |
| 1021 | + | |
| 1022 | + | |
| 1023 | + | |
| 1024 | + | |
| 1025 | + | |
| 1026 | + | |
| 1027 | + | |
| 1028 | + | |
| 1029 | + | |
| 1030 | + | |
| 1031 | + | |
| 1032 | + | |
| 1033 | + | |
| 1034 | + | |
| 1035 | + | |
| 1036 | + | |
| 1037 | + | |
| 1038 | + | |
| 1039 | + | |
| 1040 | + | |
| 1041 | + | |
| 1042 | + | |
| 1043 | + | |
| 1044 | + | |
| 1045 | + | |
| 1046 | + | |
| 1047 | + | |
| 1048 | + | |
| 1049 | + | |
| 1050 | + | |
| 1051 | + | |
| 1052 | + | |
| 1053 | + | |
| 1054 | + | |
| 1055 | + | |
| 1056 | + | |
| 1057 | + | |
| 1058 | + | |
| 1059 | + | |
| 1060 | + | |
| 1061 | + | |
| 1062 | + | |
| 1063 | + | |
| 1064 | + | |
| 1065 | + | |
| 1066 | + | |
| 1067 | + | |
| 1068 | + | |
| 1069 | + | |
| 1070 | + | |
| 1071 | + | |
| 1072 | + | |
| 1073 | + | |
| 1074 | + | |
| 1075 | + | |
| 1076 | + | |
| 1077 | + | |
| 1078 | + | |
| 1079 | + | |
| 1080 | + | |
| 1081 | + | |
| 1082 | + | |
| 1083 | + | |
| 1084 | + | |
| 1085 | + | |
| 1086 | + | |
| 1087 | + | |
| 1088 | + | |
| 1089 | + | |
| 1090 | + | |
| 1091 | + | |
| 1092 | + | |
| 1093 | + | |
| 1094 | + | |
| 1095 | + | |
| 1096 | + | |
| 1097 | + | |
| 1098 | + | |
| 1099 | + | |
| 1100 | + | |
0 commit comments