* 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
171 lines
6.7 KiB
JavaScript
171 lines
6.7 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.
|
|
|
|
/**
|
|
* Regression tests for fix/2441-sdk-decouple
|
|
*
|
|
* Verifies the architectural invariants introduced by the SDK decouple:
|
|
*
|
|
* (a) bin/install.js does NOT invoke `npm install -g` for the SDK at all.
|
|
* The old `installSdkIfNeeded()` built from source and ran `npm install -g .`
|
|
* in sdk/; the new version only verifies the prebuilt dist.
|
|
*
|
|
* (b) The parent package.json declares a `gsd-sdk` bin entry pointing at
|
|
* bin/gsd-sdk.js (the back-compat shim), so npm chmods it correctly.
|
|
*
|
|
* (c) sdk/dist/ is in the parent package `files` so it ships in the tarball.
|
|
*
|
|
* (d) sdk/package.json `prepublishOnly` runs `rm -rf dist && tsc && chmod +x dist/cli.js`
|
|
* (guards against the mode-644 bug and npm's stale-prepublishOnly issue).
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const { test, describe } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const INSTALL_JS = path.join(__dirname, '..', 'bin', 'install.js');
|
|
const ROOT_PKG = path.join(__dirname, '..', 'package.json');
|
|
const SDK_PKG = path.join(__dirname, '..', 'sdk', 'package.json');
|
|
const GSD_SDK_SHIM = path.join(__dirname, '..', 'bin', 'gsd-sdk.js');
|
|
|
|
const installContent = fs.readFileSync(INSTALL_JS, 'utf-8');
|
|
const rootPkg = JSON.parse(fs.readFileSync(ROOT_PKG, 'utf-8'));
|
|
const sdkPkg = JSON.parse(fs.readFileSync(SDK_PKG, 'utf-8'));
|
|
|
|
describe('fix #2441: SDK decouple — installer no longer builds from source', () => {
|
|
test('bin/install.js does not call npm install -g in sdk/', () => {
|
|
// The old approach ran `npm install -g .` from sdk/. This must be gone.
|
|
// We check for the specific pattern that installed the SDK globally.
|
|
const hasGlobalInstallFromSdk =
|
|
/spawnSync\(npmCmd,\s*\[['"]install['"],\s*['"](-g|--global)['"]/m.test(installContent) &&
|
|
/cwd:\s*sdkDir/.test(installContent);
|
|
assert.ok(
|
|
!hasGlobalInstallFromSdk,
|
|
'bin/install.js must not run `npm install -g .` from sdk/. ' +
|
|
'The SDK is shipped prebuilt in the tarball (fix #2441).'
|
|
);
|
|
});
|
|
|
|
test('bin/install.js does not run npm run build in sdk/', () => {
|
|
// The old approach ran `npm run build` (tsc) at install time.
|
|
const hasBuildStep =
|
|
/spawnSync\(npmCmd,\s*\[['"]run['"],\s*['"]build['"]\]/m.test(installContent) &&
|
|
/cwd:\s*sdkDir/.test(installContent);
|
|
assert.ok(
|
|
!hasBuildStep,
|
|
'bin/install.js must not run `npm run build` in sdk/ at install time. ' +
|
|
'TypeScript compilation happens at publish time via prepublishOnly.'
|
|
);
|
|
});
|
|
|
|
test('installSdkIfNeeded checks sdk/dist/cli.js exists instead of building', () => {
|
|
assert.ok(
|
|
installContent.includes('sdk/dist/cli.js') || installContent.includes("'dist', 'cli.js'"),
|
|
'installSdkIfNeeded() must reference sdk/dist/cli.js to verify the prebuilt dist.'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('fix #2441: back-compat shim — parent package bin entry', () => {
|
|
test('root package.json declares gsd-sdk bin entry', () => {
|
|
assert.ok(
|
|
rootPkg.bin && rootPkg.bin['gsd-sdk'],
|
|
'root package.json must have a bin["gsd-sdk"] entry for the back-compat shim.'
|
|
);
|
|
});
|
|
|
|
test('gsd-sdk bin entry points at bin/gsd-sdk.js', () => {
|
|
assert.equal(
|
|
rootPkg.bin['gsd-sdk'],
|
|
'bin/gsd-sdk.js',
|
|
'bin["gsd-sdk"] must point at bin/gsd-sdk.js'
|
|
);
|
|
});
|
|
|
|
test('bin/gsd-sdk.js shim file exists', () => {
|
|
assert.ok(
|
|
fs.existsSync(GSD_SDK_SHIM),
|
|
'bin/gsd-sdk.js must exist as the back-compat PATH shim.'
|
|
);
|
|
});
|
|
|
|
test('bin/gsd-sdk.js resolves sdk/dist/cli.js relative to itself', () => {
|
|
const shimContent = fs.readFileSync(GSD_SDK_SHIM, 'utf-8');
|
|
// Require the actual path.resolve call with the expected segments, not
|
|
// loose substring matches that would pass from comments or shebangs.
|
|
assert.match(
|
|
shimContent,
|
|
/path\.resolve\(\s*__dirname\s*,\s*['"]\.\.['"]\s*,\s*['"]sdk['"]\s*,\s*['"]dist['"]\s*,\s*['"]cli\.js['"]\s*\)/,
|
|
'bin/gsd-sdk.js must call path.resolve(__dirname, "..", "sdk", "dist", "cli.js") to locate the prebuilt CLI.'
|
|
);
|
|
});
|
|
|
|
test('bin/gsd-sdk.js invokes cli.js via spawnSync(process.execPath, ...)', () => {
|
|
const shimContent = fs.readFileSync(GSD_SDK_SHIM, 'utf-8');
|
|
// The shim must invoke via node (not rely on execute bit), which means
|
|
// spawnSync(process.execPath, [cliPath, ...args]).
|
|
assert.match(
|
|
shimContent,
|
|
/spawnSync\(\s*process\.execPath\s*,/,
|
|
'bin/gsd-sdk.js must spawn node via process.execPath so the execute bit on cli.js is irrelevant (#2453).'
|
|
);
|
|
assert.match(
|
|
shimContent,
|
|
/process\.argv\.slice\(\s*2\s*\)/,
|
|
'bin/gsd-sdk.js must forward user args via process.argv.slice(2).'
|
|
);
|
|
assert.match(
|
|
shimContent,
|
|
/process\.exit\(/,
|
|
'bin/gsd-sdk.js must propagate the child exit status via process.exit.'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('fix #2441: sdk/dist shipped in tarball', () => {
|
|
test('root package.json files includes sdk/dist', () => {
|
|
assert.ok(
|
|
Array.isArray(rootPkg.files) && rootPkg.files.some(f => f === 'sdk/dist' || f.startsWith('sdk/dist')),
|
|
'root package.json files must include "sdk/dist" so the prebuilt CLI ships in the tarball.'
|
|
);
|
|
});
|
|
|
|
test('root package.json files still includes sdk/src (for dev/clone builds)', () => {
|
|
assert.ok(
|
|
Array.isArray(rootPkg.files) && rootPkg.files.some(f => f === 'sdk/src' || f.startsWith('sdk/src')),
|
|
'root package.json files should still include sdk/src for developer builds.'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('fix #2453: sdk/package.json prepublishOnly guards execute bit', () => {
|
|
test('sdk prepublishOnly deletes old dist before build (npm stale-prepublishOnly guard)', () => {
|
|
const prepub = sdkPkg.scripts && sdkPkg.scripts.prepublishOnly;
|
|
assert.ok(
|
|
prepub && prepub.includes('rm -rf dist'),
|
|
'sdk/package.json prepublishOnly must start with `rm -rf dist` to avoid stale build output.'
|
|
);
|
|
});
|
|
|
|
test('sdk prepublishOnly chmods dist/cli.js after tsc', () => {
|
|
const prepub = sdkPkg.scripts && sdkPkg.scripts.prepublishOnly;
|
|
assert.ok(
|
|
prepub && prepub.includes('chmod +x dist/cli.js'),
|
|
'sdk/package.json prepublishOnly must run `chmod +x dist/cli.js` after tsc to fix mode-644 (#2453).'
|
|
);
|
|
});
|
|
|
|
test('sdk prepublishOnly runs tsc', () => {
|
|
const prepub = sdkPkg.scripts && sdkPkg.scripts.prepublishOnly;
|
|
assert.ok(
|
|
prepub && prepub.includes('tsc'),
|
|
'sdk/package.json prepublishOnly must include tsc to compile TypeScript.'
|
|
);
|
|
});
|
|
});
|