136 lines
9.1 KiB
JavaScript
136 lines
9.1 KiB
JavaScript
const assert = require('node:assert/strict')
|
|
const fs = require('node:fs/promises')
|
|
const path = require('node:path')
|
|
const os = require('node:os')
|
|
const { build } = require('esbuild')
|
|
|
|
;(async () => {
|
|
const temp = await fs.mkdtemp(path.join(os.tmpdir(), 'ycode-private-runtimes-'))
|
|
let checks = 0
|
|
const check = (name, run) => { run(); checks++; console.log('PASS', name) }
|
|
try {
|
|
const pureFile = path.join(temp, 'pure.cjs')
|
|
await build({ entryPoints: ['src/main/private-runtime-environment.ts'], outfile: pureFile, bundle: true, platform: 'node', format: 'cjs', logLevel: 'silent' })
|
|
const { normalizePrivateRuntimePreferences: normalize, validatePrivateRuntimePatch: validate, privateRuntimeView: view, applyPrivateRuntimeEnvironment: apply } = require(pureFile)
|
|
const root = 'C:\\YCode Private\\resources\\runtimes\\win32-x64'
|
|
const defaults = normalize(undefined)
|
|
const all = view('win32', root, defaults, () => true)
|
|
const base = Object.freeze({ Path: 'C:\\System Tools;C:\\Windows', SystemRoot: 'C:\\Windows', TERM: 'xterm' })
|
|
check('defaults and complete bundle activate all tools', () => assert.ok(all.tools.every(tool => tool.active)))
|
|
check('private tools precede original system paths, including spaces', () => {
|
|
const result = apply(base, all)
|
|
assert.ok(result.PATH.startsWith(root + '\\node;'))
|
|
assert.ok(result.PATH.endsWith(base.Path))
|
|
assert.ok(result.PATH.includes(root + '\\python\\Scripts'))
|
|
assert.equal(result.CLAUDE_CODE_GIT_BASH_PATH, root + '\\git\\bin\\bash.exe')
|
|
assert.equal(result.Path, undefined)
|
|
assert.equal(base.Path, 'C:\\System Tools;C:\\Windows')
|
|
})
|
|
check('disabled global preference preserves original environment exactly', () => assert.deepEqual(apply(base, view('win32', root, normalize({ enabled: false }), () => true)), base))
|
|
check('per-tool disabled does not inject its paths', () => {
|
|
const result = apply(base, view('win32', root, normalize({ tools: { node: false, git: false } }), () => true))
|
|
assert.ok(!result.PATH.includes('\\node'))
|
|
assert.ok(!result.PATH.includes('\\git'))
|
|
assert.equal(result.CLAUDE_CODE_GIT_BASH_PATH, undefined)
|
|
assert.ok(result.PATH.startsWith(root + '\\python;'))
|
|
})
|
|
check('missing and incomplete bundle never reports active', () => {
|
|
for (const missing of ['node.exe', 'npm.cmd', 'npx.cmd', 'python.exe', 'pip.cmd', 'git.exe', 'bash.exe']) {
|
|
const state = view('win32', root, defaults, file => !file.endsWith(missing))
|
|
assert.equal(state.tools.filter(tool => !tool.available).length, 1)
|
|
assert.ok(state.tools.filter(tool => !tool.available).every(tool => !tool.active && tool.path === null))
|
|
}
|
|
assert.deepEqual(apply(base, view('win32', root, defaults, () => false)), base)
|
|
})
|
|
check('Linux and macOS preserve original environment', () => {
|
|
for (const platform of ['linux', 'darwin']) {
|
|
const state = view(platform, root, defaults, () => true)
|
|
assert.equal(state.supported, false)
|
|
assert.deepEqual(apply(base, state), base)
|
|
}
|
|
})
|
|
check('case-insensitive PATH becomes one canonical key and survives MCP SDK merge', () => {
|
|
const result = apply({ PATH: 'C:\\System', Path: 'C:\\Explicit', path: 'c:\\system', claude_code_git_bash_path: 'old' }, all)
|
|
assert.deepEqual(Object.keys(result).filter(key => key.toLowerCase() === 'path'), ['PATH'])
|
|
assert.equal(result.PATH.split(';').filter(item => item.toLowerCase() === 'c:\\system').length, 1)
|
|
assert.ok(({ PATH: 'C:\\System', ...result }).PATH.startsWith(root))
|
|
assert.equal(result.claude_code_git_bash_path, undefined)
|
|
})
|
|
check('overlay introduces no credentials or unrelated variables', () => {
|
|
const result = apply({ PATH: 'C:\\System' }, all)
|
|
assert.deepEqual(Object.keys(result).sort(), ['CLAUDE_CODE_GIT_BASH_PATH', 'PATH'])
|
|
})
|
|
check('writable private caches and prefixes are scoped to active tools', () => {
|
|
const dataRoot = 'C:\\Users\\test\\AppData\\YCode\\runtime-data'
|
|
const result = apply({ ...base, npm_config_prefix: 'system', pythonuserbase: 'system' }, all, dataRoot)
|
|
assert.equal(result.NPM_CONFIG_PREFIX, dataRoot + '\\node\\global')
|
|
assert.equal(result.NPM_CONFIG_CACHE, dataRoot + '\\node\\cache')
|
|
assert.equal(result.PYTHONUSERBASE, dataRoot + '\\python\\user')
|
|
assert.equal(result.PIP_CACHE_DIR, dataRoot + '\\python\\cache')
|
|
assert.ok(result.PATH.includes(dataRoot + '\\python\\user\\Python314\\Scripts'))
|
|
assert.equal(result.PYTHONHOME, undefined)
|
|
assert.equal(result.PIP_USER, undefined)
|
|
assert.equal(result.PIP_TARGET, undefined)
|
|
assert.equal(result.npm_config_prefix, undefined)
|
|
assert.equal(result.pythonuserbase, undefined)
|
|
assert.deepEqual(apply(base, view('win32', root, normalize({ enabled: false }), () => true), dataRoot), base)
|
|
const gitOnly = apply(base, view('win32', root, normalize({ tools: { node: false, python: false } }), () => true), dataRoot)
|
|
assert.equal(gitOnly.NPM_CONFIG_PREFIX, undefined)
|
|
assert.equal(gitOnly.PYTHONUSERBASE, undefined)
|
|
})
|
|
check('overlay idempotence', () => assert.deepEqual(apply(apply(base, all), all), apply(base, all)))
|
|
check('IPC patch validation rejects malformed and unknown values', () => {
|
|
for (const bad of [null, [], 'yes', { enabled: 'true' }, { tools: null }, { tools: [] }, { tools: { ruby: true } }, { tools: { node: 1 } }, { root: 'C:\\evil' }]) assert.throws(() => validate(bad))
|
|
assert.deepEqual(validate({ enabled: false, tools: { node: true } }), { enabled: false, tools: { node: true } })
|
|
})
|
|
const userData = path.join(temp, 'user-data')
|
|
await fs.mkdir(userData)
|
|
await fs.writeFile(path.join(userData, 'config.json'), JSON.stringify({ apiKeyPlain: 'persistence-only-test', unrelated: 'preserved' }))
|
|
async function service(name) {
|
|
const outfile = path.join(temp, name + '.cjs')
|
|
await build({ entryPoints: ['src/main/private-runtimes.ts'], outfile, bundle: true, platform: 'node', format: 'cjs', logLevel: 'silent', plugins: [{ name: 'isolated-electron', setup(build) {
|
|
build.onResolve({ filter: /^electron$/ }, () => ({ path: 'electron', namespace: 'mock' }))
|
|
build.onLoad({ filter: /.*/, namespace: 'mock' }, () => ({ contents: `export const app = { isPackaged:false,getPath:()=>${JSON.stringify(userData)},getAppPath:()=>${JSON.stringify(temp)} };export const safeStorage={};`, loader: 'js' }))
|
|
} }] })
|
|
return require(outfile)
|
|
}
|
|
const api = await service('service')
|
|
await Promise.all([api.updatePrivateRuntimes({ tools: { node: false } }), api.updatePrivateRuntimes({ tools: { git: false } }), api.updatePrivateRuntimes({ enabled: false })])
|
|
const saved = JSON.parse(await fs.readFile(path.join(userData, 'private-runtimes.json'), 'utf8'))
|
|
const unrelated = JSON.parse(await fs.readFile(path.join(userData, 'config.json'), 'utf8'))
|
|
check('serialized updates merge toggles and preserve unrelated config', () => {
|
|
assert.deepEqual(saved, { enabled: false, tools: { node: false, python: true, git: false } })
|
|
assert.equal(unrelated.unrelated, 'preserved')
|
|
assert.equal(unrelated.apiKeyPlain, 'persistence-only-test')
|
|
})
|
|
const fresh = await service('reloaded')
|
|
check('settings survive module restart without exposing config credentials', () => {
|
|
assert.ok(!JSON.stringify(all).includes('persistence-only-test'))
|
|
})
|
|
assert.deepEqual((await fresh.getPrivateRuntimes()).preferences, saved)
|
|
assert.ok(!JSON.stringify(await fresh.getPrivateRuntimes()).includes('persistence-only-test'))
|
|
// Force temporary-file creation failure; the committed preferences remain readable.
|
|
await fs.mkdir(path.join(userData, 'private-runtimes.json.tmp'))
|
|
await assert.rejects(api.updatePrivateRuntimes({ enabled: true }))
|
|
assert.equal((await api.getPrivateRuntimes()).preferences.enabled, false)
|
|
check('failed atomic save leaves the committed enable policy unchanged', () => {})
|
|
await fs.rm(path.join(userData, 'private-runtimes.json.tmp'), { recursive: true })
|
|
await api.updatePrivateRuntimes({ enabled: true })
|
|
assert.equal((await api.getPrivateRuntimes()).preferences.enabled, true)
|
|
const settingsPath = path.join(userData, 'private-runtimes.json')
|
|
const validSettings = await fs.readFile(settingsPath, 'utf8')
|
|
for (const corrupt of ['{broken', '{}', 'null', '{"enabled":"true","tools":{}}']) {
|
|
await fs.writeFile(settingsPath, corrupt)
|
|
await assert.rejects(api.getPrivateRuntimes())
|
|
await assert.rejects(api.updatePrivateRuntimes({ enabled: true }))
|
|
}
|
|
await fs.rm(settingsPath)
|
|
await fs.mkdir(settingsPath)
|
|
await assert.rejects(api.getPrivateRuntimes())
|
|
await fs.rm(settingsPath, { recursive: true })
|
|
await fs.writeFile(settingsPath, validSettings)
|
|
check('corrupt, incomplete, and unreadable stored policy never silently enables tools', () => {})
|
|
console.log(`Private runtimes: ${checks} regression groups passed; no Windows executables run.`)
|
|
} finally { await fs.rm(temp, { recursive: true, force: true }) }
|
|
})().catch(error => { console.error(error); process.exitCode = 1 })
|