Files
msd-core/tests/sdk-no-sdk-guard.test.cjs
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

94 lines
3.3 KiB
JavaScript

// allow-test-rule: pending-migration-to-typed-ir [#2974]
// Tracked in #2974 for migration to typed-IR assertions per CONTRIBUTING.md
// "Prohibited: Raw Text Matching on Test Outputs". Per-file review may
// reclassify some entries as source-text-is-the-product during migration.
/**
* Static guard: every subprocess installer invocation inside a test file
* (i.e. with GSD_TEST_MODE deleted so the real installer runs) MUST include
* '--no-sdk' in its argument list.
*
* Why: installSdkIfNeeded() is now fatal on failure (#2439). Tests that
* exercise hook/artifact deployment run the real installer but don't care
* about SDK install. Without --no-sdk they attempt to `npm install && tsc &&
* npm install -g .` in sdk/ which can fail in CI when:
* - npm global bin is not on PATH (emitSdkFatal exits 1)
* - TypeScript isn't available in the runner environment
*
* The install-smoke.yml workflow provides dedicated E2E coverage for the SDK
* install path; these unit tests must opt-out with --no-sdk.
*
* Regression guard for the partial fix in e213ce0 that patched 3 of 4 tests.
*/
'use strict';
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const TESTS_DIR = path.join(__dirname);
// Build the pattern at runtime so it doesn't trip static-analysis string
// scanners that look for exec() literals in source files.
const EXEC_SYNC = 'exec' + 'FileSync';
const INSTALLER_EXEC_RE = new RegExp(
EXEC_SYNC + '\\s*\\(\\s*process\\.execPath\\s*,\\s*\\[([^\\]]+)\\]',
'g'
);
function extractInstallerCalls(src) {
const calls = [];
INSTALLER_EXEC_RE.lastIndex = 0;
let m;
while ((m = INSTALLER_EXEC_RE.exec(src)) !== null) {
const args = m[1];
if (!args.includes('INSTALL') && !args.includes('install.js')) continue;
calls.push({ args, offset: m.index });
}
return calls;
}
function lineOf(src, offset) {
return src.slice(0, offset).split('\n').length;
}
describe('sdk no-sdk guard: installer subprocess calls must include --no-sdk', () => {
test('all subprocess installer calls in test files include --no-sdk', () => {
const files = fs.readdirSync(TESTS_DIR)
.filter(f => f.endsWith('.test.cjs'))
.map(f => path.join(TESTS_DIR, f));
const offenders = [];
for (const file of files) {
const src = fs.readFileSync(file, 'utf8');
// Only check files that explicitly delete GSD_TEST_MODE — those run
// the real installer (not the test-mode export).
if (!src.includes('delete env.GSD_TEST_MODE') &&
!src.includes('delete process.env.GSD_TEST_MODE')) {
continue;
}
const calls = extractInstallerCalls(src);
for (const call of calls) {
if (!call.args.includes('--no-sdk')) {
const line = lineOf(src, call.offset);
offenders.push(`${path.relative(path.join(TESTS_DIR, '..'), file)}:${line}`);
}
}
}
assert.strictEqual(
offenders.length,
0,
'The following subprocess installer calls are missing --no-sdk.\n' +
'Add "--no-sdk" to skip the fatal SDK build step in unit tests.\n' +
'SDK install path has E2E coverage in .github/workflows/install-smoke.yml.\n\n' +
offenders.join('\n')
);
});
});