From b66ff9c1c7fa3ac83eabf23d4876b9410fe0bbd5 Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 3 Nov 2026 19:16:03 +0100 Subject: [PATCH 1/5] npm: Add real-binary pack smoke coverage Exercise the packed npm installer with the actual release binary while keeping the stub fixture as the default fast check. Share archive signing, serving, packing, and installation paths between modes, and assert the installed runtime mode and launcher resolution before running the CLI. Document the real-binary options and verification baseline for the follow-on NixOS VM integration test. Plan: npm-nixos-vm-integration-test (T01) Co-authored-by: SCE --- .../plans/npm-nixos-vm-integration-test.md | 122 ++++++++++++++++++ context/sce/cli-npm-distribution-contract.md | 7 +- npm/test/pack-smoke.sh | 76 ++++++++++- 3 files changed, 199 insertions(+), 6 deletions(-) create mode 100644 context/plans/npm-nixos-vm-integration-test.md diff --git a/context/plans/npm-nixos-vm-integration-test.md b/context/plans/npm-nixos-vm-integration-test.md new file mode 100644 index 000000000..7296b7c55 --- /dev/null +++ b/context/plans/npm-nixos-vm-integration-test.md @@ -0,0 +1,122 @@ +# Plan: npm-nixos-vm-integration-test + +## Change summary + +Extends the hardened npm installer work (PR #344, plan `harden-npm-installer`) with a production-representative NixOS VM integration test. `npm/test/pack-smoke.sh` gains a real-binary mode (`--binary `) that wraps the Nix-built `sce-release` executable in the canonical `sce-v-/{bin/sce,LICENSE,README.md}` archive layout, signs a manifest with a throwaway key swapped only into the staged package copy, serves it through the existing `fixture-release-server.mjs`, and runs the real `npm pack → npm install → postinstall → signed manifest verification → archive download → checksum verification → extraction → atomic install` path before executing the npm-generated `sce` launcher directly. The existing fixture (stub binary) mode stays the default and remains in `nix flake check` as `npm-pack-smoke`. + +A new `pkgs.testers.runNixOSTest` definition under `nix/tests/` boots a minimal NixOS VM, runs real-binary mode as a non-root user with an isolated HOME, npm cache, and prefix, and asserts launcher/binary modes, direct shebang execution, `sce --help`, and `sce version --format json` matching `.version`. Because NixOS has no FHS `/lib64` loader or `/usr/lib` glibc, successful execution proves the static-musl binary needs no unavailable dynamic libraries. The VM test is x86_64-linux only, is added to the expensive `ciChecks` aggregate (built by `release-validation` in `.github/workflows/pr-ci.yml`), and stays out of the fast `nix flake check` tier. The installer architecture, production public key, and verification code are unchanged. + +## Acceptance criteria + +How this plan is proven complete. Each criterion is observable and names the +check that proves it. `/validate` runs these checks; no task in the stack +performs final validation. + +- [ ] AC1: Fixture mode is unchanged in behavior and still runs in the fast tier: the `npm-pack-smoke` flake check passes with the stub binary and is still a member of `checks.`. + - Validate: `nix build .#checks.x86_64-linux.npm-pack-smoke --print-build-logs` passes; `nix eval .#checks.x86_64-linux --apply builtins.attrNames` lists `npm-pack-smoke` and does not list `npm-nixos-vm`. +- [ ] AC2: Real-binary mode installs the actual release binary through the full postinstall path and executes it: given `--binary /bin/sce`, the smoke packs the package, installs it with lifecycle scripts enabled and without `SCE_NPM_SKIP_DOWNLOAD`, and asserts `runtime/sce` mode `0755`, an executable packed `package/bin/sce.js`, a `.bin/sce` link resolving into the installed `@crocoder-dev/sce` package, exit 0 from `sce --help`, and `sce version --format json` `.version` equal to repo `.version`. + - Validate: `nix build .#sce-release && nix shell nixpkgs#nodejs nixpkgs#openssl -c bash npm/test/pack-smoke.sh --binary ./result/bin/sce` exits 0 and prints the reported version; `grep -n 'release-manifest-public-key.pem' npm/test/pack-smoke.sh` shows key substitution only in the staged copy and `git diff main -- npm/lib` is empty. +- [ ] AC3: The NixOS VM test boots a clean VM and, as a non-root user with isolated HOME/npm cache/prefix and no external network, installs the real packed package and runs the npm-generated `sce` launcher directly (shebang execution, not `node sce.js`), with `sce --help` succeeding and parsed `sce version --format json` `.version` equal to `.version`. + - Validate: `nix build .#npm-nixos-vm --print-build-logs` passes and the log shows the VM test steps, the installed modes, and the parsed version; inspect `nix/tests/npm-nixos-vm.nix` for a non-root user, no `SCE_NPM_SKIP_DOWNLOAD`, no manual copy/chmod into `runtime/`, and no network beyond loopback. +- [ ] AC4: `nix build .#ci-checks` on x86_64-linux builds the real `sce-release` once and includes the NixOS VM test alongside the release portability audit; other systems' `ci-checks` and macOS CI behavior are unchanged. + - Validate: `nix build .#ci-checks --print-build-logs` passes on x86_64-linux and `ls -l result` shows `sce-release`, `release-portability-audit`, and `npm-nixos-vm`; `nix derivation show .#ci-checks` (recursive) shows a single `sce-release` output path referenced by both the VM test and the audit; `nix eval .#packages.aarch64-darwin.ci-checks.drvPath` still evaluates. +- [ ] AC5: The `release-validation` CI job on `ubuntu-latest` can run the VM test with KVM acceleration and fails loudly (not skips) if KVM is unavailable. + - Validate: inspect `.github/workflows/pr-ci.yml` for KVM enablement and `kvm`/`nixos-test` system features on the Linux leg only; `nix build .#checks.x86_64-linux.workflow-actionlint` passes; a PR CI run shows the `release-validation (ubuntu-latest)` job executing the VM test. +- [ ] AC6: The tests fail for the right reason under injected faults: a packed launcher without its executable bit fails at the launcher-mode assertion; a non-executable/invalid `--binary` fails at the `sce --help` execution step; an installer that does not produce `runtime/sce` fails at the `runtime/sce` assertion. Faults are injected only in temporary worktrees or fixtures and the original state is restored. + - Validate: plan task evidence records each injected fault, the command run, and the exact failing `pack-smoke:` message; `git status` is clean afterward. +- [ ] AC7: Existing installer negative tests still reject invalid signatures, mismatched versions, bad checksums, malicious archive entries, and leave no partial `runtime/sce` on failure. + - Validate: `nix build .#checks.x86_64-linux.npm-bun-tests --print-build-logs` passes. +- [ ] AC8: Durable docs describe fixture vs VM testing, why the VM uses the real musl binary, what the VM guarantees, what remains macOS/ARM64-release-only, how to run it locally, and KVM/runner requirements. + - Validate: inspect `context/sce/cli-npm-distribution-contract.md` and `context/sce/flake-build-performance.md` for those points and matching command names. + +### Full validation + +Repository-wide checks `/validate` runs after the last task, regardless of +which criterion they map to. + +- `nix flake check` +- `nix build .#ci-checks --print-build-logs` + +### Context sync + +- `context/sce/cli-npm-distribution-contract.md` (packed-package smoke modes, NixOS VM test, verification baseline) +- `context/sce/flake-build-performance.md` (`ci-checks` membership and CI tier/KVM requirement) +- `context/context-map.md` (annotation for the npm contract entry, if its summary changes) + +## Task context synchronization lifecycle + +Persist this field in every plan; this is durable plan state, not chat state: + +- **Task context synchronization:** every task carries `pending | synced | blocked`. + A completed task must be `synced` before another task can start or the plan can + finish. +- For `blocked`, record **Blocker**, **Required action**, and **Retry condition** + beside the status. Never infer `synced` from conversation history; write every + lifecycle transition to the plan file. + +## Constraints and non-goals + +- **In scope:** `npm/test/pack-smoke.sh`, new `nix/tests/npm-nixos-vm.nix`, root `flake.nix` (VM test wiring, `ciChecks`, file sets), `.github/workflows/pr-ci.yml` (Linux `release-validation` KVM enablement), `context/sce/cli-npm-distribution-contract.md`, `context/sce/flake-build-performance.md`, `context/context-map.md`. +- **Out of scope:** changes to `npm/lib/*.js`, `npm/bin/sce.js`, the production public key, `fixture-release-server.mjs` behavior beyond what real-binary mode strictly needs, release packaging apps, macOS CI steps, aarch64 VM tests, publishing. +- **Constraints:** no Docker; no external network (all artifacts from Nix inputs or the loopback fixture server); reuse the existing signing, archive, and HTTPS server logic in `pack-smoke.sh` rather than copying it; no `SCE_NPM_SKIP_DOWNLOAD=1`; no manual copy or chmod into `runtime/`; do not weaken signature, identity, or checksum verification; `sceReleasePackage` referenced, never rebuilt with different arguments. +- **Non-goal:** a generic VM test framework, or repeating unit-level negative installer tests inside the VM. + +## Assumptions + +- The VM test is exposed as `packages.x86_64-linux.npm-nixos-vm` (built via `nix build .#npm-nixos-vm`) rather than `checks.x86_64-linux.npm-nixos-vm`, because every `checks.` member is built by `nix flake check` and the request requires the VM to stay out of the fast tier. This mirrors the existing opt-in `mutation-trace-quint-connect` package precedent. +- `nix/tests/` does not exist yet; the plan creates it with one file, `nix/tests/npm-nixos-vm.nix`, called from `flake.nix` with `sceReleasePackage` and the npm source as arguments. +- Real-binary mode reads the expected version from repo `.version` when available (passed by the caller), falling back to the staged `package.json` version; release-npm-package already enforces `.version` = `npm/package.json` = `cli/Cargo.toml`. +- Real-binary mode copies the given binary into the archive (preserving `0755`) with fixture LICENSE/README files, since the archive reader only writes `bin/sce` and the layout, not the file contents, is the contract under test. +- The npm smoke assertions added for `runtime/sce` mode `0755` and the `.bin/sce` link target apply in both modes, so the fast tier also guards them. +- GitHub-hosted `ubuntu-latest` runners expose `/dev/kvm` after the documented udev rule, and Nix needs `system-features = nixos-test benchmark big-parallel kvm` via the installer's `extra-conf`. + +## Task stack + +- [x] T01: `Add real-binary mode and install-layout assertions to pack-smoke.sh` (status:done) + - Task ID: T01 + - Scope: In — `npm/test/pack-smoke.sh`: `--binary ` (and an optional `--expected-version` / `--prefix` if needed by the VM), real-binary archive staging in the canonical layout, `sce --help` exit-status and `sce version --format json` `.version` assertions in real mode, `runtime/sce` mode `0755` and `.bin/sce` → installed package resolution assertions in both modes; usage text. Out — any change to `npm/lib`, `npm/bin`, the production key, or `fixture-release-server.mjs` signing/serving logic; flake wiring. + - Dependencies: none + - Done when: default invocation behaves as before plus the new layout assertions; `--binary` drives the same signing/server/install path with the real binary; no signing, archive, or server logic is duplicated; shellcheck clean. + - Verify: `nix build .#checks.x86_64-linux.npm-pack-smoke --print-build-logs`; `nix build .#sce-release && nix shell nixpkgs#nodejs nixpkgs#openssl -c bash npm/test/pack-smoke.sh --binary ./result/bin/sce`; `nix shell nixpkgs#shellcheck -c shellcheck npm/test/pack-smoke.sh`; in a temporary worktree inject (a) `chmod -x npm/bin/sce.js` with `--package-dir`, (b) `--binary` pointing at a non-executable/garbage file, (c) an installer that skips the rename, and record each failing message. + - Completed: 2026-11-03 + - Files changed: `npm/test/pack-smoke.sh` + - Result: Added `--binary ` (copies the given executable as `bin/sce` mode 0755 into the canonical `sce-v-/{bin/sce,LICENSE,README.md}` archive; all signing, manifest, pack, loopback-server, and install steps are shared with fixture mode), `--expected-version ` (default: staged `package.json` version), and `--prefix ` (persistent install prefix for the T02 VM assertions). Both modes now assert `runtime/sce` mode `755` (via `node fs.statSync`, portable to macOS) and that `node_modules/.bin/sce` realpath equals the installed `@crocoder-dev/sce/bin/sce.js`. Real mode asserts `sce --help` exit 0 and `sce version --format json` `.version` == expected version; fixture mode keeps its exact stub-output assertions. Usage text updated. No change to `npm/lib`, `npm/bin`, the production key, or `fixture-release-server.mjs`; key substitution stays confined to `$stage_dir/lib/release-manifest-public-key.pem`. + - Verify: + - `nix build .#checks.x86_64-linux.npm-pack-smoke --print-build-logs`: passed (log shows `installed runtime/sce mode 755`, `.bin/sce resolves to …/@crocoder-dev/sce/bin/sce.js`, `OK (sce --help, sce version)`). + - `nix build .#sce-release` + `nix shell nixpkgs#nodejs nixpkgs#openssl -c bash npm/test/pack-smoke.sh --binary /bin/sce`: passed, exit 0, `real binary reported version 0.4.0-pre-alpha-v12`; also passed with `--expected-version "$(cat .version)" --prefix ` (installed `runtime/sce` retained as `-rwxr-xr-x`). `--expected-version 9.9.9` fails with `pack-smoke: sce version reported '0.4.0-pre-alpha-v12'; expected '9.9.9'.` + - `nix shell nixpkgs#shellcheck -c shellcheck npm/test/pack-smoke.sh`: passed (no findings). + - Fault (a), scratch copy of `npm/` with `chmod -x bin/sce.js`, `--package-dir`: `pack-smoke: Packed package/bin/sce.js has mode -rw-r--r--; expected an executable mode (e.g. -rwxr-xr-x). Ensure npm/bin/sce.js is committed as Git mode 100755.` + - Fault (b), `--binary` pointing at a non-executable truncated-ELF garbage file: `pack-smoke: sce --help exited non-zero.` + - Fault (c), scratch copy of `npm/` with `installBinaryFile(...)` removed from `lib/install.js`, `--package-dir`: `pack-smoke: Installer did not produce …/node_modules/@crocoder-dev/sce/runtime/sce.` + - `git status` afterward shows only `npm/test/pack-smoke.sh` and this plan changed. + - Deviations: The first fault (b) attempt used random bytes. libuv's `execvp` falls back to `/bin/sh` on ENOEXEC, so the shell interpreted the bytes and created two empty stray files in the caller's cwd. Both were removed, and the fault was re-run from a scratch cwd with a deterministic truncated-ELF file. T02 should run garbage-binary faults from a throwaway cwd. + - Context impact: localized — npm packed-package smoke test surface (`context/sce/cli-npm-distribution-contract.md` smoke-mode description); full documentation is owned by T04. + - Context synchronization: synced + +- [ ] T02: `Add NixOS VM integration test for the real npm install` (status:todo) + - Task ID: T02 + - Scope: In — new `nix/tests/npm-nixos-vm.nix` using `pkgs.testers.runNixOSTest` (minimal node with `nodejs`, `openssl`, `gnutar`, `gzip`, `coreutils`, `jq`; non-root user; no networking beyond loopback; testScript running `pack-smoke.sh --binary ${sceReleasePackage}/bin/sce` as that user with isolated HOME/cache/prefix, then asserting modes, direct launcher execution, and parsed version); `flake.nix` exposure as `packages.x86_64-linux.npm-nixos-vm` only. Out — `ciChecks` membership and CI workflow changes (T03); aarch64/darwin. + - Dependencies: T01 + - Done when: `nix build .#npm-nixos-vm` boots the VM, completes the real install and direct CLI execution, and passes; the attribute does not evaluate on non-x86_64-linux systems; `nix flake check` does not build it. + - Verify: `nix build .#npm-nixos-vm --print-build-logs` (requires KVM locally); `nix eval .#checks.x86_64-linux --apply builtins.attrNames`; `nix flake check --no-build`; temporary-worktree fault: VM run with a broken `--binary` fails at the execution step. + - Context synchronization: pending + +- [ ] T03: `Run the NixOS VM test in ci-checks with KVM on Linux CI` (status:todo) + - Task ID: T03 + - Scope: In — `flake.nix` `ciChecks` links `npm-nixos-vm` on x86_64-linux next to `release-portability-audit`; `.github/workflows/pr-ci.yml` `release-validation` Linux leg enables `/dev/kvm` and passes `extra-conf` system features to the Nix installer, failing if KVM is missing. Out — macOS steps, `nix-ci` job, new runners unless the hosted runner is proven unable to run the test. + - Dependencies: T02 + - Done when: `nix build .#ci-checks` on x86_64-linux builds `sce-release` once and runs the VM test; Darwin `ci-checks` evaluation unchanged; workflow passes actionlint. + - Verify: `nix build .#ci-checks --print-build-logs`; `nix eval .#packages.aarch64-darwin.ci-checks.drvPath`; `nix build .#checks.x86_64-linux.workflow-actionlint`. + - Context synchronization: pending + +- [ ] T04: `Document fixture smoke versus NixOS VM npm integration testing` (status:todo) + - Task ID: T04 + - Scope: In — `context/sce/cli-npm-distribution-contract.md` (both smoke modes, VM guarantees, real musl rationale, macOS/ARM64 release-only coverage, local run command, KVM requirement, verification baseline); `context/sce/flake-build-performance.md` (`ci-checks` membership, KVM runner requirement); `context/context-map.md` annotation if needed. Out — README marketing changes, unrelated context edits. + - Dependencies: T03 + - Done when: docs state every point in AC8 concisely and their command names match the flake. + - Verify: inspect the two context files; `grep -n 'npm-nixos-vm' context/sce/*.md flake.nix`. + - Context synchronization: pending + +## Open questions + +- Hosted `ubuntu-latest` runners are expected to provide KVM after a udev rule, but this is only proven by an actual PR CI run. If the run shows KVM is unavailable or the VM is too slow for the 90-minute `release-validation` budget, choosing a dedicated or larger runner is a cost decision for the maintainers rather than something T03 can settle alone. diff --git a/context/sce/cli-npm-distribution-contract.md b/context/sce/cli-npm-distribution-contract.md index be2732b7c..c554c9cd1 100644 --- a/context/sce/cli-npm-distribution-contract.md +++ b/context/sce/cli-npm-distribution-contract.md @@ -58,15 +58,18 @@ This file captures the implemented npm distribution slice of the `sce` CLI insta ## Packed-package smoke test -- `npm/test/pack-smoke.sh` (with helper `npm/test/fixture-release-server.mjs`) verifies the real packed artifact end to end without network access: it stages a copy of the package (or `--package-dir `, or the `package/` contents of an already packed `--tarball `, whose launcher mode is asserted first), swaps a throwaway RSA manifest-signing public key into the staged copy only, builds a stub `sce` archive in the canonical `sce-v-/{bin/sce,LICENSE,README.md}` layout with a matching signed manifest, serves it over loopback HTTPS (self-signed certificate trusted via `NODE_EXTRA_CA_CERTS`, selected via `SCE_NPM_RELEASE_BASE_URL`), runs `npm pack`, fails unless the packed `package/bin/sce.js` entry is executable, installs into an isolated `--prefix` with an isolated npm cache/config, and asserts exact output from `sce --help` and `sce version`. +- `npm/test/pack-smoke.sh` (with helper `npm/test/fixture-release-server.mjs`) verifies the real packed artifact end to end without network access: it stages a copy of the package (or `--package-dir `, or the `package/` contents of an already packed `--tarball `, whose launcher mode is asserted first), swaps a throwaway RSA manifest-signing public key into the staged copy only, builds a stub `sce` archive in the canonical `sce-v-/{bin/sce,LICENSE,README.md}` layout with a matching signed manifest, serves it over loopback HTTPS (self-signed certificate trusted via `NODE_EXTRA_CA_CERTS`, selected via `SCE_NPM_RELEASE_BASE_URL`), runs `npm pack`, fails unless the packed `package/bin/sce.js` entry is executable, installs into an isolated prefix with an isolated npm cache/config, asserts the installed `runtime/sce` has mode `755` and that `node_modules/.bin/sce` resolves to the installed `@crocoder-dev/sce/bin/sce.js`, then runs the installed CLI. +- Two archive modes share every signing, manifest, pack, server, and install step. Fixture mode, the default, serves a stub `bin/sce` and asserts exact output from `sce --help` and `sce version`. Real-binary mode (`--binary `, e.g. the Nix-built `sce-release` binary) serves the given executable as `bin/sce` and asserts that `sce --help` exits 0 and that `.version` from `sce version --format json` equals the expected version. +- `--expected-version ` overrides the expected version (default: the staged `package.json` version). `--prefix ` installs into a caller-owned directory that is kept after the run; otherwise the prefix is temporary. - The fixture archive is produced by real GNU `tar`, so the smoke also exercises the in-repo archive reader against the release pipeline's archive format under Node. -- Only the host platform is exercised, with a stub binary; the production public key and manifest trust code are not modified. +- Only the host platform is exercised. The production public key and manifest trust code are not modified. - Root `flake.nix` runs the smoke in the build sandbox as `checks..npm-pack-smoke` (part of `nix flake check`; loopback HTTPS works inside the sandbox). The sandbox has no `/usr/bin/env`, so there the installed bin is run as `node ` after its `-x` check. Host runs, including the `release-npm-package` gate, exec the bin directly. ## Verification baseline - `nix develop -c sh -c 'cd npm && bun test ./test/*.test.js'` - `nix shell nixpkgs#nodejs nixpkgs#openssl -c bash npm/test/pack-smoke.sh` +- `nix build .#sce-release && nix shell nixpkgs#nodejs nixpkgs#openssl -c bash npm/test/pack-smoke.sh --binary ./result/bin/sce` - `nix build .#checks..npm-pack-smoke --print-build-logs` - `nix develop -c sh -c 'cd npm && SCE_NPM_SKIP_DOWNLOAD=1 bun ./lib/install.js'` - `nix run .#release-manifest -- --version --artifacts-dir --out-dir --signing-key-file ` diff --git a/npm/test/pack-smoke.sh b/npm/test/pack-smoke.sh index 573155b1b..9d98c5a3c 100755 --- a/npm/test/pack-smoke.sh +++ b/npm/test/pack-smoke.sh @@ -5,18 +5,36 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PACKAGE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" LAUNCHER_ENTRY="package/bin/sce.js" SERVER_START_TIMEOUT_SECONDS=10 +INSTALLED_PACKAGE_DIR="node_modules/@crocoder-dev/sce" +EXPECTED_RUNTIME_MODE="755" EXPECTED_HELP_OUTPUT="sce fixture help" SOURCE_TARBALL="" +SOURCE_BINARY="" +EXPECTED_VERSION="" +PREFIX_DIR="" usage() { cat <<'EOF' Usage: pack-smoke.sh [--package-dir | --tarball ] + [--binary ] [--expected-version ] + [--prefix ] Packs the npm launcher package, installs it into an isolated prefix against a loopback fixture release, and verifies `sce --help` and `sce version`. With --tarball, the launcher mode is asserted on the given packed tarball and its package contents are used instead of a package directory. + +With --binary, the given executable (e.g. the Nix-built sce-release binary) is +served as bin/sce in the release archive instead of the fixture stub. The +installed CLI must exit 0 for `sce --help` and report the expected version via +`sce version --format json`. + +--expected-version overrides the version asserted from `sce version` +(default: the staged package.json version). + +--prefix installs into the given directory and keeps it after the run instead +of using a temporary prefix. EOF } @@ -38,6 +56,23 @@ while [[ $# -gt 0 ]]; do SOURCE_TARBALL="$(cd "$(dirname "$2")" && pwd)/$(basename "$2")" shift 2 ;; + --binary) + [[ -n "${2:-}" ]] || fail "Missing value for --binary." + [[ -f "$2" ]] || fail "Binary $2 does not exist." + SOURCE_BINARY="$(cd "$(dirname "$2")" && pwd)/$(basename "$2")" + shift 2 + ;; + --expected-version) + [[ -n "${2:-}" ]] || fail "Missing value for --expected-version." + EXPECTED_VERSION="$2" + shift 2 + ;; + --prefix) + [[ -n "${2:-}" ]] || fail "Missing value for --prefix." + mkdir -p "$2" + PREFIX_DIR="$(cd "$2" && pwd)" + shift 2 + ;; --help | -h) usage exit 0 @@ -72,7 +107,7 @@ trap cleanup EXIT stage_dir="$work_dir/stage" release_dir="$work_dir/release" pack_dir="$work_dir/pack" -prefix_dir="$work_dir/prefix" +prefix_dir="${PREFIX_DIR:-$work_dir/prefix}" keys_dir="$work_dir/keys" mkdir -p "$stage_dir" "$release_dir" "$pack_dir" "$prefix_dir" "$keys_dir" @@ -110,6 +145,8 @@ target_triple="$(node -p "JSON.parse(process.argv[1]).targetTriple" "$platform_j target_os="$(node -p "JSON.parse(process.argv[1]).os" "$platform_json")" target_arch="$(node -p "JSON.parse(process.argv[1]).arch" "$platform_json")" +expected_version="${EXPECTED_VERSION:-$version}" + printf 'pack-smoke: version %s, target %s\n' "$version" "$target_triple" openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "$keys_dir/signing.pem" 2>/dev/null @@ -125,7 +162,11 @@ archive_name="${archive_root}.tar.gz" manifest_name="sce-v${version}-release-manifest.json" archive_src="$work_dir/archive-src" mkdir -p "$archive_src/$archive_root/bin" -cat >"$archive_src/$archive_root/bin/sce" <"$archive_src/$archive_root/bin/sce" <&2; exit 64 ;; esac EOF +fi chmod 0755 "$archive_src/$archive_root/bin/sce" printf 'fixture license\n' >"$archive_src/$archive_root/LICENSE" printf 'fixture readme\n' >"$archive_src/$archive_root/README.md" @@ -196,8 +238,21 @@ SCE_NPM_RELEASE_BASE_URL="https://127.0.0.1:${port}" \ "$tarball" || fail "npm install of the packed tarball failed." +installed_dir="$prefix_dir/$INSTALLED_PACKAGE_DIR" +runtime_bin="$installed_dir/runtime/sce" +[[ -f "$runtime_bin" ]] || fail "Installer did not produce $runtime_bin." +runtime_mode="$(node -p "(require('fs').statSync(process.argv[1]).mode & 0o777).toString(8)" "$runtime_bin")" +[[ "$runtime_mode" == "$EXPECTED_RUNTIME_MODE" ]] || + fail "Installed $runtime_bin has mode $runtime_mode; expected $EXPECTED_RUNTIME_MODE." +printf 'pack-smoke: installed runtime/sce mode %s\n' "$runtime_mode" + sce_bin="$prefix_dir/node_modules/.bin/sce" [[ -x "$sce_bin" ]] || fail "Installed sce bin $sce_bin is not executable." +sce_bin_target="$(realpath "$sce_bin")" +expected_bin_target="$(realpath "$installed_dir/bin/sce.js")" +[[ "$sce_bin_target" == "$expected_bin_target" ]] || + fail "Installed sce bin resolves to $sce_bin_target; expected $expected_bin_target." +printf 'pack-smoke: .bin/sce resolves to %s\n' "$sce_bin_target" sce_cmd=("$sce_bin") if [[ ! -x /usr/bin/env ]]; then @@ -206,11 +261,24 @@ if [[ ! -x /usr/bin/env ]]; then fi help_output="$("${sce_cmd[@]}" --help)" || fail "sce --help exited non-zero." + +if [[ -n "$SOURCE_BINARY" ]]; then + version_json="$("${sce_cmd[@]}" version --format json)" || + fail "sce version --format json exited non-zero." + reported_version="$(node -p "JSON.parse(process.argv[1]).version" "$version_json")" || + fail "sce version --format json printed invalid JSON: $version_json" + [[ "$reported_version" == "$expected_version" ]] || + fail "sce version reported '$reported_version'; expected '$expected_version'." + printf 'pack-smoke: real binary reported version %s\n' "$reported_version" + printf 'pack-smoke: OK (real binary: sce --help, sce version --format json)\n' + exit 0 +fi + [[ "$help_output" == "$EXPECTED_HELP_OUTPUT" ]] || fail "sce --help printed '$help_output'; expected '$EXPECTED_HELP_OUTPUT'." version_output="$("${sce_cmd[@]}" version)" || fail "sce version exited non-zero." -[[ "$version_output" == "sce $version" ]] || - fail "sce version printed '$version_output'; expected 'sce $version'." +[[ "$version_output" == "sce $expected_version" ]] || + fail "sce version printed '$version_output'; expected 'sce $expected_version'." printf 'pack-smoke: OK (sce --help, sce version)\n' From 9d3b045a93baea8cd79ee042b50473e9186f2914 Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 3 Nov 2026 19:41:57 +0100 Subject: [PATCH 2/5] tests: Add NixOS VM test for real npm install Exercise the packed npm package's postinstall flow in a minimal loopback-only NixOS VM as a non-root user with the release binary. Expose the test only as the opt-in x86_64-linux `npm-nixos-vm` package so normal flake checks do not boot a VM, and verify executable modes, launcher resolution, direct shebang execution, and reported version. Plan: npm-nixos-vm-integration-test (T02) Co-authored-by: SCE --- context/context-map.md | 2 +- .../plans/npm-nixos-vm-integration-test.md | 14 ++- context/sce/cli-npm-distribution-contract.md | 2 + context/sce/flake-build-performance.md | 3 + flake.nix | 8 ++ nix/tests/npm-nixos-vm.nix | 98 +++++++++++++++++++ 6 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 nix/tests/npm-nixos-vm.nix diff --git a/context/context-map.md b/context/context-map.md index 64b24fa62..758b74e35 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -113,7 +113,7 @@ Feature/domain context: - `context/sce/opencode-agent-trace-plugin-runtime.md` (current OpenCode agent-trace plugin runtime behavior, including captured `message.updated` handoff with `summary.diffs` branching: when diffs exist sends one `-patch` mixed batch containing a synthetic parent message plus per-diff `message.part` patch items, when no diffs sends the original `message.updated` payload; in-memory dedup `Set` keyed by `"${sessionID}:${messageID}"`; captured `message.part.updated` handoff to `sce hooks conversation-trace` for `text`/`reasoning` parts with non-empty text plus completed `question` tool parts emitted as `part_type: "question"` with JSON-stringified `{ question, answer }[]`; existing user-message diff extraction for `{ sessionID, diff, time, model_id }`; session-scoped OpenCode client version capture from `session.created`/`session.updated`; and CLI handoff to `sce hooks diff-trace` over STDIN JSON with required `tool_name="opencode"` plus required nullable `tool_version`; Rust hook parsing and AgentTraceDb insertion persist `oc_`-prefixed session IDs plus required payload fields including `model_id`) - `context/sce/cli-first-install-channels-contract.md` (current Nix/Cargo/npm/source-built Flatpak channel contract, release authority and workflow topology, Nix-owned Flatpak manifest/cargo-source generation and validation, reduced Flatpak app surface, and host-git bridge decision) - `context/sce/cli-release-artifact-contract.md` (shared `sce` binary release artifact naming, checksum/manifest outputs, pre-archive staged-binary preparation including macOS `libiconv` install-name sanitization/ad-hoc re-signing, native portability audit app/check for forbidden `/nix/store/` runtime references, GitHub Releases as the canonical artifact publication surface, manual dispatch `prerelease` flag behavior, the current three-target Linux/macOS release workflow topology including pre-upload extracted-archive smoke/audit validation in each native lane, implemented Flatpak source-manifest and source-built `.flatpak` bundle package assets uploaded by `.github/workflows/release-sce.yml`, and Flatpak's explicit source-built non-binary exception) -- `context/sce/cli-npm-distribution-contract.md` (implemented `sce` npm launcher package, release-manifest/checksum-verified native binary install flow with manifest identity checks, bounded validating archive extraction, and atomic `runtime/sce` install, the supported darwin/arm64 plus linux x64+arm64 npm platform matrix, the executable launcher-mode requirement and offline packed-package smoke test (`npm/test/pack-smoke.sh`, flake check `npm-pack-smoke`, and the `release-npm-package` post-pack gate), the prerequisite that npm-consumed native archives are portable and free of forbidden `/nix/store/` runtime references before upload, and dedicated `.github/workflows/publish-npm.yml` downstream npm publish-stage contract including prerelease publication via npm `next` dist-tag instead of `latest`) +- `context/sce/cli-npm-distribution-contract.md` (implemented `sce` npm launcher package, release-manifest/checksum-verified native binary install flow with manifest identity checks, bounded validating archive extraction, and atomic `runtime/sce` install, the supported darwin/arm64 plus linux x64+arm64 npm platform matrix, the executable launcher-mode requirement and offline packed-package smoke test (`npm/test/pack-smoke.sh`, flake check `npm-pack-smoke`, and the `release-npm-package` post-pack gate, plus the opt-in x86_64-linux NixOS VM real-install test `.#npm-nixos-vm`), the prerequisite that npm-consumed native archives are portable and free of forbidden `/nix/store/` runtime references before upload, and dedicated `.github/workflows/publish-npm.yml` downstream npm publish-stage contract including prerelease publication via npm `next` dist-tag instead of `latest`) - `context/sce/cli-cargo-distribution-contract.md` (implemented `sce` Cargo publication posture plus supported crates.io and wrapper-driven local-checkout install guidance, explicit unsupported `cargo install --git` boundary, dedicated crates.io publish workflow with semver prerelease guard, canonical generated-input declaration/producer ownership consumed by the Cargo wrapper, generated-output check, package-fallback preparation, and Nix pre-Cargo derivation, plus the deterministic combined checksummed fallback used for Pkl-free downstream crate builds) - `context/sce/flake-build-performance.md` (current native-vs-release package split, release-only commit embedding, shared-producer-backed pre-Cargo generated-input derivation consumed by native/release/test/Clippy but excluded from dependency/format derivations, slim default/database devShell contract, normal-vs-expensive CI tiers, durable before/after benchmark summary, validation evidence, and remaining cold-build trade-offs) - `context/sce/config-schema-publication.md` (current embedded-only publication contract for the generated SCE config JSON Schema and the canonical SCE web application URL used in config declarations) diff --git a/context/plans/npm-nixos-vm-integration-test.md b/context/plans/npm-nixos-vm-integration-test.md index 7296b7c55..6ecd90842 100644 --- a/context/plans/npm-nixos-vm-integration-test.md +++ b/context/plans/npm-nixos-vm-integration-test.md @@ -93,13 +93,23 @@ Persist this field in every plan; this is durable plan state, not chat state: - Context impact: localized — npm packed-package smoke test surface (`context/sce/cli-npm-distribution-contract.md` smoke-mode description); full documentation is owned by T04. - Context synchronization: synced -- [ ] T02: `Add NixOS VM integration test for the real npm install` (status:todo) +- [x] T02: `Add NixOS VM integration test for the real npm install` (status:done) - Task ID: T02 - Scope: In — new `nix/tests/npm-nixos-vm.nix` using `pkgs.testers.runNixOSTest` (minimal node with `nodejs`, `openssl`, `gnutar`, `gzip`, `coreutils`, `jq`; non-root user; no networking beyond loopback; testScript running `pack-smoke.sh --binary ${sceReleasePackage}/bin/sce` as that user with isolated HOME/cache/prefix, then asserting modes, direct launcher execution, and parsed version); `flake.nix` exposure as `packages.x86_64-linux.npm-nixos-vm` only. Out — `ciChecks` membership and CI workflow changes (T03); aarch64/darwin. - Dependencies: T01 - Done when: `nix build .#npm-nixos-vm` boots the VM, completes the real install and direct CLI execution, and passes; the attribute does not evaluate on non-x86_64-linux systems; `nix flake check` does not build it. - Verify: `nix build .#npm-nixos-vm --print-build-logs` (requires KVM locally); `nix eval .#checks.x86_64-linux --apply builtins.attrNames`; `nix flake check --no-build`; temporary-worktree fault: VM run with a broken `--binary` fails at the execution step. - - Context synchronization: pending + - Completed: 2026-11-03 + - Files changed: `nix/tests/npm-nixos-vm.nix` (new), `flake.nix` + - Result: Added `nix/tests/npm-nixos-vm.nix`, a `pkgs.testers.runNixOSTest` with one node. The node has `bash`, `coreutils`, `gnutar`, `gzip`, `jq`, `nodejs`, and `openssl`, a non-root `sce-tester` user, no DHCP, no interfaces, and `virtualisation.vlans = [ ]` (loopback only). The testScript checks that the user's uid is not 0, then copies `npmSrc/npm` into the user's home. As that user, with HOME, TMPDIR, `npm_config_cache`, and `npm_config_userconfig` isolated, it runs `pack-smoke.sh --binary ${sceReleasePackage}/bin/sce --expected-version <.version> --prefix ~/prefix`. It then asserts: `runtime/sce` mode `755`, an executable `.bin/sce` that resolves to the installed `bin/sce.js`, a direct shebang run of `.bin/sce --help`, and `sce version --format json | jq -er .version` equal to `.version`. There is no `SCE_NPM_SKIP_DOWNLOAD` and no manual copy or chmod into `runtime/`. `flake.nix` imports the test with `pkgs`, `npmSrc`, `sceReleasePackage`, and `expectedVersion = version`, and exposes it only as `packages.x86_64-linux.npm-nixos-vm` via `optionalAttrs (system == "x86_64-linux")`. On user request, the explanatory comments added in the new file and in `flake.nix` were removed. + - Verify: + - `nix build .#npm-nixos-vm --print-build-logs`: passed. This host has no `/dev/kvm`, so QEMU logged `failed to initialize kvm` and ran under TCG; the test script finished in 115s. The log shows `pack-smoke: installed runtime/sce mode 755`, `.bin/sce resolves to /home/sce-tester/prefix/node_modules/@crocoder-dev/sce/bin/sce.js`, `real binary reported version 0.4.0-pre-alpha-v12`, `runtime/sce mode: 755`, `launcher mode: 755`, and `parsed version: 0.4.0-pre-alpha-v12`. pack-smoke did not use its `node` fallback: `/usr/bin/env` exists, so the shebang ran directly. Removing the comments left the drv path unchanged (`d09iqhh4…-vm-test-run-npm-nixos-vm.drv`). + - `nix eval .#checks.x86_64-linux --apply builtins.attrNames`: lists `npm-pack-smoke` and does not list `npm-nixos-vm`. `nix eval .#packages.x86_64-darwin --apply 'p: builtins.hasAttr "npm-nixos-vm" p'` returns `false`, and the `aarch64-darwin` package names do not include it. + - `nix flake check --no-build`: passed (`all checks passed!`). + - Fault: a scratch copy of `npm/` and `nix/tests/` was imported with `sceReleasePackage` replaced by a derivation whose `bin/sce` is a truncated 7-byte ELF header. The VM failed at the install subtest with `pack-smoke: sce --help exited non-zero.` (test `install the packed package through postinstall with the real binary` failed). The repo worktree was not touched. + - Deviations: The new file is registered with `git add -N` (intent-to-add) so the flake can see it. The first fault-injection attempt used `builtins.getFlake` on the working directory and stalled copying the tree; it was stopped and re-run from a minimal scratch copy. + - Context impact: localized. Adds the opt-in `.#npm-nixos-vm` package (x86_64-linux), relevant to `context/sce/cli-npm-distribution-contract.md` and `context/sce/flake-build-performance.md`. Full documentation is owned by T04, and `ciChecks` membership by T03. + - Context synchronization: synced - [ ] T03: `Run the NixOS VM test in ci-checks with KVM on Linux CI` (status:todo) - Task ID: T03 diff --git a/context/sce/cli-npm-distribution-contract.md b/context/sce/cli-npm-distribution-contract.md index c554c9cd1..ae1a394f0 100644 --- a/context/sce/cli-npm-distribution-contract.md +++ b/context/sce/cli-npm-distribution-contract.md @@ -64,6 +64,7 @@ This file captures the implemented npm distribution slice of the `sce` CLI insta - The fixture archive is produced by real GNU `tar`, so the smoke also exercises the in-repo archive reader against the release pipeline's archive format under Node. - Only the host platform is exercised. The production public key and manifest trust code are not modified. - Root `flake.nix` runs the smoke in the build sandbox as `checks..npm-pack-smoke` (part of `nix flake check`; loopback HTTPS works inside the sandbox). The sandbox has no `/usr/bin/env`, so there the installed bin is run as `node ` after its `-x` check. Host runs, including the `release-npm-package` gate, exec the bin directly. +- `nix/tests/npm-nixos-vm.nix` (exposed only as `packages.x86_64-linux.npm-nixos-vm`, never a flake check) runs real-binary mode inside a minimal NixOS VM (`pkgs.testers.runNixOSTest`, loopback-only networking) as a non-root user with an isolated HOME, npm cache, and prefix, against `${sceReleasePackage}/bin/sce` and the repo `.version`. It then asserts `runtime/sce` mode `755`, an executable `.bin/sce` resolving to the installed `bin/sce.js`, direct shebang execution of `sce --help`, and the `jq`-parsed `sce version --format json` `.version`. NixOS has no FHS loader or `/usr/lib` glibc, so a passing run shows the static-musl binary needs no unavailable dynamic libraries. ## Verification baseline @@ -71,6 +72,7 @@ This file captures the implemented npm distribution slice of the `sce` CLI insta - `nix shell nixpkgs#nodejs nixpkgs#openssl -c bash npm/test/pack-smoke.sh` - `nix build .#sce-release && nix shell nixpkgs#nodejs nixpkgs#openssl -c bash npm/test/pack-smoke.sh --binary ./result/bin/sce` - `nix build .#checks..npm-pack-smoke --print-build-logs` +- `nix build .#npm-nixos-vm --print-build-logs` (x86_64-linux; uses KVM when `/dev/kvm` exists, otherwise slower QEMU emulation) - `nix develop -c sh -c 'cd npm && SCE_NPM_SKIP_DOWNLOAD=1 bun ./lib/install.js'` - `nix run .#release-manifest -- --version --artifacts-dir --out-dir --signing-key-file ` - `nix run .#release-npm-package -- --version --out-dir ` diff --git a/context/sce/flake-build-performance.md b/context/sce/flake-build-performance.md index 7af52bf02..1298d5596 100644 --- a/context/sce/flake-build-performance.md +++ b/context/sce/flake-build-performance.md @@ -10,6 +10,9 @@ The root flake separates ordinary development from release-only work: - `packages.ci-checks` is the explicit expensive tier. It forces the release package and, on Linux, audits the real release binary for forbidden `/nix/store/` runtime references. +- `packages.x86_64-linux.npm-nixos-vm` is an opt-in NixOS VM test of the real + packed npm install against the release binary; it is not a flake check, so + `nix flake check` never boots a VM. - `nix flake check` runs the normal test/lint/format/parity/static checks without building `.#sce-release`. - `devShells.default` provides development tools without `scePackage` or the diff --git a/flake.nix b/flake.nix index 431df1b53..5c2facec7 100644 --- a/flake.nix +++ b/flake.nix @@ -1701,6 +1701,11 @@ ''} ''; + npmNixosVmTest = import ./nix/tests/npm-nixos-vm.nix { + inherit pkgs npmSrc sceReleasePackage; + expectedVersion = version; + }; + # The dedicated MBT runner is opt-in as a package, not a flake check: # cli-tests already executes the same mutation_trace::mbt test module. # The Quint workflow tests the spec separately, without rebuilding Rust MBT. @@ -1745,6 +1750,9 @@ mutation-trace-quint-connect = mutationTraceQuintConnectCheck; quint-language-server = quintLanguageServerPackage; default = scePackage; + } + // pkgs.lib.optionalAttrs (system == "x86_64-linux") { + npm-nixos-vm = npmNixosVmTest; }; checks = diff --git a/nix/tests/npm-nixos-vm.nix b/nix/tests/npm-nixos-vm.nix new file mode 100644 index 000000000..193fd7096 --- /dev/null +++ b/nix/tests/npm-nixos-vm.nix @@ -0,0 +1,98 @@ +{ + pkgs, + npmSrc, + sceReleasePackage, + expectedVersion, +}: + +let + testUser = "sce-tester"; + homeDir = "/home/${testUser}"; + prefixDir = "${homeDir}/prefix"; + installedDir = "${prefixDir}/node_modules/@crocoder-dev/sce"; + launcher = "${prefixDir}/node_modules/.bin/sce"; + + asUser = + command: + builtins.toJSON '' + su - ${testUser} -s ${pkgs.bash}/bin/bash -c ${pkgs.lib.escapeShellArg '' + set -euo pipefail + export HOME=${homeDir} + export TMPDIR=${homeDir}/tmp + export npm_config_cache=${homeDir}/npm-cache + export npm_config_userconfig=${homeDir}/npmrc + ${command} + ''} + ''; +in +pkgs.testers.runNixOSTest { + name = "npm-nixos-vm"; + + nodes.machine = { + networking.useDHCP = false; + networking.interfaces = { }; + virtualisation.vlans = [ ]; + + users.users.${testUser} = { + isNormalUser = true; + home = homeDir; + }; + + environment.systemPackages = [ + pkgs.bash + pkgs.coreutils + pkgs.gnutar + pkgs.gzip + pkgs.jq + pkgs.nodejs + pkgs.openssl + ]; + + virtualisation.memorySize = 2048; + }; + + testScript = '' + machine.wait_for_unit("multi-user.target") + + with subtest("test user is non-root"): + uid = machine.succeed("id -u ${testUser}").strip() + assert uid != "0", f"test user must not be root, got uid {uid}" + + with subtest("stage npm package source"): + machine.succeed(${asUser '' + mkdir -p "$TMPDIR" "$npm_config_cache" + : >"$npm_config_userconfig" + cp -R ${npmSrc}/npm "$HOME/npm" + chmod -R u+w "$HOME/npm" + ''}) + + with subtest("install the packed package through postinstall with the real binary"): + print(machine.succeed(${asUser '' + bash "$HOME/npm/test/pack-smoke.sh" \ + --binary ${sceReleasePackage}/bin/sce \ + --expected-version ${pkgs.lib.escapeShellArg expectedVersion} \ + --prefix ${prefixDir} 2>&1 + ''})) + + with subtest("installed modes"): + runtime_mode = machine.succeed("stat -c %a ${installedDir}/runtime/sce").strip() + print(f"runtime/sce mode: {runtime_mode}") + assert runtime_mode == "755", f"runtime/sce mode {runtime_mode}, expected 755" + launcher_mode = machine.succeed("stat -L -c %a ${launcher}").strip() + print(f"launcher mode: {launcher_mode}") + assert int(launcher_mode, 8) & 0o111, f"launcher mode {launcher_mode} is not executable" + launcher_target = machine.succeed("realpath ${launcher}").strip() + expected_target = machine.succeed("realpath ${installedDir}/bin/sce.js").strip() + assert launcher_target == expected_target, f"launcher resolves to {launcher_target}, expected {expected_target}" + + with subtest("direct launcher execution"): + machine.succeed(${asUser "${launcher} --help >/dev/null"}) + version_json = machine.succeed(${asUser "${launcher} version --format json"}) + print(f"sce version --format json: {version_json.strip()}") + reported_version = machine.succeed( + ${asUser "${launcher} version --format json | jq -er .version"} + ).strip() + print(f"parsed version: {reported_version}") + assert reported_version == ${builtins.toJSON expectedVersion}, f"sce version reported {reported_version!r}" + ''; +} From 3298cbdf5352f5c83ed6f9e303816b8b4c46d92b Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 3 Nov 2026 19:49:25 +0100 Subject: [PATCH 3/5] CI: Run npm NixOS VM test in release validation Exercise the real static release binary's npm package in the expensive ci-checks tier on x86_64-linux while keeping normal flake checks unchanged. Link the NixOS VM test into ciChecks and configure Ubuntu PR runners with KVM and the required Nix system features; document the resulting CI contract. Plan: npm-nixos-vm-integration-test (T03) Co-authored-by: SCE --- .github/workflows/pr-ci.yml | 13 +++++++++++++ context/architecture.md | 2 +- context/glossary.md | 2 +- context/overview.md | 4 ++-- context/patterns.md | 2 +- context/plans/npm-nixos-vm-integration-test.md | 13 +++++++++++-- context/sce/cli-npm-distribution-contract.md | 2 +- context/sce/flake-build-performance.md | 12 ++++++++---- flake.nix | 7 ++----- 9 files changed, 40 insertions(+), 17 deletions(-) diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index 8996b3cd4..f8d2200a8 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -76,8 +76,21 @@ jobs: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Enable KVM + if: matrix.os == 'ubuntu-latest' + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + if ! test -r /dev/kvm || ! test -w /dev/kvm; then + echo '::error::/dev/kvm is not available; the NixOS VM test requires KVM.' + exit 1 + fi + - name: Install Nix uses: DeterminateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 # v22 + with: + extra-conf: ${{ matrix.os == 'ubuntu-latest' && 'system-features = nixos-test benchmark big-parallel kvm' || '' }} - name: Enable Magic Nix Cache uses: DeterminateSystems/magic-nix-cache-action@908b263ff629f4cc17666315b7fd3ec127c6244d # v14 diff --git a/context/architecture.md b/context/architecture.md index 97ac14da6..5672c38a8 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -89,7 +89,7 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `flake.nix` applies `rust-overlay` (`oxalica/rust-overlay`) to nixpkgs, pins `rust-bin.stable.1.95.0.default` with `rustfmt` + `clippy`, and reads package/check version from repo-root `.version`. Its `cliGeneratedInput` derivation invokes the shared generated-input producer from a declarative source containing the producer and canonical inputs, then publishes the producer-validated handoff as its store output. Native, release, test, and Clippy Cargo derivations receive that same store path through `SCE_CLI_GENERATED_INPUT_DIR`; their Cargo environments exclude Pkl and assert that it is unavailable. Repository-mode `cli/build.rs` validates the handoff before copying it into `OUT_DIR`; published crates use the validated packaging-only fallback. The build script stages SQL under `OUT_DIR/static/migrations` and writes `OUT_DIR/generated_migrations.rs` with deterministic migration constants sorted by numeric filename prefix. - Crane dependency-only derivations and `cli-fmt` intentionally do not receive `SCE_CLI_GENERATED_INPUT_DIR`, so canonical generation changes invalidate the producer and compiling derivations while preserving host/musl dependency artifacts and formatting. The root flake runs `cli-tests`, `cli-clippy`, and `cli-fmt` through Crane-backed paths and exposes `cli-generated-input` as the focused payload/inventory integrity check. It also exposes directory-scoped JS validation derivations for `npm/` and `config/lib/`, while `pkl-generated` uses a narrow canonical-input source set plus maybe-missing forbidden paths so reintroduced generated repository artifacts fail the check. - Git-commit embedding is release-only. `SCE_GIT_COMMIT` is injected via a `releaseCommitArgs` fragment (`{ SCE_GIT_COMMIT = shortGitCommit; }`) merged only into the release derivations — `scePackageMusl` on Linux and a dedicated native-toolchain `sceReleasePackageNative` on Darwin — and is deliberately absent from `commonCargoArgs`. As a result native `packages.sce`/`packages.default` and the `cli-tests`/`cli-clippy`/`cli-fmt` check derivations carry no commit in their inputs and stay cache-reusable across commits (native `sce version` reports `unknown`), while `.#sce-release` reports the real commit. `cli/build.rs` `emit_git_commit` emits the `rustc-env` value only when `SCE_GIT_COMMIT` is explicitly set (with `rerun-if-env-changed=SCE_GIT_COMMIT` as its sole rerun trigger) — no `git rev-parse` fallback and no `.git/HEAD`/`.git/packed-refs` watches — and `cli/src/services/version/mod.rs` resolves the commit through `option_env!("SCE_GIT_COMMIT")` with an `"unknown"` fallback. On Darwin the release derivation therefore diverges from `.#sce` (shared deps `cargoArtifacts`, but distinct final crate carrying the commit). -- `flake.nix` exposes `packages..ci-checks` (`ciChecks`) as the explicit long-running validation tier so the expensive work lives behind `nix build .#ci-checks` and never folds into `nix flake check`. It is a `runCommand` aggregate that symlinks its primary member `sceReleasePackage` into `$out/sce-release` (forcing the static-musl release build on Linux, native on Darwin) and, on Linux only, symlinks `releasePortabilityAuditCheck` into `$out/release-portability-audit`. That Linux-only `releasePortabilityAuditCheck` derivation runs `nix/release/native-portability-audit.sh --platform linux --binary ${sceReleasePackage}/bin/sce` (with `binutils`+`coreutils` on PATH) so `nix build .#ci-checks` fails when the real release binary carries forbidden `/nix/store/` references. This is distinct from `checks..native-portability-audit` (`nativePortabilityAuditCheck`), which remains a fixture-based unit test of the audit script on both platforms. CI wiring to `.#ci-checks` is owned by the separate `release-validation` matrix job in `.github/workflows/pr-ci.yml`, which builds `nix build .#ci-checks` on every PR/`main`/tag commit. +- `flake.nix` exposes `packages..ci-checks` (`ciChecks`) as the explicit long-running validation tier so the expensive work lives behind `nix build .#ci-checks` and never folds into `nix flake check`. It is a `runCommand` aggregate that symlinks its primary member `sceReleasePackage` into `$out/sce-release` (forcing the static-musl release build on Linux, native on Darwin) and, on Linux only, symlinks `releasePortabilityAuditCheck` into `$out/release-portability-audit`. That Linux-only `releasePortabilityAuditCheck` derivation runs `nix/release/native-portability-audit.sh --platform linux --binary ${sceReleasePackage}/bin/sce` (with `binutils`+`coreutils` on PATH) so `nix build .#ci-checks` fails when the real release binary carries forbidden `/nix/store/` references. On x86_64-linux only, it also symlinks `npmNixosVmTest` into `$out/npm-nixos-vm` (the NixOS VM npm install test, built against the same `sceReleasePackage`). This is distinct from `checks..native-portability-audit` (`nativePortabilityAuditCheck`), which remains a fixture-based unit test of the audit script on both platforms. CI wiring to `.#ci-checks` is owned by the separate `release-validation` matrix job in `.github/workflows/pr-ci.yml`, which builds `nix build .#ci-checks` on every PR/`main`/tag commit; its Linux leg requires KVM. - Flatpak flake tooling is Linux-only and reduced to a minimal app surface: `apps.sce-flatpak` is the umbrella entrypoint (`nix run .#sce-flatpak -- ` for `validate`, `prepare-local-manifest --repo-root `, etc.) that delegates to `packaging/flatpak/sce-flatpak.sh`; `apps.release-flatpak-package` emits deterministic Flatpak GitHub Release source-manifest assets; `apps.release-flatpak-bundle` builds a source-built `.flatpak` bundle; helper apps `apps.regenerate-flatpak-manifest` and `apps.regenerate-cargo-sources` rewrite the checked-in generated artifacts from their Nix sources. The standalone `flatpak-validate`, `flatpak-local-manifest`, and `flatpak-build` wrapper apps are removed in favor of `sce-flatpak `. `checks..flatpak-static-validation` runs the Nix-built static validator script during default `nix flake check`, and `checks..flatpak-manifest-parity` plus `checks..cargo-sources-parity` enforce the generated-artifact parity contracts; default `nix flake check` does not run `flatpak-builder`. The Linux dev shell includes `appstreamcli`, `flatpak`, and `flatpak-builder`. - The shared config-lib source set is rooted at `config/lib/` and includes the shared `package.json`, `bun.lock`, and `tsconfig.json` plus `agent-trace-plugin/` and `bash-policy-plugin/`; `config-lib-bun-tests` runs the bash-policy plugin wrapper tests from that shared root, while `config-lib-biome-check` and `config-lib-biome-format` run Biome over the copied shared package source. `.github/workflows/publish-crates.yml` copies the repository to a temporary clean workspace, enters that copy's Nix dev shell to generate `cli/package-fallback/`, and runs Cargo publication against the prepared copy so crates.io publish does not mutate repository artifacts. - The config-lib check source preserves repo-relative access to shared CLI patch fixtures: Nix copies a filtered repo-shaped source containing `config/lib/**` plus `cli/src/services/structured_patch/fixtures`, then runs Bun/Biome from `config/lib/`. diff --git a/context/glossary.md b/context/glossary.md index 63db3af6a..ba1917396 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -60,7 +60,7 @@ - `Flatpak bundle GitHub Release asset`: Implemented source-built `.flatpak` bundle release asset set for the `dev.crocoder.sce` Flatpak channel: `sce-v-x86_64.flatpak`, `sce-v-aarch64.flatpak`, plus per-architecture `.sha256` and `.json` metadata, built from source inside Flatpak using `flatpak-builder` + `flatpak build-bundle`. The bundle is a source-built Flatpak app for direct install (`flatpak install --user `), not a prebuilt binary or Flathub submission. These assets coexist with the existing Flatpak source-manifest packaging metadata on GitHub Releases. The `release-bundle` command is implemented in `packaging/flatpak/sce-flatpak.sh` and the GitHub workflow upload is implemented in `.github/workflows/release-sce-linux.yml` / `release-sce-linux-arm.yml` (x86_64/aarch64), assembled by `release-sce.yml`. - `cli flake native package`: Root-flake package output exposed as `packages.sce` with `packages.default = packages.sce`, producing the **native** development `sce` binary (`scePackage`) via `nix build .#sce` / `nix build .#default`. - `cli flake release package`: Root-flake package output exposed as `packages.sce-release` (`sceReleasePackage`): the static-musl release binary (`scePackageMusl`) on Linux and a dedicated native-toolchain build (`sceReleasePackageNative`) on Darwin, built via `nix build .#sce-release`. On Linux it is a distinct store path from `packages.sce` and passes the native portability audit; on Darwin it is likewise distinct from `packages.sce` because it embeds the commit (see `release-only Git-commit embedding`). `nix run .#release-artifacts` builds through this output. -- `cli flake ci-checks aggregate`: Root-flake package output exposed as `packages..ci-checks` (`ciChecks`), the explicit long-running validation tier run via `nix build .#ci-checks`. It is a `runCommand` symlink aggregate whose primary member is `sceReleasePackage` (forcing the static-musl release build) and which, on Linux only, also forces `releasePortabilityAuditCheck` — a derivation running `nix/release/native-portability-audit.sh` against the real `${sceReleasePackage}/bin/sce` binary. Distinct from the fixture-based `native-portability-audit` flake check. `nix flake check` never builds `.#sce-release`, keeping the expensive work behind this aggregate. CI consumes it through the `release-validation` matrix job in `.github/workflows/pr-ci.yml`, which runs `nix build .#ci-checks` on every PR/`main`/tag commit. +- `cli flake ci-checks aggregate`: Root-flake package output exposed as `packages..ci-checks` (`ciChecks`), the explicit long-running validation tier run via `nix build .#ci-checks`. It is a `runCommand` symlink aggregate whose primary member is `sceReleasePackage` (forcing the static-musl release build) and which, on Linux only, also forces `releasePortabilityAuditCheck` — a derivation running `nix/release/native-portability-audit.sh` against the real `${sceReleasePackage}/bin/sce` binary, and on x86_64-linux also forces the `npm-nixos-vm` NixOS VM npm install test. Distinct from the fixture-based `native-portability-audit` flake check. `nix flake check` never builds `.#sce-release`, keeping the expensive work behind this aggregate. CI consumes it through the `release-validation` matrix job in `.github/workflows/pr-ci.yml`, which runs `nix build .#ci-checks` on every PR/`main`/tag commit. - `release-only Git-commit embedding`: Build-input contract where `SCE_GIT_COMMIT` is injected only into the release derivations via the `releaseCommitArgs` fragment (`scePackageMusl` on Linux, `sceReleasePackageNative` on Darwin) and never into `commonCargoArgs`. Native `packages.sce`/`packages.default` and the `cli-tests`/`cli-clippy`/`cli-fmt` checks therefore carry no commit in their inputs and stay cache-reusable across commits (native `sce version` reports `unknown`), while `.#sce-release` reports the real commit. `cli/build.rs` `emit_git_commit` emits the value only when the env var is explicitly set (sole rerun trigger `rerun-if-env-changed=SCE_GIT_COMMIT`, no `git rev-parse` fallback, no `.git` watches); `cli/src/services/version/mod.rs` reads it via `option_env!` with an `"unknown"` fallback. - `cli flake runnable app`: Root-flake app outputs where `apps.sce` and `apps.default` execute the native `${scePackage}/bin/sce` (dev ergonomics), supporting explicit `nix run .#sce -- ...` and default-app flows such as `nix run . -- ...` / `nix run github:crocoder-dev/shared-context-engineering -- ...`; `apps.sce-release` executes the release `${sceReleasePackage}/bin/sce` for explicit `nix run .#sce-release -- ...`. - `slimmed default dev shell`: `devShells..default` (`nix develop`) no longer includes `scePackage` or `tursoPackage`, so entering it compiles neither the CLI package nor the Turso CLI — it provides only the Rust toolchain plus JS/pkl tooling for `cargo`/`biome`/`pkl` work. Turso stays available as `packages..turso` and via the opt-in `database dev shell`. The shared package list + shellHook are factored into `defaultDevShellPackages`/`defaultDevShellHook` `let` bindings so the two shells cannot drift. diff --git a/context/overview.md b/context/overview.md index 77dd982bc..b82bea4b4 100644 --- a/context/overview.md +++ b/context/overview.md @@ -90,7 +90,7 @@ Generated config now includes repo-local OpenCode plugin assets: `sce-bash-polic Claude bash-policy enforcement is also generated through`.claude/settings.json`as a`PreToolUse` `Bash`command hook running`sce policy bash`, so Claude and OpenCode both delegate to the Rust policy evaluator without a Claude TypeScript runtime. Pi bash-policy enforcement is delegated the same way through a project-local Pi extension (`config/lib/pi-plugin/sce-pi-extension.ts`, emitted to `config/.pi/extensions/sce/index.ts`) whose `tool_call`handler blocks denied bash commands via`sce policy bash`and fails open when the policy check cannot run (see`context/sce/pi-extension-runtime.md`). Local database bootstrap is now owned by `LocalDbLifecycle::setup`and`AgentTraceDbLifecycle::setup`aggregated by the setup command. Agent Trace lifecycle setup creates or migrates the repository-scoped`/sce/repos//agent-trace.db`; hook runtime uses the same repository-storage identity/path resolution without running migrations, and missing or stale schema causes hook paths to fail open with the existing`Run 'sce setup'.`guidance. Doctor validates the repository-scoped DB path/health and can bootstrap missing parent directories; outside a Git repository it reports an actionable "requires a Git repository" diagnostic instead of probing a sentinel path.`sce sync`is fully implemented: it resolves repository-scoped storage, authenticates against the control plane with stored WorkOS credentials, fetches authoritative cursors once, synchronizes three of the four local Agent Trace capture streams (`messages`, `parts`, `agent_traces`) concurrently while preserving sequential batches within each stream, leaves `diff_traces` local, and renders the documented concise text/JSON output (see`context/cli/sync-command.md`). The former `sce trace` command group and its database inspection surfaces are unavailable. The repository-root flake (`flake.nix`) applies a Rust overlay-backed stable toolchain pinned to `1.95.0`(with`rustfmt`and`clippy`), reads package/check version from the repo-root `.version`file, and builds`packages.sce`through a Crane`buildDepsOnly`+`buildPackage`pipeline. One deterministic pre-Cargo Nix derivation invokes the shared generated-input producer and supplies its validated`SCE_CLI_GENERATED_INPUT_DIR`store path to native, release, test, and Clippy Cargo derivations. Pkl is absent from those Cargo environments; dependency-only and format derivations do not receive the handoff, so canonical generation changes invalidate compiling outputs without invalidating dependency artifacts or formatting.`cli-tests`, `cli-clippy`, and `cli-fmt`remain Crane-backed check derivations. -The root flake splits native and release outputs:`packages.sce`and`packages.default` build the **native** development binary (`scePackage`), while `packages.sce-release` builds the release binary (`sceReleasePackage`: static musl on Linux, native on Darwin). So `nix build .#sce`/`.#default`, `nix run . -- --help`, `nix run .#sce -- --help`, and `nix profile install github:crocoder-dev/shared-context-engineering`target the native binary, and`nix build .#sce-release`/`nix run .#sce-release -- ...`(plus`nix run .#release-artifacts`, which builds `.#sce-release`) target the release binary. On Linux the native and release outputs are distinct store paths, and the release output passes the native portability audit. `packages..ci-checks`is the explicit long-running validation tier:`nix build .#ci-checks`builds the`.#sce-release`package and, on Linux, audits the real release binary for forbidden`/nix/store/`references, so the expensive work stays out of`nix flake check`(which never builds`.#sce-release`). +The root flake splits native and release outputs:`packages.sce`and`packages.default` build the **native** development binary (`scePackage`), while `packages.sce-release` builds the release binary (`sceReleasePackage`: static musl on Linux, native on Darwin). So `nix build .#sce`/`.#default`, `nix run . -- --help`, `nix run .#sce -- --help`, and `nix profile install github:crocoder-dev/shared-context-engineering`target the native binary, and`nix build .#sce-release`/`nix run .#sce-release -- ...`(plus`nix run .#release-artifacts`, which builds `.#sce-release`) target the release binary. On Linux the native and release outputs are distinct store paths, and the release output passes the native portability audit. `packages..ci-checks`is the explicit long-running validation tier:`nix build .#ci-checks`builds the`.#sce-release`package and, on Linux, audits the real release binary for forbidden`/nix/store/`references (on x86_64-linux it also runs the `npm-nixos-vm` NixOS VM npm install test), so the expensive work stays out of`nix flake check`(which never builds`.#sce-release`). Git-commit embedding is **release-only**: `SCE_GIT_COMMIT`is injected via a`releaseCommitArgs` fragment applied only to the release derivations (`scePackageMusl`on Linux,`sceReleasePackageNative`on Darwin), not to`commonCargoArgs`. So native `.#sce`/`.#default`and every`nix flake check` derivation (`cli-tests`, `cli-clippy`, `cli-fmt`) build without the commit in their inputs and stay cache-reusable across commits (native `sce version`reports`unknown`), while `.#sce-release`still reports the real commit via`sce version`. `cli/build.rs` `emit_git_commit`emits`SCE_GIT_COMMIT`only when the env var is explicitly set — no`git rev-parse`fallback and no`.git/HEAD`/`.git/packed-refs`rerun watches. On Darwin the release now uses a distinct native-toolchain derivation (native toolchain + commit), so it diverges from`.#sce`to carry the commit while native stays commit-independent. The default development shell is slimmed for fast iteration:`devShells.default`no longer includes`scePackage`or`tursoPackage`, so `nix develop`compiles neither the CLI package nor the Turso CLI — it provides only the Rust toolchain and JS/pkl tooling for`cargo`/`biome`/`pkl`work. Turso stays available as`packages..turso`and through a new opt-in`devShells..database`shell (default tools +`tursoPackage`), entered via `nix develop .#database`. Both shells share `defaultDevShellPackages`/`defaultDevShellHook` `let`bindings so they cannot drift. The CLI Cargo package metadata now includes crates.io publication-ready fields with crate-local install guidance in`cli/README.md`; supported Cargo install paths are `cargo install shared-context-engineering --locked`and local checkout installation through`./scripts/run-cli-cargo.sh install --path cli --locked`. Direct `cargo install --git`is unsupported because it cannot run the repository pre-Cargo producer. The published crate installs the`sce`binary. The crate also keeps`cargo clippy --manifest-path cli/Cargo.toml`warnings-denied through`cli/Cargo.toml`lint configuration, so an extra`-- -D warnings`flag is redundant. @@ -151,7 +151,7 @@ Lightweight post-task verification baseline (required after each completed task) - For architecture decisions: `context/decisions/` - No generated snapshot-parity workflow is checked in; deterministic ephemeral generation is enforced through `nix run .#pkl-check-generated` and the root `nix flake check` `pkl-generated` derivation. GitHub Actions workflow lint is enforced through the root `nix flake check` `workflow-actionlint` derivation (all `.github/workflows/*.yml`). -- PR validation runs through `.github/workflows/pr-ci.yml` on `pull_request`, `push` to `main`, and `workflow_dispatch` and now defines two matrix jobs. A workflow-level concurrency group (`${{ github.workflow }}-${{ github.ref }}`, `cancel-in-progress: true`) cancels superseded runs on the same ref, and every matrix job installs Nix via `DeterminateSystems/nix-installer-action@v22`, enables `DeterminateSystems/magic-nix-cache-action@v14` immediately after, runs on an `ubuntu-latest` + `macos-latest` matrix (`fail-fast: false`), and has a `timeout-minutes: 90` guard. The `nix-ci` job (name `Nix CI (ubuntu-latest)` / `Nix CI (macos-latest)`, for required branch protection checks) runs timed `nix flake check --print-build-logs`, then builds and smoke-tests the **native** package via timed `nix build .#sce --out-link result --print-build-logs` plus `./result/bin/sce --help` / `./result/bin/sce version` — it no longer builds the musl release (`.#default`/`.#sce` is native since the flake-speedup split). A separate `release-validation` job (name `Release validation (ubuntu-latest)` / `Release validation (macos-latest)`) builds `nix build .#ci-checks --out-link result-ci-checks --print-build-logs` on every commit, which forces the `.#sce-release` static-musl build and, on Linux, the real-binary portability audit guarded inside `ciChecks`; macOS builds the native-on-Darwin release package. This always-on release coverage on every PR/push (no path filters) is the intentional trade-off recorded in the plan Decisions section. The Nix store cache is the only cache layer; no separate Cargo/Bun/npm/`node_modules` caches. Release-publish workflows (`release-sce*.yml`) build the release binary via `nix run .#release-artifacts` (which targets `.#sce-release`), not `.#default`. +- PR validation runs through `.github/workflows/pr-ci.yml` on `pull_request`, `push` to `main`, and `workflow_dispatch` and now defines two matrix jobs. A workflow-level concurrency group (`${{ github.workflow }}-${{ github.ref }}`, `cancel-in-progress: true`) cancels superseded runs on the same ref, and every matrix job installs Nix via `DeterminateSystems/nix-installer-action@v22`, enables `DeterminateSystems/magic-nix-cache-action@v14` immediately after, runs on an `ubuntu-latest` + `macos-latest` matrix (`fail-fast: false`), and has a `timeout-minutes: 90` guard. The `nix-ci` job (name `Nix CI (ubuntu-latest)` / `Nix CI (macos-latest)`, for required branch protection checks) runs timed `nix flake check --print-build-logs`, then builds and smoke-tests the **native** package via timed `nix build .#sce --out-link result --print-build-logs` plus `./result/bin/sce --help` / `./result/bin/sce version` — it no longer builds the musl release (`.#default`/`.#sce` is native since the flake-speedup split). A separate `release-validation` job (name `Release validation (ubuntu-latest)` / `Release validation (macos-latest)`) builds `nix build .#ci-checks --out-link result-ci-checks --print-build-logs` on every commit, which forces the `.#sce-release` static-musl build and, on Linux, the real-binary portability audit guarded inside `ciChecks`, plus the NixOS VM npm install test on x86_64-linux. The `ubuntu-latest` leg enables `/dev/kvm` via a udev rule, fails if KVM is unavailable, and passes `nixos-test`/`kvm` system features through the Nix installer's `extra-conf`. macOS builds the native-on-Darwin release package. This always-on release coverage on every PR/push (no path filters) is the intentional trade-off recorded in the plan Decisions section. The Nix store cache is the only cache layer; no separate Cargo/Bun/npm/`node_modules` caches. Release-publish workflows (`release-sce*.yml`) build the release binary via `nix run .#release-artifacts` (which targets `.#sce-release`), not `.#default`. - The generated SCE config JSON Schema is produced ephemerally through the canonical `nix run .#pkl-generate` path and embedded into the CLI or packaging fallback. Release snapshots are checked in under `schema/v/config.json`; see `context/sce/config-schema-publication.md`. ## Cross-target parity diff --git a/context/patterns.md b/context/patterns.md index 454d9c362..4696144d3 100644 --- a/context/patterns.md +++ b/context/patterns.md @@ -15,7 +15,7 @@ - Keep direct Cargo verification commands as secondary targeted-debugging tools rather than the default repo-validation path. - Keep `cargo fmt` available for explicit autofix formatting flows; do not present it as the preferred verification command. - Gate workflow YAML edits through `checks..workflow-actionlint` inside root `nix flake check`; run `nix run nixpkgs#actionlint -- .github/workflows/*.yml` for targeted workflow lint during CI workflow edits. -- Keep PR validation in `.github/workflows/pr-ci.yml` (`Nix CI`): triggers on `pull_request`, `push` to `main`, and `workflow_dispatch`; two matrix jobs (`nix-ci` and `release-validation`), each on an `ubuntu-latest` + `macos-latest` matrix with `fail-fast: false`; pinned `DeterminateSystems/nix-installer-action@v22` with `DeterminateSystems/magic-nix-cache-action@v14` immediately after install; workflow-level concurrency (`${{ github.workflow }}-${{ github.ref }}`, `cancel-in-progress: true`) and job `timeout-minutes: 90`. The `nix-ci` quality gates are timed `nix flake check --print-build-logs`, timed `nix build .#sce --out-link result --print-build-logs` (the **native** package), and smoke tests against `./result/bin/sce` from that already-built package (no diagnostic `nix flake metadata` step and no separate `nix run` smoke evaluations); it does not build the musl release. The `release-validation` job builds `nix build .#ci-checks --out-link result-ci-checks --print-build-logs` on every commit, forcing the `.#sce-release` build plus the Linux-guarded real-binary portability audit. Pin third-party Actions to explicit version tags, not `@main`. +- Keep PR validation in `.github/workflows/pr-ci.yml` (`Nix CI`): triggers on `pull_request`, `push` to `main`, and `workflow_dispatch`; two matrix jobs (`nix-ci` and `release-validation`), each on an `ubuntu-latest` + `macos-latest` matrix with `fail-fast: false`; pinned `DeterminateSystems/nix-installer-action@v22` with `DeterminateSystems/magic-nix-cache-action@v14` immediately after install; workflow-level concurrency (`${{ github.workflow }}-${{ github.ref }}`, `cancel-in-progress: true`) and job `timeout-minutes: 90`. The `nix-ci` quality gates are timed `nix flake check --print-build-logs`, timed `nix build .#sce --out-link result --print-build-logs` (the **native** package), and smoke tests against `./result/bin/sce` from that already-built package (no diagnostic `nix flake metadata` step and no separate `nix run` smoke evaluations); it does not build the musl release. The `release-validation` job builds `nix build .#ci-checks --out-link result-ci-checks --print-build-logs` on every commit, forcing the `.#sce-release` build plus the Linux-guarded real-binary portability audit and the x86_64-linux-only `npm-nixos-vm` test; its `ubuntu-latest` leg first enables `/dev/kvm` (failing loudly when unavailable) and passes `extra-conf: system-features = nixos-test benchmark big-parallel kvm` to the Nix installer. Pin third-party Actions to explicit version tags, not `@main`. ## Root Biome scoping diff --git a/context/plans/npm-nixos-vm-integration-test.md b/context/plans/npm-nixos-vm-integration-test.md index 6ecd90842..6449ffd20 100644 --- a/context/plans/npm-nixos-vm-integration-test.md +++ b/context/plans/npm-nixos-vm-integration-test.md @@ -111,13 +111,22 @@ Persist this field in every plan; this is durable plan state, not chat state: - Context impact: localized. Adds the opt-in `.#npm-nixos-vm` package (x86_64-linux), relevant to `context/sce/cli-npm-distribution-contract.md` and `context/sce/flake-build-performance.md`. Full documentation is owned by T04, and `ciChecks` membership by T03. - Context synchronization: synced -- [ ] T03: `Run the NixOS VM test in ci-checks with KVM on Linux CI` (status:todo) +- [x] T03: `Run the NixOS VM test in ci-checks with KVM on Linux CI` (status:done) - Task ID: T03 - Scope: In — `flake.nix` `ciChecks` links `npm-nixos-vm` on x86_64-linux next to `release-portability-audit`; `.github/workflows/pr-ci.yml` `release-validation` Linux leg enables `/dev/kvm` and passes `extra-conf` system features to the Nix installer, failing if KVM is missing. Out — macOS steps, `nix-ci` job, new runners unless the hosted runner is proven unable to run the test. - Dependencies: T02 - Done when: `nix build .#ci-checks` on x86_64-linux builds `sce-release` once and runs the VM test; Darwin `ci-checks` evaluation unchanged; workflow passes actionlint. - Verify: `nix build .#ci-checks --print-build-logs`; `nix eval .#packages.aarch64-darwin.ci-checks.drvPath`; `nix build .#checks.x86_64-linux.workflow-actionlint`. - - Context synchronization: pending + - Completed: 2026-11-03 + - Files changed: `flake.nix`, `.github/workflows/pr-ci.yml` + - Result: `ciChecks` now links `npm-nixos-vm` (`npmNixosVmTest`) on x86_64-linux only, using `optionalString (system == "x86_64-linux")`. The new guard is concatenated onto the existing Linux audit guard, so the build script and drv are byte-identical on other systems. In `.github/workflows/pr-ci.yml`, the `release-validation` job gets an `Enable KVM` step, gated on `matrix.os == 'ubuntu-latest'`. It installs the `99-kvm4all` udev rule, reloads and triggers it, and exits 1 with `::error::/dev/kvm is not available; the NixOS VM test requires KVM.` when `/dev/kvm` is not readable and writable. The Install Nix step passes `extra-conf: system-features = nixos-test benchmark big-parallel kvm` on ubuntu only (empty on macOS). The `nix-ci` job and the macOS steps are unchanged. On user request, the explanatory comment above `ciChecks` and the new workflow comment were removed. + - Verify: + - `nix build .#ci-checks --print-build-logs`: passed (exit 0). `result` contains `sce-release`, `release-portability-audit`, and `npm-nixos-vm`, and its out path equals `nix eval .#packages.x86_64-linux.ci-checks.outPath` (`9zpgkyb9…-sce-ci-checks`). The log shows the portability audit passing on `dm1zsfzx…-sce-0.4.0-pre-alpha-v12/bin/sce`, plus the VM test lines `pack-smoke: installed runtime/sce mode 755`, `real binary reported version 0.4.0-pre-alpha-v12`, `runtime/sce mode: 755`, `launcher mode: 755`, `parsed version: 0.4.0-pre-alpha-v12`, and `test script finished in 109.70s`. This host has no `/dev/kvm` (QEMU logged `failed to initialize kvm`), so the VM ran under TCG. `nix-store -qR` on the `ci-checks` drv finds a single `sce-0.4.0-pre-alpha-v12.drv` (`yc4j00d7…`), referenced by both `vm-test-run-npm-nixos-vm.drv` and `sce-release-portability-audit.drv`. + - `nix eval .#packages.aarch64-darwin.ci-checks.drvPath`: evaluates to `zxz1djgg…-sce-ci-checks.drv`, identical to the HEAD worktree. `x86_64-darwin` (`cc5cbl22…`) and `aarch64-linux` (`9w6kpm8m…`) are also identical to HEAD. + - `nix build .#checks.x86_64-linux.workflow-actionlint`: passed (also re-run after the comment removal). + - Deviations: The first guard was placed on its own line. That added a blank line to the Darwin build script and changed the aarch64-darwin drv. It was folded onto the existing `optionalString` line to keep non-x86_64-linux drvs unchanged. KVM availability on hosted `ubuntu-latest` is still proven only by a PR CI run (see Open questions). + - Context impact: localized. `ci-checks` membership on x86_64-linux and the KVM requirement of the Linux `release-validation` job affect `context/sce/flake-build-performance.md` and `context/sce/cli-npm-distribution-contract.md`. Full documentation is owned by T04. + - Context synchronization: synced - [ ] T04: `Document fixture smoke versus NixOS VM npm integration testing` (status:todo) - Task ID: T04 diff --git a/context/sce/cli-npm-distribution-contract.md b/context/sce/cli-npm-distribution-contract.md index ae1a394f0..dfe7c19b9 100644 --- a/context/sce/cli-npm-distribution-contract.md +++ b/context/sce/cli-npm-distribution-contract.md @@ -64,7 +64,7 @@ This file captures the implemented npm distribution slice of the `sce` CLI insta - The fixture archive is produced by real GNU `tar`, so the smoke also exercises the in-repo archive reader against the release pipeline's archive format under Node. - Only the host platform is exercised. The production public key and manifest trust code are not modified. - Root `flake.nix` runs the smoke in the build sandbox as `checks..npm-pack-smoke` (part of `nix flake check`; loopback HTTPS works inside the sandbox). The sandbox has no `/usr/bin/env`, so there the installed bin is run as `node ` after its `-x` check. Host runs, including the `release-npm-package` gate, exec the bin directly. -- `nix/tests/npm-nixos-vm.nix` (exposed only as `packages.x86_64-linux.npm-nixos-vm`, never a flake check) runs real-binary mode inside a minimal NixOS VM (`pkgs.testers.runNixOSTest`, loopback-only networking) as a non-root user with an isolated HOME, npm cache, and prefix, against `${sceReleasePackage}/bin/sce` and the repo `.version`. It then asserts `runtime/sce` mode `755`, an executable `.bin/sce` resolving to the installed `bin/sce.js`, direct shebang execution of `sce --help`, and the `jq`-parsed `sce version --format json` `.version`. NixOS has no FHS loader or `/usr/lib` glibc, so a passing run shows the static-musl binary needs no unavailable dynamic libraries. +- `nix/tests/npm-nixos-vm.nix` (exposed as `packages.x86_64-linux.npm-nixos-vm` and linked into `.#ci-checks` on x86_64-linux, never a flake check) runs real-binary mode inside a minimal NixOS VM (`pkgs.testers.runNixOSTest`, loopback-only networking) as a non-root user with an isolated HOME, npm cache, and prefix, against `${sceReleasePackage}/bin/sce` and the repo `.version`. It then asserts `runtime/sce` mode `755`, an executable `.bin/sce` resolving to the installed `bin/sce.js`, direct shebang execution of `sce --help`, and the `jq`-parsed `sce version --format json` `.version`. NixOS has no FHS loader or `/usr/lib` glibc, so a passing run shows the static-musl binary needs no unavailable dynamic libraries. ## Verification baseline diff --git a/context/sce/flake-build-performance.md b/context/sce/flake-build-performance.md index 1298d5596..40f406b73 100644 --- a/context/sce/flake-build-performance.md +++ b/context/sce/flake-build-performance.md @@ -9,16 +9,20 @@ The root flake separates ordinary development from release-only work: release on Darwin. - `packages.ci-checks` is the explicit expensive tier. It forces the release package and, on Linux, audits the real release binary for forbidden - `/nix/store/` runtime references. -- `packages.x86_64-linux.npm-nixos-vm` is an opt-in NixOS VM test of the real - packed npm install against the release binary; it is not a flake check, so + `/nix/store/` runtime references. On x86_64-linux it also links + `npm-nixos-vm`, sharing the same `sceReleasePackage` build. +- `packages.x86_64-linux.npm-nixos-vm` is a NixOS VM test of the real packed + npm install against the release binary; it is not a flake check, so `nix flake check` never boots a VM. - `nix flake check` runs the normal test/lint/format/parity/static checks without building `.#sce-release`. - `devShells.default` provides development tools without `scePackage` or the Turso CLI. `devShells.database` adds Turso. - `.github/workflows/pr-ci.yml` runs native package smoke tests in `nix-ci` and - runs `.#ci-checks` separately in `release-validation` on every commit. + runs `.#ci-checks` separately in `release-validation` on every commit. Its + `ubuntu-latest` leg enables `/dev/kvm` through a udev rule, fails if KVM is + unavailable, and gives Nix the `nixos-test benchmark big-parallel kvm` system + features through the installer's `extra-conf`. Git-commit embedding is release-only. Native packages and normal CLI check derivations do not receive `SCE_GIT_COMMIT`; release packages do. diff --git a/flake.nix b/flake.nix index 5c2facec7..6f259771f 100644 --- a/flake.nix +++ b/flake.nix @@ -1683,11 +1683,6 @@ mkdir -p "$out" ''; - # Explicit long-running validation tier. Its primary member is the - # static-musl release build; on Linux it also forces the release - # portability audit over the real binary. Building this aggregate - # (nix build .#ci-checks) is the expensive command, keeping - # nix flake check fast (it never builds .#sce-release). ciChecks = pkgs.runCommand "sce-ci-checks" { } @@ -1698,6 +1693,8 @@ ln -s ${sceReleasePackage} "$out/sce-release" ${pkgs.lib.optionalString pkgs.stdenv.isLinux '' ln -s ${releasePortabilityAuditCheck} "$out/release-portability-audit" + ''}${pkgs.lib.optionalString (system == "x86_64-linux") '' + ln -s ${npmNixosVmTest} "$out/npm-nixos-vm" ''} ''; From a4611a4bc2b58fcc6d3a316bfe87c2824b54e691 Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 3 Nov 2026 20:02:26 +0100 Subject: [PATCH 4/5] npm: Document fixture and NixOS VM integration coverage Record completion of the npm-nixos-vm-integration-test plan's T04 work and document the fixture-versus-VM boundary, static-musl rationale, platform coverage, local invocation, and KVM CI requirements. Mark the completed acceptance criteria and task evidence while preserving the outstanding PR CI follow-up. Co-authored-by: SCE --- .github/workflows/pr-ci.yml | 13 --- context/architecture.md | 2 +- context/glossary.md | 2 +- context/overview.md | 4 +- context/patterns.md | 2 +- .../plans/npm-nixos-vm-integration-test.md | 92 ++++++++++++++++--- context/sce/cli-npm-distribution-contract.md | 5 +- context/sce/flake-build-performance.md | 16 ++-- flake.nix | 2 - 9 files changed, 95 insertions(+), 43 deletions(-) diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index f8d2200a8..8996b3cd4 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -76,21 +76,8 @@ jobs: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Enable KVM - if: matrix.os == 'ubuntu-latest' - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - if ! test -r /dev/kvm || ! test -w /dev/kvm; then - echo '::error::/dev/kvm is not available; the NixOS VM test requires KVM.' - exit 1 - fi - - name: Install Nix uses: DeterminateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 # v22 - with: - extra-conf: ${{ matrix.os == 'ubuntu-latest' && 'system-features = nixos-test benchmark big-parallel kvm' || '' }} - name: Enable Magic Nix Cache uses: DeterminateSystems/magic-nix-cache-action@908b263ff629f4cc17666315b7fd3ec127c6244d # v14 diff --git a/context/architecture.md b/context/architecture.md index 5672c38a8..f9cf7fd14 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -89,7 +89,7 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `flake.nix` applies `rust-overlay` (`oxalica/rust-overlay`) to nixpkgs, pins `rust-bin.stable.1.95.0.default` with `rustfmt` + `clippy`, and reads package/check version from repo-root `.version`. Its `cliGeneratedInput` derivation invokes the shared generated-input producer from a declarative source containing the producer and canonical inputs, then publishes the producer-validated handoff as its store output. Native, release, test, and Clippy Cargo derivations receive that same store path through `SCE_CLI_GENERATED_INPUT_DIR`; their Cargo environments exclude Pkl and assert that it is unavailable. Repository-mode `cli/build.rs` validates the handoff before copying it into `OUT_DIR`; published crates use the validated packaging-only fallback. The build script stages SQL under `OUT_DIR/static/migrations` and writes `OUT_DIR/generated_migrations.rs` with deterministic migration constants sorted by numeric filename prefix. - Crane dependency-only derivations and `cli-fmt` intentionally do not receive `SCE_CLI_GENERATED_INPUT_DIR`, so canonical generation changes invalidate the producer and compiling derivations while preserving host/musl dependency artifacts and formatting. The root flake runs `cli-tests`, `cli-clippy`, and `cli-fmt` through Crane-backed paths and exposes `cli-generated-input` as the focused payload/inventory integrity check. It also exposes directory-scoped JS validation derivations for `npm/` and `config/lib/`, while `pkl-generated` uses a narrow canonical-input source set plus maybe-missing forbidden paths so reintroduced generated repository artifacts fail the check. - Git-commit embedding is release-only. `SCE_GIT_COMMIT` is injected via a `releaseCommitArgs` fragment (`{ SCE_GIT_COMMIT = shortGitCommit; }`) merged only into the release derivations — `scePackageMusl` on Linux and a dedicated native-toolchain `sceReleasePackageNative` on Darwin — and is deliberately absent from `commonCargoArgs`. As a result native `packages.sce`/`packages.default` and the `cli-tests`/`cli-clippy`/`cli-fmt` check derivations carry no commit in their inputs and stay cache-reusable across commits (native `sce version` reports `unknown`), while `.#sce-release` reports the real commit. `cli/build.rs` `emit_git_commit` emits the `rustc-env` value only when `SCE_GIT_COMMIT` is explicitly set (with `rerun-if-env-changed=SCE_GIT_COMMIT` as its sole rerun trigger) — no `git rev-parse` fallback and no `.git/HEAD`/`.git/packed-refs` watches — and `cli/src/services/version/mod.rs` resolves the commit through `option_env!("SCE_GIT_COMMIT")` with an `"unknown"` fallback. On Darwin the release derivation therefore diverges from `.#sce` (shared deps `cargoArtifacts`, but distinct final crate carrying the commit). -- `flake.nix` exposes `packages..ci-checks` (`ciChecks`) as the explicit long-running validation tier so the expensive work lives behind `nix build .#ci-checks` and never folds into `nix flake check`. It is a `runCommand` aggregate that symlinks its primary member `sceReleasePackage` into `$out/sce-release` (forcing the static-musl release build on Linux, native on Darwin) and, on Linux only, symlinks `releasePortabilityAuditCheck` into `$out/release-portability-audit`. That Linux-only `releasePortabilityAuditCheck` derivation runs `nix/release/native-portability-audit.sh --platform linux --binary ${sceReleasePackage}/bin/sce` (with `binutils`+`coreutils` on PATH) so `nix build .#ci-checks` fails when the real release binary carries forbidden `/nix/store/` references. On x86_64-linux only, it also symlinks `npmNixosVmTest` into `$out/npm-nixos-vm` (the NixOS VM npm install test, built against the same `sceReleasePackage`). This is distinct from `checks..native-portability-audit` (`nativePortabilityAuditCheck`), which remains a fixture-based unit test of the audit script on both platforms. CI wiring to `.#ci-checks` is owned by the separate `release-validation` matrix job in `.github/workflows/pr-ci.yml`, which builds `nix build .#ci-checks` on every PR/`main`/tag commit; its Linux leg requires KVM. +- `flake.nix` exposes `packages..ci-checks` (`ciChecks`) as the explicit long-running validation tier so the expensive work lives behind `nix build .#ci-checks` and never folds into `nix flake check`. It is a `runCommand` aggregate that symlinks its primary member `sceReleasePackage` into `$out/sce-release` (forcing the static-musl release build on Linux, native on Darwin) and, on Linux only, symlinks `releasePortabilityAuditCheck` into `$out/release-portability-audit`. That Linux-only `releasePortabilityAuditCheck` derivation runs `nix/release/native-portability-audit.sh --platform linux --binary ${sceReleasePackage}/bin/sce` (with `binutils`+`coreutils` on PATH) so `nix build .#ci-checks` fails when the real release binary carries forbidden `/nix/store/` references. The NixOS VM npm install test (`npmNixosVmTest`, `packages.x86_64-linux.npm-nixos-vm`) is not a member; it is run locally only. This is distinct from `checks..native-portability-audit` (`nativePortabilityAuditCheck`), which remains a fixture-based unit test of the audit script on both platforms. CI wiring to `.#ci-checks` is owned by the separate `release-validation` matrix job in `.github/workflows/pr-ci.yml`, which builds `nix build .#ci-checks` on every PR/`main`/tag commit. - Flatpak flake tooling is Linux-only and reduced to a minimal app surface: `apps.sce-flatpak` is the umbrella entrypoint (`nix run .#sce-flatpak -- ` for `validate`, `prepare-local-manifest --repo-root `, etc.) that delegates to `packaging/flatpak/sce-flatpak.sh`; `apps.release-flatpak-package` emits deterministic Flatpak GitHub Release source-manifest assets; `apps.release-flatpak-bundle` builds a source-built `.flatpak` bundle; helper apps `apps.regenerate-flatpak-manifest` and `apps.regenerate-cargo-sources` rewrite the checked-in generated artifacts from their Nix sources. The standalone `flatpak-validate`, `flatpak-local-manifest`, and `flatpak-build` wrapper apps are removed in favor of `sce-flatpak `. `checks..flatpak-static-validation` runs the Nix-built static validator script during default `nix flake check`, and `checks..flatpak-manifest-parity` plus `checks..cargo-sources-parity` enforce the generated-artifact parity contracts; default `nix flake check` does not run `flatpak-builder`. The Linux dev shell includes `appstreamcli`, `flatpak`, and `flatpak-builder`. - The shared config-lib source set is rooted at `config/lib/` and includes the shared `package.json`, `bun.lock`, and `tsconfig.json` plus `agent-trace-plugin/` and `bash-policy-plugin/`; `config-lib-bun-tests` runs the bash-policy plugin wrapper tests from that shared root, while `config-lib-biome-check` and `config-lib-biome-format` run Biome over the copied shared package source. `.github/workflows/publish-crates.yml` copies the repository to a temporary clean workspace, enters that copy's Nix dev shell to generate `cli/package-fallback/`, and runs Cargo publication against the prepared copy so crates.io publish does not mutate repository artifacts. - The config-lib check source preserves repo-relative access to shared CLI patch fixtures: Nix copies a filtered repo-shaped source containing `config/lib/**` plus `cli/src/services/structured_patch/fixtures`, then runs Bun/Biome from `config/lib/`. diff --git a/context/glossary.md b/context/glossary.md index ba1917396..63db3af6a 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -60,7 +60,7 @@ - `Flatpak bundle GitHub Release asset`: Implemented source-built `.flatpak` bundle release asset set for the `dev.crocoder.sce` Flatpak channel: `sce-v-x86_64.flatpak`, `sce-v-aarch64.flatpak`, plus per-architecture `.sha256` and `.json` metadata, built from source inside Flatpak using `flatpak-builder` + `flatpak build-bundle`. The bundle is a source-built Flatpak app for direct install (`flatpak install --user `), not a prebuilt binary or Flathub submission. These assets coexist with the existing Flatpak source-manifest packaging metadata on GitHub Releases. The `release-bundle` command is implemented in `packaging/flatpak/sce-flatpak.sh` and the GitHub workflow upload is implemented in `.github/workflows/release-sce-linux.yml` / `release-sce-linux-arm.yml` (x86_64/aarch64), assembled by `release-sce.yml`. - `cli flake native package`: Root-flake package output exposed as `packages.sce` with `packages.default = packages.sce`, producing the **native** development `sce` binary (`scePackage`) via `nix build .#sce` / `nix build .#default`. - `cli flake release package`: Root-flake package output exposed as `packages.sce-release` (`sceReleasePackage`): the static-musl release binary (`scePackageMusl`) on Linux and a dedicated native-toolchain build (`sceReleasePackageNative`) on Darwin, built via `nix build .#sce-release`. On Linux it is a distinct store path from `packages.sce` and passes the native portability audit; on Darwin it is likewise distinct from `packages.sce` because it embeds the commit (see `release-only Git-commit embedding`). `nix run .#release-artifacts` builds through this output. -- `cli flake ci-checks aggregate`: Root-flake package output exposed as `packages..ci-checks` (`ciChecks`), the explicit long-running validation tier run via `nix build .#ci-checks`. It is a `runCommand` symlink aggregate whose primary member is `sceReleasePackage` (forcing the static-musl release build) and which, on Linux only, also forces `releasePortabilityAuditCheck` — a derivation running `nix/release/native-portability-audit.sh` against the real `${sceReleasePackage}/bin/sce` binary, and on x86_64-linux also forces the `npm-nixos-vm` NixOS VM npm install test. Distinct from the fixture-based `native-portability-audit` flake check. `nix flake check` never builds `.#sce-release`, keeping the expensive work behind this aggregate. CI consumes it through the `release-validation` matrix job in `.github/workflows/pr-ci.yml`, which runs `nix build .#ci-checks` on every PR/`main`/tag commit. +- `cli flake ci-checks aggregate`: Root-flake package output exposed as `packages..ci-checks` (`ciChecks`), the explicit long-running validation tier run via `nix build .#ci-checks`. It is a `runCommand` symlink aggregate whose primary member is `sceReleasePackage` (forcing the static-musl release build) and which, on Linux only, also forces `releasePortabilityAuditCheck` — a derivation running `nix/release/native-portability-audit.sh` against the real `${sceReleasePackage}/bin/sce` binary. Distinct from the fixture-based `native-portability-audit` flake check. `nix flake check` never builds `.#sce-release`, keeping the expensive work behind this aggregate. CI consumes it through the `release-validation` matrix job in `.github/workflows/pr-ci.yml`, which runs `nix build .#ci-checks` on every PR/`main`/tag commit. - `release-only Git-commit embedding`: Build-input contract where `SCE_GIT_COMMIT` is injected only into the release derivations via the `releaseCommitArgs` fragment (`scePackageMusl` on Linux, `sceReleasePackageNative` on Darwin) and never into `commonCargoArgs`. Native `packages.sce`/`packages.default` and the `cli-tests`/`cli-clippy`/`cli-fmt` checks therefore carry no commit in their inputs and stay cache-reusable across commits (native `sce version` reports `unknown`), while `.#sce-release` reports the real commit. `cli/build.rs` `emit_git_commit` emits the value only when the env var is explicitly set (sole rerun trigger `rerun-if-env-changed=SCE_GIT_COMMIT`, no `git rev-parse` fallback, no `.git` watches); `cli/src/services/version/mod.rs` reads it via `option_env!` with an `"unknown"` fallback. - `cli flake runnable app`: Root-flake app outputs where `apps.sce` and `apps.default` execute the native `${scePackage}/bin/sce` (dev ergonomics), supporting explicit `nix run .#sce -- ...` and default-app flows such as `nix run . -- ...` / `nix run github:crocoder-dev/shared-context-engineering -- ...`; `apps.sce-release` executes the release `${sceReleasePackage}/bin/sce` for explicit `nix run .#sce-release -- ...`. - `slimmed default dev shell`: `devShells..default` (`nix develop`) no longer includes `scePackage` or `tursoPackage`, so entering it compiles neither the CLI package nor the Turso CLI — it provides only the Rust toolchain plus JS/pkl tooling for `cargo`/`biome`/`pkl` work. Turso stays available as `packages..turso` and via the opt-in `database dev shell`. The shared package list + shellHook are factored into `defaultDevShellPackages`/`defaultDevShellHook` `let` bindings so the two shells cannot drift. diff --git a/context/overview.md b/context/overview.md index b82bea4b4..0eaa0537f 100644 --- a/context/overview.md +++ b/context/overview.md @@ -90,7 +90,7 @@ Generated config now includes repo-local OpenCode plugin assets: `sce-bash-polic Claude bash-policy enforcement is also generated through`.claude/settings.json`as a`PreToolUse` `Bash`command hook running`sce policy bash`, so Claude and OpenCode both delegate to the Rust policy evaluator without a Claude TypeScript runtime. Pi bash-policy enforcement is delegated the same way through a project-local Pi extension (`config/lib/pi-plugin/sce-pi-extension.ts`, emitted to `config/.pi/extensions/sce/index.ts`) whose `tool_call`handler blocks denied bash commands via`sce policy bash`and fails open when the policy check cannot run (see`context/sce/pi-extension-runtime.md`). Local database bootstrap is now owned by `LocalDbLifecycle::setup`and`AgentTraceDbLifecycle::setup`aggregated by the setup command. Agent Trace lifecycle setup creates or migrates the repository-scoped`/sce/repos//agent-trace.db`; hook runtime uses the same repository-storage identity/path resolution without running migrations, and missing or stale schema causes hook paths to fail open with the existing`Run 'sce setup'.`guidance. Doctor validates the repository-scoped DB path/health and can bootstrap missing parent directories; outside a Git repository it reports an actionable "requires a Git repository" diagnostic instead of probing a sentinel path.`sce sync`is fully implemented: it resolves repository-scoped storage, authenticates against the control plane with stored WorkOS credentials, fetches authoritative cursors once, synchronizes three of the four local Agent Trace capture streams (`messages`, `parts`, `agent_traces`) concurrently while preserving sequential batches within each stream, leaves `diff_traces` local, and renders the documented concise text/JSON output (see`context/cli/sync-command.md`). The former `sce trace` command group and its database inspection surfaces are unavailable. The repository-root flake (`flake.nix`) applies a Rust overlay-backed stable toolchain pinned to `1.95.0`(with`rustfmt`and`clippy`), reads package/check version from the repo-root `.version`file, and builds`packages.sce`through a Crane`buildDepsOnly`+`buildPackage`pipeline. One deterministic pre-Cargo Nix derivation invokes the shared generated-input producer and supplies its validated`SCE_CLI_GENERATED_INPUT_DIR`store path to native, release, test, and Clippy Cargo derivations. Pkl is absent from those Cargo environments; dependency-only and format derivations do not receive the handoff, so canonical generation changes invalidate compiling outputs without invalidating dependency artifacts or formatting.`cli-tests`, `cli-clippy`, and `cli-fmt`remain Crane-backed check derivations. -The root flake splits native and release outputs:`packages.sce`and`packages.default` build the **native** development binary (`scePackage`), while `packages.sce-release` builds the release binary (`sceReleasePackage`: static musl on Linux, native on Darwin). So `nix build .#sce`/`.#default`, `nix run . -- --help`, `nix run .#sce -- --help`, and `nix profile install github:crocoder-dev/shared-context-engineering`target the native binary, and`nix build .#sce-release`/`nix run .#sce-release -- ...`(plus`nix run .#release-artifacts`, which builds `.#sce-release`) target the release binary. On Linux the native and release outputs are distinct store paths, and the release output passes the native portability audit. `packages..ci-checks`is the explicit long-running validation tier:`nix build .#ci-checks`builds the`.#sce-release`package and, on Linux, audits the real release binary for forbidden`/nix/store/`references (on x86_64-linux it also runs the `npm-nixos-vm` NixOS VM npm install test), so the expensive work stays out of`nix flake check`(which never builds`.#sce-release`). +The root flake splits native and release outputs:`packages.sce`and`packages.default` build the **native** development binary (`scePackage`), while `packages.sce-release` builds the release binary (`sceReleasePackage`: static musl on Linux, native on Darwin). So `nix build .#sce`/`.#default`, `nix run . -- --help`, `nix run .#sce -- --help`, and `nix profile install github:crocoder-dev/shared-context-engineering`target the native binary, and`nix build .#sce-release`/`nix run .#sce-release -- ...`(plus`nix run .#release-artifacts`, which builds `.#sce-release`) target the release binary. On Linux the native and release outputs are distinct store paths, and the release output passes the native portability audit. `packages..ci-checks`is the explicit long-running validation tier:`nix build .#ci-checks`builds the`.#sce-release`package and, on Linux, audits the real release binary for forbidden`/nix/store/`references, so the expensive work stays out of`nix flake check`(which never builds`.#sce-release`). Git-commit embedding is **release-only**: `SCE_GIT_COMMIT`is injected via a`releaseCommitArgs` fragment applied only to the release derivations (`scePackageMusl`on Linux,`sceReleasePackageNative`on Darwin), not to`commonCargoArgs`. So native `.#sce`/`.#default`and every`nix flake check` derivation (`cli-tests`, `cli-clippy`, `cli-fmt`) build without the commit in their inputs and stay cache-reusable across commits (native `sce version`reports`unknown`), while `.#sce-release`still reports the real commit via`sce version`. `cli/build.rs` `emit_git_commit`emits`SCE_GIT_COMMIT`only when the env var is explicitly set — no`git rev-parse`fallback and no`.git/HEAD`/`.git/packed-refs`rerun watches. On Darwin the release now uses a distinct native-toolchain derivation (native toolchain + commit), so it diverges from`.#sce`to carry the commit while native stays commit-independent. The default development shell is slimmed for fast iteration:`devShells.default`no longer includes`scePackage`or`tursoPackage`, so `nix develop`compiles neither the CLI package nor the Turso CLI — it provides only the Rust toolchain and JS/pkl tooling for`cargo`/`biome`/`pkl`work. Turso stays available as`packages..turso`and through a new opt-in`devShells..database`shell (default tools +`tursoPackage`), entered via `nix develop .#database`. Both shells share `defaultDevShellPackages`/`defaultDevShellHook` `let`bindings so they cannot drift. The CLI Cargo package metadata now includes crates.io publication-ready fields with crate-local install guidance in`cli/README.md`; supported Cargo install paths are `cargo install shared-context-engineering --locked`and local checkout installation through`./scripts/run-cli-cargo.sh install --path cli --locked`. Direct `cargo install --git`is unsupported because it cannot run the repository pre-Cargo producer. The published crate installs the`sce`binary. The crate also keeps`cargo clippy --manifest-path cli/Cargo.toml`warnings-denied through`cli/Cargo.toml`lint configuration, so an extra`-- -D warnings`flag is redundant. @@ -151,7 +151,7 @@ Lightweight post-task verification baseline (required after each completed task) - For architecture decisions: `context/decisions/` - No generated snapshot-parity workflow is checked in; deterministic ephemeral generation is enforced through `nix run .#pkl-check-generated` and the root `nix flake check` `pkl-generated` derivation. GitHub Actions workflow lint is enforced through the root `nix flake check` `workflow-actionlint` derivation (all `.github/workflows/*.yml`). -- PR validation runs through `.github/workflows/pr-ci.yml` on `pull_request`, `push` to `main`, and `workflow_dispatch` and now defines two matrix jobs. A workflow-level concurrency group (`${{ github.workflow }}-${{ github.ref }}`, `cancel-in-progress: true`) cancels superseded runs on the same ref, and every matrix job installs Nix via `DeterminateSystems/nix-installer-action@v22`, enables `DeterminateSystems/magic-nix-cache-action@v14` immediately after, runs on an `ubuntu-latest` + `macos-latest` matrix (`fail-fast: false`), and has a `timeout-minutes: 90` guard. The `nix-ci` job (name `Nix CI (ubuntu-latest)` / `Nix CI (macos-latest)`, for required branch protection checks) runs timed `nix flake check --print-build-logs`, then builds and smoke-tests the **native** package via timed `nix build .#sce --out-link result --print-build-logs` plus `./result/bin/sce --help` / `./result/bin/sce version` — it no longer builds the musl release (`.#default`/`.#sce` is native since the flake-speedup split). A separate `release-validation` job (name `Release validation (ubuntu-latest)` / `Release validation (macos-latest)`) builds `nix build .#ci-checks --out-link result-ci-checks --print-build-logs` on every commit, which forces the `.#sce-release` static-musl build and, on Linux, the real-binary portability audit guarded inside `ciChecks`, plus the NixOS VM npm install test on x86_64-linux. The `ubuntu-latest` leg enables `/dev/kvm` via a udev rule, fails if KVM is unavailable, and passes `nixos-test`/`kvm` system features through the Nix installer's `extra-conf`. macOS builds the native-on-Darwin release package. This always-on release coverage on every PR/push (no path filters) is the intentional trade-off recorded in the plan Decisions section. The Nix store cache is the only cache layer; no separate Cargo/Bun/npm/`node_modules` caches. Release-publish workflows (`release-sce*.yml`) build the release binary via `nix run .#release-artifacts` (which targets `.#sce-release`), not `.#default`. +- PR validation runs through `.github/workflows/pr-ci.yml` on `pull_request`, `push` to `main`, and `workflow_dispatch` and now defines two matrix jobs. A workflow-level concurrency group (`${{ github.workflow }}-${{ github.ref }}`, `cancel-in-progress: true`) cancels superseded runs on the same ref, and every matrix job installs Nix via `DeterminateSystems/nix-installer-action@v22`, enables `DeterminateSystems/magic-nix-cache-action@v14` immediately after, runs on an `ubuntu-latest` + `macos-latest` matrix (`fail-fast: false`), and has a `timeout-minutes: 90` guard. The `nix-ci` job (name `Nix CI (ubuntu-latest)` / `Nix CI (macos-latest)`, for required branch protection checks) runs timed `nix flake check --print-build-logs`, then builds and smoke-tests the **native** package via timed `nix build .#sce --out-link result --print-build-logs` plus `./result/bin/sce --help` / `./result/bin/sce version` — it no longer builds the musl release (`.#default`/`.#sce` is native since the flake-speedup split). A separate `release-validation` job (name `Release validation (ubuntu-latest)` / `Release validation (macos-latest)`) builds `nix build .#ci-checks --out-link result-ci-checks --print-build-logs` on every commit, which forces the `.#sce-release` static-musl build and, on Linux, the real-binary portability audit guarded inside `ciChecks`. The local-only `.#npm-nixos-vm` NixOS VM npm install test is not part of CI because hosted runners lack `/dev/kvm`. macOS builds the native-on-Darwin release package. This always-on release coverage on every PR/push (no path filters) is the intentional trade-off recorded in the plan Decisions section. The Nix store cache is the only cache layer; no separate Cargo/Bun/npm/`node_modules` caches. Release-publish workflows (`release-sce*.yml`) build the release binary via `nix run .#release-artifacts` (which targets `.#sce-release`), not `.#default`. - The generated SCE config JSON Schema is produced ephemerally through the canonical `nix run .#pkl-generate` path and embedded into the CLI or packaging fallback. Release snapshots are checked in under `schema/v/config.json`; see `context/sce/config-schema-publication.md`. ## Cross-target parity diff --git a/context/patterns.md b/context/patterns.md index 4696144d3..466d6a056 100644 --- a/context/patterns.md +++ b/context/patterns.md @@ -15,7 +15,7 @@ - Keep direct Cargo verification commands as secondary targeted-debugging tools rather than the default repo-validation path. - Keep `cargo fmt` available for explicit autofix formatting flows; do not present it as the preferred verification command. - Gate workflow YAML edits through `checks..workflow-actionlint` inside root `nix flake check`; run `nix run nixpkgs#actionlint -- .github/workflows/*.yml` for targeted workflow lint during CI workflow edits. -- Keep PR validation in `.github/workflows/pr-ci.yml` (`Nix CI`): triggers on `pull_request`, `push` to `main`, and `workflow_dispatch`; two matrix jobs (`nix-ci` and `release-validation`), each on an `ubuntu-latest` + `macos-latest` matrix with `fail-fast: false`; pinned `DeterminateSystems/nix-installer-action@v22` with `DeterminateSystems/magic-nix-cache-action@v14` immediately after install; workflow-level concurrency (`${{ github.workflow }}-${{ github.ref }}`, `cancel-in-progress: true`) and job `timeout-minutes: 90`. The `nix-ci` quality gates are timed `nix flake check --print-build-logs`, timed `nix build .#sce --out-link result --print-build-logs` (the **native** package), and smoke tests against `./result/bin/sce` from that already-built package (no diagnostic `nix flake metadata` step and no separate `nix run` smoke evaluations); it does not build the musl release. The `release-validation` job builds `nix build .#ci-checks --out-link result-ci-checks --print-build-logs` on every commit, forcing the `.#sce-release` build plus the Linux-guarded real-binary portability audit and the x86_64-linux-only `npm-nixos-vm` test; its `ubuntu-latest` leg first enables `/dev/kvm` (failing loudly when unavailable) and passes `extra-conf: system-features = nixos-test benchmark big-parallel kvm` to the Nix installer. Pin third-party Actions to explicit version tags, not `@main`. +- Keep PR validation in `.github/workflows/pr-ci.yml` (`Nix CI`): triggers on `pull_request`, `push` to `main`, and `workflow_dispatch`; two matrix jobs (`nix-ci` and `release-validation`), each on an `ubuntu-latest` + `macos-latest` matrix with `fail-fast: false`; pinned `DeterminateSystems/nix-installer-action@v22` with `DeterminateSystems/magic-nix-cache-action@v14` immediately after install; workflow-level concurrency (`${{ github.workflow }}-${{ github.ref }}`, `cancel-in-progress: true`) and job `timeout-minutes: 90`. The `nix-ci` quality gates are timed `nix flake check --print-build-logs`, timed `nix build .#sce --out-link result --print-build-logs` (the **native** package), and smoke tests against `./result/bin/sce` from that already-built package (no diagnostic `nix flake metadata` step and no separate `nix run` smoke evaluations); it does not build the musl release. The `release-validation` job builds `nix build .#ci-checks --out-link result-ci-checks --print-build-logs` on every commit, forcing the `.#sce-release` build plus the Linux-guarded real-binary portability audit; the NixOS VM npm test (`.#npm-nixos-vm`) stays local-only because hosted runners lack `/dev/kvm`. Pin third-party Actions to explicit version tags, not `@main`. ## Root Biome scoping diff --git a/context/plans/npm-nixos-vm-integration-test.md b/context/plans/npm-nixos-vm-integration-test.md index 6449ffd20..046c16a88 100644 --- a/context/plans/npm-nixos-vm-integration-test.md +++ b/context/plans/npm-nixos-vm-integration-test.md @@ -4,7 +4,7 @@ Extends the hardened npm installer work (PR #344, plan `harden-npm-installer`) with a production-representative NixOS VM integration test. `npm/test/pack-smoke.sh` gains a real-binary mode (`--binary `) that wraps the Nix-built `sce-release` executable in the canonical `sce-v-/{bin/sce,LICENSE,README.md}` archive layout, signs a manifest with a throwaway key swapped only into the staged package copy, serves it through the existing `fixture-release-server.mjs`, and runs the real `npm pack → npm install → postinstall → signed manifest verification → archive download → checksum verification → extraction → atomic install` path before executing the npm-generated `sce` launcher directly. The existing fixture (stub binary) mode stays the default and remains in `nix flake check` as `npm-pack-smoke`. -A new `pkgs.testers.runNixOSTest` definition under `nix/tests/` boots a minimal NixOS VM, runs real-binary mode as a non-root user with an isolated HOME, npm cache, and prefix, and asserts launcher/binary modes, direct shebang execution, `sce --help`, and `sce version --format json` matching `.version`. Because NixOS has no FHS `/lib64` loader or `/usr/lib` glibc, successful execution proves the static-musl binary needs no unavailable dynamic libraries. The VM test is x86_64-linux only, is added to the expensive `ciChecks` aggregate (built by `release-validation` in `.github/workflows/pr-ci.yml`), and stays out of the fast `nix flake check` tier. The installer architecture, production public key, and verification code are unchanged. +A new `pkgs.testers.runNixOSTest` definition under `nix/tests/` boots a minimal NixOS VM, runs real-binary mode as a non-root user with an isolated HOME, npm cache, and prefix, and asserts launcher/binary modes, direct shebang execution, `sce --help`, and `sce version --format json` matching `.version`. Because NixOS has no FHS `/lib64` loader or `/usr/lib` glibc, successful execution proves the static-musl binary needs no unavailable dynamic libraries. The VM test is x86_64-linux only and local-only: it stays out of the fast `nix flake check` tier and, after hosted `ubuntu-latest` runners proved to lack `/dev/kvm` (PR CI run 41700381363), out of `ciChecks` and CI as well (T05). The installer architecture, production public key, and verification code are unchanged. ## Acceptance criteria @@ -12,21 +12,21 @@ How this plan is proven complete. Each criterion is observable and names the check that proves it. `/validate` runs these checks; no task in the stack performs final validation. -- [ ] AC1: Fixture mode is unchanged in behavior and still runs in the fast tier: the `npm-pack-smoke` flake check passes with the stub binary and is still a member of `checks.`. +- [x] AC1: Fixture mode is unchanged in behavior and still runs in the fast tier: the `npm-pack-smoke` flake check passes with the stub binary and is still a member of `checks.`. - Validate: `nix build .#checks.x86_64-linux.npm-pack-smoke --print-build-logs` passes; `nix eval .#checks.x86_64-linux --apply builtins.attrNames` lists `npm-pack-smoke` and does not list `npm-nixos-vm`. -- [ ] AC2: Real-binary mode installs the actual release binary through the full postinstall path and executes it: given `--binary /bin/sce`, the smoke packs the package, installs it with lifecycle scripts enabled and without `SCE_NPM_SKIP_DOWNLOAD`, and asserts `runtime/sce` mode `0755`, an executable packed `package/bin/sce.js`, a `.bin/sce` link resolving into the installed `@crocoder-dev/sce` package, exit 0 from `sce --help`, and `sce version --format json` `.version` equal to repo `.version`. +- [x] AC2: Real-binary mode installs the actual release binary through the full postinstall path and executes it: given `--binary /bin/sce`, the smoke packs the package, installs it with lifecycle scripts enabled and without `SCE_NPM_SKIP_DOWNLOAD`, and asserts `runtime/sce` mode `0755`, an executable packed `package/bin/sce.js`, a `.bin/sce` link resolving into the installed `@crocoder-dev/sce` package, exit 0 from `sce --help`, and `sce version --format json` `.version` equal to repo `.version`. - Validate: `nix build .#sce-release && nix shell nixpkgs#nodejs nixpkgs#openssl -c bash npm/test/pack-smoke.sh --binary ./result/bin/sce` exits 0 and prints the reported version; `grep -n 'release-manifest-public-key.pem' npm/test/pack-smoke.sh` shows key substitution only in the staged copy and `git diff main -- npm/lib` is empty. -- [ ] AC3: The NixOS VM test boots a clean VM and, as a non-root user with isolated HOME/npm cache/prefix and no external network, installs the real packed package and runs the npm-generated `sce` launcher directly (shebang execution, not `node sce.js`), with `sce --help` succeeding and parsed `sce version --format json` `.version` equal to `.version`. +- [x] AC3: The NixOS VM test boots a clean VM and, as a non-root user with isolated HOME/npm cache/prefix and no external network, installs the real packed package and runs the npm-generated `sce` launcher directly (shebang execution, not `node sce.js`), with `sce --help` succeeding and parsed `sce version --format json` `.version` equal to `.version`. - Validate: `nix build .#npm-nixos-vm --print-build-logs` passes and the log shows the VM test steps, the installed modes, and the parsed version; inspect `nix/tests/npm-nixos-vm.nix` for a non-root user, no `SCE_NPM_SKIP_DOWNLOAD`, no manual copy/chmod into `runtime/`, and no network beyond loopback. -- [ ] AC4: `nix build .#ci-checks` on x86_64-linux builds the real `sce-release` once and includes the NixOS VM test alongside the release portability audit; other systems' `ci-checks` and macOS CI behavior are unchanged. - - Validate: `nix build .#ci-checks --print-build-logs` passes on x86_64-linux and `ls -l result` shows `sce-release`, `release-portability-audit`, and `npm-nixos-vm`; `nix derivation show .#ci-checks` (recursive) shows a single `sce-release` output path referenced by both the VM test and the audit; `nix eval .#packages.aarch64-darwin.ci-checks.drvPath` still evaluates. -- [ ] AC5: The `release-validation` CI job on `ubuntu-latest` can run the VM test with KVM acceleration and fails loudly (not skips) if KVM is unavailable. - - Validate: inspect `.github/workflows/pr-ci.yml` for KVM enablement and `kvm`/`nixos-test` system features on the Linux leg only; `nix build .#checks.x86_64-linux.workflow-actionlint` passes; a PR CI run shows the `release-validation (ubuntu-latest)` job executing the VM test. -- [ ] AC6: The tests fail for the right reason under injected faults: a packed launcher without its executable bit fails at the launcher-mode assertion; a non-executable/invalid `--binary` fails at the `sce --help` execution step; an installer that does not produce `runtime/sce` fails at the `runtime/sce` assertion. Faults are injected only in temporary worktrees or fixtures and the original state is restored. +- [ ] AC4: `nix build .#ci-checks` on x86_64-linux is unchanged by this plan: it builds `sce-release` and the release portability audit and does not include `npm-nixos-vm`; other systems' `ci-checks` evaluate. + - Validate: `nix build .#ci-checks --print-build-logs` passes on x86_64-linux and `ls -l result` shows only `sce-release` and `release-portability-audit`; `nix eval .#packages.aarch64-darwin.ci-checks.drvPath` still evaluates. +- [ ] AC5: CI does not run or require KVM: `.github/workflows/pr-ci.yml` has no KVM enablement step and no `kvm`/`nixos-test` system features, and the NixOS VM test is documented as a local-only run. + - Validate: `grep -n -i 'kvm\|nixos-test' .github/workflows/pr-ci.yml` finds nothing; `nix build .#checks.x86_64-linux.workflow-actionlint` passes; AC8 inspection confirms the local-only wording. +- [x] AC6: The tests fail for the right reason under injected faults: a packed launcher without its executable bit fails at the launcher-mode assertion; a non-executable/invalid `--binary` fails at the `sce --help` execution step; an installer that does not produce `runtime/sce` fails at the `runtime/sce` assertion. Faults are injected only in temporary worktrees or fixtures and the original state is restored. - Validate: plan task evidence records each injected fault, the command run, and the exact failing `pack-smoke:` message; `git status` is clean afterward. -- [ ] AC7: Existing installer negative tests still reject invalid signatures, mismatched versions, bad checksums, malicious archive entries, and leave no partial `runtime/sce` on failure. +- [x] AC7: Existing installer negative tests still reject invalid signatures, mismatched versions, bad checksums, malicious archive entries, and leave no partial `runtime/sce` on failure. - Validate: `nix build .#checks.x86_64-linux.npm-bun-tests --print-build-logs` passes. -- [ ] AC8: Durable docs describe fixture vs VM testing, why the VM uses the real musl binary, what the VM guarantees, what remains macOS/ARM64-release-only, how to run it locally, and KVM/runner requirements. +- [ ] AC8: Durable docs describe fixture vs VM testing, why the VM uses the real musl binary, what the VM guarantees, what remains macOS/ARM64-release-only, how to run it locally, and that it is local-only because hosted CI runners lack KVM. - Validate: inspect `context/sce/cli-npm-distribution-contract.md` and `context/sce/flake-build-performance.md` for those points and matching command names. ### Full validation @@ -128,14 +128,78 @@ Persist this field in every plan; this is durable plan state, not chat state: - Context impact: localized. `ci-checks` membership on x86_64-linux and the KVM requirement of the Linux `release-validation` job affect `context/sce/flake-build-performance.md` and `context/sce/cli-npm-distribution-contract.md`. Full documentation is owned by T04. - Context synchronization: synced -- [ ] T04: `Document fixture smoke versus NixOS VM npm integration testing` (status:todo) +- [x] T04: `Document fixture smoke versus NixOS VM npm integration testing` (status:done) - Task ID: T04 - Scope: In — `context/sce/cli-npm-distribution-contract.md` (both smoke modes, VM guarantees, real musl rationale, macOS/ARM64 release-only coverage, local run command, KVM requirement, verification baseline); `context/sce/flake-build-performance.md` (`ci-checks` membership, KVM runner requirement); `context/context-map.md` annotation if needed. Out — README marketing changes, unrelated context edits. - Dependencies: T03 - Done when: docs state every point in AC8 concisely and their command names match the flake. - Verify: inspect the two context files; `grep -n 'npm-nixos-vm' context/sce/*.md flake.nix`. - - Context synchronization: pending + - Completed: 2026-11-03 + - Files changed: `context/sce/cli-npm-distribution-contract.md`, `context/sce/flake-build-performance.md` + - Result: The earlier T01–T03 syncs already documented both smoke modes, the VM's guarantees and musl/no-FHS rationale, `ci-checks` membership, the CI KVM enablement, and the verification baseline. This task filled the remaining AC8 gaps. The npm contract now has three new bullets in its packed-package smoke section: (1) fixture mode versus the VM, including why the VM needs the real static-musl binary (the stub cannot reveal native linkage problems); (2) the VM covers only `linux/x64`, while `linux/arm64` and `darwin/arm64` remain covered only by the release pipeline's portability audits and extracted-binary smoke; (3) the local run command with KVM/TCG behavior, and the CI path through `.#ci-checks` in `release-validation (ubuntu-latest)`: KVM enabled by a udev rule, a hard failure when KVM is missing, the `nixos-test benchmark big-parallel kvm` system features, and no VM on the macOS leg. `flake-build-performance.md` gained the standalone local build command and its non-KVM behavior. `context/context-map.md` already names `.#npm-nixos-vm` accurately and was left unchanged. + - Verify: + - Inspection of both context files: every AC8 point is present (fixture vs VM, real musl rationale, VM guarantees, macOS/ARM64 release-only coverage, local run command, KVM/runner requirements). + - `grep -n 'npm-nixos-vm' context/sce/*.md flake.nix`: the docs reference `.#npm-nixos-vm`, `packages.x86_64-linux.npm-nixos-vm`, and `nix/tests/npm-nixos-vm.nix`, matching `flake.nix` (`npm-nixos-vm = npmNixosVmTest;`, the `$out/npm-nixos-vm` link in `ci-checks`, and the `./nix/tests/npm-nixos-vm.nix` import). The workflow wording matches `.github/workflows/pr-ci.yml` lines 82–93. + - Deviations: none. + - Context impact: localized. Documentation-only within `context/sce/`; no code, interface, or terminology change. + - Context synchronization: synced + +- [x] T05: `Drop the NixOS VM test and KVM enablement from CI; keep it local-only` (status:done) + - Task ID: T05 + - Scope: In — `flake.nix` (remove the `npm-nixos-vm` link from `ciChecks`; keep `packages.x86_64-linux.npm-nixos-vm`), `.github/workflows/pr-ci.yml` (remove the `Enable KVM` step and the `extra-conf` system features), and the context docs that described CI coverage of the VM (`context/sce/cli-npm-distribution-contract.md`, `context/sce/flake-build-performance.md`, `context/overview.md`, `context/patterns.md`, `context/glossary.md`, `context/architecture.md`). Out — `nix/tests/npm-nixos-vm.nix`, `npm/test/pack-smoke.sh`. + - Dependencies: T04 + - Reason: PR CI run `41700381363`, job `126141783713` (`Release validation (ubuntu-latest)`, commit `41bdb5e9`) failed at `Enable KVM` with `/dev/kvm is not available; the NixOS VM test requires KVM.` The user decided to drop KVM from CI and run the VM test locally only. + - Done when: `ciChecks` no longer includes the VM test; the workflow matches its pre-T03 state; docs describe the VM test as local-only; AC4/AC5/AC8 amended accordingly. + - Verify: `nix build .#checks.x86_64-linux.workflow-actionlint`; `nix build .#ci-checks` and `ls result/`; `nix flake check`; `nix build .#npm-nixos-vm` still builds. + - Completed: 2026-11-03 + - Files changed: `flake.nix`, `.github/workflows/pr-ci.yml`, `context/sce/cli-npm-distribution-contract.md`, `context/sce/flake-build-performance.md`, `context/overview.md`, `context/patterns.md`, `context/glossary.md`, `context/architecture.md`, this plan + - Context synchronization: synced ## Open questions -- Hosted `ubuntu-latest` runners are expected to provide KVM after a udev rule, but this is only proven by an actual PR CI run. If the run shows KVM is unavailable or the VM is too slow for the 90-minute `release-validation` budget, choosing a dedicated or larger runner is a cost decision for the maintainers rather than something T03 can settle alone. +- Resolved by T05: hosted `ubuntu-latest` runners do not provide `/dev/kvm` (PR CI run 41700381363), so the VM test is local-only. Originally: hosted runners were expected to provide KVM after a udev rule, but this is only proven by an actual PR CI run. If the run shows KVM is unavailable or the VM is too slow for the 90-minute `release-validation` budget, choosing a dedicated or larger runner is a cost decision for the maintainers rather than something T03 can settle alone. + +## Validation Report + +**Status:** failed +**Date:** 2026-11-03 + +### Commands run + +- `nix flake check` -> exit 0 (all checks passed; x86_64-linux only, other systems omitted as incompatible) +- `nix build .#ci-checks --print-build-logs` -> exit 0 (`result/` links `sce-release`, `release-portability-audit`, `npm-nixos-vm`; portability audit passed; VM test finished in 104.78s under TCG because this host has no `/dev/kvm`) +- `nix build .#checks.x86_64-linux.npm-pack-smoke .#checks.x86_64-linux.npm-bun-tests .#checks.x86_64-linux.workflow-actionlint` -> exit 0 (all three passed) +- `nix eval .#checks.x86_64-linux --apply builtins.attrNames` -> exit 0 (lists `npm-pack-smoke`; no `npm-nixos-vm`) +- `nix build .#sce-release` + `nix shell nixpkgs#nodejs nixpkgs#openssl -c bash npm/test/pack-smoke.sh --binary /bin/sce` -> exit 0 (`installed runtime/sce mode 755`, `.bin/sce resolves to …/@crocoder-dev/sce/bin/sce.js`, `real binary reported version 0.4.0-pre-alpha-v12`, `OK (real binary: sce --help, sce version --format json)`; run from a scratch cwd, no stray files) +- `grep -n 'release-manifest-public-key.pem' npm/test/pack-smoke.sh` -> exit 0 (single hit, line 153, writes only `$stage_dir/lib/…`) +- `git diff 77a84b60 HEAD -- npm/lib npm/bin` -> exit 0 (empty; this plan's commits do not touch `npm/lib` or `npm/bin`) +- `nix-store -qR` on the `ci-checks` drv -> exit 0 (one `sce-0.4.0-pre-alpha-v12.drv`, `5f9rgjkl…`, in the closure of both `vm-test-run-npm-nixos-vm.drv` and `sce-release-portability-audit.drv`) +- `nix eval .#packages.aarch64-darwin.ci-checks.drvPath` -> exit 0 (evaluates to `506gxj81…-sce-ci-checks.drv`) +- `gh pr list --head npm-nixos-integration-test` / `gh run list --branch npm-nixos-integration-test` -> exit 0 (no PR and no CI runs; branch not pushed) + +### Success-criteria verification + +- [x] AC1: Fixture mode unchanged and in the fast tier -> `npm-pack-smoke` built and passed; it is listed in `checks.x86_64-linux` and `npm-nixos-vm` is not. +- [x] AC2: Real-binary mode installs and executes the actual release binary -> host `--binary` smoke passed with the asserted mode, link, `--help`, and version; key substitution only in the staged copy; no `npm/lib` changes from this plan (`git diff main -- npm/lib` is non-empty only because of the parent `harden-npm-installer` commits not yet on `main`). +- [x] AC3: NixOS VM test runs the real install as a non-root user and executes the launcher directly -> `ci-checks` log shows the VM test steps, `runtime/sce mode: 755`, `launcher mode: 755`, `parsed version: 0.4.0-pre-alpha-v12`; `nix/tests/npm-nixos-vm.nix` inspected: `sce-tester` with uid != 0 asserted, no `SCE_NPM_SKIP_DOWNLOAD`, no copy/chmod into `runtime/`, `useDHCP = false`, `interfaces = { }`, `vlans = [ ]`. +- [x] AC4: `ci-checks` builds `sce-release` once with VM test and audit -> passed; single shared `sce-release` drv; aarch64-darwin `ci-checks` still evaluates. +- [ ] AC5: `release-validation (ubuntu-latest)` runs the VM test with KVM and fails loudly without it -> workflow inspection passes (ubuntu-only `Enable KVM` step exits 1 with `::error::` when `/dev/kvm` is missing; ubuntu-only `extra-conf` system features) and `workflow-actionlint` passed, but the required PR CI run does not exist yet. +- [x] AC6: Injected faults fail for the right reason -> T01/T02 evidence records faults (a) launcher mode, (b) non-executable `--binary` at `sce --help` (host and VM), (c) missing `runtime/sce`, each with its exact `pack-smoke:` message; `git status` shows no fault residue (only the staged T04 context/plan edits). +- [x] AC7: Existing installer negative tests still pass -> `npm-bun-tests` built and passed (also in `nix flake check`). +- [x] AC8: Durable docs cover fixture vs VM, musl rationale, guarantees, macOS/ARM64 release-only coverage, local command, KVM/runner requirements -> all present in `context/sce/cli-npm-distribution-contract.md` (Packed-package smoke test section) and `context/sce/flake-build-performance.md`; command names match `flake.nix`. + +### Failed checks and follow-ups + +- AC5 PR CI run: no PR and no GitHub Actions run exist for `npm-nixos-integration-test` (branch not pushed); evidence: `gh pr list --head npm-nixos-integration-test` returned `[]`, `gh run list` returned nothing; required: commit the staged T04 changes, push the branch, open the PR, and confirm `release-validation (ubuntu-latest)` passes `Enable KVM` and builds `npm-nixos-vm` within the 90-minute budget. + +### Residual risks + +- KVM availability and VM runtime on hosted `ubuntu-latest` are unproven until the PR CI run (plan Open questions). +- Local VM runs used TCG emulation, not KVM. +- `linux/arm64` and `darwin/arm64` npm installs remain covered only by release-pipeline audits and extracted-binary smoke. + +### Retry + +After repairs, rerun: + +`/validate context/plans/npm-nixos-vm-integration-test.md` diff --git a/context/sce/cli-npm-distribution-contract.md b/context/sce/cli-npm-distribution-contract.md index dfe7c19b9..642a724fe 100644 --- a/context/sce/cli-npm-distribution-contract.md +++ b/context/sce/cli-npm-distribution-contract.md @@ -64,7 +64,10 @@ This file captures the implemented npm distribution slice of the `sce` CLI insta - The fixture archive is produced by real GNU `tar`, so the smoke also exercises the in-repo archive reader against the release pipeline's archive format under Node. - Only the host platform is exercised. The production public key and manifest trust code are not modified. - Root `flake.nix` runs the smoke in the build sandbox as `checks..npm-pack-smoke` (part of `nix flake check`; loopback HTTPS works inside the sandbox). The sandbox has no `/usr/bin/env`, so there the installed bin is run as `node ` after its `-x` check. Host runs, including the `release-npm-package` gate, exec the bin directly. -- `nix/tests/npm-nixos-vm.nix` (exposed as `packages.x86_64-linux.npm-nixos-vm` and linked into `.#ci-checks` on x86_64-linux, never a flake check) runs real-binary mode inside a minimal NixOS VM (`pkgs.testers.runNixOSTest`, loopback-only networking) as a non-root user with an isolated HOME, npm cache, and prefix, against `${sceReleasePackage}/bin/sce` and the repo `.version`. It then asserts `runtime/sce` mode `755`, an executable `.bin/sce` resolving to the installed `bin/sce.js`, direct shebang execution of `sce --help`, and the `jq`-parsed `sce version --format json` `.version`. NixOS has no FHS loader or `/usr/lib` glibc, so a passing run shows the static-musl binary needs no unavailable dynamic libraries. +- `nix/tests/npm-nixos-vm.nix` (exposed as `packages.x86_64-linux.npm-nixos-vm`; local-only, neither a flake check nor a `.#ci-checks` member) runs real-binary mode inside a minimal NixOS VM (`pkgs.testers.runNixOSTest`, loopback-only networking) as a non-root user with an isolated HOME, npm cache, and prefix, against `${sceReleasePackage}/bin/sce` and the repo `.version`. It then asserts `runtime/sce` mode `755`, an executable `.bin/sce` resolving to the installed `bin/sce.js`, direct shebang execution of `sce --help`, and the `jq`-parsed `sce version --format json` `.version`. NixOS has no FHS loader or `/usr/lib` glibc, so a passing run shows the static-musl binary needs no unavailable dynamic libraries. +- Fixture mode versus the VM: fixture mode proves the packed package, signing/manifest/checksum/archive flow, and launcher wiring on the host, cheaply on every `nix flake check`, but its stub cannot reveal native linkage problems. The VM installs the actual static-musl `x86_64-unknown-linux-musl` release binary that the archive pipeline ships, so it also proves that binary runs through the npm launcher on a system with no FHS libraries. +- The VM covers only `linux/x64`. `linux/arm64` and `darwin/arm64` npm installs are not executed by any npm-level test; their native archives are covered only by the release pipeline's portability audits and extracted-binary `sce version --format json` smoke before upload. +- Running the VM test locally: `nix build .#npm-nixos-vm --print-build-logs` on x86_64-linux. With `/dev/kvm` it runs accelerated. Without it, QEMU falls back to TCG emulation, which is slower but still passes. CI does not run it: hosted `ubuntu-latest` runners do not expose `/dev/kvm`, so the test is run manually before release-affecting npm or musl-build changes. ## Verification baseline diff --git a/context/sce/flake-build-performance.md b/context/sce/flake-build-performance.md index 40f406b73..7d9119fea 100644 --- a/context/sce/flake-build-performance.md +++ b/context/sce/flake-build-performance.md @@ -9,20 +9,20 @@ The root flake separates ordinary development from release-only work: release on Darwin. - `packages.ci-checks` is the explicit expensive tier. It forces the release package and, on Linux, audits the real release binary for forbidden - `/nix/store/` runtime references. On x86_64-linux it also links - `npm-nixos-vm`, sharing the same `sceReleasePackage` build. + `/nix/store/` runtime references. - `packages.x86_64-linux.npm-nixos-vm` is a NixOS VM test of the real packed - npm install against the release binary; it is not a flake check, so - `nix flake check` never boots a VM. + npm install against the release binary. It is local-only: neither a flake + check nor a `ci-checks` member, so no automated tier boots a VM. Build it with + `nix build .#npm-nixos-vm --print-build-logs`; it runs without `/dev/kvm` + under slower QEMU emulation. - `nix flake check` runs the normal test/lint/format/parity/static checks without building `.#sce-release`. - `devShells.default` provides development tools without `scePackage` or the Turso CLI. `devShells.database` adds Turso. - `.github/workflows/pr-ci.yml` runs native package smoke tests in `nix-ci` and - runs `.#ci-checks` separately in `release-validation` on every commit. Its - `ubuntu-latest` leg enables `/dev/kvm` through a udev rule, fails if KVM is - unavailable, and gives Nix the `nixos-test benchmark big-parallel kvm` system - features through the installer's `extra-conf`. + runs `.#ci-checks` separately in `release-validation` on every commit. Hosted + `ubuntu-latest` runners do not expose `/dev/kvm`, so CI does not run the + NixOS VM test. Git-commit embedding is release-only. Native packages and normal CLI check derivations do not receive `SCE_GIT_COMMIT`; release packages do. diff --git a/flake.nix b/flake.nix index 6f259771f..aaaf2a99c 100644 --- a/flake.nix +++ b/flake.nix @@ -1693,8 +1693,6 @@ ln -s ${sceReleasePackage} "$out/sce-release" ${pkgs.lib.optionalString pkgs.stdenv.isLinux '' ln -s ${releasePortabilityAuditCheck} "$out/release-portability-audit" - ''}${pkgs.lib.optionalString (system == "x86_64-linux") '' - ln -s ${npmNixosVmTest} "$out/npm-nixos-vm" ''} ''; From 9c02eb5160eed7fc85a99a216a3c801f7de52ccb Mon Sep 17 00:00:00 2001 From: davidabram Date: Tue, 3 Nov 2026 23:03:47 +0100 Subject: [PATCH 5/5] tests: Use the production deadline for patch ID error tests Pass deadlines explicitly to the fake Git runner so error-reporting tests use COMMIT_PATCH_ID_DEADLINE. Keep the 500 ms deadline confined to the Linux timeout test that verifies both Git processes are killed. --- cli/src/services/hooks/commit_patch_id.rs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/cli/src/services/hooks/commit_patch_id.rs b/cli/src/services/hooks/commit_patch_id.rs index c5fe61507..0556630f9 100644 --- a/cli/src/services/hooks/commit_patch_id.rs +++ b/cli/src/services/hooks/commit_patch_id.rs @@ -369,10 +369,11 @@ mod tests { use super::{ compute_commit_patch_id, compute_commit_patch_id_with, parse_patch_id_output, - CommitPatchIdRunner, + CommitPatchIdRunner, COMMIT_PATCH_ID_DEADLINE, }; - const FAKE_GIT_DEADLINE: Duration = Duration::from_millis(500); + #[cfg(target_os = "linux")] + const FAKE_GIT_TIMEOUT_DEADLINE: Duration = Duration::from_millis(500); #[cfg(target_os = "linux")] const CHILD_EXIT_TIMEOUT: Duration = Duration::from_secs(10); #[cfg(target_os = "linux")] @@ -730,12 +731,12 @@ mod tests { } } - async fn run_fake_git(fake: &FakeGit) -> anyhow::Result> { + async fn run_fake_git(fake: &FakeGit, deadline: Duration) -> anyhow::Result> { let leading_args = [fake.script.as_os_str()]; let runner = CommitPatchIdRunner { program: OsStr::new("/bin/sh"), leading_args: &leading_args, - deadline: FAKE_GIT_DEADLINE, + deadline, ..CommitPatchIdRunner::default() }; compute_commit_patch_id_with(&runner, Path::new("."), FAKE_COMMIT_OID).await @@ -769,7 +770,9 @@ mod tests { #[tokio::test] async fn commit_patch_id_reports_non_zero_git_exit() { let fake = fake_git("exit 3", "cat >/dev/null"); - let error = run_fake_git(&fake).await.expect_err("non-zero show"); + let error = run_fake_git(&fake, COMMIT_PATCH_ID_DEADLINE) + .await + .expect_err("non-zero show"); assert!( error.to_string().contains("git show exited with status 3"), "{error:#}" @@ -779,7 +782,9 @@ mod tests { #[tokio::test] async fn commit_patch_id_reports_malformed_patch_id_output() { let fake = fake_git("echo patch", "cat >/dev/null; echo not-a-patch-id"); - let error = run_fake_git(&fake).await.expect_err("malformed output"); + let error = run_fake_git(&fake, COMMIT_PATCH_ID_DEADLINE) + .await + .expect_err("malformed output"); assert!(error.to_string().contains("malformed"), "{error:#}"); } @@ -788,7 +793,9 @@ mod tests { async fn commit_patch_id_times_out_and_kills_both_git_processes() { let fake = fake_git("exec sleep 30", "exec cat"); let started = std::time::Instant::now(); - let error = run_fake_git(&fake).await.expect_err("timeout"); + let error = run_fake_git(&fake, FAKE_GIT_TIMEOUT_DEADLINE) + .await + .expect_err("timeout"); assert!(error.to_string().contains("deadline"), "{error:#}"); assert!(started.elapsed() < CHILD_EXIT_TIMEOUT);