[E00-S01-T10] Strict base tsconfig committed #377

Merged
kpcto merged 1 commits from feature/163 into main 2026-08-28 22:36:30 +00:00
+170
View File
@@ -0,0 +1,170 @@
/**
* Strict tsconfig test — locks in the [E00-S01-T10] strict base tsconfig
* committed at the workspace root and shared by every workspace package.
*
* Acceptance criteria covered (each test fails without the committed config):
* - "strict base tsconfig is committed" → root `tsconfig.base.json` exists
* and carries `strict: true` plus the strict family the workspace relies
* on (`noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`,
* `noImplicitOverride`, `useUnknownInCatchVariables`,
* `verbatimModuleSyntax`) and NodeNext module resolution; every workspace
* package (apps/server, packages/core, extensions/example) commits a
* `tsconfig.json` that extends that root base config.
* - "all packages compile under the strict base config" → running
* `tsc -p <pkg>/tsconfig.json --noEmit` with the workspace-resolved
* TypeScript (exact 6.0.3, locked in by E00-S01-T09) exits 0 for every
* workspace package.
* - (non-vacuousness) a scratch file that violates the strict family fails
* to compile under the committed base config with the expected
* strict-family error codes, proving the flags are enforced rather than
* merely declared.
*
* Run: `node --test tests/strict-tsconfig.test.mjs`
* (node:test — built into Node >= 18; no dependencies, lockfile untouched.)
*/
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const read = (relPath) => readFileSync(path.join(REPO_ROOT, relPath), 'utf8');
/** The workspace packages that must compile under the strict base config. */
const WORKSPACE_PACKAGES = ['apps/server', 'packages/core', 'extensions/example'];
/** The strict-family flags the committed base config must set. */
const STRICT_FAMILY_FLAGS = [
'strict',
'noUncheckedIndexedAccess',
'exactOptionalPropertyTypes',
'noImplicitOverride',
'useUnknownInCatchVariables',
'verbatimModuleSyntax',
];
/** The TypeScript compiler resolved by the frozen install (exact 6.0.3, E00-S01-T09). */
const TSC_BIN = path.join(REPO_ROOT, 'node_modules', 'typescript', 'bin', 'tsc');
/**
* Runs the workspace-resolved TypeScript against a tsconfig (the same binary
* the package `typecheck` scripts use; see E00-S01-T05/T09), executed by the
* Node runtime running this test so it works from any cwd.
*/
function runTsc(cwd, args) {
return spawnSync(process.execPath, [TSC_BIN, ...args], {
cwd,
encoding: 'utf8',
timeout: 120_000,
});
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
test('tsconfig.base.json is committed with the strict family enabled', () => {
const base = JSON.parse(read('tsconfig.base.json'));
assert.ok(
base && typeof base === 'object' && base.compilerOptions,
'tsconfig.base.json must be a committed JSON object declaring compilerOptions',
);
for (const flag of STRICT_FAMILY_FLAGS) {
assert.equal(
base.compilerOptions[flag],
true,
`tsconfig.base.json must set "${flag}": true (strict family)`,
);
}
assert.equal(
base.compilerOptions.module,
'NodeNext',
'tsconfig.base.json must set module to NodeNext (ESM boundary source of truth)',
);
assert.equal(
base.compilerOptions.moduleResolution,
'NodeNext',
'tsconfig.base.json must set moduleResolution to NodeNext',
);
assert.equal(
base.compilerOptions.target,
'ES2023',
'tsconfig.base.json must set target to ES2023',
);
});
test('every workspace package tsconfig.json extends the committed base config', () => {
for (const pkg of WORKSPACE_PACKAGES) {
const tsconfig = JSON.parse(read(path.join(pkg, 'tsconfig.json')));
assert.equal(
tsconfig.extends,
'../../tsconfig.base.json',
`"${pkg}/tsconfig.json" must extend the committed root tsconfig.base.json`,
);
}
});
test('every workspace package compiles under the strict base config (tsc --noEmit)', () => {
for (const pkg of WORKSPACE_PACKAGES) {
const result = runTsc(path.join(REPO_ROOT, pkg), ['-p', 'tsconfig.json', '--noEmit']);
assert.equal(
result.status,
0,
`"${pkg}" must compile under the strict base config:\n${(result.stderr || result.stdout || '').trim()}`,
);
}
});
test('the committed base config enforces the strict family (non-vacuous probe)', () => {
const scratch = mkdtempSync(path.join(tmpdir(), 'strict-base-probe-'));
try {
writeFileSync(
path.join(scratch, 'tsconfig.json'),
JSON.stringify(
{
extends: path.join(REPO_ROOT, 'tsconfig.base.json'),
compilerOptions: { noEmit: true },
include: ['.'],
},
null,
2,
),
);
writeFileSync(
path.join(scratch, 'probe.ts'),
[
'// Each line below violates one strict-family flag of tsconfig.base.json.',
'function implicitAny(param) { return param; } // TS7006 - noImplicitAny (strict)',
'const items: string[] = [];',
'const first = items[0].toUpperCase(); // TS2532 - noUncheckedIndexedAccess',
'interface Options { port?: number; }',
'const opts: Options = { port: undefined }; // TS2375 - exactOptionalPropertyTypes',
'class Base { greet(): string { return "hi"; } }',
'class Derived extends Base { greet(): string { return "yo"; } } // TS4114 - noImplicitOverride',
'try { JSON.parse(""); } catch (err) { const m = err.message; } // TS18046 - useUnknownInCatchVariables',
'',
].join('\n'),
);
const result = runTsc(scratch, ['-p', 'tsconfig.json', '--noEmit']);
assert.notEqual(
result.status,
0,
'a file violating the strict family must fail to compile under the committed base config',
);
const output = (result.stderr || result.stdout || '');
for (const code of ['TS7006', 'TS2532', 'TS2375', 'TS4114', 'TS18046']) {
assert.ok(
output.includes(code),
`probe output must contain ${code} (strict family not enforced):\n${output}`,
);
}
} finally {
rmSync(scratch, { recursive: true, force: true });
}
});