fix(runtime.node): Improve Node runtime adapter to use tsrun.spawnPath, strengthen tsrun detection, and improve process lifecycle and loader handling; update tsrun dependency.

This commit is contained in:
2025-10-17 06:50:44 +00:00
parent 175b4463fa
commit d6842326ad
7 changed files with 160 additions and 146 deletions

View File

@@ -35,18 +35,11 @@ export class NodeRuntimeAdapter extends RuntimeAdapter {
// Check Node.js version
const nodeVersion = process.version;
// Check if tsrun is available
const result = await this.smartshellInstance.exec('tsrun --version', {
cwd: process.cwd(),
onError: () => {
// Ignore error
}
});
if (result.exitCode !== 0) {
// Check if tsrun module is available (imported as dependency)
if (!plugins.tsrun || !plugins.tsrun.spawnPath) {
return {
available: false,
error: 'tsrun not found. Install with: pnpm install --save-dev @git.zone/tsrun',
error: 'tsrun module not found or outdated (requires version 1.6.0+)',
};
}
@@ -96,7 +89,7 @@ export class NodeRuntimeAdapter extends RuntimeAdapter {
}
/**
* Execute a test file in Node.js
* Execute a test file in Node.js using tsrun's spawnPath API
*/
async run(
testFile: string,
@@ -109,28 +102,35 @@ export class NodeRuntimeAdapter extends RuntimeAdapter {
const mergedOptions = this.mergeOptions(options);
// Build tsrun command
let tsrunOptions = '';
// Build spawn options
const spawnOptions: any = {
cwd: mergedOptions.cwd || process.cwd(),
env: { ...mergedOptions.env },
args: [] as string[],
stdio: 'pipe' as const,
};
// Add --web flag if needed
if (process.argv.includes('--web')) {
tsrunOptions += ' --web';
spawnOptions.args.push('--web');
}
// Set filter tags as environment variable
if (this.filterTags.length > 0) {
process.env.TSTEST_FILTER_TAGS = this.filterTags.join(',');
spawnOptions.env.TSTEST_FILTER_TAGS = this.filterTags.join(',');
}
// Check for 00init.ts file in test directory
const testDir = plugins.path.dirname(testFile);
const initFile = plugins.path.join(testDir, '00init.ts');
let runCommand = `tsrun ${testFile}${tsrunOptions}`;
const initFileExists = await plugins.smartfile.fs.fileExists(initFile);
// If 00init.ts exists, run it first
// Determine which file to run
let fileToRun = testFile;
let loaderPath: string | null = null;
// If 00init.ts exists, create a loader file
if (initFileExists) {
// Create a temporary loader file that imports both 00init.ts and the test file
const absoluteInitFile = plugins.path.resolve(initFile);
const absoluteTestFile = plugins.path.resolve(testFile);
const loaderContent = `
@@ -139,10 +139,12 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
`;
loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`);
await plugins.smartfile.memory.toFs(loaderContent, loaderPath);
runCommand = `tsrun ${loaderPath}${tsrunOptions}`;
fileToRun = loaderPath;
}
const execResultStreaming = await this.smartshellInstance.execStreamingSilent(runCommand);
// Spawn the test process using tsrun's spawnPath API
// Pass undefined for fromFileUrl since fileToRun is already an absolute path
const tsrunProcess = plugins.tsrun.spawnPath(fileToRun, undefined, spawnOptions);
// If we created a loader file, clean it up after test execution
if (loaderPath) {
@@ -156,8 +158,8 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
}
};
execResultStreaming.childProcess.on('exit', cleanup);
execResultStreaming.childProcess.on('error', cleanup);
tsrunProcess.childProcess.on('exit', cleanup);
tsrunProcess.childProcess.on('error', cleanup);
}
// Start warning timer if no timeout was specified
@@ -180,15 +182,15 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
const timeoutPromise = new Promise<void>((_resolve, reject) => {
timeoutId = setTimeout(async () => {
// Use smartshell's terminate() to kill entire process tree
await execResultStreaming.terminate();
// Use tsrun's terminate() to gracefully kill the process
await tsrunProcess.terminate();
reject(new Error(`Test file timed out after ${this.timeoutSeconds} seconds`));
}, timeoutMs);
});
try {
await Promise.race([
tapParser.handleTapProcess(execResultStreaming.childProcess),
tapParser.handleTapProcess(tsrunProcess.childProcess),
timeoutPromise
]);
// Clear timeout if test completed successfully
@@ -200,16 +202,16 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
}
// Handle timeout error
tapParser.handleTimeout(this.timeoutSeconds);
// Ensure entire process tree is killed if still running
// Ensure process is killed if still running
try {
await execResultStreaming.kill(); // This kills the entire process tree with SIGKILL
tsrunProcess.kill('SIGKILL');
} catch (killError) {
// Process tree might already be dead
// Process might already be dead
}
await tapParser.evaluateFinalResult();
}
} else {
await tapParser.handleTapProcess(execResultStreaming.childProcess);
await tapParser.handleTapProcess(tsrunProcess.childProcess);
}
// Clear warning timer if it was set