Apply user priority: save/recovery/privacy/custody assurance after all rules. Add quick types/lint, concise failure-preserving reporter, verbose mode, actual empty-execution guard, signal handling and focused verify name selection; compact canonical context/handoffs without mass moves. Tooling15, six negative CLI, named/empty source-bound smokes,206 doc links; same15-case278B vs2455B,2.35s vs2.36s no compute speedclaim. Native Advanced Ix/CHOAM47-card Stronghold profile: actual firstMentat/HMS/sixeffects,18native/four legalpolicies,five genuine2..6games3129actions/no rejection, human390px LAUDR2WNv14 bank2/personal1 plan and real battle. Earlier162/6513 checks before assurance amendment; finalquick25/build/guide pass. No new preservation/privacy audit or public/deployed/full-mode claim.
253 lines
8.8 KiB
TypeScript
253 lines
8.8 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
cpSync,
|
|
mkdtempSync,
|
|
mkdirSync,
|
|
rmSync,
|
|
symlinkSync,
|
|
writeFileSync,
|
|
} from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { spawn, spawnSync } from 'node:child_process';
|
|
import { fileURLToPath } from 'node:url';
|
|
import {
|
|
classifyTest,
|
|
discoverTests,
|
|
selectTests,
|
|
} from '../tools/test-discovery';
|
|
|
|
void test('discovery includes new and nested tests, excluding fixture helpers', (t) => {
|
|
const root = mkdtempSync(join(tmpdir(), 'dune-discovery-'));
|
|
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
mkdirSync(join(root, 'tests/nested'), { recursive: true });
|
|
writeFileSync(join(root, 'tests/rules.test.ts'), '');
|
|
writeFileSync(join(root, 'tests/fixture.ts'), '');
|
|
writeFileSync(
|
|
join(root, 'tests/nested/restart.test.ts'),
|
|
"import { DatabaseSync } from 'node:sqlite';",
|
|
);
|
|
writeFileSync(
|
|
join(root, 'tests/http.test.ts'),
|
|
'// @dune-suite integration\n',
|
|
);
|
|
const files = discoverTests(root);
|
|
assert.equal(files.length, 3);
|
|
assert.deepEqual(
|
|
selectTests(files, 'offline')
|
|
.map((f) => f.kind)
|
|
.sort(),
|
|
['recovery', 'unit'],
|
|
);
|
|
assert.deepEqual(
|
|
selectTests(files, 'integration').map((f) => f.path),
|
|
['tests/http.test.ts'],
|
|
);
|
|
assert.equal(selectTests(files, 'multiplayer').length, 2);
|
|
assert.equal(selectTests(files, 'recovery').length, 1);
|
|
assert.equal(selectTests(files, 'unit').length, 1);
|
|
assert.equal(selectTests(files, 'all').length, 3);
|
|
});
|
|
|
|
void test('HTTP annotation takes precedence over in-memory SQLite imports', () => {
|
|
assert.equal(
|
|
classifyTest(
|
|
"// @dune-suite integration\nimport { DatabaseSync } from 'node:sqlite';",
|
|
),
|
|
'integration',
|
|
);
|
|
assert.equal(classifyTest('// Uses node:sqlite in a mocked example'), 'unit');
|
|
});
|
|
|
|
void test('an unmarked HTTP test cannot silently enter the offline suite', (t) => {
|
|
const root = mkdtempSync(join(tmpdir(), 'dune-http-discovery-'));
|
|
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
mkdirSync(join(root, 'tests'));
|
|
// Assemble the fixture expression so this offline regression isn't an HTTP test.
|
|
writeFileSync(
|
|
join(root, 'tests/http.test.ts'),
|
|
['process', 'env', 'DUNE_TEST_URL'].join('.'),
|
|
);
|
|
assert.throws(() => discoverTests(root), /HTTP tests need/);
|
|
});
|
|
|
|
void test('each filename filter must match within its suite, including combined filters', () => {
|
|
const files = [{ path: 'tests/rules.test.ts', kind: 'unit' as const }];
|
|
assert.equal(selectTests(files, 'offline', ['rules']).length, 1);
|
|
assert.throws(() => selectTests(files, 'offline', ['']));
|
|
assert.throws(
|
|
() => selectTests(files, 'offline', ['rules', 'typo']),
|
|
/No offline test files match/,
|
|
);
|
|
assert.throws(
|
|
() => selectTests(files, 'integration', ['rules']),
|
|
/No integration test files match/,
|
|
);
|
|
assert.throws(() => selectTests([], 'all'), /No test files/);
|
|
});
|
|
|
|
function runnerFixture(t: test.TestContext, sources: Record<string, string>) {
|
|
const root = mkdtempSync(join(tmpdir(), 'dune-test-runner-'));
|
|
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
mkdirSync(join(root, 'tools'));
|
|
mkdirSync(join(root, 'tests'));
|
|
writeFileSync(join(root, 'package.json'), '{"type":"module"}\n');
|
|
for (const tool of ['test.ts', 'test-discovery.ts', 'test-reporter.ts'])
|
|
cpSync(new URL(`../tools/${tool}`, import.meta.url), join(root, 'tools', tool));
|
|
symlinkSync(
|
|
fileURLToPath(new URL('../node_modules', import.meta.url)),
|
|
join(root, 'node_modules'),
|
|
'dir',
|
|
);
|
|
for (const [file, source] of Object.entries(sources))
|
|
writeFileSync(join(root, 'tests', file), source);
|
|
// A nested runner must not inherit the parent test worker's IPC context.
|
|
const env = { ...process.env };
|
|
delete env.NODE_TEST_CONTEXT;
|
|
const args = ['--import', import.meta.resolve('tsx'), 'tools/test.ts'];
|
|
return {
|
|
root,
|
|
env,
|
|
args,
|
|
run: (...options: string[]) =>
|
|
spawnSync(process.execPath, [...args, ...options], {
|
|
cwd: root,
|
|
env,
|
|
timeout: 15_000,
|
|
encoding: 'utf8',
|
|
}),
|
|
};
|
|
}
|
|
|
|
void test('concise CLI preserves nested assertion details, output and failure exit', (t) => {
|
|
const fixture = runnerFixture(t, {
|
|
'failure.test.mjs': `
|
|
import { test, describe } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
describe('parent suite', () => {
|
|
test('passing sentinel', () => {});
|
|
test('failure sentinel', t => {
|
|
console.log('stdout sentinel');
|
|
console.error('stderr sentinel');
|
|
t.diagnostic('diagnostic sentinel');
|
|
assert.deepEqual({ payment: 7 }, { payment: 11 });
|
|
});
|
|
});
|
|
`,
|
|
});
|
|
for (const options of [[], ['--verbose']]) {
|
|
const result = fixture.run(...options);
|
|
assert.ifError(result.error);
|
|
assert.equal(result.status, 1);
|
|
const output = result.stdout + result.stderr;
|
|
assert.match(output, /failure sentinel/);
|
|
assert.match(output, /failure\.test\.mjs:\d+:\d+/);
|
|
assert.match(output, /payment: 7/);
|
|
assert.match(output, /payment: 11/);
|
|
for (const channel of ['stdout', 'stderr', 'diagnostic'])
|
|
assert.match(output, new RegExp(`${channel} sentinel`));
|
|
if (options.length) assert.match(output, /passing sentinel/);
|
|
else assert.doesNotMatch(output, /passing sentinel/);
|
|
}
|
|
});
|
|
|
|
void test('CLI applies union filename selection and name intersection without green empty runs', (t) => {
|
|
const fixture = runnerFixture(t, {
|
|
'empty.test.mjs': '',
|
|
'skipped.test.mjs': `
|
|
import test from 'node:test';
|
|
test('pending case', { skip: true }, () => {});
|
|
`,
|
|
'alpha.test.mjs': `
|
|
import test from 'node:test';
|
|
test('chosen alpha', () => {});
|
|
test('excluded failure', () => { throw new Error('must not execute'); });
|
|
`,
|
|
'beta.test.mjs': `
|
|
import test from 'node:test';
|
|
test('chosen beta', () => {});
|
|
`,
|
|
'other.test.mjs': `
|
|
import test from 'node:test';
|
|
test('chosen outside selection', () => { throw new Error('must not execute'); });
|
|
`,
|
|
});
|
|
const result = fixture.run('alpha', 'beta', '--name', '^chosen', '--verbose');
|
|
assert.ifError(result.error);
|
|
assert.equal(result.status, 0);
|
|
assert.match(result.stdout, /chosen alpha/);
|
|
assert.match(result.stdout, /chosen beta/);
|
|
assert.doesNotMatch(result.stdout + result.stderr, /must not execute/);
|
|
const listed = fixture.run('alpha', 'beta', '--list');
|
|
assert.equal(listed.status, 0);
|
|
assert.match(listed.stdout, /tests\/alpha\.test\.mjs/);
|
|
assert.match(listed.stdout, /tests\/beta\.test\.mjs/);
|
|
assert.doesNotMatch(listed.stdout, /tests\/other\.test\.mjs/);
|
|
for (const options of [
|
|
['empty'],
|
|
['skipped'],
|
|
['alpha', '--name', '^no matching tests$'],
|
|
['alpha', '--name', '^no matching tests$', '--verbose'],
|
|
['alpha', '--name', '['],
|
|
['alpha', '--name', ''],
|
|
['', '--name', '^chosen'],
|
|
['alpha', 'missing'],
|
|
['--suite', 'unknown'],
|
|
]) {
|
|
const invalid = fixture.run(...options);
|
|
assert.ifError(invalid.error);
|
|
assert.equal(invalid.status, 1, JSON.stringify(options));
|
|
}
|
|
});
|
|
|
|
void test('CLI cancellation remains failure when its test driver handles the signal with exit zero', { timeout: 30_000 }, async (t) => {
|
|
for (const [signal, expected] of [
|
|
['SIGINT', 130],
|
|
['SIGTERM', 143],
|
|
] as const) {
|
|
const fixture = runnerFixture(t, {
|
|
'waiting.test.mjs': `
|
|
import test from 'node:test';
|
|
test('waiting', () => {});
|
|
`,
|
|
});
|
|
// A real Node test driver whose reporter gracefully handles cancellation.
|
|
writeFileSync(
|
|
join(fixture.root, 'tools/test-reporter.ts'),
|
|
`import { MessageChannel } from 'node:worker_threads';
|
|
export default async function* reporter(source) {
|
|
// Keep the driver alive until the real OS signal, without timer guesses.
|
|
const { port1, port2 } = new MessageChannel();
|
|
port1.on('message', () => {});
|
|
process.on('${signal}', () => {
|
|
port1.close();
|
|
port2.close();
|
|
process.exit(0);
|
|
});
|
|
console.log('cancellation-ready');
|
|
for await (const event of source) yield '';
|
|
await Promise.withResolvers().promise;
|
|
}\n`,
|
|
);
|
|
const child = spawn(process.execPath, fixture.args, {
|
|
cwd: fixture.root,
|
|
env: fixture.env,
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
t.after(() => child.kill('SIGKILL'));
|
|
let output = '';
|
|
child.stdout.on('data', (chunk: Buffer) => {
|
|
output += chunk.toString();
|
|
if (output.includes('cancellation-ready')) {
|
|
output = '';
|
|
child.kill(signal);
|
|
}
|
|
});
|
|
const { promise, resolve, reject } = Promise.withResolvers<number | null>();
|
|
child.once('error', reject);
|
|
child.once('close', resolve);
|
|
assert.equal(await promise, expected);
|
|
}
|
|
});
|