#!/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: path.join(project, "src/build.rs") }, breakpoints: [{ line: 22 }, { line: 31 }, { line: 60 }] }); 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, 22); assert.strictEqual(stack[0].source.path, path.join(project, "src/build.rs")); assert.strictEqual(stack[0].source.sourceReference || 0, 0); const sourceRequest = client.send("source", { source: stack[0].source }); const source = (await client.response(sourceRequest, "source")).body; assert.match(source.content, /compile_linux/); const nextRequest = client.send("next", { threadId: linuxThread.id }); await client.response(nextRequest, "next"); const stepped = await client.waitFor( (message) => message.type === "event" && message.event === "stopped" && message.body.reason === "step" ); assert.strictEqual(stepped.body.threadId, linuxThread.id); const steppedStackRequest = client.send("stackTrace", { threadId: linuxThread.id, startFrame: 0, levels: 1 }); const steppedStack = (await client.response(steppedStackRequest, "stackTrace")).body.stackFrames; assert.strictEqual(steppedStack[0].line, 23); const scopesRequest = client.send("scopes", { frameId: steppedStack[0].id }); const scopes = (await client.response(scopesRequest, "scopes")).body.scopes; const localsScope = scopes.find((scope) => scope.name === "Source Locals"); const argsScope = scopes.find((scope) => scope.name === "Task Args and Handles"); const runtimeScope = scopes.find((scope) => scope.name === "Disasmer Runtime"); const outputScope = scopes.find((scope) => scope.name === "Recent Output"); assert(localsScope, "F5 launch must expose source locals scope"); assert(argsScope, "F5 launch must expose task args and handles"); assert(runtimeScope, "F5 launch must expose Disasmer runtime state"); assert(outputScope, "F5 launch must expose recent output state"); const localsRequest = client.send("variables", { variablesReference: localsScope.variablesReference }); const locals = (await client.response(localsRequest, "variables")).body.variables; assert( locals.some( (variable) => variable.name === "unavailable-local-diagnostic" && String(variable.value).includes("cannot be inspected") ), "source locals scope must report unavailable real Rust locals explicitly" ); const argsRequest = client.send("variables", { variablesReference: argsScope.variablesReference }); const args = (await client.response(argsRequest, "variables")).body.variables; assert(args.some((variable) => variable.name === "return_value")); assert(args.some((variable) => variable.name === "artifact")); assert(args.some((variable) => variable.name === "source_snapshot")); assert(args.some((variable) => variable.name === "blob")); assert(args.some((variable) => variable.name === "vfs_mounts")); 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") ) ); assert(runtime.some((variable) => variable.name === "command_spec")); assert(runtime.some((variable) => variable.name === "stdout_tail")); assert(runtime.some((variable) => variable.name === "stderr_tail")); const outputRequest = client.send("variables", { variablesReference: outputScope.variablesReference }); const output = (await client.response(outputRequest, "variables")).body.variables; assert(output.some((variable) => variable.name === "stdout_tail")); assert(output.some((variable) => variable.name === "stderr_tail")); const continueToWasmLocals = client.send("continue", { threadId: linuxThread.id }); await client.response(continueToWasmLocals, "continue"); await client.waitFor((message) => message.type === "event" && message.event === "continued"); const wasmLocalsStop = await client.waitFor( (message) => message.type === "event" && message.event === "stopped" && message.body.reason === "breakpoint" && message.body.threadId === 1 ); assert.strictEqual(wasmLocalsStop.body.allThreadsStopped, true); const wasmLocalsStackRequest = client.send("stackTrace", { threadId: 1, startFrame: 0, levels: 1 }); const wasmLocalsStack = (await client.response(wasmLocalsStackRequest, "stackTrace")).body .stackFrames; assert.strictEqual(wasmLocalsStack[0].line, 31); const wasmLocalsScopesRequest = client.send("scopes", { frameId: wasmLocalsStack[0].id }); const wasmLocalsScopes = (await client.response(wasmLocalsScopesRequest, "scopes")).body.scopes; const wasmLocalsScope = wasmLocalsScopes.find((scope) => scope.name === "Wasm Frame Locals"); assert(wasmLocalsScope, "F5 launch must expose Wasm frame locals scope"); const wasmLocalsRequest = client.send("variables", { variablesReference: wasmLocalsScope.variablesReference }); const wasmLocals = (await client.response(wasmLocalsRequest, "variables")).body.variables; assert( wasmLocals.some( (variable) => variable.name === "wasm_local_0" && String(variable.value).includes("41") && variable.type === "wasm-frame-local" ), "Wasm frame locals must expose runtime values in the VS Code variables path" ); const continueToLocals = client.send("continue", { threadId: 1 }); await client.response(continueToLocals, "continue"); await client.waitFor((message) => message.type === "event" && message.event === "continued"); const sourceLocalsStop = await client.waitFor( (message) => message.type === "event" && message.event === "stopped" && message.body.reason === "breakpoint" && message.body.threadId === 1 ); assert.strictEqual(sourceLocalsStop.body.allThreadsStopped, true); const sourceLocalsStackRequest = client.send("stackTrace", { threadId: 1, startFrame: 0, levels: 1 }); const sourceLocalsStack = (await client.response(sourceLocalsStackRequest, "stackTrace")).body .stackFrames; assert.strictEqual(sourceLocalsStack[0].line, 60); const sourceLocalsScopesRequest = client.send("scopes", { frameId: sourceLocalsStack[0].id }); const sourceLocalsScopes = (await client.response(sourceLocalsScopesRequest, "scopes")).body .scopes; const sourceLocalsScope = sourceLocalsScopes.find((scope) => scope.name === "Source Locals"); assert(sourceLocalsScope, "source-local breakpoint must expose source locals"); const sourceLocalsRequest = client.send("variables", { variablesReference: sourceLocalsScope.variablesReference }); const sourceLocals = (await client.response(sourceLocalsRequest, "variables")).body.variables; assert( sourceLocals.some( (variable) => variable.name === "linux" && String(variable.value).includes("TaskHandle") && String(variable.value).includes("compile-linux") && String(variable.value).includes("virtual_thread_id = 2") ), "source locals must include the spawned Linux task handle value" ); assert( sourceLocals.some((variable) => variable.name === "linux_thread" && variable.value === "2"), "source locals must include the Linux virtual thread id value" ); assert( sourceLocals.some( (variable) => variable.name === "linux_artifact" && String(variable.value).includes("Artifact") ), "source locals must include the Linux artifact handle value" ); 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); });