diff --git a/docs/test/front-door.test.mjs b/docs/test/front-door.test.mjs index d1fe75f..06f7186 100644 --- a/docs/test/front-door.test.mjs +++ b/docs/test/front-door.test.mjs @@ -789,9 +789,28 @@ test("root package stays private and package_npm.sh packs a runnable simbroker b test("package_cli.sh writes a runnable CLI tarball without tests or the app", () => { const outputDir = fs.mkdtempSync(path.join(os.tmpdir(), "simbroker-package-cli-")); + const fakeBin = path.join(outputDir, "fake-bin"); + const npmInvocationSentinel = path.join(outputDir, "npm-command-was-executed"); + fs.mkdirSync(fakeBin); + fs.writeFileSync( + path.join(fakeBin, "npm"), + `#!/usr/bin/env bash +set -euo pipefail +: > "$SIMBROKER_TEST_NPM_SENTINEL" +printf '%s\n' 'SIMBROKER_PACKAGE_TEST_COMMAND_OUTPUT' +printf '%s\n' "$SIMBROKER_TEST_CHECKOUT_ROOT/artifacts/npm/should-not-appear.tgz" +`, + { mode: 0o755 }, + ); const result = spawnSync("bash", [path.join(repoRoot, "scripts/package_cli.sh"), "--output-dir", outputDir], { encoding: "utf8", cwd: repoRoot, + env: { + ...process.env, + PATH: `${fakeBin}${path.delimiter}${process.env.PATH ?? ""}`, + SIMBROKER_TEST_CHECKOUT_ROOT: repoRoot, + SIMBROKER_TEST_NPM_SENTINEL: npmInvocationSentinel, + }, }); assert.equal(result.status, 0, result.stderr); @@ -814,6 +833,24 @@ test("package_cli.sh writes a runnable CLI tarball without tests or the app", () assert.equal(fs.existsSync(path.join(root, "app")), false); assert.equal(fs.existsSync(path.join(root, "LICENSE")), true); const packagedReadme = fs.readFileSync(path.join(root, "README.md"), "utf8"); + assert.equal( + fs.existsSync(npmInvocationSentinel), + false, + "CLI README generation must not execute Markdown command examples", + ); + assert.equal( + packagedReadme.includes(repoRoot), + false, + "the generated README must not capture the exact packaging checkout root", + ); + assert.equal(packagedReadme.includes("SIMBROKER_PACKAGE_TEST_COMMAND_OUTPUT"), false); + assert.ok( + packagedReadme.includes( + "Formula/simbroker.rb. The packable npm CLI is packages/simbroker\n" + + "(`npm run package:npm`). The macOS operator app is separate.\n", + ), + "the generated README must preserve its literal Markdown command", + ); assert.ok(packagedReadme.includes(`./${cliArchiveDirectory}/bin/simbroker --help`)); assert.equal(packagedReadme.includes("`./bin/simbroker --help`"), false); }); diff --git a/scripts/package_cli.sh b/scripts/package_cli.sh index dda3c39..f58aefd 100755 --- a/scripts/package_cli.sh +++ b/scripts/package_cli.sh @@ -62,22 +62,23 @@ cp "$repo_root/package.json" "$bundle/package.json" cp "$repo_root/LICENSE" "$bundle/LICENSE" cp "$repo_root/CHANGELOG.md" "$bundle/CHANGELOG.md" -cat > "$bundle/README.md" < "$bundle/README.md" cat > "$bundle/bin/simbroker" <<'EOF' #!/usr/bin/env bash diff --git a/spec/build-and-test.md b/spec/build-and-test.md index e74bbd7..f9ddd7a 100644 --- a/spec/build-and-test.md +++ b/spec/build-and-test.md @@ -150,6 +150,18 @@ A first extracted implementation slice now exists: The current deterministic verification contract includes implementation tests plus spec integrity. +### CLI archive README contract + +| ID | Requirement | Verifier | +| --- | --- | --- | +| SB-PKG-CLI-001 | `scripts/package_cli.sh` treats static README Markdown as literal payload data and interpolates only the validated package version and derived archive directory. README command examples must never execute or contribute captured stdout while the archive is built. | `docs/test/front-door.test.mjs` `package_cli.sh writes a runnable CLI tarball without tests or the app` fake-command sentinel | +| SB-PKG-CLI-002 | The README extracted from the final CLI tarball contains the literal `` `npm run package:npm` `` example and contains neither the exact packaging checkout root nor command-fixture output. | The same extracted-archive test | + +These are final-payload checks because a source-only public-surface scan cannot +observe shell interpretation during archive generation. Any command execution, +captured output, or exact checkout-root leak fails `test:docs` before release +packaging. This contract does not prohibit generic temporary-directory guidance. + ### Release asset contract A complete tagged Alpha has exactly four custom GitHub Release assets: @@ -469,7 +481,7 @@ Add stronger profiles next for: - `./script/build_and_run.sh --telemetry` proves the app emits filterable `AppLifecycle` and `Refresh` unified logs during a live run, while `bash scripts/test_app.sh` exercises `Setup` and `Commands` events in focused app tests - the installer prints the installed CLI path, app path when an app was installed, env helper path, any current-shell PATH warning, PATH persist result, and the next command (`command -v simbroker` after persist, or `source ""` when persist is skipped) - `bash scripts/install_local.sh --cli-only` installs the CLI runtime without invoking `xcodegen` or `xcodebuild` and without requiring an app bundle -- `npm run package:cli` writes `artifacts/cli/simulator-broker--cli.tar.gz` plus a SHA-256 checksum and does not invoke XcodeGen or `xcodebuild` +- `npm run package:cli` writes `artifacts/cli/simulator-broker--cli.tar.gz` plus a SHA-256 checksum, does not invoke XcodeGen or `xcodebuild`, and its extracted README preserves literal Markdown without executing package commands or capturing the exact checkout root - `npm run package:cask-zip` writes `artifacts/distribution/Simulator-Broker-.zip` plus a SHA-256 checksum from a Developer ID-signed, stapled `Simulator Broker.app` using `ditto -c -k --keepParent`. It verifies the sealed signature with `codesign --verify --deep --strict`, requires `CFBundleIdentifier`/`Identifier` `dev.codex.simulator-broker-app`, runs `xcrun stapler validate` before writing the zip, and does not build, sign, notarize, staple, tag, or publish - `.github/workflows/ci.yml` runs `test:docs`, `test:broker-core`, and `test:harness-adoption` on `ubuntu-latest` (10 minutes) and `test:client` on