Wave 2 / Step 4 of the remediation plan (docs/remediation/plan.md). PRIMARY (the real fresh-clone failure): - scripts/analytics/node_modules is gitignored, so a fresh clone has neither tsx nor csv-parse. Surface an idempotent `npm install --silent` prerequisite at point-of-use in report.md (Step 1b) and import.md (Step 4). DEVIATION FROM PLAN (correction-in-scope, to satisfy the plan's own Verify gate): - The plan assumed prepending `npm install` was sufficient. Verified it is NOT: the commands invoke the CLI with an absolute src/cli.ts path but from the user's arbitrary CWD, and `node --import tsx` resolves the `tsx` specifier relative to CWD, not the script. There is no global tsx, so the call still fails with ERR_MODULE_NOT_FOUND from any CWD other than scripts/analytics. - Complete fix: invoke the locally-installed tsx by its absolute node_modules/.bin/tsx path in all CLI calls (report.md x10, import.md x3), so they resolve from any working directory once the install above has run. Verified: 0 ERR_MODULE_NOT_FOUND running `report` from /tmp. SECONDARY (latent correctness / hardening): - Add findPluginRoot(): walks up to the dir holding .claude-plugin/plugin.json and anchors getAnalyticsRoot() on it, falling back to the legacy 4-up count. MEASURED that ../../../../ already resolved to the plugin root from BOTH src/utils and build/utils (both 4 levels deep), so the plan's "src-vs-build depth miscalibration" premise was false — this is correct-by-construction hardening (survives a future source move), not a live-bug fix. - Reconcile cli.ts usage/help text: `node build/cli.js` -> `node --import tsx src/cli.ts` (the real runtime). - Fix report.md troubleshooting: "Verify tsx is available" -> the actual install command on ERR_MODULE_NOT_FOUND. Test-first: scripts/analytics/tests/storage-root.test.ts (red on missing findPluginRoot export, green after). Full suite 106/106, tsc --noEmit clean, structural lint 0 failed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
77 lines
3 KiB
TypeScript
77 lines
3 KiB
TypeScript
import { describe, test, afterEach } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, existsSync } from "node:fs";
|
|
import { join, resolve } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
import { getAnalyticsRoot, findPluginRoot } from "../src/utils/storage.js";
|
|
|
|
// Regression lock for the fresh-clone / foreign-CWD analytics root resolution.
|
|
// The root must anchor on the .claude-plugin/plugin.json marker (correct by
|
|
// construction), NOT on a fragile count of "../" segments that silently breaks
|
|
// if the source layout is ever moved.
|
|
describe("analytics root resolution", () => {
|
|
let tempDir: string | undefined;
|
|
const savedEnv = process.env.ANALYTICS_ROOT;
|
|
|
|
afterEach(() => {
|
|
if (tempDir && existsSync(tempDir)) {
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
}
|
|
tempDir = undefined;
|
|
if (savedEnv === undefined) {
|
|
delete process.env.ANALYTICS_ROOT;
|
|
} else {
|
|
process.env.ANALYTICS_ROOT = savedEnv;
|
|
}
|
|
});
|
|
|
|
describe("findPluginRoot", () => {
|
|
test("walks up to the directory containing .claude-plugin/plugin.json", () => {
|
|
tempDir = mkdtempSync(join(tmpdir(), "plugin-root-"));
|
|
mkdirSync(join(tempDir, ".claude-plugin"), { recursive: true });
|
|
writeFileSync(join(tempDir, ".claude-plugin", "plugin.json"), "{}");
|
|
// Mimic the real depth: scripts/analytics/src/utils
|
|
const start = join(tempDir, "scripts", "analytics", "src", "utils");
|
|
mkdirSync(start, { recursive: true });
|
|
|
|
assert.equal(findPluginRoot(start), tempDir);
|
|
});
|
|
|
|
test("returns null when no marker exists up-tree", () => {
|
|
tempDir = mkdtempSync(join(tmpdir(), "no-marker-"));
|
|
const start = join(tempDir, "a", "b", "c");
|
|
mkdirSync(start, { recursive: true });
|
|
|
|
assert.equal(findPluginRoot(start), null);
|
|
});
|
|
});
|
|
|
|
describe("getAnalyticsRoot", () => {
|
|
test("honors ANALYTICS_ROOT override (resolved env path)", () => {
|
|
tempDir = mkdtempSync(join(tmpdir(), "analytics-root-"));
|
|
process.env.ANALYTICS_ROOT = tempDir;
|
|
|
|
assert.equal(getAnalyticsRoot(), resolve(tempDir));
|
|
});
|
|
|
|
test("default (no env) anchors on the plugin dir, not scripts/analytics/assets", () => {
|
|
delete process.env.ANALYTICS_ROOT;
|
|
|
|
const root = getAnalyticsRoot();
|
|
const suffix = join("assets", "analytics");
|
|
assert.ok(root.endsWith(suffix), `expected to end with ${suffix}, got ${root}`);
|
|
|
|
// The parent of assets/analytics must be the real plugin root (holds the marker),
|
|
// proving the root is NOT scripts/analytics/assets/analytics.
|
|
const pluginRoot = root.slice(0, root.length - (suffix.length + 1));
|
|
assert.ok(
|
|
existsSync(join(pluginRoot, ".claude-plugin", "plugin.json")),
|
|
`plugin marker missing under resolved root parent: ${pluginRoot}`,
|
|
);
|
|
assert.ok(
|
|
!pluginRoot.endsWith(join("scripts", "analytics")),
|
|
`root wrongly anchored under scripts/analytics: ${pluginRoot}`,
|
|
);
|
|
});
|
|
});
|
|
});
|