Files
ECC/tests/hooks/continuous-learning-shebang-consistency.test.js
Gaurav DubeyandGitHub 8b6543929e chore(continuous-learning-v2): standardize shell shebangs to env bash (#2401)
* chore(continuous-learning-v2): standardize shell shebangs to env bash

Three scripts under skills/continuous-learning-v2/ used the hardcoded
`#!/bin/bash` shebang while the other four already used the portable
`#!/usr/bin/env bash`:

- hooks/observe.sh (runs on every hook invocation)
- scripts/detect-project.sh
- agents/start-observer.sh

The hardcoded interpreter path fails to execute on systems where bash is
not installed at /bin/bash (NixOS, some Homebrew layouts, FreeBSD).
Standardize all three to `#!/usr/bin/env bash`, matching the repo-wide
majority convention, and add a regression test that asserts shebang
uniformity for every shell script in this skill so the inconsistency
cannot reappear.

Fixes #2303

* test(continuous-learning-v2): harden shebang test runner

Address review feedback on the shebang-consistency regression test:

- firstLine() now splits on /\r?\n/ so a script checked out with CRLF
  line endings does not leave a trailing carriage return that would
  break the shebang comparison on Windows.
- The test() helper now surfaces the full error (stack trace, not just
  the message) on failure and writes pass/fail lines via
  process.stdout/stderr so diagnostics are preserved.

* test(continuous-learning-v2): skip hidden dirs in shebang scan

The recursive shell-script scan now skips hidden directories (e.g. the
observer's runtime `.observer-tmp`). This keeps the shebang-consistency
check deterministic: only committed skill scripts are examined, and an
untracked local artifact left over from an observer run can no longer
cause a false failure.
2026-07-03 20:40:44 -07:00

102 lines
3.3 KiB
JavaScript

/**
* Tests for shebang consistency across continuous-learning-v2 shell scripts
*
* Every `*.sh` script under skills/continuous-learning-v2/ must use the
* portable `#!/usr/bin/env bash` shebang rather than the hardcoded
* `#!/bin/bash`. The hardcoded interpreter path fails on systems where bash
* is not installed at /bin/bash (NixOS, some Homebrew layouts, FreeBSD), and
* observe.sh runs on every hook invocation. This guards against the
* inconsistency (#2303) reappearing.
*
* Run with: node tests/hooks/continuous-learning-shebang-consistency.test.js
*/
'use strict';
const assert = require('assert');
const path = require('path');
const fs = require('fs');
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
process.stdout.write(` ✓ ${name}\n`);
passed++;
} catch (err) {
process.stderr.write(` ✗ ${name}\n`);
process.stderr.write(` ${err && err.stack ? err.stack : String(err)}\n`);
failed++;
}
}
const repoRoot = path.resolve(__dirname, '..', '..');
const skillDir = path.join(repoRoot, 'skills', 'continuous-learning-v2');
const PORTABLE_SHEBANG = '#!/usr/bin/env bash';
const HARDCODED_SHEBANG = '#!/bin/bash';
function collectShellScripts(dir, acc = []) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
// Skip hidden/runtime directories (e.g. the observer's `.observer-tmp`)
// so an untracked local artifact cannot trigger a false failure; only
// committed skill scripts are checked.
if (entry.name.startsWith('.')) {
continue;
}
collectShellScripts(fullPath, acc);
} else if (entry.isFile() && entry.name.endsWith('.sh')) {
acc.push(fullPath);
}
}
return acc;
}
function firstLine(filePath) {
return fs.readFileSync(filePath, 'utf8').split(/\r?\n/, 1)[0];
}
console.log('\n=== continuous-learning-v2 shebang consistency ===\n');
const scripts = collectShellScripts(skillDir);
test('skill directory contains shell scripts to check', () => {
assert.ok(scripts.length > 0, `expected at least one .sh under ${skillDir}`);
});
for (const scriptPath of scripts) {
const rel = path.relative(repoRoot, scriptPath).split(path.sep).join('/');
test(`${rel} uses portable '#!/usr/bin/env bash'`, () => {
assert.strictEqual(
firstLine(scriptPath),
PORTABLE_SHEBANG,
`${rel} should start with '${PORTABLE_SHEBANG}'`
);
});
}
test('no continuous-learning-v2 script uses hardcoded #!/bin/bash', () => {
const offenders = scripts
.filter(scriptPath => firstLine(scriptPath) === HARDCODED_SHEBANG)
.map(scriptPath => path.relative(repoRoot, scriptPath).split(path.sep).join('/'));
assert.strictEqual(
offenders.length,
0,
`hardcoded #!/bin/bash found in: ${offenders.join(', ')}`
);
});
// ──────────────────────────────────────────────────────
// Summary
// ──────────────────────────────────────────────────────
console.log('\n=== Test Results ===');
console.log(`Passed: ${passed}`);
console.log(`Failed: ${failed}`);
console.log(`Total: ${passed + failed}\n`);
process.exit(failed > 0 ? 1 : 0);