import assert from "node:assert/strict"; import * as vscode from "vscode"; import { control, spawnCounter, startImage } from "./server"; import { BehaviorTree, SourceDocuments } from "../src/behaviors"; import { ImageClient } from "../src/client"; export async function run(): Promise { const image = await startImage(); try { await vscode.commands.executeCommand("runa.connect", image.address); await browseBehaviors(image.address); const spawned = await vscode.commands.executeCommand("runa.spawn", { behavior: "counter", state: "5", }); assert.equal(typeof spawned, "string"); const selection = { actor_ref: String(spawned) }; await vscode.commands.executeCommand("runa.inspect", selection); const controlled = vscode.window.activeTextEditor?.document; assert.ok(controlled); await vscode.commands.executeCommand("runa.send", selection, '{"op":"add","value":7}'); const controlPaused = documentChanged(controlled, (text) => text.includes('"status": "paused"'), ); await vscode.commands.executeCommand("runa.pause", selection); await vscode.commands.executeCommand("runa.tick", 100); await controlPaused; let snapshot = JSON.parse(controlled.getText()).actor; assert.equal(snapshot.status.status, "paused"); assert.equal(snapshot.state.data, 5); assert.equal(snapshot.queued.length, 1); const controlExecuted = documentChanged( controlled, (text) => JSON.parse(text).actor.state.data === 12, ); await vscode.commands.executeCommand("runa.resume", selection); await vscode.commands.executeCommand("runa.tick", 100); await controlExecuted; snapshot = JSON.parse(controlled.getText()).actor; assert.equal(snapshot.state.data, 12); assert.equal(snapshot.queued.length, 0); const controlStopped = documentChanged(controlled, (text) => text.includes('"status": "stopped"'), ); await vscode.commands.executeCommand("runa.stop", selection); await controlStopped; assert.equal(JSON.parse(controlled.getText()).actor.status.status, "stopped"); const controlRestarted = documentChanged( controlled, (text) => JSON.parse(text).actor.incarnation === 2, ); await vscode.commands.executeCommand("runa.restart", selection); await controlRestarted; snapshot = JSON.parse(controlled.getText()).actor; assert.equal(snapshot.incarnation, 2); assert.equal(snapshot.state.data, 12); const actorRef = await spawnCounter(image.address); await vscode.commands.executeCommand("runa.connect", image.address); await vscode.commands.executeCommand("runa.refresh"); await vscode.commands.executeCommand("runa.inspect", { actor_ref: actorRef }); let document = vscode.window.activeTextEditor?.document; assert.ok(document); assert.equal(document.uri.scheme, "runa"); assert.equal(document.languageId, "json"); assert.match(document.getText(), /9223372036854775807/); assert.match(document.getText(), /"connected": true/); const firstUri = document.uri.toString(); const refreshed = documentChanged(document, (text) => text.includes("queued from another client"), ); assert.deepEqual( await control(image.address, { operation: "send", parameters: { actor_ref: actorRef, message: { type: "string", data: "queued from another client" }, }, }), { version: 3, request_id: "integration-control", result: { Ok: { kind: "acknowledged" } } }, ); await refreshed; assert.match(document.getText(), /queued from another client/); const paused = documentChanged(document, (text) => text.includes('"status": "paused"')); await control(image.address, { operation: "pause_actor", parameters: { actor_ref: actorRef } }); await paused; const failed = documentChanged(document, (text) => text.includes('"status": "failed"')); await control(image.address, { operation: "resume_actor", parameters: { actor_ref: actorRef }, }); await control(image.address, { operation: "tick", parameters: { ticks: 100, instruction_budget: 10 }, }); await failed; const disconnected = documentChanged(document, (text) => text.includes('"connected": false')); await vscode.commands.executeCommand("runa.disconnect"); await disconnected; assert.match(document.getText(), /"connected": false/); await vscode.commands.executeCommand("runa.connect", image.address); await vscode.commands.executeCommand("runa.inspect", { actor_ref: actorRef }); document = vscode.window.activeTextEditor?.document; assert.ok(document); assert.notEqual(document.uri.toString(), firstUri); assert.match(document.getText(), /queued from another client/); const old = await vscode.workspace.openTextDocument(vscode.Uri.parse(firstUri)); assert.match(old.getText(), /"connected": false/); console.log( "Runa extension host: spawn, send, lifecycle, ticks, inspection, automatic refresh, reconnect passed", ); } finally { await vscode.commands.executeCommand("runa.disconnect"); await image.stop(); } } async function browseBehaviors(address: string): Promise { const client = await ImageClient.connect(address, (error) => assert.equal(error, undefined)); const tree = new BehaviorTree(); const sources = new SourceDocuments(); try { const source = await client.behaviorSource("counter", 1n); await control(address, { operation: "publish_behavior", parameters: { name: "browse-test", expected_revision: null, source, }, }); const listing = await client.behaviors(); tree.replace(listing); const behavior = tree.getChildren().find((node) => node.name === "browse-test"); assert.ok(behavior); assert.equal( tree.getTreeItem(behavior).collapsibleState, vscode.TreeItemCollapsibleState.Collapsed, ); const version = tree.getChildren(behavior)[0]; assert.ok(version); assert.equal(tree.getTreeItem(version).description, "current"); assert.deepEqual(tree.getChildren(version), []); const item = tree.getTreeItem(behavior); const opened = await vscode.commands.executeCommand( "runa.openBehaviorSource", ...item.command!.arguments!, ); assert.ok(opened); const original = vscode.window.activeTextEditor?.document; assert.ok(original); assert.equal(original.uri.scheme, "runa-source"); assert.match(original.uri.path, /\/1\.rn$/); assert.equal(original.getText(), source); const originalText = original.getText(); await vscode.commands.executeCommand("type", { text: "// cannot edit\n" }); assert.equal(original.getText(), originalText); const newerSource = "pub fn handle(state, message) { (state + 2, []) }\n"; await control(address, { operation: "publish_behavior", parameters: { name: "browse-test", expected_revision: 1, source: newerSource, }, }); await vscode.commands.executeCommand("runa.refresh"); // Context-menu arguments are raw tree nodes, unlike item command arguments. const updated = (await client.behaviors()).find((node) => node.name === "browse-test"); assert.ok(updated); tree.replace([updated]); assert.deepEqual( tree.getChildren(updated).map((node) => node.revision), [2n, 1n], ); await vscode.commands.executeCommand("runa.openBehaviorSource", updated); const newer = vscode.window.activeTextEditor?.document; assert.ok(newer); assert.equal(newer.getText(), newerSource); assert.notEqual(newer.uri.toString(), original.uri.toString()); assert.equal(original.getText(), originalText); const uri = sources.set(1, "name/with?#% spaces", 1n, originalText); assert.equal(sources.provideTextDocumentContent(uri), originalText); assert.notEqual( sources.set(2, "name/with?#% spaces", 1n, newerSource).toString(), uri.toString(), ); assert.throws(() => sources.set(1, "name/with?#% spaces", 1n, newerSource), /Immutable/); sources.remove(uri); assert.throws(() => sources.provideTextDocumentContent(uri), /no longer cached/); await vscode.commands.executeCommand("runa.disconnect"); assert.equal(original.getText(), originalText); await vscode.commands.executeCommand("runa.connect", address); await vscode.commands.executeCommand("runa.openBehaviorSource", { name: "browse-test", revision: "1", }); const reconnected = vscode.window.activeTextEditor?.document; assert.ok(reconnected); assert.equal(reconnected.getText(), originalText); assert.notEqual(reconnected.uri.toString(), original.uri.toString()); console.log( "Runa behavior browsing: revision tree, immutable source, read-only documents, session isolation passed", ); } finally { client.close(); tree.dispose(); sources.dispose(); } } function documentChanged( document: vscode.TextDocument, expected: (text: string) => boolean, ): Promise { return new Promise((resolve, reject) => { const subscription = vscode.workspace.onDidChangeTextDocument((event) => { if ( event.document.uri.toString() === document.uri.toString() && expected(event.document.getText()) ) { clearTimeout(timeout); subscription.dispose(); resolve(); } }); const timeout = setTimeout(() => { subscription.dispose(); reject(new Error("Actor inspector did not refresh")); }, 5000); }); }