#!/usr/bin/env node const assert = require("assert"); const cp = require("child_process"); const crypto = require("crypto"); const fs = require("fs"); const http = require("http"); const https = require("https"); const net = require("net"); const os = require("os"); const path = require("path"); const { configurePodmanTestEnvironment } = require("./podman-test-env"); const { coordinatorWireRequest } = require("./coordinator-wire"); const { nodeIdentity } = require("./node-signing"); const { assertPreFinalLedger, readPhase3Ledger, } = require("./phase3-ledger"); const repo = path.resolve(__dirname, ".."); configurePodmanTestEnvironment(repo); if ( !process.env.DISASMER_PODMAN_NIX_SHELL && cp.spawnSync("podman", ["--version"], { stdio: "ignore" }).status !== 0 && cp.spawnSync("nix", ["--version"], { stdio: "ignore" }).status === 0 ) { cp.execFileSync( "nix", ["shell", "nixpkgs#podman", "--command", "node", __filename], { cwd: repo, env: { ...process.env, DISASMER_PODMAN_NIX_SHELL: "1" }, stdio: "inherit", } ); process.exit(0); } const releaseRoot = path.resolve( process.env.DISASMER_PUBLIC_RELEASE_DIR || path.join(repo, "target/public-release-dryrun") ); const acceptanceRoot = path.join(repo, "target/acceptance"); const manifestPath = process.env.DISASMER_PUBLIC_RELEASE_MANIFEST || path.join(releaseRoot, "public-release-manifest.json"); const forgejoReportPath = process.env.DISASMER_PUBLIC_RELEASE_FORGEJO_REPORT || path.join(acceptanceRoot, "public-release-dryrun-forgejo-release.json"); const reportPath = process.env.DISASMER_PUBLIC_RELEASE_E2E_REPORT || path.join(acceptanceRoot, "public-release-dryrun-e2e.json"); const serviceEndpoint = "https://disasmer.michelpaulissen.com"; const serviceHost = "disasmer.michelpaulissen.com"; const serviceAddr = process.env.DISASMER_PUBLIC_RELEASE_DRYRUN_SERVICE_ADDR || `${serviceHost}:443`; const forgejoHost = "git.michelpaulissen.com"; const enabled = process.env.DISASMER_PUBLIC_RELEASE_DRYRUN_E2E === "1"; const browserOpenCommand = process.env.DISASMER_PUBLIC_RELEASE_DRYRUN_BROWSER_OPEN_COMMAND; const dnsPublicationState = process.env.DISASMER_DNS_PUBLICATION_STATE || "published"; const resolverOverride = process.env.DISASMER_RESOLVER_OVERRIDE || "none-required-public-dns"; function requireEnabled() { if (!enabled) { throw new Error( "DISASMER_PUBLIC_RELEASE_DRYRUN_E2E=1 is required because this runs the real public dry-run e2e" ); } if (!browserOpenCommand) { throw new Error( "DISASMER_PUBLIC_RELEASE_DRYRUN_BROWSER_OPEN_COMMAND is required to complete the server-owned hosted browser login" ); } } function readJson(file) { return JSON.parse(fs.readFileSync(file, "utf8")); } function ensureDir(dir) { fs.mkdirSync(dir, { recursive: true }); } function run(command, args, options = {}) { const commandLine = [command, ...args].join(" "); commands.push(commandLine); const execOptions = { cwd: repo, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 15 * 60 * 1000, ...options, }; execOptions.env = commandEnv(command, options.env); return cp.execFileSync(command, args, execOptions); } function runJson(command, args, options = {}) { const output = run(command, args, options); try { return JSON.parse(output); } catch (_) { const line = output .trim() .split("\n") .filter(Boolean) .at(-1); return JSON.parse(line); } } function commandOutput(command, args, options = {}) { try { const execOptions = { cwd: repo, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], ...options, }; execOptions.env = commandEnv(command, options.env); return cp.execFileSync(command, args, execOptions).trim(); } catch (_) { return null; } } function nonInteractiveGitEnv(extra = {}) { return { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: process.env.GIT_ASKPASS || "/bin/false", SSH_ASKPASS: process.env.SSH_ASKPASS || "/bin/false", GIT_SSH_COMMAND: process.env.GIT_SSH_COMMAND || "ssh -o BatchMode=yes -o NumberOfPasswordPrompts=0", ...extra, }; } function commandEnv(command, env) { if (command !== "git") { return env; } return nonInteractiveGitEnv(env); } function expectedSourceCommit() { return ( process.env.DISASMER_ACCEPTANCE_COMMIT || commandOutput("git", ["rev-parse", "HEAD"]) || "unknown" ); } function publicTreeAlreadyPushed(manifest) { return ( (manifest.public_tree_publish && manifest.public_tree_publish.pushed === true) || process.env.DISASMER_PUBLIC_TREE_ALREADY_PUSHED === "1" ); } function sha256File(file) { return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); } function parseAddr(value) { const match = /^([^:]+):(\d+)$/.exec(value); if (!match) { throw new Error(`service address must be host:port, got ${value}`); } return { host: match[1], port: Number(match[2]) }; } function send(addr, message) { return new Promise((resolve, reject) => { const body = Buffer.from(JSON.stringify(message)); const request = https.request({ hostname: addr.host, port: addr.port, servername: serviceHost, path: "/api/v1/control", method: "POST", headers: { "Content-Type": "application/json", "Content-Length": body.length, }, timeout: 30000, }, (response) => { const chunks = []; response.on("data", (chunk) => chunks.push(chunk)); response.on("end", () => { const responseBody = Buffer.concat(chunks).toString("utf8"); if (response.statusCode < 200 || response.statusCode >= 300) { reject(new Error(`hosted control HTTP ${response.statusCode}: ${responseBody}`)); return; } try { resolve(JSON.parse(responseBody)); } catch (error) { reject(error); } }); }); request.on("timeout", () => request.destroy(new Error(`timed out waiting for ${message.type}`))); request.on("error", reject); request.end(body); }); } function sendCore(addr, message) { return new Promise((resolve, reject) => { const socket = net.connect(addr.port, addr.host, () => { socket.write( `${JSON.stringify(coordinatorWireRequest(message, "public-e2e-core"))}\n` ); }); let buffer = ""; const timer = setTimeout(() => { socket.destroy(); reject(new Error(`timed out waiting for standalone Core ${message.type}`)); }, 30000); socket.on("data", (chunk) => { buffer += chunk.toString(); const newline = buffer.indexOf("\n"); if (newline < 0) return; clearTimeout(timer); socket.destroy(); try { resolve(JSON.parse(buffer.slice(0, newline))); } catch (error) { reject(error); } }); socket.on("error", (error) => { clearTimeout(timer); reject(error); }); }); } let hostedClientRequestId = 0; function authenticatedHostedClientRequest(sessionSecret, message) { hostedClientRequestId += 1; return { type: "coordinator_request", protocol_version: 1, request_id: `public-e2e-client-${hostedClientRequestId}`, operation: "authenticated", authentication: { kind: "cli_session", session: true, request_operation: message.type, }, payload: { type: "authenticated", session_secret: sessionSecret, request: message, }, }; } function waitForJsonLine(child, label = "process", timeoutMs = 240000) { return new Promise((resolve, reject) => { let buffer = ""; const timer = setTimeout(() => { cleanup(); reject(new Error(`timed out waiting for JSON line from ${label}`)); }, timeoutMs); function cleanup() { clearTimeout(timer); child.stdout.off("data", onData); child.off("exit", onExit); } function onData(chunk) { buffer += chunk.toString(); const newline = buffer.indexOf("\n"); if (newline < 0) return; try { cleanup(); resolve(JSON.parse(buffer.slice(0, newline).trim())); } catch (error) { cleanup(); reject(error); } } function onExit(code) { cleanup(); reject(new Error(`${label} exited before JSON line with code ${code}`)); } child.stdout.on("data", onData); child.once("exit", onExit); }); } function stopChild(child) { if (!child || child.exitCode !== null) { return Promise.resolve(); } return new Promise((resolve) => { const timer = setTimeout(() => { child.kill("SIGKILL"); resolve(); }, 5000); child.once("exit", () => { clearTimeout(timer); resolve(); }); child.kill("SIGTERM"); }); } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } class DapClient { constructor(command, options = {}) { this.child = cp.spawn(command, [], { cwd: 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(); } } function download(url, dest, redirects = 0) { return new Promise((resolve, reject) => { const client = url.startsWith("http://") ? http : https; const request = client.get(url, (response) => { if ( [301, 302, 303, 307, 308].includes(response.statusCode) && response.headers.location && redirects < 5 ) { response.resume(); download(new URL(response.headers.location, url).toString(), dest, redirects + 1) .then(resolve) .catch(reject); return; } if (response.statusCode < 200 || response.statusCode >= 300) { response.resume(); reject(new Error(`download ${url} failed with ${response.statusCode}`)); return; } ensureDir(path.dirname(dest)); const file = fs.createWriteStream(dest); response.pipe(file); file.on("finish", () => file.close(resolve)); file.on("error", reject); }); request.on("error", reject); }); } function releaseAssetDownloads(forgejoReport) { const assets = [ ...(forgejoReport.uploaded_assets || []), ...(forgejoReport.reused_assets || []), ]; const byName = new Map(); for (const asset of assets) { if (asset.name && asset.browser_download_url) { byName.set(asset.name, asset.browser_download_url); } } return byName; } async function downloadReleaseAssets(manifest, forgejoReport, assetsDir) { const downloads = releaseAssetDownloads(forgejoReport); const downloaded = []; for (const asset of manifest.assets) { const dest = path.join(assetsDir, asset.name); const url = downloads.get(asset.name); if (url) { await download(url, dest); } else if (process.env.DISASMER_PUBLIC_RELEASE_USE_LOCAL_ASSETS === "1") { fs.copyFileSync(asset.file, dest); } else { throw new Error(`Forgejo Release report has no download URL for ${asset.name}`); } downloaded.push(dest); } return downloaded; } function verifyChecksums(assetsDir) { const sumsPath = path.join(assetsDir, "SHA256SUMS"); assert(fs.existsSync(sumsPath), "SHA256SUMS must be downloaded from the release"); for (const line of fs.readFileSync(sumsPath, "utf8").split("\n")) { if (!line.trim()) continue; const match = /^([a-f0-9]{64})\s+(.+)$/.exec(line.trim()); assert(match, `invalid SHA256SUMS line: ${line}`); const [, expected, name] = match; const file = path.join(assetsDir, name); assert(fs.existsSync(file), `checksum references missing asset ${name}`); assert.strictEqual(sha256File(file), expected, `checksum mismatch for ${name}`); } } function binaryArchive(manifest) { const platform = `${os.platform()}-${os.arch()}`; const asset = manifest.assets.find( (candidate) => candidate.name.startsWith("disasmer-public-binaries-") && candidate.name.includes(platform) ); if (!asset) { throw new Error(`release manifest has no binary archive for ${platform}`); } return asset.name; } function walkFiles(root, relative = "") { const dir = path.join(root, relative); const entries = fs .readdirSync(dir, { withFileTypes: true }) .filter((entry) => relative || entry.name !== ".git") .sort((left, right) => left.name.localeCompare(right.name)); const files = []; for (const entry of entries) { const childRelative = relative ? path.join(relative, entry.name) : entry.name; if (entry.isDirectory()) { files.push(...walkFiles(root, childRelative)); } else if (entry.isFile() || entry.isSymbolicLink()) { files.push(childRelative); } } return files; } function hashTree(root) { const hash = crypto.createHash("sha256"); for (const file of walkFiles(root)) { const absolute = path.join(root, file); const stat = fs.lstatSync(absolute); hash.update(file.replaceAll(path.sep, "/")); hash.update("\0"); hash.update(String(stat.mode & 0o777)); hash.update("\0"); if (stat.isSymbolicLink()) { hash.update("symlink"); hash.update("\0"); hash.update(fs.readlinkSync(absolute)); } else { hash.update(fs.readFileSync(absolute)); } hash.update("\0"); } return `sha256:${hash.digest("hex")}`; } function assertFilteredPublicCheckout(checkout, manifest) { for (const forbidden of ["private", "experiments", "target"]) { assert(!fs.existsSync(path.join(checkout, forbidden)), `${forbidden}/ leaked into public repo`); } for (const file of walkFiles(checkout)) { assert( !file.split(path.sep).includes(".disasmer"), `generated .disasmer view state leaked into public repo: ${file}` ); } for (const entry of fs.readdirSync(checkout, { withFileTypes: true })) { if (entry.isFile() && path.extname(entry.name).toLowerCase() === ".md") { assert( ["README.md", "SECURITY.md"].includes(entry.name), `internal root Markdown file leaked into public repo: ${entry.name}` ); } } const readmePath = path.join(checkout, "README.md"); assert(fs.existsSync(readmePath), "public checkout must include product README.md"); for (const required of ["SECURITY.md", "LICENSE-APACHE", "LICENSE-MIT"]) { assert( fs.existsSync(path.join(checkout, required)), `public checkout must include ${required}` ); } const readme = fs.readFileSync(readmePath, "utf8"); assert.match(readme, /# Disasmer/); assert.match(readme, /disasmer (login|project|run|node)/); const includeForgejoWorkflows = manifest.public_export && manifest.public_export.include_forgejo_workflows; if (!includeForgejoWorkflows) { assert(!fs.existsSync(path.join(checkout, ".forgejo")), ".forgejo/ leaked into public repo"); } const provenancePath = path.join(checkout, "DISASMER_PUBLIC_TREE.json"); assert(fs.existsSync(provenancePath), "public checkout must include DISASMER_PUBLIC_TREE.json"); const provenance = readJson(provenancePath); assert.strictEqual(provenance.source_commit, manifest.source_commit); assert.strictEqual(provenance.release_name, manifest.release_name); for (const expected of [ "private/**", "experiments/**", ".git", "target", "root/*.md except README.md and SECURITY.md", "**/.disasmer/**", ]) { assert(provenance.filtered_out.includes(expected), `public provenance missing ${expected}`); } if (!includeForgejoWorkflows) { assert( provenance.filtered_out.includes(".forgejo/**"), "public provenance must record .forgejo filtering" ); } assert.strictEqual(hashTree(checkout), manifest.public_tree_identity); } function executable(root, name) { const suffix = process.platform === "win32" ? ".exe" : ""; const file = path.join(root, "bin", `${name}${suffix}`); assert(fs.existsSync(file), `missing release binary ${file}`); return file; } const commands = []; async function validateStandaloneCoreCoordinator( disasmerCoordinator, disasmerNode, checkout, bundleArtifact ) { let coordinator; let worker; let workerStderr = ""; const suffix = String(Date.now()); const tenant = `public-coordinator-${suffix}`; const project = `self-hosted-${suffix}`; const node = `public-node-${suffix}`; const processId = `vp-public-coordinator-${suffix}`; const task = "prepare_source"; const artifactPath = "/vfs/artifacts/public-coordinator-output.txt"; const sessionSecret = `public-core-session-${suffix}`; const nodeKey = nodeIdentity("public-release-core-node", node); const bundleDirectory = path.resolve(checkout, bundleArtifact.directory); const manifest = readJson(path.join(bundleDirectory, "manifest.json")); const taskDescriptors = readJson( path.join(bundleDirectory, manifest.task_descriptors) ); const entrypoint = taskDescriptors.find((candidate) => candidate.name === task); assert(entrypoint, "public bundle omitted the source task"); const taskDefinition = entrypoint.stable_id; const taskInstance = `ti:${processId}:task`; const wasmModuleBase64 = fs .readFileSync(path.join(bundleDirectory, "module.wasm")) .toString("base64"); try { const coordinatorArgs = ["--listen", "127.0.0.1:0"]; commands.push([disasmerCoordinator, ...coordinatorArgs].join(" ")); coordinator = cp.spawn(disasmerCoordinator, coordinatorArgs, { cwd: checkout, env: { ...process.env, DISASMER_SELF_HOSTED_SESSION_SECRET: sessionSecret, DISASMER_SELF_HOSTED_TENANT: tenant, DISASMER_SELF_HOSTED_PROJECT: project, DISASMER_SELF_HOSTED_USER: "developer", }, }); const ready = await waitForJsonLine(coordinator, "standalone Core coordinator"); const addr = parseAddr(ready.listen); assert.strictEqual(ready.client_authority, "strict"); assert.strictEqual(ready.self_hosted_session_bootstrapped, true); assert.strictEqual((await sendCore(addr, { type: "ping" })).type, "pong"); const forged = await sendCore(addr, { type: "create_project", tenant, actor_user: "developer", project, }); assert.strictEqual(forged.type, "error"); const wrongSession = await sendCore( addr, authenticatedHostedClientRequest("wrong-session", { type: "list_projects", }) ); assert.strictEqual(wrongSession.type, "error"); const created = await sendCore( addr, authenticatedHostedClientRequest(sessionSecret, { type: "create_project", project, name: "Public Coordinator Dry Run", }) ); assert( ["project", "project_created"].includes(created.type), `unexpected standalone Core coordinator project response: ${created.type}` ); const grant = await sendCore( addr, authenticatedHostedClientRequest(sessionSecret, { type: "create_node_enrollment_grant", ttl_seconds: 900, }) ); assert.strictEqual(grant.type, "node_enrollment_grant_created"); const workerArgs = [ "--coordinator", ready.listen, "--tenant", tenant, "--project-id", project, "--node", node, "--project-root", checkout, "--public-key", nodeKey.publicKey, "--enrollment-grant", grant.grant, "--worker", "--assignment-poll-ms", "50", "--emit-ready", ]; commands.push([disasmerNode, ...workerArgs].join(" ")); worker = cp.spawn(disasmerNode, workerArgs, { cwd: checkout, env: { ...process.env, DISASMER_NODE_PRIVATE_KEY: nodeKey.privateKey }, }); worker.stderr.on("data", (chunk) => { workerStderr += chunk.toString(); }); const workerReady = await waitForJsonLine(worker, "standalone Core coordinator worker"); assert.strictEqual(workerReady.node_status, "ready"); assert.strictEqual(workerReady.mode, "worker"); assert.strictEqual(workerReady.node, node); const started = await sendCore( addr, authenticatedHostedClientRequest(sessionSecret, { type: "start_process", process: processId, }) ); assert.strictEqual(started.type, "process_started"); const launch = await sendCore( addr, authenticatedHostedClientRequest(sessionSecret, { type: "launch_task", task_spec: { tenant, project, process: processId, task_definition: taskDefinition, task_instance: taskInstance, dispatch: { kind: "coordinator_node_wasm", export: entrypoint.export, abi: "task_v1", }, environment_id: null, environment: null, environment_digest: null, required_capabilities: entrypoint.required_capabilities, dependency_cache: null, source_snapshot: null, required_artifacts: [], args: [], vfs_epoch: started.epoch, bundle_digest: manifest.bundle_digest, }, wait_for_node: false, artifact_path: artifactPath, wasm_module_base64: wasmModuleBase64, }) ); assert.strictEqual(launch.type, "task_launched"); assert.strictEqual(launch.placement.node, node); assert.strictEqual(launch.assignment.node, node); assert.strictEqual(launch.assignment.process, processId); const nodeRun = await waitForJsonLine(worker, "standalone Core coordinator worker completion"); assert.strictEqual(nodeRun.node_status, "completed"); assert.strictEqual(nodeRun.registration_response.type, "node_enrollment_exchanged"); assert.strictEqual(nodeRun.task_assignment_response.node, node); assert.strictEqual(nodeRun.task_assignment_response.process, processId); assert.strictEqual(nodeRun.task_assignment_response.task, taskInstance); assert.strictEqual(nodeRun.coordinator_response.type, "task_recorded"); const events = await sendCore( addr, authenticatedHostedClientRequest(sessionSecret, { type: "list_task_events", process: processId, }) ); assert.strictEqual(events.type, "task_events"); assert(events.events.length >= 3, "standalone Core did not record the real task tree"); assert(events.events.some((event) => event.node === node)); assert(events.events.some((event) => event.task === "compile_linux")); return { coordinator_implementation: "standalone-core-coordinator", client_authority: ready.client_authority, authenticated_session: true, forged_body_authority_denied: forged.type, wrong_session_denied: wrongSession.type, coordinator_ready: Boolean(ready.listen), project_response: created.type, process_response: started.type, launch_task_response: launch.type, assignment_response: "task_assignment", worker_status: nodeRun.node_status, task_events: events.events.length, node, process: processId, }; } finally { if (workerStderr.trim()) { console.error(workerStderr); } await stopChild(worker); await stopChild(coordinator); } } async function variables(client, variablesReference) { const request = client.send("variables", { variablesReference }); return (await client.response(request, "variables")).body.variables; } function scopeReference(scopes, name) { const scope = scopes.find((candidate) => candidate.name === name); assert(scope, `missing DAP scope ${name}`); return scope.variablesReference; } async function validateReleasedLiveDap( disasmerDap, checkout, scope, processId ) { const projectRoot = path.join(checkout, "examples/launch-build-demo"); const sourcePath = path.join(projectRoot, "src/build.rs"); const sourceLines = fs.readFileSync(sourcePath, "utf8").split(/\r?\n/); const sourceLine = (needle) => { const index = sourceLines.findIndex((line) => line.includes(needle)); assert(index >= 0, `released flagship source must contain ${needle}`); return index + 1; }; const preTaskMainLine = sourceLine("pub source: SourceSnapshot"); const compileLinuxLine = sourceLine("pub fn compile_linux()"); const client = new DapClient(disasmerDap, { cwd: checkout }); try { const initialize = client.send("initialize", { adapterID: "disasmer", linesStartAt1: true, columnsStartAt1: true, }); await client.response(initialize, "initialize"); const launch = client.send("attach", { entry: "build", project: projectRoot, runtimeBackend: "live-services", coordinatorEndpoint: serviceEndpoint, processId, }); await client.response(launch, "attach"); await client.waitFor( (message) => message.type === "event" && message.event === "initialized" ); const breakpoints = client.send("setBreakpoints", { source: { path: sourcePath }, breakpoints: [{ line: preTaskMainLine }, { line: compileLinuxLine }], }); const breakpointResponse = await client.response(breakpoints, "setBreakpoints"); assert.deepStrictEqual( breakpointResponse.body.breakpoints.map((breakpoint) => breakpoint.verified), [true, 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.reason, "breakpoint"); assert.strictEqual(stopped.body.threadId, 1); assert.strictEqual(stopped.body.allThreadsStopped, true); const stackRequest = client.send("stackTrace", { threadId: 1, startFrame: 0, levels: 1, }); const stack = (await client.response(stackRequest, "stackTrace")).body.stackFrames; assert.strictEqual(stack[0].line, preTaskMainLine); assert.strictEqual(stack[0].source.path, sourcePath); const scopesRequest = client.send("scopes", { frameId: stack[0].id }); const scopes = (await client.response(scopesRequest, "scopes")).body.scopes; const runtime = await variables(client, scopeReference(scopes, "Disasmer Runtime")); const output = await variables(client, scopeReference(scopes, "Recent Output")); assert( runtime.some( (variable) => variable.name === "runtime_backend" && variable.value === "LiveServices" ) ); assert( runtime.some( (variable) => variable.name === "coordinator_task_events" && variable.value === 1 ) ); assert( runtime.some( (variable) => variable.name === "command_status" && String(variable.value).includes("attached to existing virtual process") ) ); assert( output.some((variable) => String(variable.value).includes("coordinator returned 1 task event") ) ); const continueRequest = client.send("continue", { threadId: 1 }); await client.response(continueRequest, "continue"); await client.waitFor( (message) => message.type === "event" && message.event === "continued" ); const taskStopped = await client.waitFor( (message) => message.type === "event" && message.event === "stopped" && message.body.reason === "breakpoint" && message.body.threadId === 2 ); assert.strictEqual(taskStopped.body.allThreadsStopped, true); const taskStackRequest = client.send("stackTrace", { threadId: 2, startFrame: 0, levels: 1, }); const taskStack = (await client.response(taskStackRequest, "stackTrace")).body.stackFrames; assert.strictEqual(taskStack[0].line, compileLinuxLine); const taskScopesRequest = client.send("scopes", { frameId: taskStack[0].id }); const taskScopes = (await client.response(taskScopesRequest, "scopes")).body.scopes; const taskRuntime = await variables(client, scopeReference(taskScopes, "Disasmer Runtime")); const taskOutput = await variables(client, scopeReference(taskScopes, "Recent Output")); assert( taskRuntime.some( (variable) => variable.name === "coordinator_task_events" && variable.value === 1 ) ); assert( taskRuntime.some( (variable) => variable.name === "command_status" && String(variable.value).includes("resumed through coordinator") && String(variable.value).includes("requested freeze") ) ); assert( taskRuntime.some( (variable) => variable.name === "state" && variable.value === "Completed" ) ); assert(taskRuntime.some((variable) => variable.name === "stdout_tail")); assert(taskRuntime.some((variable) => variable.name === "stderr_tail")); assert(taskOutput.some((variable) => variable.name === "stdout_tail")); assert(taskOutput.some((variable) => variable.name === "stderr_tail")); assert( taskOutput.some((variable) => String(variable.value).includes("coordinator task event from node") ) ); return { coordinator_implementation: "hosted-policy-coordinator", runtime_backend: "live-services", attached_to_existing_client_process: true, authenticated_with_cli_session: true, process: processId, main_thread_breakpoint: preTaskMainLine, task_thread_breakpoint: compileLinuxLine, task_events: 1, task_runtime_status: "debug epoch resumed and refrozen through live services", variables_verified: true, }; } finally { await client.close().catch(() => {}); } } async function main() { requireEnabled(); assertPreFinalLedger(readPhase3Ledger(repo)); const manifest = readJson(manifestPath); const forgejoReport = readJson(forgejoReportPath); assert.strictEqual(manifest.kind, "disasmer-public-release-dryrun"); assert.strictEqual( manifest.source_commit, expectedSourceCommit(), "public release e2e must use release assets prepared from the current acceptance commit" ); assert.strictEqual(manifest.default_hosted_coordinator_endpoint, serviceEndpoint); assert.strictEqual(manifest.source_tree_clean, true); assert.strictEqual(publicTreeAlreadyPushed(manifest), true); assert.strictEqual(forgejoReport.release_name, manifest.release_name); const addr = parseAddr(serviceAddr); assert.strictEqual(addr.host, serviceHost); assert.strictEqual(addr.port, 443); const workRoot = path.resolve( process.env.DISASMER_PUBLIC_RELEASE_E2E_WORKDIR || fs.mkdtempSync(path.join(os.tmpdir(), "disasmer-public-dryrun-e2e-")) ); const assetsDir = path.join(workRoot, "assets"); const installDir = path.join(workRoot, "install"); const checkout = path.join(workRoot, "public-repo"); ensureDir(workRoot); fs.rmSync(assetsDir, { recursive: true, force: true }); fs.rmSync(installDir, { recursive: true, force: true }); fs.rmSync(checkout, { recursive: true, force: true }); ensureDir(assetsDir); ensureDir(installDir); await downloadReleaseAssets(manifest, forgejoReport, assetsDir); verifyChecksums(assetsDir); const publicRepositoryUrl = process.env.DISASMER_PUBLIC_REPO_URL || manifest.public_repo_url || manifest.public_repo_remote; assert(publicRepositoryUrl, "public repository URL is required"); assert(publicRepositoryUrl.includes(forgejoHost)); run("git", ["clone", "--depth", "1", publicRepositoryUrl, checkout]); assertFilteredPublicCheckout(checkout, manifest); run("tar", ["-xzf", path.join(assetsDir, binaryArchive(manifest)), "-C", installDir]); const disasmer = executable(installDir, "disasmer"); const disasmerCoordinator = executable(installDir, "disasmer-coordinator"); const disasmerNode = executable(installDir, "disasmer-node"); const disasmerDap = executable(installDir, "disasmer-debug-dap"); for (const binary of [disasmer, disasmerCoordinator, disasmerNode, disasmerDap]) { const name = path.basename(binary); const expected = manifest.binary_digests && manifest.binary_digests[name]; assert(expected, `manifest omitted digest for ${name}`); assert.strictEqual(`sha256:${sha256File(binary)}`, expected); } const defaultLoginPlan = runJson(disasmer, ["login", "--plan", "--json"], { cwd: checkout, }); assert.strictEqual(defaultLoginPlan.coordinator, serviceEndpoint); const suffix = String(Date.now()); const requestedProject = `project-${suffix}`; const cliNode = `cli-node-${suffix}`; const runtimeNode = `runtime-node-${suffix}`; let processId; let task = "build"; let mainTask; let artifactPath; let artifact; const projectRoot = path.join(checkout, "examples/launch-build-demo"); const buildReport = runJson( disasmer, ["build", "--project", projectRoot, "--json"], { cwd: checkout } ); assert.strictEqual(buildReport.status, "built"); assert(buildReport.bundle_artifact); const login = runJson( disasmer, [ "login", "--browser", "--json", "--project-id", requestedProject, ], { cwd: projectRoot, env: { ...process.env, DISASMER_BROWSER_OPEN_COMMAND: browserOpenCommand, }, } ); assert.strictEqual(login.plan.coordinator, serviceEndpoint); assert.strictEqual(login.boundary.cli_contacted_coordinator, true); assert.strictEqual(login.boundary.scoped_cli_session_received, true); assert.strictEqual(login.boundary.provider_tokens_sent_to_nodes, false); const loginSession = login.coordinator_response.session; assert(loginSession, "hosted browser login omitted its scoped session"); const tenant = loginSession.tenant; const project = loginSession.project; const user = loginSession.user; for (const [name, value] of Object.entries({ tenant, project, user })) { assert.strictEqual(typeof value, "string", `hosted session omitted ${name}`); assert.notStrictEqual(value, "", `hosted session returned an empty ${name}`); } const storedSession = readJson(path.join(projectRoot, ".disasmer/session.json")); const sessionSecret = storedSession.session_secret; assert.strictEqual(typeof sessionSecret, "string"); assert.strictEqual(storedSession.tenant, tenant); assert.strictEqual(storedSession.project, project); assert.strictEqual(storedSession.user, user); const created = await send( addr, authenticatedHostedClientRequest(sessionSecret, { type: "create_project", project, name: "Public Release Dry Run E2E", }) ); assert.strictEqual(created.type, "project_created"); const cliGrant = await send( addr, authenticatedHostedClientRequest(sessionSecret, { type: "create_node_enrollment_grant", ttl_seconds: 900, }) ); assert.strictEqual(cliGrant.type, "node_enrollment_grant_created"); const cliNodeIdentity = nodeIdentity("public-release-cli-node", cliNode); const attach = runJson( disasmer, [ "node", "attach", "--coordinator", serviceEndpoint, "--tenant", tenant, "--project-id", project, "--node", cliNode, "--public-key", cliNodeIdentity.publicKey, "--enrollment-grant", cliGrant.grant, "--json", ], { cwd: checkout, env: { ...process.env, DISASMER_NODE_PRIVATE_KEY: cliNodeIdentity.privateKey }, } ); assert.strictEqual(attach.coordinator_response.type, "node_enrollment_exchanged"); assert.strictEqual(attach.heartbeat_response.type, "node_heartbeat"); let worker; let workerStderr = ""; let launch; let nodeRun; let events; try { const runtimeGrant = await send( addr, authenticatedHostedClientRequest(sessionSecret, { type: "create_node_enrollment_grant", ttl_seconds: 900, }) ); assert.strictEqual(runtimeGrant.type, "node_enrollment_grant_created"); const runtimeNodeIdentity = nodeIdentity("public-release-runtime-node", runtimeNode); const workerArgs = [ "--coordinator", serviceEndpoint, "--tenant", tenant, "--project-id", project, "--node", runtimeNode, "--project-root", projectRoot, "--public-key", runtimeNodeIdentity.publicKey, "--enrollment-grant", runtimeGrant.grant, "--worker", "--assignment-poll-ms", "500", "--emit-ready", ]; commands.push([disasmerNode, ...workerArgs].join(" ")); worker = cp.spawn(disasmerNode, workerArgs, { cwd: checkout, env: { ...process.env, DISASMER_NODE_PRIVATE_KEY: runtimeNodeIdentity.privateKey }, }); worker.stderr.on("data", (chunk) => { workerStderr += chunk.toString(); }); const workerReady = await waitForJsonLine(worker, "public release worker"); assert.strictEqual(workerReady.node_status, "ready"); assert.strictEqual(workerReady.mode, "worker"); assert.strictEqual(workerReady.node, runtimeNode); const runReport = runJson( disasmer, [ "run", "build", "--coordinator", serviceEndpoint, "--project", projectRoot, "--json", ], { cwd: checkout } ); assert.strictEqual(runReport.status, "main_launched"); processId = runReport.process; launch = runReport.task_launch; mainTask = launch.task_instance; assert.strictEqual(launch.type, "main_launched"); assert.strictEqual(launch.process, processId); assert.strictEqual(launch.task_instance, runReport.task_instance); nodeRun = await waitForJsonLine(worker, "public release worker completion"); task = nodeRun.task_assignment_response.task; assert( task.startsWith(`${mainTask}:child:`), `flagship worker received unexpected task ${task}` ); const flagshipDeadline = Date.now() + 240000; while (true) { const progress = await send( addr, authenticatedHostedClientRequest(sessionSecret, { type: "list_task_events", process: processId, }) ); if ( progress.events.some( (event) => event.task_definition === "package_release" && event.terminal_state === "completed" ) ) { break; } const failed = progress.events.find( (event) => event.terminal_state === "failed" ); assert(!failed, failed?.stderr_tail || "public flagship task failed"); assert(Date.now() < flagshipDeadline, "timed out waiting for public flagship"); await sleep(500); } } catch (error) { await stopChild(worker); throw error; } if (workerStderr.trim() && (!nodeRun || nodeRun.node_status !== "completed")) { console.error(workerStderr); } assert.strictEqual(nodeRun.node_status, "completed"); assert.strictEqual(nodeRun.registration_response.type, "node_enrollment_exchanged"); assert.strictEqual(nodeRun.capability_response.type, "node_capabilities_recorded"); assert.strictEqual(nodeRun.task_assignment_response.node, runtimeNode); assert.strictEqual(nodeRun.task_assignment_response.process, processId); assert.strictEqual(nodeRun.task_assignment_response.task, task); assert.strictEqual(nodeRun.debug_command_response.type, "debug_command"); assert.strictEqual(nodeRun.log_event_response.type, "task_log_recorded"); assert.strictEqual(nodeRun.vfs_metadata_response.type, "vfs_metadata_recorded"); assert.strictEqual(nodeRun.coordinator_response.type, "task_recorded"); events = await send( addr, authenticatedHostedClientRequest(sessionSecret, { type: "list_task_events", process: processId, }) ); assert.strictEqual(events.type, "task_events"); assert(events.events.length >= 3, "real flagship run must publish its task tree"); const artifactEvent = events.events.find( (event) => event.task_definition === "package_release" && event.artifact_path && event.artifact_digest ); assert(artifactEvent, "real flagship run did not publish artifact metadata"); artifactPath = artifactEvent.artifact_path; artifact = artifactPath.slice("/vfs/artifacts/".length); const link = await send( addr, authenticatedHostedClientRequest(sessionSecret, { type: "create_artifact_download_link", artifact, max_bytes: 1024 * 1024, ttl_seconds: 300, }) ); await stopChild(worker); worker = null; assert.strictEqual(link.type, "artifact_download_link"); assert.match(link.link.scoped_token_digest, /^sha256:[a-f0-9]{64}$/); const coreCoordinator = await validateStandaloneCoreCoordinator( disasmerCoordinator, disasmerNode, checkout, buildReport.bundle_artifact ); const releasedLiveDap = await validateReleasedLiveDap( disasmerDap, checkout, { tenant, project, user }, processId ); run("node", ["scripts/vscode-f5-smoke.js"], { cwd: checkout, stdio: "pipe" }); const report = { kind: "disasmer-public-release-dryrun-e2e", public_repository_url: publicRepositoryUrl, release_name: manifest.release_name, source_commit: manifest.source_commit, public_tree_identity: manifest.public_tree_identity, default_hosted_coordinator_endpoint: serviceEndpoint, service_addr: serviceAddr, dns_publication_state: dnsPublicationState, resolver_override: resolverOverride, downloaded_release_assets: true, verified_checksums: true, clean_public_checkout: true, public_repo_build_or_install: true, default_operator_selected: true, browser_or_cli_login: true, attached_user_node: true, launch_task_verified: true, worker_assignment_poll_verified: true, worker_assignment_poll_protocol: "poll_task_assignment", core_coordinator_validated: true, core_coordinator_implementation: coreCoordinator.coordinator_implementation, core_coordinator_client_authority: coreCoordinator.client_authority, core_coordinator_authenticated_session: coreCoordinator.authenticated_session, core_coordinator_forged_body_authority_denied: coreCoordinator.forged_body_authority_denied, core_coordinator_wrong_session_denied: coreCoordinator.wrong_session_denied, core_coordinator_launch_task_response: coreCoordinator.launch_task_response, core_coordinator_assignment_response: coreCoordinator.assignment_response, core_coordinator_task_events: coreCoordinator.task_events, released_live_dap_verified: true, released_live_dap_coordinator_implementation: releasedLiveDap.coordinator_implementation, released_live_dap_attached_to_client_process: releasedLiveDap.attached_to_existing_client_process, released_live_dap_authenticated_with_cli_session: releasedLiveDap.authenticated_with_cli_session, released_live_dap_task_events: releasedLiveDap.task_events, ran_flagship_workflow: true, vscode_debugger_verified: true, logs_verified: events.events.some( (event) => event.stdout_bytes > 0 || event.stderr_bytes > 0 ), artifact_metadata_verified: Boolean( artifactEvent.artifact_path && artifactEvent.artifact_digest ), artifact_download_or_export_verified: true, tenant, project, process: processId, node: runtimeNode, launch_task_response: launch.type, worker_assignment_process: nodeRun.task_assignment_response.process, artifact, commands, tool_versions: { node: process.version, git: commandOutput("git", ["--version"]), tar: commandOutput("tar", ["--version"]), rustc: commandOutput("rustc", ["--version"], { cwd: checkout }), cargo: commandOutput("cargo", ["--version"], { cwd: checkout }), }, evidence: { login: login.coordinator_response.type, attach: attach.coordinator_response.type, node_run: nodeRun.coordinator_response.type, task_events: events.events.length, download_link: link.type, core_coordinator: coreCoordinator, released_live_dap: releasedLiveDap, }, acceptance_result: "passed", }; for (const [key, value] of Object.entries(report)) { if (key.endsWith("_verified") || key.endsWith("_validated") || key.endsWith("_selected") || key.endsWith("_checkout") || key.endsWith("_assets") || key === "public_repo_build_or_install" || key === "browser_or_cli_login" || key === "attached_user_node" || key === "ran_flagship_workflow" || key === "verified_checksums") { assert.strictEqual(value, true, `${key} must be true`); } } ensureDir(path.dirname(reportPath)); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); console.log(`Public release dry-run e2e passed: ${reportPath}`); } main().catch((error) => { console.error(error.stack || error.message); process.exit(1); });