Commit Graph

6 Commits

Author SHA1 Message Date
Tom Boucher
3a98ce83e5 fix(3597): windows parity batch — sdk/install/hook/worktree clusters (21 files)
Three parallel diagnostic agents (G: sdk/install path-sep, H: hook scripts,
I: worktree/workspace) characterised the remaining windows-22 failures on
23b52f1a. Patches by class:

CRLF in test parsers / file-content reads
  - bug-2136-sh-hook-version: shebang check split('\n') → split(/\r?\n/)
  - bug-3542-executor-git-stash-prohibition: strip \r from read content
    (git rewrites stashed text with CRLF on win autocrlf=true checkout)
  - workspace: BOM-strip + explicit \r strip in parseCommandFile (BOM at
    byte 0 defeats /^---/ anchor → fmMatch null → fm.name undefined)

Windows path-separator / 8.3-shortname normalization
  - bug-3491-nested-git-worktree: use fs.realpathSync.native to expand
    %TEMP% RUNNER~1 → runneradmin; normalize sep before path-equality
  - prune-orphaned-worktrees: normalize \\→/ before substring includes
    (git emits forward-slash in --porcelain on Windows even when
    path.join produced backslashes)
  - bug-3017-codex-hook-absolute-node: accept POSIX path OR path with
    drive-letter prefix in hookPath equality assertion
  - bug-3126-global-skills-base-runtime-path: use path.join for expected
    /xdg/<runtime> values (production calls path.join → \xdg\… on win32)

Test under-specified platform / forgot win32 env
  - bug-2979-hook-absolute-node: pass {platform:'linux'} to
    buildHookCommand + rewriteLegacyManagedNodeHookCommands so the
    POSIX-branch tests don't pick up the #3393 GitBash code path
  - bug-3288-model-catalog + bug-3571-config-manifest: also set
    USERPROFILE alongside HOME so os.homedir() on win32 redirects to
    the test fixture instead of the runner's real ~

External-cmd resolution
  - bug-2647-outer-tarball-sdk-dist: use npm.cmd + {shell:true} on win32
    so execFileSync resolves PATHEXT (literal `npm` is ENOENT)

ESM loader: tests/runtime-bridge-sync-smoke.test.cjs already migrated to
pathToFileURL in 23b52f1a (cluster E).

Explicit per-test/describe skip on win32 (with required string reasons
to satisfy no-unconditional-win32-skip guard)
  - feat-3347-graphify-auto-update-hook: 3 describes — harness spawns
    bash/kill/sleep + the hook itself is bash
  - feat-3595-fs-fault-injection: move \t and \n filenames into the
    POSIX-only branch (NTFS forbids 0x00–0x1F in filenames)
  - bug-2775/2829/3033/3231/3359: POSIX shim under ~/.local/bin with
    chmod 0o755 — not how Windows install works
  - bug-3211: single subtest where cp.execSync reassignment isn't
    picked up on win32 (POSIX coverage via the mock; live windows
    behavior covered by 3211-D which keeps running)
  - install-path-detection: parses sh-style export PATH= rc files;
    Windows has no rc files (registry Path)
  - worktree-safety-policy: single test using POSIX /repo/wt fixture
    paths that can't be expressed under win32 path.resolve

Validated: plex2 (ubuntu docker) 11224/0 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 12:16:20 -04:00
Tom Boucher
6ea0051672 fix(tests): add structural-regression-guard annotation for shebang assertion
The shebang check added in #3105 falls under structural-regression-guard:
the portability constraint (#!/usr/bin/env bash vs #!/bin/bash) cannot
be caught at runtime on distros that have /bin/bash. Adding the annotation
satisfies the adversarial review finding that new tests cannot use
pending-migration-to-typed-ir as their exemption category.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 15:42:46 -04:00
Otavio Salvador
8ca86b5e24 fix: use #!/usr/bin/env bash in community .sh hooks for distro portability
The three opt-in bash hooks (gsd-phase-boundary.sh, gsd-session-state.sh,
gsd-validate-commit.sh) shipped with #!/bin/bash, which fails on distros
that don't ship bash at /bin/bash (NixOS, minimal Alpine images, some
container runtimes). POSIX guarantees /bin/sh but not /bin/bash.

This is latent in the default install path because Claude Code wires the
hooks as `bash <path>` from settings.json (PATH-resolved — the script's
own shebang is read as a comment by bash). The fix matters when scripts
are run directly: tests, future installer changes, or manual debugging.

Changes:
- hooks/gsd-{phase-boundary,session-state,validate-commit}.sh: shebang
  switched to #!/usr/bin/env bash, matching the convention already used
  in scripts/*.sh.
- tests/bug-2136-sh-hook-version.test.cjs: assertion updated to expect
  the new shebang; comment updated to spell out the rationale.
- tests/bug-2979-hook-absolute-node.test.cjs: doc-comment updated — the
  prior wording cited "POSIX std PATH always has /bin" as the reason
  bare `bash` is OK. The actual reason is that bare `bash` is
  PATH-resolved, which is portable across distros that don't ship
  /bin/bash. POSIX std PATH guarantees /bin/sh, not /bin/bash.
- bin/install.js::buildHookCommand: comment block clarifying the same.
  No behavior change in this file — bare `bash` was already correct.
- .changeset/portable-bash-shebang-hooks.md: changeset entry.

Verified locally on NixOS:
- npm run build:hooks: hooks/dist/*.sh shebangs propagate correctly.
- node --test tests/bug-2136-*.cjs tests/bug-2979-*.cjs
  tests/bug-1817-*.cjs tests/bug-1834-*.cjs tests/bug-1906-*.cjs
  tests/bug-2557-*.cjs tests/bug-3017-*.cjs tests/security-scan.test.cjs
  tests/hooks-doc-parity.test.cjs: 126/126 pass.
- node scripts/run-tests.cjs (full suite): 6944 pass / 0 fail / 5 skip.
2026-05-06 15:41:27 -04:00
Tom Boucher
918f987a19 feat(#2982): extend no-source-grep lint to catch var-binding readFileSync.includes() (#2985)
* feat(#2982): extend no-source-grep lint to catch var-binding readFileSync.includes()

The base lint (scripts/lint-no-source-grep.cjs) only catches
readFileSync(...).<text-method>() chained directly. The much more
common var-binding form escapes it:

  const src = fs.readFileSync(p, 'utf8');
  // 50 lines later
  if (src.includes('foo')) {}        // ← still grep, lint missed it

Scan of the test suite found ~141 files using this pattern.

Implementation built TDD per #2982 with structured-IR assertions:

  scripts/lint-no-source-grep-extras.cjs
    - detectVarBindingViolations(src) — pure detector, two passes:
      pass 1 collects vars bound from readFileSync, pass 2 finds any
      <var>.<includes|startsWith|endsWith|match|search>( on those vars.
    - detectWrappedAssertOkMatch(src) — flags
      assert.ok(<expr>.match(...)) which escapes the assert.match rule.
    - VIOLATION enum exposes stable codes for tests to assert on.

  scripts/lint-no-source-grep.cjs
    - Wires the new detectors into the existing per-file check; one
      additional violation row per file with the first 3 sample tokens.

  tests/bug-2982-lint-var-binding.test.cjs
    - 13 tests, all assertions on typed VIOLATION enum / structured
      records. Covers all 5 text-match methods, multi-var, no-bind,
      string literal (must NOT trigger), wrapped assert.ok(.match),
      and assert.match (must NOT double-flag).

Migration backlog (#2974 expanded scope):

  - 42 files annotated `// allow-test-rule: source-text-is-the-product`
    (legitimate — they read .md/.json/.yml files whose deployed text
    IS the product)
  - 3 files annotated `// allow-test-rule: pending-migration-to-typed-ir [#2974]`
    (read .cjs/.js source — clear migration debt)
  - 95 files annotated `pending-migration-to-typed-ir [#2974]` with
    `Per-file review may reclassify as source-text-is-the-product
    during migration` (mixed — manual review under #2974)

After this lands the lint reports 0 violations on main; new
violations in PRs surface immediately.

Closes #2982
Refs #2974

* test(#2982): fix truncated test name per CR

The label ended with a bare '(' from a copy-paste mishap. Now reads
'does NOT flag .matchAll(...) — matchAll is not match, so
assert.ok(.matchAll(...)) is not flagged'.

* chore(#2982): add changeset fragment for PR #2985

* chore(#2982): add changeset fragment for PR #2985
2026-05-01 19:50:10 -04:00
Jeremy McSpadden
04fab926b5 test: add --no-sdk to hook-deployment installer tests
Tests #1834, #1924, #2136 exercise hook/artifact deployment and don't
care about SDK install. Now that installSdkIfNeeded() failures are
fatal, these tests fail on any CI runner without gsd-sdk pre-built
because the sdk/ tsc build path runs and can fail in CI env.

Pass --no-sdk so each test focuses on its actual subject. SDK install
path has dedicated end-to-end coverage in install-smoke.yml.
2026-04-19 18:39:32 -05:00
Tom Boucher
50f61bfd9a fix(hooks): complete stale-hooks false-positive fix — stamp .sh version headers + fix detector regex (#2224)
* fix(hooks): stamp gsd-hook-version in .sh hooks and fix stale detection regex (#2136, #2206)

Three-part fix for the persistent "⚠ stale hooks — run /gsd-update" false
positive that appeared on every session after a fresh install.

Root cause: the stale-hook detector (gsd-check-update.js) could only match
the JS comment syntax // in its version regex — never the bash # syntax used
in .sh hooks. And the bash hooks had no version header at all, so they always
landed in the "unknown / stale" branch regardless.

Neither partial fix (PR #2207 regex only, PR #2215 install stamping only) was
sufficient alone:
  - Regex fix without install stamping: hooks install with literal
    "{{GSD_VERSION}}", the {{-guard silently skips them, bash hook staleness
    permanently undetectable after future updates.
  - Install stamping without regex fix: hooks are stamped correctly with
    "# gsd-hook-version: 1.36.0" but the detector's // regex can't read it;
    still falls to the unknown/stale branch on every session.

Fix:
  1. Add "# gsd-hook-version: {{GSD_VERSION}}" header to
     gsd-phase-boundary.sh, gsd-session-state.sh, gsd-validate-commit.sh
  2. Extend install.js (both bundled and Codex paths) to substitute
     {{GSD_VERSION}} in .sh files at install time (same as .js hooks)
  3. Extend gsd-check-update.js versionMatch regex to handle bash "#"
     comment syntax: /(?:\/\/|#) gsd-hook-version:\s*(.+)/

Tests: 11 new assertions across 5 describe blocks covering all three fix
parts independently plus an E2E install+detect round-trip. 3885/3885 pass.

Approach credit: PR #2207 (j2h4u / Maxim Brashenko) for the regex fix;
PR #2215 (nitsan2dots) for the install.js substitution approach.

Closes #2136, #2206, #2209, #2210, #2212

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(hooks): extract check-update worker to dedicated file, eliminating template-literal regex escaping

Move stale-hook detection logic from inline `node -e '<template literal>'` subprocess
to a standalone gsd-check-update-worker.js. Benefits:
- Regex is plain JS with no double-escaping (root cause of the (?:\\/\\/|#) confusion)
- Worker is independently testable and can be read directly by tests
- Uses execFileSync (array args) to satisfy security hook that blocks execSync
- MANAGED_HOOKS now includes gsd-check-update-worker.js itself

Update tests to read worker file instead of main hook for regex/configDir assertions.
All 3886 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 17:57:38 -04:00