diff --git a/CHANGELOG.md b/CHANGELOG.md index 45b72b75..7978a055 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), `npm run check:changelog` holds this file's shape; the CI step "CHANGELOG per-PR entry check" holds each PR's diff. Details and known costs: CONTRIBUTING.md, "CHANGELOG entries". --> +## [6.32.1] - 2026-09-28 + +### Fixed + +- 2026-09-28: **The 6.31.2 L6 `projectRoot()` test passes on macOS, and now tests what its title says** + ([`pharn/floor/run-marker.test.mjs`](./pharn/floor/run-marker.test.mjs)). The change is test-only, but every + merge to `main` is a release, so it ships as its own patch version: `SKILLS_VERSION` 6.32.0 → 6.32.1, with the + README badge. No product-surface byte changes; `MIN_CLI` is unchanged. + - **The failure.** On macOS `os.tmpdir()` is under `/var`, a symlink to `/private/var`. `projectRoot()` walks up + from `process.cwd()`, which the OS reports resolved, so it returned `/private/var/…` while the test expected the + `mkdtemp` spelling `/var/…`. Linux CI has no such symlink, so the test only ever failed locally. The resolved + root is correct: the hooks' `workTreeRoot()` resolves the same way, and `run-marker.mjs` is unchanged. + - **A second defect, found while fixing it.** The fixture also held a `.git`, which stops the walk on its own. So + the test passed with the `CLAUDE_PROJECT_DIR` branch deleted from a copy of `run-marker.mjs` (measured, with + only the `realpathSync` fix applied). + - **Fix.** The test compares against `realpathSync(root)`, and the marker check uses the same resolved root. It + now runs three phases on one fixture: no stop (a negative control, not root), `CLAUDE_PROJECT_DIR` alone + (root), then a `.git` alone (root). Measured on copies of `run-marker.mjs`: deleting the + `CLAUDE_PROJECT_DIR` stop fails the second phase, and deleting the `.git` stop fails the third. + ## [6.32.0] - 2026-09-28 ### Changed diff --git a/README.md b/README.md index 596f1052..28a9fabb 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ model or human judgment remains advisory. npx @pharn-dev/pharn@latest init ``` -[![pharn](https://img.shields.io/badge/pharn-6.32.0-blue)](./CHANGELOG.md) +[![pharn](https://img.shields.io/badge/pharn-6.32.1-blue)](./CHANGELOG.md) [![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-green)](./LICENSE) [![CI](https://github.com/pharn-dev/pharn-oss/actions/workflows/ci.yml/badge.svg)](https://github.com/pharn-dev/pharn-oss/actions/workflows/ci.yml) [![CodeQL](https://github.com/pharn-dev/pharn-oss/actions/workflows/codeql.yml/badge.svg)](https://github.com/pharn-dev/pharn-oss/actions/workflows/codeql.yml) diff --git a/SKILLS_VERSION b/SKILLS_VERSION index d6b71af5..45d6fb34 100644 --- a/SKILLS_VERSION +++ b/SKILLS_VERSION @@ -1 +1 @@ -6.32.0 +6.32.1 diff --git a/pharn/floor/run-marker.test.mjs b/pharn/floor/run-marker.test.mjs index 2ff7a20c..9a380e5f 100644 --- a/pharn/floor/run-marker.test.mjs +++ b/pharn/floor/run-marker.test.mjs @@ -4,13 +4,13 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, symlinkSync, existsSync, utimesSync } from "node:fs"; +import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, symlinkSync, existsSync, utimesSync, realpathSync } from "node:fs"; import { join, dirname, resolve } from "node:path"; import { spawnSync } from "node:child_process"; import { tmpdir } from "node:os"; import { fileURLToPath } from "node:url"; -import { RUN_MARKER_COMMANDS, markerPath, openRun, closeRun } from "./run-marker.mjs"; +import { RUN_MARKER_COMMANDS, markerPath, openRun, closeRun, projectRoot } from "./run-marker.mjs"; // A command's text is its file plus its parts (6.32.0): pharn-ship.md's --close line sits in its close part. import { commandFamilyText } from "../../.dev/floor/command-family.mjs"; @@ -563,22 +563,33 @@ test("✧ pinnedLine() executes the WHOLE line — an appended `|| true` would b ); }); -test("★ L6: projectRoot() follows CLAUDE_PROJECT_DIR when cwd is a subdirectory", async () => { +test("★ L6: projectRoot() follows CLAUDE_PROJECT_DIR, or a .git entry, when cwd is a subdirectory", () => { const root = tmp(); - mkdirSync(join(root, ".git"), { recursive: true }); - writeFileSync(join(root, ".git", "HEAD"), "ref: refs/heads/main\n"); const sub = join(root, "pkg"); mkdirSync(sub, { recursive: true }); + // projectRoot() walks up from process.cwd(), which the OS reports resolved, and stops at + // realpath($CLAUDE_PROJECT_DIR) — the hooks' workTreeRoot(). On macOS tmpdir() is under /var, a symlink + // to /private/var, so the mkdtemp spelling is not what comes back; compare against the resolved root. + const realRoot = realpathSync(root); const prior = process.cwd(); const envDir = process.env.CLAUDE_PROJECT_DIR; try { process.chdir(sub); + // Negative control: no .git and no CLAUDE_PROJECT_DIR, so nothing stops the walk at root. With a .git + // in the fixture from the start, the CLAUDE_PROJECT_DIR assertion held with the variable unset too. + delete process.env.CLAUDE_PROJECT_DIR; + assert.notEqual(projectRoot(), realRoot, "control: without either stop the walk does not end at root"); + process.env.CLAUDE_PROJECT_DIR = root; - const { projectRoot } = await import(`./run-marker.mjs?subroot=${Date.now()}`); - assert.equal(projectRoot(), root); + assert.equal(projectRoot(), realRoot); const opened = openRun({ root: projectRoot(), command: "pharn-ship", name: "sub-run" }); assert.equal(opened.ok, true); - assert.ok(existsSync(markerPath(root, "pharn-ship", "sub-run"))); + assert.ok(existsSync(markerPath(realRoot, "pharn-ship", "sub-run"))); + + delete process.env.CLAUDE_PROJECT_DIR; + mkdirSync(join(root, ".git"), { recursive: true }); + writeFileSync(join(root, ".git", "HEAD"), "ref: refs/heads/main\n"); + assert.equal(projectRoot(), realRoot, "a .git entry is a stop on its own"); } finally { process.chdir(prior); if (envDir === undefined) delete process.env.CLAUDE_PROJECT_DIR;