#!/usr/bin/env node const assert = require("assert"); const cp = require("child_process"); const path = require("path"); const extension = require("../vscode-extension/extension"); class DapClient { constructor(spec) { this.child = cp.spawn(spec.command, spec.args, { cwd: spec.options && spec.options.cwd, detached: process.platform !== "win32" }); this.seq = 1; this.buffer = Buffer.alloc(0); this.messages = []; this.waiters = []; this.stderr = ""; this.child.stdout.on("data", (chunk) => { this.buffer = Buffer.concat([this.buffer, chunk]); this.parse(); }); this.child.stderr.on("data", (chunk) => { this.stderr += chunk.toString(); }); this.child.on("exit", () => this.flushWaiters()); } send(command, args = {}) { const seq = this.seq++; const message = { seq, type: "request", command, arguments: args }; const payload = Buffer.from(JSON.stringify(message)); this.child.stdin.write(`Content-Length: ${payload.length}\r\n\r\n`); this.child.stdin.write(payload); return seq; } async response(seq, command) { const message = await this.waitFor( (item) => item.type === "response" && item.request_seq === seq && item.command === command ); if (!message.success) { throw new Error(`DAP ${command} failed: ${message.message || JSON.stringify(message)}`); } return message; } terminate() { if (this.child.exitCode !== null) return; if (process.platform === "win32") { this.child.kill("SIGKILL"); return; } try { process.kill(-this.child.pid, "SIGKILL"); } catch (_) { this.child.kill("SIGKILL"); } } waitFor(predicate, timeoutMs = 240000) { const existing = this.messages.find(predicate); if (existing) return Promise.resolve(existing); return new Promise((resolve, reject) => { const timer = setTimeout(() => { this.terminate(); reject(new Error(`timed out waiting for DAP message\n${this.stderr}`)); }, timeoutMs); this.waiters.push({ predicate, resolve, timer }); }); } parse() { while (true) { const headerEnd = this.buffer.indexOf("\r\n\r\n"); if (headerEnd < 0) return; const header = this.buffer.slice(0, headerEnd).toString(); const match = header.match(/Content-Length: (\d+)/i); if (!match) throw new Error(`bad DAP header: ${header}`); const length = Number(match[1]); const start = headerEnd + 4; const end = start + length; if (this.buffer.length < end) return; const payload = this.buffer.slice(start, end).toString(); this.buffer = this.buffer.slice(end); this.messages.push(JSON.parse(payload)); this.flushWaiters(); } } flushWaiters() { for (const waiter of [...this.waiters]) { const message = this.messages.find(waiter.predicate); if (!message) continue; clearTimeout(waiter.timer); this.waiters.splice(this.waiters.indexOf(waiter), 1); waiter.resolve(message); } } async close() { if (this.child.exitCode !== null) return; const seq = this.send("disconnect"); await this.response(seq, "disconnect"); this.child.stdin.end(); } } (async () => { const repo = path.resolve(__dirname, ".."); const project = path.join(repo, "examples/launch-build-demo"); const launchConfig = extension.resolveDisasmerDebugConfiguration( { uri: { fsPath: project } }, {} ); assert.strictEqual(launchConfig.type, "disasmer"); assert.strictEqual(launchConfig.request, "launch"); assert.strictEqual(launchConfig.entry, "build"); assert.strictEqual(launchConfig.project, project); assert.strictEqual(launchConfig.runtimeBackend, "local-services"); const inspection = extension.refreshBundleBeforeLaunch(project, repo); assert.match(inspection.metadata.identity, /^sha256:/); const client = new DapClient(extension.debugAdapterExecutableSpec(repo)); try { const initialize = client.send("initialize", { adapterID: "disasmer", linesStartAt1: true, columnsStartAt1: true }); await client.response(initialize, "initialize"); const launch = client.send("launch", launchConfig); await client.response(launch, "launch"); await client.waitFor( (message) => message.type === "event" && message.event === "initialized" ); const breakpoints = client.send("setBreakpoints", { source: { path: "src/build.rs" }, breakpoints: [{ line: 42 }] }); const breakpointResponse = await client.response(breakpoints, "setBreakpoints"); assert.strictEqual(breakpointResponse.body.breakpoints[0].verified, true); const configurationDone = client.send("configurationDone"); await client.response(configurationDone, "configurationDone"); const stopped = await client.waitFor( (message) => message.type === "event" && message.event === "stopped" ); assert.strictEqual(stopped.body.allThreadsStopped, true); assert.strictEqual(stopped.body.reason, "breakpoint"); const threadsRequest = client.send("threads"); const threads = (await client.response(threadsRequest, "threads")).body.threads; const linuxThread = threads.find((thread) => thread.name.includes("compile linux")); assert(linuxThread, "F5 launch must expose the Linux task as a virtual thread"); const stackRequest = client.send("stackTrace", { threadId: linuxThread.id, startFrame: 0, levels: 1 }); const stack = (await client.response(stackRequest, "stackTrace")).body.stackFrames; assert.strictEqual(stack[0].line, 42); const scopesRequest = client.send("scopes", { frameId: stack[0].id }); const scopes = (await client.response(scopesRequest, "scopes")).body.scopes; const runtimeScope = scopes.find((scope) => scope.name === "Disasmer Runtime"); assert(runtimeScope, "F5 launch must expose Disasmer runtime state"); const runtimeRequest = client.send("variables", { variablesReference: runtimeScope.variablesReference }); const runtime = (await client.response(runtimeRequest, "variables")).body.variables; assert( runtime.some( (variable) => variable.name === "runtime_backend" && variable.value === "LocalServices" ), "extension-resolved F5 launch must use the real local-services backend" ); assert( runtime.some( (variable) => variable.name === "coordinator_task_events" && variable.value === 1 ), "F5 launch must cross the coordinator/node boundary and record a task event" ); assert( runtime.some( (variable) => variable.name === "command_status" && String(variable.value).includes("completed through local services") ) ); await client.close(); } catch (error) { client.terminate(); throw error; } console.log("VS Code F5 smoke passed"); })().catch((error) => { console.error(error.stack || error.message); process.exit(1); });