232 lines
9.4 KiB
TypeScript
232 lines
9.4 KiB
TypeScript
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<void> {
|
|
const image = await startImage();
|
|
try {
|
|
await vscode.commands.executeCommand("runa.connect", image.address);
|
|
await browseBehaviors(image.address);
|
|
const spawned = await vscode.commands.executeCommand<unknown>("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<void> {
|
|
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<string>(
|
|
"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<void> {
|
|
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);
|
|
});
|
|
}
|