Each test fails on fd7de23:
- own-working-tree: venv site-packages, vendor/, config-dir skills/
(punkt 1); ~/.claude/plugins next to $CLAUDE_CONFIG_DIR and a leading
~ in the variable (punkt 2); NODE_MODULES case variant and a
case-mismatched parent segment (punkt 3, closes v8.1.2 punkt 4).
- watch-cron-scope: a watched project's own ignore file is honored
(punkt 5).
- av-surface (b2): no runnable base64-to-shell line with a short
command blob (addendum a; 3 hits today).
- doc-consistency: scanner-reference Knowledge Files matches knowledge/
(addendum b); ci-cd-guide makes no offline claim (addendum c).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
325 lines
14 KiB
JavaScript
325 lines
14 KiB
JavaScript
// own-working-tree.test.mjs — isOwnWorkingTree() rule (v8.1.1).
|
|
//
|
|
// v8.1.0 treated any target at or below the process cwd as the caller's own
|
|
// working tree. That is too wide: a foreign clone that sits UNDER cwd (cwd =
|
|
// $HOME, or a project with a cloned vendor dir) still had its .llm-security
|
|
// config read. v8.1.1 narrows the rule: the target must be at or below cwd
|
|
// AND have the same git root as cwd — the nearest ancestor holding a `.git`
|
|
// (directory for a clone, file for a submodule/worktree), or none for both.
|
|
// A nested clone therefore counts as foreign. The failure direction is safe:
|
|
// foreign means the target's config is ignored, so more findings, never fewer.
|
|
//
|
|
// Fixtures live under $HOME (outside os.tmpdir(), which is always foreign,
|
|
// and outside this repo's git tree); $HOME itself must not be a git root.
|
|
|
|
import { describe, it, before, after } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { join } from 'node:path';
|
|
import { mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs';
|
|
import { homedir } from 'node:os';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { isOwnWorkingTree } from '../../scanners/lib/own-working-tree.mjs';
|
|
import { mkOwnTreeDir } from '../helpers/own-tree.mjs';
|
|
|
|
function gitInit(dir) {
|
|
mkdirSync(dir, { recursive: true });
|
|
const r = spawnSync('git', ['init', '-q', dir], { encoding: 'utf8' });
|
|
assert.equal(r.status, 0, `git init failed: ${r.stderr}`);
|
|
}
|
|
|
|
function ownFrom(cwd, target) {
|
|
const prev = process.cwd();
|
|
process.chdir(cwd);
|
|
try {
|
|
return isOwnWorkingTree(target);
|
|
} finally {
|
|
process.chdir(prev);
|
|
}
|
|
}
|
|
|
|
describe('isOwnWorkingTree(): same git root as cwd (v8.1.1)', () => {
|
|
let root;
|
|
let repo;
|
|
let homeish;
|
|
|
|
before(() => {
|
|
assert.ok(!existsSync(join(homedir(), '.git')), 'precondition: $HOME is not a git root');
|
|
root = mkOwnTreeDir('owt-unit-');
|
|
// A repo the user works in, with a plain subdir, a nested clone and a
|
|
// submodule-style checkout (`.git` is a file).
|
|
repo = join(root, 'repo');
|
|
gitInit(repo);
|
|
mkdirSync(join(repo, 'sub', 'deeper'), { recursive: true });
|
|
gitInit(join(repo, 'vendor', 'clone'));
|
|
mkdirSync(join(repo, 'vendor', 'clone', 'inner'), { recursive: true });
|
|
mkdirSync(join(repo, 'submod'), { recursive: true });
|
|
writeFileSync(join(repo, 'submod', '.git'), 'gitdir: ../.git/modules/submod\n');
|
|
// A $HOME-like dir: no git root, holding one clone and one plain dir.
|
|
homeish = join(root, 'homeish');
|
|
mkdirSync(join(homeish, 'plain'), { recursive: true });
|
|
gitInit(join(homeish, 'clone'));
|
|
});
|
|
|
|
after(() => { rmSync(root, { recursive: true, force: true }); });
|
|
|
|
it('cwd itself is own', () => {
|
|
assert.equal(ownFrom(repo, repo), true);
|
|
assert.equal(ownFrom(repo, '.'), true);
|
|
});
|
|
|
|
it('(b) a subdir of the same repo is own (known-positive)', () => {
|
|
assert.equal(ownFrom(repo, join(repo, 'sub')), true);
|
|
assert.equal(ownFrom(join(repo, 'sub'), join(repo, 'sub', 'deeper')), true);
|
|
});
|
|
|
|
it('(a) a nested clone under the repo is foreign', () => {
|
|
assert.equal(ownFrom(repo, join(repo, 'vendor', 'clone')), false);
|
|
});
|
|
|
|
it('(a) a subdir inside a nested clone is foreign', () => {
|
|
assert.equal(ownFrom(repo, join(repo, 'vendor', 'clone', 'inner')), false);
|
|
});
|
|
|
|
it('a submodule/worktree checkout (`.git` file) is foreign', () => {
|
|
assert.equal(ownFrom(repo, join(repo, 'submod')), false);
|
|
});
|
|
|
|
it('(c) a clone under a cwd with no git root is foreign', () => {
|
|
assert.equal(ownFrom(homeish, join(homeish, 'clone')), false);
|
|
});
|
|
|
|
it('a plain subdir of a cwd with no git root is own', () => {
|
|
assert.equal(ownFrom(homeish, join(homeish, 'plain')), true);
|
|
});
|
|
|
|
it('a clone the user has cd\'d into is own (it is cwd)', () => {
|
|
assert.equal(ownFrom(join(homeish, 'clone'), '.'), true);
|
|
});
|
|
|
|
it('a target outside cwd is foreign', () => {
|
|
assert.equal(ownFrom(join(repo, 'sub'), repo), false);
|
|
});
|
|
});
|
|
|
|
// v8.1.2: a target with no `.git` of its own under cwd shares cwd's git root,
|
|
// so the v8.1.1 rule alone calls it own. Two concrete places foreign code lands
|
|
// under a user's working directory without a `.git`: an installed package
|
|
// (a `node_modules` segment on the path from cwd to the target) and Claude
|
|
// Code's plugin directory (`$CLAUDE_CONFIG_DIR/plugins`, default
|
|
// `~/.claude/plugins` — cache/ and marketplaces/). Both are foreign now.
|
|
describe('isOwnWorkingTree(): node_modules and the plugin dir are foreign (v8.1.2)', () => {
|
|
let root;
|
|
let repo;
|
|
let savedConfigDir;
|
|
let savedHome;
|
|
|
|
before(() => {
|
|
root = mkOwnTreeDir('owt-812-');
|
|
repo = join(root, 'repo');
|
|
gitInit(repo);
|
|
mkdirSync(join(repo, 'node_modules', 'evil-pkg', 'lib'), { recursive: true });
|
|
mkdirSync(join(repo, 'node_modules', '@scope', 'pkg'), { recursive: true });
|
|
mkdirSync(join(repo, 'packages', 'app', 'node_modules', 'dep'), { recursive: true });
|
|
mkdirSync(join(repo, 'my-node_modules-notes'), { recursive: true });
|
|
// A config dir with a plugin-cache copy (no `.git`, like 234/235 real ones)
|
|
// and a plugin config dir sitting in a plain (no git root) parent.
|
|
mkdirSync(join(root, 'cfg', 'plugins', 'cache', 'mkt', 'evil', '1.0.0'), { recursive: true });
|
|
mkdirSync(join(root, 'cfg', 'plugins', 'marketplaces', 'mkt'), { recursive: true });
|
|
mkdirSync(join(root, 'cfg', 'projects'), { recursive: true });
|
|
mkdirSync(join(root, 'fakehome', '.claude', 'plugins', 'cache', 'mkt', 'p', '2.0.0'), { recursive: true });
|
|
savedConfigDir = process.env.CLAUDE_CONFIG_DIR;
|
|
savedHome = process.env.HOME;
|
|
});
|
|
|
|
after(() => {
|
|
if (savedConfigDir === undefined) delete process.env.CLAUDE_CONFIG_DIR;
|
|
else process.env.CLAUDE_CONFIG_DIR = savedConfigDir;
|
|
process.env.HOME = savedHome;
|
|
rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
it('(a) an installed package under the repo is foreign', () => {
|
|
assert.equal(ownFrom(repo, join(repo, 'node_modules', 'evil-pkg')), false);
|
|
assert.equal(ownFrom(repo, join(repo, 'node_modules', 'evil-pkg', 'lib')), false);
|
|
assert.equal(ownFrom(repo, join(repo, 'node_modules', '@scope', 'pkg')), false);
|
|
assert.equal(ownFrom(repo, join(repo, 'node_modules')), false);
|
|
});
|
|
|
|
it('(a) a nested node_modules (workspace package) is foreign', () => {
|
|
assert.equal(ownFrom(repo, join(repo, 'packages', 'app', 'node_modules', 'dep')), false);
|
|
});
|
|
|
|
it('(c) the workspace package itself and a look-alike name stay own (known-positive)', () => {
|
|
assert.equal(ownFrom(repo, join(repo, 'packages', 'app')), true);
|
|
assert.equal(ownFrom(repo, join(repo, 'my-node_modules-notes')), true);
|
|
});
|
|
|
|
it('a package the user has cd\'d into is own (only the path from cwd counts)', () => {
|
|
assert.equal(ownFrom(join(repo, 'node_modules', 'evil-pkg'), '.'), true);
|
|
assert.equal(ownFrom(join(repo, 'node_modules', 'evil-pkg'), 'lib'), true);
|
|
});
|
|
|
|
it('(b) a copy under $CLAUDE_CONFIG_DIR/plugins/cache is foreign', () => {
|
|
process.env.CLAUDE_CONFIG_DIR = join(root, 'cfg');
|
|
const target = join(root, 'cfg', 'plugins', 'cache', 'mkt', 'evil', '1.0.0');
|
|
assert.equal(ownFrom(root, target), false);
|
|
assert.equal(ownFrom(join(root, 'cfg'), target), false);
|
|
assert.equal(ownFrom(target, '.'), false);
|
|
});
|
|
|
|
it('(b) a clone under $CLAUDE_CONFIG_DIR/plugins/marketplaces is foreign', () => {
|
|
process.env.CLAUDE_CONFIG_DIR = join(root, 'cfg');
|
|
assert.equal(ownFrom(join(root, 'cfg'), join(root, 'cfg', 'plugins', 'marketplaces', 'mkt')), false);
|
|
});
|
|
|
|
it('(b) a relative CLAUDE_CONFIG_DIR is resolved against cwd', () => {
|
|
process.env.CLAUDE_CONFIG_DIR = 'cfg';
|
|
assert.equal(ownFrom(root, join(root, 'cfg', 'plugins', 'cache', 'mkt', 'evil', '1.0.0')), false);
|
|
});
|
|
|
|
it('(c) the rest of the config dir stays own (known-positive)', () => {
|
|
process.env.CLAUDE_CONFIG_DIR = join(root, 'cfg');
|
|
assert.equal(ownFrom(join(root, 'cfg'), join(root, 'cfg', 'projects')), true);
|
|
});
|
|
|
|
it('(b) without CLAUDE_CONFIG_DIR the default is ~/.claude/plugins', () => {
|
|
delete process.env.CLAUDE_CONFIG_DIR;
|
|
process.env.HOME = join(root, 'fakehome');
|
|
try {
|
|
const home = join(root, 'fakehome');
|
|
assert.equal(ownFrom(home, join(home, '.claude', 'plugins', 'cache', 'mkt', 'p', '2.0.0')), false);
|
|
assert.equal(ownFrom(home, join(home, '.claude')), true);
|
|
} finally {
|
|
process.env.HOME = savedHome;
|
|
}
|
|
});
|
|
});
|
|
|
|
// v8.1.3 (order 20260923T092223Z, PM decisions):
|
|
// punkt 1 — other install locations are foreign like `node_modules`: a
|
|
// `site-packages` segment (Python venv) or a `vendor` segment
|
|
// (composer/bundler/Go) on the path from cwd to the target, and anything
|
|
// under a Claude Code config dir's `skills/` (third-party skills copied
|
|
// into a git-tracked ~/.claude share its git root).
|
|
// punkt 2 — BOTH `~/.claude` and `$CLAUDE_CONFIG_DIR` count as config dirs;
|
|
// a leading `~` in the variable is expanded to the home dir.
|
|
// punkt 3 — case is canonicalized (realpathSync.native), so `NODE_MODULES/x`
|
|
// on a case-insensitive volume is the `node_modules` it names, and a
|
|
// case-mismatched path to an own subdir is own (§ v8.1.2 punkt 4).
|
|
describe('isOwnWorkingTree(): v8.1.3 install locations, config dirs, case', () => {
|
|
let root;
|
|
let repo;
|
|
let savedConfigDir;
|
|
let savedHome;
|
|
|
|
before(() => {
|
|
root = mkOwnTreeDir('owt-813-');
|
|
repo = join(root, 'repo');
|
|
gitInit(repo);
|
|
mkdirSync(join(repo, '.venv', 'lib', 'python3.12', 'site-packages', 'evilpkg'), { recursive: true });
|
|
mkdirSync(join(repo, 'vendor', 'acme', 'lib'), { recursive: true });
|
|
mkdirSync(join(repo, 'my-vendor-notes'), { recursive: true });
|
|
mkdirSync(join(repo, 'node_modules', 'evil'), { recursive: true });
|
|
mkdirSync(join(repo, 'Sub'), { recursive: true });
|
|
// A git-tracked config dir (cwd = it) with third-party skills and its own
|
|
// non-skill content, plus a $HOME-like dir with a default ~/.claude.
|
|
gitInit(join(root, 'cfg'));
|
|
mkdirSync(join(root, 'cfg', 'skills', 'third-party'), { recursive: true });
|
|
mkdirSync(join(root, 'cfg', 'agents'), { recursive: true });
|
|
mkdirSync(join(root, 'fakehome', '.claude', 'plugins', 'cache', 'mkt', 'p', '1.0.0'), { recursive: true });
|
|
mkdirSync(join(root, 'fakehome', '.claude', 'skills', 'x'), { recursive: true });
|
|
mkdirSync(join(root, 'fakehome', 'alt', 'plugins', 'cache', 'mkt', 'q'), { recursive: true });
|
|
mkdirSync(join(root, 'fakehome', 'work'), { recursive: true });
|
|
savedConfigDir = process.env.CLAUDE_CONFIG_DIR;
|
|
savedHome = process.env.HOME;
|
|
});
|
|
|
|
after(() => {
|
|
if (savedConfigDir === undefined) delete process.env.CLAUDE_CONFIG_DIR;
|
|
else process.env.CLAUDE_CONFIG_DIR = savedConfigDir;
|
|
process.env.HOME = savedHome;
|
|
rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
it('punkt 1: a package in a Python venv site-packages is foreign', () => {
|
|
assert.equal(ownFrom(repo, join(repo, '.venv', 'lib', 'python3.12', 'site-packages', 'evilpkg')), false);
|
|
});
|
|
|
|
it('punkt 1: a composer/bundler vendor/ package is foreign', () => {
|
|
assert.equal(ownFrom(repo, join(repo, 'vendor', 'acme')), false);
|
|
assert.equal(ownFrom(repo, join(repo, 'vendor', 'acme', 'lib')), false);
|
|
});
|
|
|
|
it('punkt 1: the venv root and a look-alike name stay own (known-positive)', () => {
|
|
assert.equal(ownFrom(repo, join(repo, '.venv')), true);
|
|
assert.equal(ownFrom(repo, join(repo, 'my-vendor-notes')), true);
|
|
});
|
|
|
|
it('punkt 1: skills in a git-tracked config dir are foreign, the rest stays own', () => {
|
|
process.env.CLAUDE_CONFIG_DIR = join(root, 'cfg');
|
|
try {
|
|
assert.equal(ownFrom(join(root, 'cfg'), join(root, 'cfg', 'skills', 'third-party')), false);
|
|
assert.equal(ownFrom(join(root, 'cfg'), join(root, 'cfg', 'agents')), true);
|
|
} finally {
|
|
if (savedConfigDir === undefined) delete process.env.CLAUDE_CONFIG_DIR;
|
|
else process.env.CLAUDE_CONFIG_DIR = savedConfigDir;
|
|
}
|
|
});
|
|
|
|
it('punkt 1: skills under the default ~/.claude are foreign', () => {
|
|
delete process.env.CLAUDE_CONFIG_DIR;
|
|
process.env.HOME = join(root, 'fakehome');
|
|
try {
|
|
const home = join(root, 'fakehome');
|
|
assert.equal(ownFrom(home, join(home, '.claude', 'skills', 'x')), false);
|
|
} finally {
|
|
process.env.HOME = savedHome;
|
|
}
|
|
});
|
|
|
|
it('punkt 2: ~/.claude/plugins stays foreign when CLAUDE_CONFIG_DIR points at another profile', () => {
|
|
process.env.HOME = join(root, 'fakehome');
|
|
process.env.CLAUDE_CONFIG_DIR = join(root, 'fakehome', 'alt');
|
|
try {
|
|
const home = join(root, 'fakehome');
|
|
assert.equal(ownFrom(home, join(home, '.claude', 'plugins', 'cache', 'mkt', 'p', '1.0.0')), false);
|
|
assert.equal(ownFrom(home, join(home, 'alt', 'plugins', 'cache', 'mkt', 'q')), false);
|
|
assert.equal(ownFrom(home, join(home, 'work')), true);
|
|
} finally {
|
|
process.env.HOME = savedHome;
|
|
if (savedConfigDir === undefined) delete process.env.CLAUDE_CONFIG_DIR;
|
|
else process.env.CLAUDE_CONFIG_DIR = savedConfigDir;
|
|
}
|
|
});
|
|
|
|
it('punkt 2: a leading ~ in CLAUDE_CONFIG_DIR is expanded to the home dir', () => {
|
|
process.env.HOME = join(root, 'fakehome');
|
|
process.env.CLAUDE_CONFIG_DIR = '~/alt';
|
|
try {
|
|
const home = join(root, 'fakehome');
|
|
assert.equal(ownFrom(home, join(home, 'alt', 'plugins', 'cache', 'mkt', 'q')), false);
|
|
} finally {
|
|
process.env.HOME = savedHome;
|
|
if (savedConfigDir === undefined) delete process.env.CLAUDE_CONFIG_DIR;
|
|
else process.env.CLAUDE_CONFIG_DIR = savedConfigDir;
|
|
}
|
|
});
|
|
|
|
it('punkt 3: a case variant of node_modules is foreign (case-insensitive volume)', (t) => {
|
|
if (!existsSync(join(repo, 'NODE_MODULES'))) {
|
|
t.skip('volume is case-sensitive: NODE_MODULES is a different directory');
|
|
return;
|
|
}
|
|
assert.equal(ownFrom(repo, join(repo, 'NODE_MODULES', 'evil')), false);
|
|
});
|
|
|
|
it('punkt 3: a case-mismatched path to an own subdir is own (§ v8.1.2 punkt 4)', (t) => {
|
|
if (!existsSync(join(root, 'REPO'))) {
|
|
t.skip('volume is case-sensitive: REPO is not repo');
|
|
return;
|
|
}
|
|
// The mismatch sits in a parent segment: realpathSync keeps `REPO`, so the
|
|
// target did not start with cwd and was called foreign.
|
|
assert.equal(ownFrom(repo, join(root, 'REPO', 'Sub')), true);
|
|
});
|
|
});
|