Files
msd-core/tests/bug-2698-crlf-install.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

163 lines
6.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.
/**
* Regression test for #2698: CRLF line endings break agent-block strip regexes
*
* The legacy `gsd-update-check` hook migration in bin/install.js uses two
* separate .replace() calls:
* 1. LF-only regex: /\n# GSD Hooks\n\[\[hooks\]\]\nevent = ...\n/
* 2. CRLF-only regex: /\r\n# GSD Hooks\r\n\[\[hooks\]\]\r\nevent = ...\r\n/
*
* These patterns fail when config.toml has mixed line endings — e.g. the
* "# GSD Hooks" header uses LF but the body uses CRLF, or vice versa. This
* can happen when the file is created cross-platform (Windows/Linux), when
* editors convert only part of the file, or when a previous GSD version wrote
* the block with different EOL than the file's dominant EOL.
*
* Fix: consolidate to a single \r?\n-aware regex that handles LF, CRLF, and
* any mix in a single pass, making the migration robust regardless of the
* platform the file was last written on.
*
* Test approach: write a `.codex/config.toml` with a stale gsd-update-check
* block that uses mixed line endings (header in LF, body in CRLF), then run
* install() and assert the stale block is gone.
*
* Note: The local Codex install writes to `.codex/` in the current directory.
* Tests `process.chdir(tmpDir)` and write fixtures to `tmpDir/.codex/`.
*/
'use strict';
process.env.GSD_TEST_MODE = '1';
const { describe, test, before, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const os = require('os');
const { execFileSync } = require('child_process');
const INSTALL_SRC = path.join(__dirname, '..', 'bin', 'install.js');
const BUILD_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js');
const { install, GSD_CODEX_MARKER } = require(INSTALL_SRC);
// Ensure hooks/dist/ is populated before install tests
before(() => {
execFileSync(process.execPath, [BUILD_SCRIPT], {
encoding: 'utf-8',
stdio: 'pipe',
});
});
describe('#2698: CRLF stale gsd-update-check block is removed on Codex reinstall', () => {
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-crlf-install-2698-'));
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
// Helper: pre-populate .codex/config.toml with a GSD marker + stale hooks block
// using the given line ending for the stale hooks block header, and a potentially
// different EOL for the hooks body. This exercises the cross-platform mixed scenario.
function writeCodexConfigWithStaleHooks(dir, headerEol, bodyEol) {
// Build the stale block with header EOL for the "# GSD Hooks" line, but body EOL
// for the content lines (simulates a file edited by two different platforms).
const staleBlock = [
'# GSD Hooks', // line that starts the stale section
'[[hooks]]',
'event = "SessionStart"',
'command = "node /old/path/gsd-update-check.js"',
].join(bodyEol);
// Put the stale block in user content BEFORE the GSD marker. The GSD marker area
// will be regenerated by mergeCodexConfig during install(); the stale block in
// the user area is what the hooks migration must remove.
const content = [
'[features]',
'codex_hooks = true',
'',
].join(headerEol) + headerEol + staleBlock + headerEol + headerEol + GSD_CODEX_MARKER + headerEol;
const codexDir = path.join(dir, '.codex');
fs.mkdirSync(codexDir, { recursive: true });
const configPath = path.join(codexDir, 'config.toml');
fs.writeFileSync(configPath, content, 'utf-8');
return configPath;
}
test('LF config.toml: stale gsd-update-check block removed on reinstall', (t) => {
const origCwd = process.cwd();
t.after(() => { process.chdir(origCwd); });
process.chdir(tmpDir);
writeCodexConfigWithStaleHooks(tmpDir, '\n', '\n');
install(false, 'codex');
const configPath = path.join(tmpDir, '.codex', 'config.toml');
const content = fs.readFileSync(configPath, 'utf-8');
assert.ok(
!content.includes('gsd-update-check'),
'Stale gsd-update-check entry must be removed from LF config.toml (#2698)'
);
assert.ok(
content.includes('gsd-check-update'),
'New gsd-check-update hook must appear after reinstall'
);
});
test('CRLF config.toml: stale gsd-update-check block removed on reinstall', (t) => {
const origCwd = process.cwd();
t.after(() => { process.chdir(origCwd); });
process.chdir(tmpDir);
writeCodexConfigWithStaleHooks(tmpDir, '\r\n', '\r\n');
install(false, 'codex');
const configPath = path.join(tmpDir, '.codex', 'config.toml');
const content = fs.readFileSync(configPath, 'utf-8');
assert.ok(
!content.includes('gsd-update-check'),
'Stale gsd-update-check entry must be removed from CRLF config.toml (#2698)'
);
assert.ok(
content.includes('gsd-check-update'),
'New gsd-check-update hook must appear after reinstall'
);
});
test('mixed-EOL config.toml: stale block with LF header but CRLF body removed on reinstall', (t) => {
// This is the primary failure case: header line uses LF but the body uses CRLF.
// The old LF-only regex requires all-\n separators; the old CRLF-only regex requires
// all-\r\n separators. Neither matches a block with mixed endings, so the stale
// block survives reinstall with the old code (#2698).
const origCwd = process.cwd();
t.after(() => { process.chdir(origCwd); });
process.chdir(tmpDir);
// headerEol='\n' (file dominant), bodyEol='\r\n' (hook block from another platform)
writeCodexConfigWithStaleHooks(tmpDir, '\n', '\r\n');
install(false, 'codex');
const configPath = path.join(tmpDir, '.codex', 'config.toml');
const content = fs.readFileSync(configPath, 'utf-8');
assert.ok(
!content.includes('gsd-update-check'),
[
'Stale gsd-update-check block with mixed LF/CRLF endings must be removed (#2698).',
'Old code used two separate LF-only and CRLF-only regexes; neither matched mixed content.',
'Fix consolidates to a single \\r?\\n-aware regex.',
].join(' ')
);
});
});