mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
fix(runtime): contain and clean temporary workspaces
This commit is contained in:
1 parent
c34f71ed2d
commit
7d156040f8
75 files changed
+801
-276
No files matched your search
@@ -9,10 +9,10 @@
|
||||
* Requires the native module to be built first: pnpm build:native
|
||||
*/
|
||||
|
||||
import { createWriteStream, mkdtempSync, rmSync, statSync } from 'node:fs'
|
||||
import { createWriteStream, rmSync, statSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { once } from 'node:events'
|
||||
import { createChatLabTempDir } from './chatlab-temp.mjs'
|
||||
|
||||
import { parseFileSync } from '../packages/parser/src/index'
|
||||
import { loadNativeParser } from '../packages/parser/src/native/loader'
|
||||
@@ -115,7 +115,7 @@ async function main() {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const dir = mkdtempSync(join(tmpdir(), 'chatlab-weflow-bench-'))
|
||||
const dir = createChatLabTempDir('bench', 'weflow-')
|
||||
const filePath = join(dir, 'bench.json')
|
||||
try {
|
||||
console.log(`Generating fixture with ${messageCount.toLocaleString()} messages...`)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { chmodSync, lstatSync, mkdirSync, mkdtempSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
|
||||
export const CHATLAB_TEMP_ROOT_ENV = 'CHATLAB_TEMP_ROOT'
|
||||
|
||||
const CHATLAB_TEMP_SCOPES = new Set(['tests', 'runtime', 'imports', 'merge', 'sync', 'parser', 'build', 'bench'])
|
||||
|
||||
export function resolveChatLabTempRoot(env = process.env) {
|
||||
const configured = env[CHATLAB_TEMP_ROOT_ENV]?.trim()
|
||||
if (configured) return resolve(configured)
|
||||
return resolve(process.platform === 'darwin' ? '/private/tmp/chatlab' : join(tmpdir(), 'chatlab'))
|
||||
}
|
||||
|
||||
function ensurePrivateDirectory(dirPath) {
|
||||
mkdirSync(dirPath, { recursive: true, mode: 0o700 })
|
||||
const stats = lstatSync(dirPath)
|
||||
if (stats.isSymbolicLink()) throw new Error(`ChatLab temp directory must not be a symbolic link: ${dirPath}`)
|
||||
if (!stats.isDirectory()) throw new Error(`ChatLab temp path must be a directory: ${dirPath}`)
|
||||
if (typeof process.getuid === 'function' && stats.uid !== process.getuid()) {
|
||||
throw new Error(`ChatLab temp directory is owned by another user: ${dirPath}`)
|
||||
}
|
||||
if (process.platform !== 'win32') chmodSync(dirPath, 0o700)
|
||||
}
|
||||
|
||||
export function getChatLabTempScopeDir(scope, env = process.env) {
|
||||
if (!CHATLAB_TEMP_SCOPES.has(scope)) throw new Error(`Invalid ChatLab temp scope: ${scope}`)
|
||||
const root = resolveChatLabTempRoot(env)
|
||||
ensurePrivateDirectory(root)
|
||||
env[CHATLAB_TEMP_ROOT_ENV] = root
|
||||
const scopeDir = join(root, scope)
|
||||
ensurePrivateDirectory(scopeDir)
|
||||
return scopeDir
|
||||
}
|
||||
|
||||
export function createChatLabTempDir(scope, prefix, env = process.env) {
|
||||
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(prefix)) throw new Error(`Invalid ChatLab temp prefix: ${prefix}`)
|
||||
return mkdtempSync(join(getChatLabTempScopeDir(scope, env), prefix))
|
||||
}
|
||||
+29
-5
@@ -1,9 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { readdirSync, statSync } from 'node:fs'
|
||||
import { join, relative, sep } from 'node:path'
|
||||
import { mkdtempSync, readdirSync, rmSync, statSync } from 'node:fs'
|
||||
import { basename, dirname, join, relative, resolve, sep } from 'node:path'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { CHATLAB_TEMP_ROOT_ENV, getChatLabTempScopeDir } from './chatlab-temp.mjs'
|
||||
|
||||
const SKIP_DIRS = new Set([
|
||||
'.git',
|
||||
@@ -19,6 +20,8 @@ const SKIP_DIRS = new Set([
|
||||
|
||||
const TEST_FILE_RE = /\.(?:test|spec)\.(?:ts|tsx|js|jsx|mjs|mts|cjs|cts)$/
|
||||
const SUPPORTED_NODE_MAJOR = 24
|
||||
const TEST_RUN_TEMP_PREFIX = 'run-'
|
||||
export const TEST_RUN_TEMP_ENV = 'CHATLAB_TEST_TMPDIR'
|
||||
|
||||
function normalizePath(filePath) {
|
||||
return filePath.split(sep).join('/')
|
||||
@@ -74,6 +77,24 @@ export function checkSupportedNodeVersion(version = process.versions.node) {
|
||||
}
|
||||
}
|
||||
|
||||
export function buildTestEnvironment(tempRoot, env = process.env) {
|
||||
return { ...env, TMPDIR: tempRoot, [TEST_RUN_TEMP_ENV]: tempRoot, [CHATLAB_TEMP_ROOT_ENV]: tempRoot }
|
||||
}
|
||||
|
||||
export function withTestRunTempRoot(callback, parentDir = getChatLabTempScopeDir('tests')) {
|
||||
const resolvedParent = resolve(parentDir)
|
||||
const tempRoot = mkdtempSync(join(resolvedParent, TEST_RUN_TEMP_PREFIX))
|
||||
const resolvedRoot = resolve(tempRoot)
|
||||
if (dirname(resolvedRoot) !== resolvedParent || !basename(resolvedRoot).startsWith(TEST_RUN_TEMP_PREFIX)) {
|
||||
throw new Error(`Refusing to use non-owned test run temp root: ${resolvedRoot}`)
|
||||
}
|
||||
try {
|
||||
return callback(tempRoot)
|
||||
} finally {
|
||||
rmSync(resolvedRoot, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
function run() {
|
||||
const nodeVersion = checkSupportedNodeVersion()
|
||||
if (!nodeVersion.ok) {
|
||||
@@ -93,9 +114,12 @@ function run() {
|
||||
console.log(`Running ${testArgs.length} default test files.`)
|
||||
}
|
||||
|
||||
const result = spawnSync(process.execPath, buildNodeTestArgs(testArgs), {
|
||||
stdio: 'inherit',
|
||||
})
|
||||
const result = withTestRunTempRoot((tempRoot) =>
|
||||
spawnSync(process.execPath, buildNodeTestArgs(testArgs), {
|
||||
stdio: 'inherit',
|
||||
env: buildTestEnvironment(tempRoot),
|
||||
})
|
||||
)
|
||||
|
||||
if (result.error) {
|
||||
throw result.error
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { existsSync, writeFileSync } from 'node:fs'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import test from 'node:test'
|
||||
|
||||
import { buildNodeTestArgs, checkSupportedNodeVersion, filterDefaultTestFiles } from './run-tests.mjs'
|
||||
import {
|
||||
buildNodeTestArgs,
|
||||
buildTestEnvironment,
|
||||
checkSupportedNodeVersion,
|
||||
filterDefaultTestFiles,
|
||||
withTestRunTempRoot,
|
||||
} from './run-tests.mjs'
|
||||
import { CHATLAB_TEMP_ROOT_ENV } from './chatlab-temp.mjs'
|
||||
|
||||
test('default test collection excludes e2e, smoke, and real external tests', () => {
|
||||
const files = [
|
||||
@@ -39,3 +48,65 @@ test('node version check rejects unsupported test runtimes before native modules
|
||||
'ChatLab tests require Node.js >=24 <25. Current Node.js is 22.20.0. Switch to Node 24 before running tests.',
|
||||
})
|
||||
})
|
||||
|
||||
test('test run temp root is removed after success and exceptions', () => {
|
||||
let successfulRoot = ''
|
||||
withTestRunTempRoot((tempRoot) => {
|
||||
successfulRoot = tempRoot
|
||||
writeFileSync(`${tempRoot}/success.txt`, 'ok')
|
||||
assert.equal(existsSync(tempRoot), true)
|
||||
})
|
||||
assert.equal(existsSync(successfulRoot), false)
|
||||
|
||||
let failedRoot = ''
|
||||
assert.throws(
|
||||
() =>
|
||||
withTestRunTempRoot((tempRoot) => {
|
||||
failedRoot = tempRoot
|
||||
throw new Error('simulated test runner failure')
|
||||
}),
|
||||
/simulated test runner failure/
|
||||
)
|
||||
assert.equal(existsSync(failedRoot), false)
|
||||
})
|
||||
|
||||
test('test subprocesses receive the owned run root as every temp environment entry', () => {
|
||||
const env = buildTestEnvironment('/tmp/chatlab/tests/run-example', { KEEP_ME: 'yes' })
|
||||
assert.equal(env.TMPDIR, '/tmp/chatlab/tests/run-example')
|
||||
assert.equal(env.CHATLAB_TEST_TMPDIR, '/tmp/chatlab/tests/run-example')
|
||||
assert.equal(env[CHATLAB_TEMP_ROOT_ENV], '/tmp/chatlab/tests/run-example')
|
||||
assert.equal(env.KEEP_ME, 'yes')
|
||||
})
|
||||
|
||||
test('interrupted child exits before its owned test run temp root is removed', () => {
|
||||
let tempRoot = ''
|
||||
const result = withTestRunTempRoot((root) => {
|
||||
tempRoot = root
|
||||
return spawnSync(process.execPath, ['-e', "process.kill(process.pid, 'SIGTERM')"], {
|
||||
env: buildTestEnvironment(root),
|
||||
})
|
||||
})
|
||||
|
||||
assert.equal(result.signal, 'SIGTERM')
|
||||
assert.equal(existsSync(tempRoot), false)
|
||||
})
|
||||
|
||||
test('parallel test runs receive isolated temp roots and never remove each other', () => {
|
||||
let firstRoot = ''
|
||||
let secondRoot = ''
|
||||
|
||||
withTestRunTempRoot((rootA) => {
|
||||
firstRoot = rootA
|
||||
withTestRunTempRoot((rootB) => {
|
||||
secondRoot = rootB
|
||||
assert.notEqual(rootA, rootB)
|
||||
assert.equal(existsSync(rootA), true)
|
||||
assert.equal(existsSync(rootB), true)
|
||||
})
|
||||
assert.equal(existsSync(rootA), true)
|
||||
assert.equal(existsSync(secondRoot), false)
|
||||
})
|
||||
|
||||
assert.equal(existsSync(firstRoot), false)
|
||||
assert.equal(existsSync(secondRoot), false)
|
||||
})
|
||||
Reference in new issue
Block a user