chore: establish YCode replication baseline and TODO
This commit is contained in:
commit
6dc59f89ba
1118 files changed
+196002
No files matched your search
@@ -0,0 +1,119 @@
|
||||
// Real Windows ConPTY + real Electron owners; commands only inspect temporary test workspaces.
|
||||
const path = require('node:path')
|
||||
const fs = require('node:fs/promises')
|
||||
const assert = require('node:assert/strict')
|
||||
const directory = path.resolve('.tmp/terminal-host-regression')
|
||||
if (!process.versions.electron) {
|
||||
;(async () => {
|
||||
await fs.mkdir(directory, { recursive: true })
|
||||
await require('esbuild').build({ entryPoints: [path.resolve('src/main/terminal-host.ts')], outfile: path.join(directory, 'host.cjs'), bundle: true,
|
||||
platform: 'node', format: 'cjs', external: ['electron'], logLevel: 'silent', plugins: [{ name: 'native-pty-capture', setup(build) {
|
||||
build.onResolve({ filter: /^node-pty$/ }, () => ({ path: 'native-pty', namespace: 'capture' }))
|
||||
build.onLoad({ filter: /.*/, namespace: 'capture' }, () => ({ loader: 'js', contents: `
|
||||
const real = globalThis.__YCODE_PTY_NATIVE_TEST__.pty;
|
||||
export function spawn(file,args,options){ const pty=real.spawn(file,args,options);globalThis.__YCODE_PTY_NATIVE_TEST__.children.push({pid:pty.pid,env:options.env,args});return pty }
|
||||
` }))
|
||||
} }] })
|
||||
const env = { ...process.env }; delete env.ELECTRON_RUN_AS_NODE
|
||||
const result = require('node:child_process').spawnSync(require('electron'), [__filename], { env, windowsHide: true, stdio: 'inherit', timeout: 60000 })
|
||||
process.exitCode = result.status ?? 1
|
||||
})().catch(error => { console.error(error); process.exitCode = 1 })
|
||||
} else {
|
||||
const { app, BrowserWindow } = require('electron')
|
||||
app.setPath('userData', path.join(directory, 'user-data'))
|
||||
app.disableHardwareAcceleration()
|
||||
const state = { children: [], events: [], pty: require('node-pty') }
|
||||
globalThis.__YCODE_PTY_NATIVE_TEST__ = state
|
||||
const host = require(path.join(directory, 'host.cjs'))
|
||||
const pause = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
const plain = value => value.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, '').replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '')
|
||||
const alive = pid => { try { process.kill(pid, 0); return true } catch { return false } }
|
||||
async function until(test, label, timeout = 12000) {
|
||||
const start = Date.now()
|
||||
while (Date.now() - start < timeout) { if (await test()) return; await pause(25) }
|
||||
throw Error('Timed out: ' + label)
|
||||
}
|
||||
const windows = []
|
||||
const previousSecret = process.env.YCODE_MODEL_API_KEY
|
||||
process.env.YCODE_MODEL_API_KEY = 'native-test-host-secret'
|
||||
app.whenReady().then(async () => {
|
||||
const cwdA = path.join(directory, 'workspace A'), cwdB = path.join(directory, 'workspace B')
|
||||
await Promise.all([fs.mkdir(cwdA, { recursive: true }), fs.mkdir(cwdB, { recursive: true })])
|
||||
const createOwner = async () => {
|
||||
const owner = new BrowserWindow({ show: false, webPreferences: { offscreen: true } })
|
||||
await owner.loadURL('about:blank')
|
||||
const original = owner.webContents.send.bind(owner.webContents)
|
||||
owner.webContents.send = (channel, event) => { if (channel === 'terminal:event') state.events.push({ windowId: owner.id, ...event }); return original(channel, event) }
|
||||
windows.push(owner); return owner
|
||||
}
|
||||
const ownerA = await createOwner(), ownerB = await createOwner()
|
||||
assert.equal(process.platform, 'win32', 'this regression specifically checks Windows ConPTY')
|
||||
const first = await host.createTerminal(ownerA, cwdA, 'session-A', 90, 26)
|
||||
const second = await host.createTerminal(ownerB, cwdB, 'session-B', 80, 24)
|
||||
assert.match(first.shell, /powershell\.exe$/i)
|
||||
assert.deepEqual(state.children[0].args, ['-NoLogo', '-NoProfile'])
|
||||
for (const child of state.children) {
|
||||
assert.equal(Object.keys(child.env).some(name => /KEY|TOKEN|SECRET|AUTH|COOKIE|PASSWORD|CREDENTIAL/i.test(name)), false)
|
||||
assert.equal(child.env.ELECTRON_RUN_AS_NODE, undefined)
|
||||
assert.ok(child.env.SystemRoot || child.env.SYSTEMROOT || child.env.windir, 'normal Windows system environment remains available')
|
||||
}
|
||||
assert.throws(() => host.getTerminal(ownerB, first.id, 'session-A'), /不属于/)
|
||||
assert.throws(() => host.getTerminal(ownerA, first.id, 'session-B'), /不属于/)
|
||||
assert.throws(() => host.writeTerminal(ownerB, first.id, 'exit\r', 'session-A'), /不属于/)
|
||||
assert.throws(() => host.resizeTerminal(ownerA, first.id, 1, 30, 'session-A'), /尺寸/)
|
||||
await assert.rejects(() => host.createTerminal(ownerA, path.join(directory, 'missing'), 'session-A'), /不存在/)
|
||||
host.writeTerminal(ownerA, first.id, "Write-Output ('__READY_'+'A__');[Console]::WriteLine((Get-Location).Path)\r", 'session-A')
|
||||
host.writeTerminal(ownerB, second.id, "Write-Output ('__READY_'+'B__');[Console]::WriteLine((Get-Location).Path)\r", 'session-B')
|
||||
await until(() => plain(host.getTerminal(ownerA, first.id, 'session-A').buffer).includes('__READY_A__'), 'real PowerShell A output')
|
||||
await until(() => plain(host.getTerminal(ownerB, second.id, 'session-B').buffer).includes('__READY_B__'), 'real PowerShell B output')
|
||||
assert.ok(plain(host.getTerminal(ownerA, first.id, 'session-A').buffer).includes(cwdA))
|
||||
assert.ok(plain(host.getTerminal(ownerB, second.id, 'session-B').buffer).includes(cwdB))
|
||||
host.resizeTerminal(ownerA, first.id, 120, 40, 'session-A')
|
||||
host.writeTerminal(ownerA, first.id, "Write-Output ('__DIM__'+[Console]::WindowWidth);Write-Output ('__ENV__'+[string]::IsNullOrEmpty($env:YCODE_MODEL_API_KEY))\r", 'session-A')
|
||||
await until(() => plain(host.getTerminal(ownerA, first.id, 'session-A').buffer).includes('__DIM__120'), 'ConPTY terminal resize reaches Windows Console API')
|
||||
await until(() => plain(host.getTerminal(ownerA, first.id, 'session-A').buffer).includes('__ENV__True'), 'child process cannot read gateway key')
|
||||
host.writeTerminal(ownerA, first.id, "Write-Output ('__BUS'+'Y__');Start-Sleep -Seconds 30;Write-Output ('__MUST_NOT_'+'RUN__')\r", 'session-A')
|
||||
await until(() => plain(host.getTerminal(ownerA, first.id, 'session-A').buffer).includes('__BUSY__'), 'cancellable command entered')
|
||||
host.writeTerminal(ownerA, first.id, '\x03', 'session-A')
|
||||
await pause(150)
|
||||
host.writeTerminal(ownerA, first.id, "Write-Output ('__CANCEL'+'LED__')\r", 'session-A')
|
||||
await until(() => plain(host.getTerminal(ownerA, first.id, 'session-A').buffer).includes('__CANCELLED__'), 'Ctrl+C interrupts foreground PowerShell command')
|
||||
assert.equal(plain(host.getTerminal(ownerA, first.id, 'session-A').buffer).includes('__MUST_NOT_RUN__'), false)
|
||||
host.clearTerminal(ownerA, first.id, 'session-A')
|
||||
assert.equal(host.getTerminal(ownerA, first.id, 'session-A').buffer, '\x1b[2J\x1b[3J\x1b[H')
|
||||
host.writeTerminal(ownerA, first.id, "Write-Output ('__BUFFER_'+'BEGIN__');[Console]::Write('x' * 280000);Write-Output ('__BUFFER_'+'END__')\r", 'session-A')
|
||||
await until(() => plain(host.getTerminal(ownerA, first.id, 'session-A').buffer).includes('__BUFFER_END__'), 'large native output', 20000)
|
||||
const replay = host.getTerminal(ownerA, first.id, 'session-A')
|
||||
assert.ok(replay.buffer.length <= host.TERMINAL_HOST_LIMITS.bufferCharacters)
|
||||
assert.equal(replay.replay.map(chunk => chunk.data).join(''), replay.buffer)
|
||||
assert.equal(replay.buffer.includes('__BUFFER_BEGIN__'), false)
|
||||
const draft = await host.createTerminal(ownerA, cwdA)
|
||||
assert.throws(() => host.rebindTerminal(ownerA, draft.id, undefined, 'new-session', cwdB), /工作目录/)
|
||||
const bound = host.rebindTerminal(ownerA, draft.id, undefined, 'new-session', cwdA)
|
||||
assert.equal(bound.sessionId, 'new-session')
|
||||
assert.equal(bound.cwd, cwdA)
|
||||
assert.throws(() => host.getTerminal(ownerA, draft.id), /不属于/)
|
||||
host.writeTerminal(ownerA, draft.id, "Write-Output ('__BOUND_'+'LIVE__')\r", 'new-session')
|
||||
await until(() => plain(host.getTerminal(ownerA, draft.id, 'new-session').buffer).includes('__BOUND_LIVE__'), 'draft terminal keeps its process after binding')
|
||||
host.closeTerminal(ownerA, draft.id, 'new-session')
|
||||
assert.throws(() => host.getTerminal(ownerA, draft.id, 'new-session'), /不存在/)
|
||||
host.writeTerminal(ownerB, second.id, 'exit 7\r', 'session-B')
|
||||
await until(() => host.getTerminal(ownerB, second.id, 'session-B').exited, 'native process exit')
|
||||
assert.equal(host.getTerminal(ownerB, second.id, 'session-B').exitCode, 7)
|
||||
assert.throws(() => host.writeTerminal(ownerB, second.id, 'echo after-exit\r', 'session-B'), /已退出/)
|
||||
const firstEvents = state.events.filter(event => event.id === first.id)
|
||||
assert.ok(firstEvents.length > 0)
|
||||
assert.ok(firstEvents.every((event, index) => event.windowId === ownerA.id && event.sessionId === 'session-A' && (!index || event.sequence > firstEvents[index - 1].sequence)))
|
||||
assert.ok(state.events.some(event => event.id === second.id && event.type === 'exit' && event.exitCode === 7))
|
||||
ownerA.destroy()
|
||||
await until(() => state.children.filter(child => child.pid !== state.children[1].pid).every(child => !alive(child.pid)), 'closing owner destroys all its native PTY processes')
|
||||
host.disposeTerminalHosts()
|
||||
const report = { result: 'PASS Windows native terminal host', platform: process.platform, electron: process.versions.electron,
|
||||
checks: ['ConPTY PowerShell -NoProfile', 'cwd isolation', 'credential env exclusion', 'native 120-column resize', 'Ctrl+C cancel', 'clear', 'bounded ANSI replay', 'owner/session access control', 'draft rebind', 'exit status', 'window-close process cleanup'], nativeProcesses: state.children.length }
|
||||
await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify(report, null, 2))
|
||||
console.log(JSON.stringify(report))
|
||||
if (previousSecret === undefined) delete process.env.YCODE_MODEL_API_KEY; else process.env.YCODE_MODEL_API_KEY = previousSecret
|
||||
windows.forEach(owner => { if (!owner.isDestroyed()) owner.destroy() })
|
||||
app.exit(0)
|
||||
}).catch(error => { console.error(error); host.disposeTerminalHosts(); windows.forEach(owner => { if (!owner.isDestroyed()) owner.destroy() }); app.exit(1) })
|
||||
}
|
||||
Reference in new issue
Block a user