chore: establish YCode replication baseline and TODO

This commit is contained in:
xiaohan.yuan committed 2026-10-17 02:43:49 +08:00
commit 6dc59f89ba
1118 files changed
+196002

No files matched your search

+119
View File
@@ -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) })
}