Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD across contents and paths, upstream package/repo coordinates -> @golem15/msd-core and golem15com/msd-core. Deep links into upstream history, sibling upstream packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is. Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line, package/plugin identity, regenerated lockfile, install-tree fixtures, derived registries and benchmark baseline; migration checksum baseline re-locked (MSD keeps its own install state, so no install had applied the old sums); sort-order and regex-escaped expectations in tests adjusted.
342 lines
10 KiB
JavaScript
342 lines
10 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* no-private-binary-resolution.rule.test.cjs
|
|
*
|
|
* RuleTester unit tests for the local/no-private-binary-resolution ESLint rule.
|
|
* Ids (V1-V8, I1-I9) map to .msd/phase/chore-3619-no-bare-binary-spawn/50-test-matrix.md.
|
|
*
|
|
* RuleTester feeds fixtures to the rule directly and does not scan this test
|
|
* file's own source, so the self-flagging problem that forced eslint.config.mjs
|
|
* to carve the eslint-rules directory out of the scripts .cjs block does not
|
|
* arise here (ADR-1703 rule 5 / 40-design.md).
|
|
*/
|
|
|
|
const { test, describe } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { RuleTester } = require('eslint');
|
|
|
|
const rule = require('../eslint-rules/no-private-binary-resolution.cjs');
|
|
|
|
const ruleTester = new RuleTester({
|
|
languageOptions: {
|
|
ecmaVersion: 2022,
|
|
sourceType: 'commonjs',
|
|
},
|
|
});
|
|
|
|
const OUTSIDE_SEAM_FILE = 'src/some-other-module.cts';
|
|
const SEAM_FILE = 'src/shell-command-projection.cts';
|
|
|
|
// ─── module shape ─────────────────────────────────────────────────────────────
|
|
|
|
describe('no-private-binary-resolution rule module', () => {
|
|
test('exports meta and create', () => {
|
|
assert.strictEqual(typeof rule.meta, 'object');
|
|
assert.strictEqual(typeof rule.create, 'function');
|
|
assert.strictEqual(rule.meta.type, 'problem');
|
|
assert.ok(rule.meta.messages.pathextRead, 'pathextRead message must exist');
|
|
assert.ok(rule.meta.messages.extensionList, 'extensionList message must exist');
|
|
});
|
|
});
|
|
|
|
// ─── VALID cases (V1-V8) ────────────────────────────────────────────────────
|
|
|
|
describe('no-private-binary-resolution: valid cases', () => {
|
|
test('V1: process.env.PATHEXT in src/shell-command-projection.cts — the seam is exempt', () => {
|
|
ruleTester.run('no-private-binary-resolution', rule, {
|
|
valid: [
|
|
{
|
|
code: `const ext = process.env.PATHEXT;`,
|
|
filename: SEAM_FILE,
|
|
},
|
|
],
|
|
invalid: [],
|
|
});
|
|
});
|
|
|
|
test("V2: ['.exe'] — one extension is classification, not a candidate list", () => {
|
|
ruleTester.run('no-private-binary-resolution', rule, {
|
|
valid: [
|
|
{
|
|
code: `const exts = ['.exe'];`,
|
|
filename: OUTSIDE_SEAM_FILE,
|
|
},
|
|
],
|
|
invalid: [],
|
|
});
|
|
});
|
|
|
|
test("V3: p.endsWith('.cmd') — one extension", () => {
|
|
ruleTester.run('no-private-binary-resolution', rule, {
|
|
valid: [
|
|
{
|
|
code: `const isCmd = p.endsWith('.cmd');`,
|
|
filename: OUTSIDE_SEAM_FILE,
|
|
},
|
|
],
|
|
invalid: [],
|
|
});
|
|
});
|
|
|
|
test("V4: scriptPath.replace(/\\.js$/, '.cmd') — shim-path derivation (runtime-hooks-surface.cts shape)", () => {
|
|
ruleTester.run('no-private-binary-resolution', rule, {
|
|
valid: [
|
|
{
|
|
code: `const shimPath = scriptPath.replace(/\\.js$/, '.cmd');`,
|
|
filename: OUTSIDE_SEAM_FILE,
|
|
},
|
|
],
|
|
invalid: [],
|
|
});
|
|
});
|
|
|
|
test('V5: pathEnv.split(path.delimiter) — PATH scans are deliberately not flagged', () => {
|
|
ruleTester.run('no-private-binary-resolution', rule, {
|
|
valid: [
|
|
{
|
|
code: `const segments = pathEnv.split(path.delimiter);`,
|
|
filename: OUTSIDE_SEAM_FILE,
|
|
},
|
|
],
|
|
invalid: [],
|
|
});
|
|
});
|
|
|
|
test('V6: process.env.PATH — only PATHEXT is the signal', () => {
|
|
ruleTester.run('no-private-binary-resolution', rule, {
|
|
valid: [
|
|
{
|
|
code: `const p = process.env.PATH;`,
|
|
filename: OUTSIDE_SEAM_FILE,
|
|
},
|
|
],
|
|
invalid: [],
|
|
});
|
|
});
|
|
|
|
test("V7: ['.exe', '.txt'] — one exe extension plus an unrelated one, below the two-or-more threshold", () => {
|
|
ruleTester.run('no-private-binary-resolution', rule, {
|
|
valid: [
|
|
{
|
|
code: `const exts = ['.exe', '.txt'];`,
|
|
filename: OUTSIDE_SEAM_FILE,
|
|
},
|
|
],
|
|
invalid: [],
|
|
});
|
|
});
|
|
|
|
test("V8: '.execute'/'.compacting' are substrings only — the src/host-integration.cts:724 false-positive fix", () => {
|
|
ruleTester.run('no-private-binary-resolution', rule, {
|
|
valid: [
|
|
{
|
|
code: `const OPENCODE_EXTENSION_EVENTS = ['.execute', '.compacting'];`,
|
|
filename: OUTSIDE_SEAM_FILE,
|
|
},
|
|
],
|
|
invalid: [],
|
|
});
|
|
});
|
|
|
|
test('V9: const { PATH } = process.env; — PATH is not the signal', () => {
|
|
ruleTester.run('no-private-binary-resolution', rule, {
|
|
valid: [
|
|
{
|
|
code: `const { PATH } = process.env;`,
|
|
filename: OUTSIDE_SEAM_FILE,
|
|
},
|
|
],
|
|
invalid: [],
|
|
});
|
|
});
|
|
|
|
test('V10: const { [key]: v } = process.env; — computed key is not statically decidable', () => {
|
|
ruleTester.run('no-private-binary-resolution', rule, {
|
|
valid: [
|
|
{
|
|
code: `const { [key]: v } = process.env;`,
|
|
filename: OUTSIDE_SEAM_FILE,
|
|
},
|
|
],
|
|
invalid: [],
|
|
});
|
|
});
|
|
});
|
|
|
|
// ─── INVALID cases (I1-I9) ──────────────────────────────────────────────────
|
|
|
|
describe('no-private-binary-resolution: invalid cases', () => {
|
|
test('I1: process.env.PATHEXT outside the seam — 1 error', () => {
|
|
ruleTester.run('no-private-binary-resolution', rule, {
|
|
valid: [],
|
|
invalid: [
|
|
{
|
|
code: `const ext = process.env.PATHEXT;`,
|
|
filename: OUTSIDE_SEAM_FILE,
|
|
errors: [{ messageId: 'pathextRead' }],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test("I2: process.env['PATHEXT'] — bracket form", () => {
|
|
ruleTester.run('no-private-binary-resolution', rule, {
|
|
valid: [],
|
|
invalid: [
|
|
{
|
|
code: `const ext = process.env['PATHEXT'];`,
|
|
filename: OUTSIDE_SEAM_FILE,
|
|
errors: [{ messageId: 'pathextRead' }],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test("I3: env['Pathext'] — Windows env names are case-insensitive", () => {
|
|
ruleTester.run('no-private-binary-resolution', rule, {
|
|
valid: [],
|
|
invalid: [
|
|
{
|
|
code: `const ext = env['Pathext'];`,
|
|
filename: OUTSIDE_SEAM_FILE,
|
|
errors: [{ messageId: 'pathextRead' }],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test('I4: opts.env.pathext — lower case, non-process receiver', () => {
|
|
ruleTester.run('no-private-binary-resolution', rule, {
|
|
valid: [],
|
|
invalid: [
|
|
{
|
|
code: `const ext = opts.env.pathext;`,
|
|
filename: OUTSIDE_SEAM_FILE,
|
|
errors: [{ messageId: 'pathextRead' }],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test("I5: ['a.exe','a.cmd','a.bat','a'] — the deleted fallow-runner shape verbatim", () => {
|
|
ruleTester.run('no-private-binary-resolution', rule, {
|
|
valid: [],
|
|
invalid: [
|
|
{
|
|
code: `const candidates = ['a.exe', 'a.cmd', 'a.bat', 'a'];`,
|
|
filename: OUTSIDE_SEAM_FILE,
|
|
errors: [{ messageId: 'extensionList' }],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test("I6: '.EXE;.CMD;.BAT;.COM' — the deleted msd-tools shape verbatim", () => {
|
|
ruleTester.run('no-private-binary-resolution', rule, {
|
|
valid: [],
|
|
invalid: [
|
|
{
|
|
code: `const exts = '.EXE;.CMD;.BAT;.COM';`,
|
|
filename: OUTSIDE_SEAM_FILE,
|
|
errors: [{ messageId: 'extensionList' }],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test("I7: ['.cmd', '.bat'] — exactly two, the threshold boundary from below", () => {
|
|
ruleTester.run('no-private-binary-resolution', rule, {
|
|
valid: [],
|
|
invalid: [
|
|
{
|
|
code: `const exts = ['.cmd', '.bat'];`,
|
|
filename: OUTSIDE_SEAM_FILE,
|
|
errors: [{ messageId: 'extensionList' }],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test('I8: a file that trips BOTH signals — two errors, not one', () => {
|
|
ruleTester.run('no-private-binary-resolution', rule, {
|
|
valid: [],
|
|
invalid: [
|
|
{
|
|
code: `
|
|
const ext = process.env.PATHEXT;
|
|
const candidates = ['a.exe', 'a.cmd'];
|
|
`,
|
|
filename: OUTSIDE_SEAM_FILE,
|
|
errors: 2,
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test('I9: process.env.PATHEXT in a file whose path merely CONTAINS the seam name as a substring — still errors (path-anchored, not substring-matched)', () => {
|
|
ruleTester.run('no-private-binary-resolution', rule, {
|
|
valid: [],
|
|
invalid: [
|
|
{
|
|
code: `const ext = process.env.PATHEXT;`,
|
|
filename: 'tests/shell-command-projection-dispatch.test.cjs',
|
|
errors: [{ messageId: 'pathextRead' }],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test('I10: const { PATHEXT } = process.env; — destructuring evades a MemberExpression-only check', () => {
|
|
ruleTester.run('no-private-binary-resolution', rule, {
|
|
valid: [],
|
|
invalid: [
|
|
{
|
|
code: `const { PATHEXT } = process.env;`,
|
|
filename: OUTSIDE_SEAM_FILE,
|
|
errors: [{ messageId: 'pathextRead' }],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test('I11: const { PATHEXT: exts } = process.env; — renamed destructuring', () => {
|
|
ruleTester.run('no-private-binary-resolution', rule, {
|
|
valid: [],
|
|
invalid: [
|
|
{
|
|
code: `const { PATHEXT: exts } = process.env;`,
|
|
filename: OUTSIDE_SEAM_FILE,
|
|
errors: [{ messageId: 'pathextRead' }],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test('I12: const { Pathext } = opts.env; — casing plus non-process receiver', () => {
|
|
ruleTester.run('no-private-binary-resolution', rule, {
|
|
valid: [],
|
|
invalid: [
|
|
{
|
|
code: `const { Pathext } = opts.env;`,
|
|
filename: OUTSIDE_SEAM_FILE,
|
|
errors: [{ messageId: 'pathextRead' }],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test("I13: const { 'PATHEXT': v } = env; — string-key destructuring", () => {
|
|
ruleTester.run('no-private-binary-resolution', rule, {
|
|
valid: [],
|
|
invalid: [
|
|
{
|
|
code: `const { 'PATHEXT': v } = env;`,
|
|
filename: OUTSIDE_SEAM_FILE,
|
|
errors: [{ messageId: 'pathextRead' }],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
});
|