CI / Frozen lockfile install (pull_request) Successful in 43s
CI / Secrets not embedded (E00-S02-T08) (pull_request) Successful in 30s
CI / Database-postgres import isolation (E00-S03-T02) (pull_request) Successful in 31s
CI / Compose config (E00-S03-T01) (pull_request) Successful in 24s
- database-postgres-imports.test.mjs: static scan of every workspace package source proves pg/kysely import specifiers resolve only to packages/database-postgres; owner manifest pins the driver and no other package declares it; mutation probes prove the scan catches a driver import injected into apps/server/src/index.ts; comment-stripping and specifier matcher unit probes; CI-enforcement assertion - workspace-layout / workspace-config / strict-tsconfig / typescript-pin: package-set fixtures updated to include packages/database-postgres - docs/development/non-container.md: workspace package table and build expectations updated for the new package
171 lines
6.4 KiB
JavaScript
171 lines
6.4 KiB
JavaScript
/**
|
|
* 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', 'packages/database-postgres', '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 });
|
|
}
|
|
});
|