debug: run opencode prompt session in Windows PTY
This commit is contained in:
parent
e8a0a413e6
commit
bd18aebca6
3 changed files with 258 additions and 8 deletions
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
|
@ -181,5 +181,5 @@ jobs:
|
||||||
node-version: "24.11.1"
|
node-version: "24.11.1"
|
||||||
|
|
||||||
- name: Run Windows standalone repro
|
- name: Run Windows standalone repro
|
||||||
timeout-minutes: 12
|
timeout-minutes: 15
|
||||||
run: ./script/repro-windows-opentui-crash.ps1 -Version "${{ matrix.version }}"
|
run: ./script/repro-windows-opentui-crash.ps1 -Version "${{ matrix.version }}"
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$Version,
|
[string]$Version,
|
||||||
[int]$McpLoops = 8,
|
[int]$McpLoops = 0,
|
||||||
[int]$McpServers = 80,
|
[int]$McpServers = 20,
|
||||||
[int]$StartupSeconds = 130
|
[int]$StartupSeconds = 20,
|
||||||
|
[int]$PtySeconds = 120
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
@ -134,7 +135,8 @@ New-Item -ItemType Directory -Force $env:XDG_CONFIG_HOME, $env:XDG_DATA_HOME, $e
|
||||||
|
|
||||||
$emptyProject = Join-Path $root "empty-project"
|
$emptyProject = Join-Path $root "empty-project"
|
||||||
$mcpProject = Join-Path $root "mcp-project"
|
$mcpProject = Join-Path $root "mcp-project"
|
||||||
New-Item -ItemType Directory -Force $emptyProject, $mcpProject | Out-Null
|
$sessionProject = Join-Path $root "session-project"
|
||||||
|
New-Item -ItemType Directory -Force $emptyProject, $mcpProject, $sessionProject | Out-Null
|
||||||
|
|
||||||
Invoke-Opencode -Label "version" -Exe $exe -Args @("--version") -WorkingDirectory $emptyProject -TimeoutSeconds 30
|
Invoke-Opencode -Label "version" -Exe $exe -Args @("--version") -WorkingDirectory $emptyProject -TimeoutSeconds 30
|
||||||
Invoke-Opencode -Label "help" -Exe $exe -Args @("--help") -WorkingDirectory $emptyProject -TimeoutSeconds 30
|
Invoke-Opencode -Label "help" -Exe $exe -Args @("--help") -WorkingDirectory $emptyProject -TimeoutSeconds 30
|
||||||
|
|
@ -161,9 +163,18 @@ for ($i = 1; $i -le $McpLoops; $i++) {
|
||||||
}
|
}
|
||||||
|
|
||||||
Invoke-Opencode -Label "empty-startup-tui" -Exe $exe -Args @() -WorkingDirectory $emptyProject -TimeoutSeconds $StartupSeconds -AllowTimeout -AllowNonZero
|
Invoke-Opencode -Label "empty-startup-tui" -Exe $exe -Args @() -WorkingDirectory $emptyProject -TimeoutSeconds $StartupSeconds -AllowTimeout -AllowNonZero
|
||||||
Invoke-Opencode -Label "mini-demo-question" -Exe $exe -Args @("--mini", "--demo", "--prompt", "/question single") -WorkingDirectory $emptyProject -TimeoutSeconds 30 -AllowTimeout -AllowNonZero
|
|
||||||
Invoke-Opencode -Label "mini-demo-permission" -Exe $exe -Args @("--mini", "--demo", "--prompt", "/permission bash") -WorkingDirectory $emptyProject -TimeoutSeconds 30 -AllowTimeout -AllowNonZero
|
Write-Section "Install PTY dependency"
|
||||||
Invoke-Opencode -Label "mini-demo-mix" -Exe $exe -Args @("--mini", "--demo", "--prompt", "/fmt mix") -WorkingDirectory $emptyProject -TimeoutSeconds 30 -AllowTimeout -AllowNonZero
|
$ptyRoot = Join-Path $root "pty-harness"
|
||||||
|
New-Item -ItemType Directory -Force $ptyRoot | Out-Null
|
||||||
|
Push-Location $ptyRoot
|
||||||
|
try {
|
||||||
|
npm init -y | Out-Host
|
||||||
|
npm install "@lydell/node-pty@1.2.0-beta.12" | Out-Host
|
||||||
|
node (Join-Path $PSScriptRoot "repro-windows-opentui-pty-session.mjs") -- --exe $exe --project $sessionProject --version $Version --seconds $PtySeconds
|
||||||
|
} finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
|
|
||||||
Write-Section "Result"
|
Write-Section "Result"
|
||||||
Write-Host "No native crash signature detected for opencode-ai@$Version"
|
Write-Host "No native crash signature detected for opencode-ai@$Version"
|
||||||
|
|
|
||||||
239
script/repro-windows-opentui-pty-session.mjs
Normal file
239
script/repro-windows-opentui-pty-session.mjs
Normal file
|
|
@ -0,0 +1,239 @@
|
||||||
|
import { createRequire } from "node:module"
|
||||||
|
import { createServer } from "node:http"
|
||||||
|
import { mkdirSync, writeFileSync } from "node:fs"
|
||||||
|
import { join, resolve } from "node:path"
|
||||||
|
import { setTimeout as delay } from "node:timers/promises"
|
||||||
|
|
||||||
|
const require = createRequire(join(process.cwd(), "pty-harness.cjs"))
|
||||||
|
const pty = require("@lydell/node-pty")
|
||||||
|
|
||||||
|
const args = process.argv.slice(2)
|
||||||
|
const value = (name, fallback) => {
|
||||||
|
const index = args.indexOf(name)
|
||||||
|
if (index === -1) return fallback
|
||||||
|
return args[index + 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
const exe = value("--exe")
|
||||||
|
const project = value("--project")
|
||||||
|
const version = value("--version")
|
||||||
|
const seconds = Number(value("--seconds", "120"))
|
||||||
|
|
||||||
|
if (!exe || !project || !version) {
|
||||||
|
throw new Error("Usage: repro-windows-opentui-pty-session.mjs -- --exe <path> --project <path> --version <version>")
|
||||||
|
}
|
||||||
|
|
||||||
|
const crashPattern = /Segmentation fault|Bun has crashed|ACCESS_VIOLATION|0xC0000005|322122|panic\(main thread\)/i
|
||||||
|
const outputFile = join(project, `pty-output-${version}.log`)
|
||||||
|
const requestsFile = join(project, `provider-requests-${version}.jsonl`)
|
||||||
|
let output = ""
|
||||||
|
let providerRequests = 0
|
||||||
|
let exited = false
|
||||||
|
let exitCode = undefined
|
||||||
|
let exitSignal = undefined
|
||||||
|
|
||||||
|
mkdirSync(project, { recursive: true })
|
||||||
|
|
||||||
|
function appendOutput(text) {
|
||||||
|
output += text
|
||||||
|
writeFileSync(outputFile, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readBody(req) {
|
||||||
|
const chunks = []
|
||||||
|
for await (const chunk of req) chunks.push(chunk)
|
||||||
|
return Buffer.concat(chunks).toString("utf8")
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeSse(res, event) {
|
||||||
|
res.write(`data: ${JSON.stringify(event)}\n\n`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = createServer(async (req, res) => {
|
||||||
|
const body = await readBody(req)
|
||||||
|
providerRequests++
|
||||||
|
writeFileSync(
|
||||||
|
requestsFile,
|
||||||
|
JSON.stringify({ method: req.method, url: req.url, body: body ? JSON.parse(body) : undefined }) + "\n",
|
||||||
|
{ flag: "a" },
|
||||||
|
)
|
||||||
|
|
||||||
|
if (req.url?.endsWith("/models")) {
|
||||||
|
res.writeHead(200, { "content-type": "application/json" })
|
||||||
|
res.end(JSON.stringify({ object: "list", data: [{ id: "test-model", object: "model" }] }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.url?.endsWith("/chat/completions")) {
|
||||||
|
const parsed = body ? JSON.parse(body) : {}
|
||||||
|
if (parsed.stream) {
|
||||||
|
res.writeHead(200, {
|
||||||
|
"content-type": "text/event-stream",
|
||||||
|
"cache-control": "no-cache",
|
||||||
|
connection: "keep-alive",
|
||||||
|
})
|
||||||
|
writeSse(res, {
|
||||||
|
id: "chatcmpl-opentui-repro",
|
||||||
|
object: "chat.completion.chunk",
|
||||||
|
created: Math.floor(Date.now() / 1000),
|
||||||
|
model: "test-model",
|
||||||
|
choices: [{ index: 0, delta: { role: "assistant" }, finish_reason: null }],
|
||||||
|
})
|
||||||
|
await delay(50)
|
||||||
|
writeSse(res, {
|
||||||
|
id: "chatcmpl-opentui-repro",
|
||||||
|
object: "chat.completion.chunk",
|
||||||
|
created: Math.floor(Date.now() / 1000),
|
||||||
|
model: "test-model",
|
||||||
|
choices: [{ index: 0, delta: { content: "This is a deterministic CI response from the fake provider." }, finish_reason: null }],
|
||||||
|
})
|
||||||
|
await delay(50)
|
||||||
|
writeSse(res, {
|
||||||
|
id: "chatcmpl-opentui-repro",
|
||||||
|
object: "chat.completion.chunk",
|
||||||
|
created: Math.floor(Date.now() / 1000),
|
||||||
|
model: "test-model",
|
||||||
|
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
||||||
|
})
|
||||||
|
res.write("data: [DONE]\n\n")
|
||||||
|
res.end()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(200, { "content-type": "application/json" })
|
||||||
|
res.end(
|
||||||
|
JSON.stringify({
|
||||||
|
id: "chatcmpl-opentui-repro",
|
||||||
|
object: "chat.completion",
|
||||||
|
created: Math.floor(Date.now() / 1000),
|
||||||
|
model: "test-model",
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
message: { role: "assistant", content: "This is a deterministic CI response from the fake provider." },
|
||||||
|
finish_reason: "stop",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(404, { "content-type": "application/json" })
|
||||||
|
res.end(JSON.stringify({ error: { message: `Unhandled fake provider route: ${req.url}` } }))
|
||||||
|
})
|
||||||
|
|
||||||
|
await new Promise((resolveListen) => server.listen(0, "127.0.0.1", resolveListen))
|
||||||
|
const { port } = server.address()
|
||||||
|
const baseURL = `http://127.0.0.1:${port}/v1`
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
join(project, "opencode.json"),
|
||||||
|
`${JSON.stringify(
|
||||||
|
{
|
||||||
|
$schema: "https://opencode.ai/config.json",
|
||||||
|
autoupdate: false,
|
||||||
|
enabled_providers: ["test"],
|
||||||
|
provider: {
|
||||||
|
test: {
|
||||||
|
name: "Test",
|
||||||
|
id: "test",
|
||||||
|
env: [],
|
||||||
|
npm: "@ai-sdk/openai-compatible",
|
||||||
|
models: {
|
||||||
|
"test-model": {
|
||||||
|
id: "test-model",
|
||||||
|
name: "Test Model",
|
||||||
|
attachment: false,
|
||||||
|
reasoning: false,
|
||||||
|
temperature: false,
|
||||||
|
tool_call: true,
|
||||||
|
release_date: "2025-01-01",
|
||||||
|
limit: { context: 100000, output: 10000 },
|
||||||
|
cost: { input: 0, output: 0 },
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: { apiKey: "test-key", baseURL },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}\n`,
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log(`fake provider baseURL=${baseURL}`)
|
||||||
|
console.log(`pty output log=${outputFile}`)
|
||||||
|
console.log(`provider requests log=${requestsFile}`)
|
||||||
|
|
||||||
|
const env = {
|
||||||
|
...process.env,
|
||||||
|
OPENCODE_DISABLE_AUTOUPDATE: "1",
|
||||||
|
OPENCODE_DISABLE_EXTERNAL_SKILLS: "1",
|
||||||
|
OPENCODE_DISABLE_LSP_DOWNLOAD: "1",
|
||||||
|
OPENCODE_LOG_LEVEL: "DEBUG",
|
||||||
|
OPENCODE_PRINT_LOGS: "1",
|
||||||
|
OPENCODE_PURE: "1",
|
||||||
|
OTUI_DEBUG: "1",
|
||||||
|
OTUI_DUMP_CAPTURES: "1",
|
||||||
|
OTUI_USE_CONSOLE: "1",
|
||||||
|
OTUI_SHOW_STATS: "1",
|
||||||
|
}
|
||||||
|
|
||||||
|
const proc = pty.spawn(
|
||||||
|
exe,
|
||||||
|
["--model", "test/test-model", "--prompt", "start a CI repro session and answer briefly"],
|
||||||
|
{
|
||||||
|
name: "xterm-256color",
|
||||||
|
cols: 120,
|
||||||
|
rows: 40,
|
||||||
|
cwd: resolve(project),
|
||||||
|
env,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
proc.onData((data) => {
|
||||||
|
appendOutput(data)
|
||||||
|
process.stdout.write(data)
|
||||||
|
})
|
||||||
|
|
||||||
|
proc.onExit((event) => {
|
||||||
|
exited = true
|
||||||
|
exitCode = event.exitCode
|
||||||
|
exitSignal = event.signal
|
||||||
|
})
|
||||||
|
|
||||||
|
const deadline = Date.now() + seconds * 1000
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
if (crashPattern.test(output)) break
|
||||||
|
if (exited) break
|
||||||
|
if (providerRequests > 0 && output.includes("fake provider")) break
|
||||||
|
await delay(500)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!exited) {
|
||||||
|
proc.write("\x03")
|
||||||
|
await delay(1500)
|
||||||
|
}
|
||||||
|
if (!exited) {
|
||||||
|
proc.kill()
|
||||||
|
await delay(500)
|
||||||
|
}
|
||||||
|
|
||||||
|
server.close()
|
||||||
|
|
||||||
|
console.log(`\npty exitCode=${exitCode} signal=${exitSignal} providerRequests=${providerRequests}`)
|
||||||
|
|
||||||
|
if (crashPattern.test(output)) {
|
||||||
|
throw new Error("native crash signature detected in PTY session")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exitCode === 3 || exitCode === -1073741819 || exitCode === -1073740791 || exitCode === -1073741571) {
|
||||||
|
throw new Error(`native crash exit code detected in PTY session: ${exitCode}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (providerRequests === 0) {
|
||||||
|
throw new Error("PTY session did not send a request to the fake provider")
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue