runa/vscode/integration/host.ts

678 lines
28 KiB
TypeScript

import assert from "node:assert/strict";
import * as vscode from "vscode";
import { control, spawnCounter, startImage } from "./server";
import { ImageSourceTree, RevisionDocuments } from "../src/sourceProvider";
import { actorFileUri } from "../src/actorFiles";
import type { activate } from "../src/extension";
export async function run(): Promise<void> {
const image = await startImage();
const folders = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.toString());
try {
const extension = vscode.extensions.all.find(
(candidate) => candidate.packageJSON.name === "runa",
);
assert.ok(extension);
const contributions = extension.packageJSON.contributes;
assert.ok(
contributions.viewsContainers.activitybar.some(
(container: { id: string }) => container.id === "runa",
),
);
assert.deepEqual(
contributions.views.runa.map((view: { id: string }) => view.id),
["runa.sources", "runa.actors"],
);
assert.equal(contributions.views.explorer, undefined);
assert.ok(
contributions.menus["view/title"].some(
(item: { command: string; when: string }) =>
item.command === "runa.tick" && item.when.includes("runa.actors"),
),
);
await vscode.commands.executeCommand("workbench.view.extension.runa");
await vscode.commands.executeCommand("runa.connect", image.address);
assert.ok(!(await vscode.commands.getCommands(true)).includes("runa.addSourceFolder"));
assert.deepEqual(
vscode.workspace.workspaceFolders?.map((folder) => folder.uri.toString()),
folders,
);
await editSources(image.address);
await editActorFiles(image.address);
const spawned = await vscode.commands.executeCommand<unknown>("runa.spawn", {
path: "actors/counter",
state: "5",
});
assert.equal(typeof spawned, "string");
const selection = { actor_ref: String(spawned) };
await vscode.commands.executeCommand("runa.copyActorReference", selection);
assert.equal(await vscode.env.clipboard.readText(), 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 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);
await vscode.commands.executeCommand("runa.reload", selection);
await vscode.commands.executeCommand("runa.tick", 10);
await vscode.commands.executeCommand("runa.stop", selection);
assert.equal(JSON.parse(controlled.getText()).removed, undefined);
const controlStopped = documentChanged(controlled, (text) => JSON.parse(text).removed === true);
await vscode.commands.executeCommand("runa.tick", 10);
await controlStopped;
assert.equal(
(await vscode.workspace.fs.stat(controlled.uri)).permissions,
vscode.FilePermission.Readonly,
);
const doomed = await vscode.commands.executeCommand<string>("runa.spawn", {
path: "actors/counter",
state: "0",
});
await vscode.commands.executeCommand("runa.inspect", { actor_ref: doomed });
const killed = vscode.window.activeTextEditor?.document;
assert.ok(killed);
const controlKilled = documentChanged(killed, (text) => JSON.parse(text).removed === true);
await vscode.commands.executeCommand("runa.kill", { actor_ref: doomed });
await controlKilled;
assert.equal(
(await vscode.workspace.fs.stat(killed.uri)).permissions,
vscode.FilePermission.Readonly,
);
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: 5, 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: "system",
parameters: { actor_ref: actorRef, message: { kind: "pause" } },
});
await paused;
const failed = documentChanged(document, (text) => text.includes('"status": "failed"'));
await control(image.address, {
operation: "system",
parameters: { actor_ref: actorRef, message: { kind: "resume" } },
});
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/);
assert.equal((await vscode.workspace.fs.stat(document.uri)).permissions, undefined);
const updated = JSON.parse(document.getText());
updated.actor.state = { type: "integer", data: 77 };
updated.actor.reserved = null;
updated.actor.queued = [
{
type: "record",
data: { op: { type: "string", data: "add" }, value: { type: "integer", data: 2 } },
},
];
updated.actor.log_level = "warn";
updated.actor.incarnation = 999;
updated.connection.endpoint = "not the actual endpoint";
const canonical = documentChanged(document, (text) => {
const saved = JSON.parse(text);
return (
saved.actor.state.data === 77 &&
saved.actor.incarnation !== 999 &&
saved.actor.log_level === "warn" &&
saved.connection.endpoint === image.address
);
});
const edit = new vscode.WorkspaceEdit();
edit.replace(
document.uri,
new vscode.Range(document.positionAt(0), document.positionAt(document.getText().length)),
JSON.stringify(updated, null, 2),
);
assert.equal(await vscode.workspace.applyEdit(edit), true);
const unsaved = document.getText();
await vscode.commands.executeCommand("runa.refresh");
assert.equal(document.getText(), unsaved, "refresh must preserve unsaved inspection edits");
const window = vscode.window as { showErrorMessage: (...args: unknown[]) => unknown };
const showError = window.showErrorMessage;
const saveErrors: unknown[][] = [];
window.showErrorMessage = async (...args: unknown[]) => {
saveErrors.push(args);
return undefined;
};
try {
assert.equal(await document.save(), true);
await canonical;
assert.deepEqual(saveErrors, [], "successful inspection save must not report refresh errors");
assert.equal(document.isDirty, false);
} finally {
window.showErrorMessage = showError;
}
const recovered = documentChanged(document, (text) => {
const actor = JSON.parse(text).actor;
return (
actor.state.data === 79 && actor.log_level === "warn" && actor.status.status === "idle"
);
});
await vscode.commands.executeCommand("runa.logLevel", { actor_ref: actorRef }, "warn");
await vscode.commands.executeCommand("runa.restart", { actor_ref: actorRef });
await vscode.commands.executeCommand("runa.tick", 100);
await recovered;
await vscode.commands.executeCommand("runa.inspect", { actor_ref: actorRef });
const repaired = vscode.window.activeTextEditor?.document;
assert.ok(repaired);
assert.equal(JSON.parse(repaired.getText()).actor.state.data, 79, repaired.getText());
assert.equal(JSON.parse(repaired.getText()).actor.log_level, "warn");
assert.equal(JSON.parse(repaired.getText()).actor.status.status, "idle");
assert.equal(
(await vscode.workspace.fs.stat(repaired.uri)).permissions,
vscode.FilePermission.Readonly,
);
await assert.rejects(async () => {
await vscode.workspace.fs.writeFile(repaired.uri, Buffer.from(repaired.getText()));
}, /paused or failed/);
console.log(
"Runa extension host: spawn, send, system messages, ticks, inspection, automatic refresh, reconnect passed",
);
} finally {
await vscode.commands.executeCommand("runa.disconnect");
await image.stop();
}
}
async function editSources(address: string): Promise<void> {
const extension = vscode.extensions.all.find(
(candidate) => candidate.packageJSON.name === "runa",
);
assert.ok(extension);
const { sourceTree } = extension.exports as { sourceTree: ImageSourceTree };
const [root] = sourceTree.getChildren();
assert.ok(root);
assert.equal(root.uri.toString(), `runa-image://${address}/source`);
assert.deepEqual(await vscode.workspace.fs.readDirectory(root.uri), [
["actors", vscode.FileType.Directory],
]);
const [directory] = sourceTree.getChildren(root);
assert.ok(directory);
assert.equal(directory.directory, true);
const [file] = sourceTree.getChildren(directory);
assert.ok(file);
assert.equal(sourceTree.getTreeItem(file).contextValue, "runa.sourceFile");
const actors = vscode.Uri.joinPath(root.uri, "actors");
assert.deepEqual(await vscode.workspace.fs.readDirectory(actors), [
["counter.rn", vscode.FileType.File],
]);
const uri = vscode.Uri.joinPath(actors, "counter.rn");
const document = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(document);
const original = document.getText();
assert.match(original, /pub fn handle\(ctx, state, sender, message\)/);
const edited = "pub fn handle(ctx, state, sender, message) {\n state + 2\n}\n";
await replaceText(document, edited);
const window = vscode.window as { showWarningMessage: (...args: unknown[]) => unknown };
const warning = window.showWarningMessage;
const prompts: unknown[][] = [];
let answer: string | undefined;
window.showWarningMessage = async (...args: unknown[]) => {
prompts.push(args);
return answer;
};
try {
// Unsaved edits are never committed or spawned without an explicit save.
assert.equal(await vscode.commands.executeCommand("runa.commitSource", uri), undefined);
assert.equal(await vscode.commands.executeCommand("runa.spawnSource", uri), undefined);
assert.equal(prompts.length, 2);
assert.deepEqual(prompts[0]?.[1], { modal: true });
assert.equal(document.isDirty, true);
const unsaved = JSON.stringify(
await control(address, { operation: "read_source", parameters: { path: "actors/counter" } }),
);
assert.ok(unsaved.includes(JSON.stringify(original)));
assert.match(
JSON.stringify(await control(address, { operation: "list_actors" })),
/"data":\[\]/,
);
answer = "Save and Spawn";
const saved = await vscode.commands.executeCommand<string>("runa.spawnSource", uri);
assert.match(String(saved), /^[A-Za-z0-9_-]{21}$/);
assert.equal(prompts.length, 3);
assert.equal(document.isDirty, false, "the explicit choice saves before spawning");
} finally {
window.showWarningMessage = warning;
}
assert.equal(document.isDirty, false);
assert.equal(document.isDirty, false);
let reply = JSON.stringify(
await control(address, { operation: "read_source", parameters: { path: "actors/counter" } }),
);
assert.ok(reply.includes(JSON.stringify(edited)), reply);
reply = JSON.stringify(
await control(address, { operation: "source_history", parameters: { path: "actors/counter" } }),
);
assert.match(reply, /"kind":"source_history","data":\[\]/);
// Spawn context-menu actions receive the explorer URI and use the saved, uncommitted source.
const nullActor = await vscode.commands.executeCommand<string>("runa.spawnSource", uri);
assert.match(String(nullActor), /^[A-Za-z0-9_-]{21}$/);
reply = JSON.stringify(
await control(address, { operation: "inspect_actor", parameters: { actor_ref: nullActor } }),
);
assert.match(reply, /"state":\{"type":"null"\}/);
const inputWindow = vscode.window as { showInputBox: (...args: unknown[]) => unknown };
const originalInput = inputWindow.showInputBox;
const inputs: unknown[][] = [];
let stateAnswer: string | undefined;
inputWindow.showInputBox = async (...args: unknown[]) => {
inputs.push(args);
return stateAnswer;
};
let actor: string | undefined;
try {
assert.equal(
await vscode.commands.executeCommand("runa.spawnSourceWithState", file, [file]),
undefined,
);
stateAnswer = "40";
actor = await vscode.commands.executeCommand<string>("runa.spawnSourceWithState", file, [file]);
assert.equal(inputs.length, 2, "context menus always prompt for state");
assert.match(JSON.stringify(inputs[1]), /Initial state for actors\/counter/);
} finally {
inputWindow.showInputBox = originalInput;
}
assert.match(String(actor), /^[A-Za-z0-9_-]{21}$/);
await vscode.commands.executeCommand("runa.send", { actor_ref: actor }, "null");
await vscode.commands.executeCommand("runa.tick", 10);
reply = JSON.stringify(
await control(address, { operation: "inspect_actor", parameters: { actor_ref: actor } }),
);
assert.match(reply, /"state":\{"type":"integer","data":42\}/);
const revision = await vscode.commands.executeCommand<string>("runa.commitSource", uri);
assert.match(String(revision), /^[A-Za-z0-9]{1,64}$/);
const opened = await vscode.commands.executeCommand<string>("runa.sourceHistory", uri, revision);
const historical = vscode.window.activeTextEditor?.document;
assert.ok(historical);
assert.equal(historical.uri.toString(), opened);
assert.equal(historical.uri.scheme, "runa-source");
assert.ok(historical.uri.path.endsWith(`/source/actors/counter@${revision}.rn`));
assert.equal(historical.getText(), edited);
await vscode.commands.executeCommand("type", { text: "// cannot edit\n" });
assert.equal(historical.getText(), edited);
// Saving again changes only the working source; history is unchanged.
await vscode.window.showTextDocument(document);
await replaceText(document, original);
assert.ok(await document.save());
assert.equal(historical.getText(), edited);
prompts.length = 0;
answer = undefined;
window.showWarningMessage = async (...args: unknown[]) => {
prompts.push(args);
return answer;
};
try {
await vscode.commands.executeCommand("runa.rollbackSource", uri, revision);
assert.equal(prompts.length, 1);
assert.deepEqual(prompts[0]?.[1], { modal: true });
assert.equal(document.getText(), original, "cancelled rollback changes nothing");
answer = "Roll Back";
const reloaded = documentChanged(document, (text) => text === edited);
await vscode.commands.executeCommand("runa.rollbackSource", uri, revision);
await reloaded;
} finally {
window.showWarningMessage = warning;
}
assert.equal(document.isDirty, false);
reply = JSON.stringify(
await control(address, { operation: "source_history", parameters: { path: "actors/counter" } }),
);
assert.equal((reply.match(/"revision"/g) ?? []).length, 1);
// Saves from another client reload clean editors through image notifications.
const current = JSON.parse(
JSON.stringify(
await control(address, { operation: "read_source", parameters: { path: "actors/counter" } }),
),
).result.Ok.data;
const external = documentChanged(document, (text) => text === original);
await control(address, {
operation: "write_source",
parameters: { path: "actors/counter", expected_source_id: current.source_id, source: original },
});
await external;
// Revision documents stay isolated per connection session.
await vscode.commands.executeCommand("runa.disconnect");
assert.equal(historical.getText(), edited);
await vscode.commands.executeCommand("runa.connect", address);
assert.equal(
vscode.workspace.workspaceFolders?.filter((candidate) => candidate.uri.scheme === "runa-image")
.length ?? 0,
0,
);
const reopened = await vscode.commands.executeCommand<string>(
"runa.sourceHistory",
uri,
revision,
);
assert.ok(reopened);
assert.notEqual(reopened, opened);
assert.equal(vscode.window.activeTextEditor?.document.getText(), edited);
const revisions = new RevisionDocuments();
try {
const cached = revisions.set(1, "actors/counter", "abc", edited);
assert.equal(revisions.provideTextDocumentContent(cached), edited);
assert.notEqual(
revisions.set(2, "actors/counter", "abc", original).toString(),
cached.toString(),
);
assert.throws(() => revisions.set(1, "actors/counter", "abc", original), /Immutable/);
revisions.remove(cached);
assert.throws(() => revisions.provideTextDocumentContent(cached), /no longer cached/);
} finally {
revisions.dispose();
}
console.log(
"Runa sources: automatic tree, state input, save, uncommitted spawn, commit, history, rollback, session isolation passed",
);
}
async function replaceText(document: vscode.TextDocument, text: string): Promise<void> {
const edit = new vscode.WorkspaceEdit();
edit.replace(
document.uri,
new vscode.Range(document.positionAt(0), document.positionAt(document.getText().length)),
text,
);
assert.ok(await vscode.workspace.applyEdit(edit));
}
async function editActorFiles(address: string): Promise<void> {
const extension = vscode.extensions.all.find(
(candidate) => candidate.packageJSON.name === "runa",
);
assert.ok(extension);
const { actorTree, sourceTree } = extension.exports as ReturnType<typeof activate>;
const inputWindow = vscode.window as { showInputBox: (...args: unknown[]) => unknown };
const input = inputWindow.showInputBox;
const [root] = sourceTree.getChildren();
assert.ok(root);
inputWindow.showInputBox = async () => "tests/files.rn";
try {
await vscode.commands.executeCommand("runa.newSource", root, [root]);
} finally {
inputWindow.showInputBox = input;
}
const sourceDocument = vscode.window.activeTextEditor?.document;
assert.ok(sourceDocument);
const original =
'pub fn handle(ctx, state, sender, message) { ctx.info("original"); state + message }\n';
await replaceText(sourceDocument, original);
assert.ok(await sourceDocument.save());
const ref = await vscode.commands.executeCommand<string>("runa.spawn", {
path: "tests/files",
state: "1",
});
assert.ok(ref);
const selected = { actor_ref: ref };
await vscode.commands.executeCommand("runa.refresh");
const parent = actorTree.getChildren().find((node) => node.actor_ref === ref);
assert.ok(parent);
assert.equal(
actorTree.getTreeItem(parent).collapsibleState,
vscode.TreeItemCollapsibleState.Collapsed,
);
assert.deepEqual(
actorTree.getChildren(parent).map((node) => ("file" in node ? node.file : "")),
["meta.json", "config.json", "state.json", "mailbox.json", "log"],
);
const session = actorTree.session;
const uri = (name: string) => actorFileUri(session, ref, name);
const meta = await vscode.workspace.openTextDocument(uri("meta.json"));
await vscode.window.showTextDocument(meta, { preview: false });
assert.equal(JSON.parse(meta.getText()).behavior_path, "tests/files");
assert.equal(JSON.parse(meta.getText()).state, undefined);
assert.equal(
(await vscode.workspace.fs.stat(meta.uri)).permissions,
vscode.FilePermission.Readonly,
);
assert.equal(
(await vscode.workspace.fs.stat(uri("state.json"))).permissions,
vscode.FilePermission.Readonly,
);
await assert.rejects(
async () => vscode.workspace.fs.writeFile(meta.uri, Buffer.from("{}")),
/read-only/,
);
const config = await vscode.workspace.openTextDocument(uri("config.json"));
await vscode.window.showTextDocument(config);
await replaceText(config, '{"log_level":"trace"}');
assert.ok(await config.save(), "configuration is writable while running");
await vscode.commands.executeCommand("runa.pause", selected);
await vscode.commands.executeCommand("runa.refresh");
assert.equal((await vscode.workspace.fs.stat(uri("state.json"))).permissions, undefined);
const state = await vscode.workspace.openTextDocument(uri("state.json"));
assert.equal(state.getText().trim(), "1");
await vscode.window.showTextDocument(state, { preview: false });
await replaceText(state, "40");
assert.ok(await state.save());
const mailbox = await vscode.workspace.openTextDocument(uri("mailbox.json"));
await vscode.window.showTextDocument(mailbox);
await replaceText(mailbox, '{"reserved":[2],"queued":[3]}');
assert.ok(await mailbox.save());
await assert.rejects(
async () =>
vscode.workspace.fs.writeFile(mailbox.uri, Buffer.from('{"reserved":[1,2],"queued":[]}')),
/zero or one/,
);
const executed = documentChanged(state, (text) => text.trim() === "45");
await vscode.commands.executeCommand("runa.restart", selected);
await vscode.commands.executeCommand("runa.tick", 20);
await executed;
const log = await vscode.workspace.openTextDocument(uri("log"));
await vscode.window.showTextDocument(log, { preview: false });
assert.match(log.getText(), /INFO: original/);
assert.equal(
(await vscode.workspace.fs.stat(log.uri)).permissions,
vscode.FilePermission.Readonly,
);
const working = original.replace("state + message", "state + message * 10");
await vscode.workspace.fs.writeFile(sourceDocument.uri, Buffer.from(working));
await vscode.commands.executeCommand("runa.editBehavior", selected);
const behavior = vscode.window.activeTextEditor?.document;
assert.ok(behavior);
assert.equal(behavior.uri.toString(), uri("behavior").toString());
assert.equal(
behavior.getText(),
original,
"edit starts from loaded behavior, not current working source",
);
const temporary = original
.replace('"original"', '"temporary"')
.replace("state + message", "state + message * 100");
await replaceText(behavior, temporary);
assert.ok(await behavior.save());
assert.equal(
Buffer.from(await vscode.workspace.fs.readFile(sourceDocument.uri)).toString(),
working,
);
await vscode.commands.executeCommand("runa.editBehavior", selected);
assert.equal(vscode.window.activeTextEditor?.document.uri.toString(), behavior.uri.toString());
await vscode.commands.executeCommand("runa.refresh");
const withTemporary = actorTree.getChildren().find((node) => node.actor_ref === ref);
assert.ok(withTemporary);
assert.ok(
actorTree.getChildren(withTemporary).some((node) => "file" in node && node.file === "behavior"),
);
const tempExecuted = documentChanged(state, (text) => text.trim() === "145");
const logged = documentChanged(log, (text) => text.includes("INFO: temporary"));
await vscode.commands.executeCommand("runa.send", selected, "1");
await vscode.commands.executeCommand("runa.tick", 20);
await tempExecuted;
await logged;
const window = vscode.window as { showWarningMessage: (...args: unknown[]) => unknown };
const warning = window.showWarningMessage;
let answer: string | undefined;
window.showWarningMessage = async () => answer;
const renamed = vscode.Uri.from({
scheme: "runa-image",
authority: address,
path: "/source/moved/files.rn",
});
try {
await vscode.commands.executeCommand("runa.updateBehavior", selected);
assert.equal(
Buffer.from(await vscode.workspace.fs.readFile(sourceDocument.uri)).toString(),
working,
);
answer = "Update Behavior";
await vscode.commands.executeCommand("runa.updateBehavior", selected);
assert.equal(
Buffer.from(await vscode.workspace.fs.readFile(sourceDocument.uri)).toString(),
temporary,
);
answer = undefined;
await assert.rejects(
async () => vscode.workspace.fs.rename(sourceDocument.uri, renamed),
/[Cc]ancel/,
);
assert.equal(
Buffer.from(await vscode.workspace.fs.readFile(sourceDocument.uri)).toString(),
temporary,
);
answer = "Rename and Update Actors";
const renamedMeta = documentChanged(
meta,
(text) => JSON.parse(text).behavior_path === "moved/files",
);
inputWindow.showInputBox = async () => "moved/files.rn";
try {
await vscode.commands.executeCommand("runa.renameSource", { uri: sourceDocument.uri }, [
{ uri: sourceDocument.uri },
]);
} finally {
inputWindow.showInputBox = input;
}
await renamedMeta;
await vscode.commands.executeCommand("runa.goToBehavior", selected);
assert.equal(vscode.window.activeTextEditor?.document.uri.toString(), renamed.toString());
await assert.rejects(async () => vscode.workspace.fs.delete(renamed), /stop the actors first/);
await vscode.commands.executeCommand("runa.reload", selected);
await vscode.commands.executeCommand("runa.tick", 20);
await vscode.commands.executeCommand("runa.refresh");
const reloaded = actorTree.getChildren().find((node) => node.actor_ref === ref);
assert.ok(reloaded);
assert.ok(
!actorTree.getChildren(reloaded).some((node) => "file" in node && node.file === "behavior"),
);
await vscode.commands.executeCommand("runa.kill", selected);
await vscode.commands.executeCommand("runa.refresh");
assert.match(
Buffer.from(await vscode.workspace.fs.readFile(log.uri)).toString(),
/INFO: temporary/,
);
assert.equal(
JSON.parse(Buffer.from(await vscode.workspace.fs.readFile(meta.uri)).toString()).removed,
true,
);
await vscode.workspace.fs.delete(renamed);
assert.equal(
(await vscode.workspace.fs.stat(state.uri)).permissions,
vscode.FilePermission.Readonly,
);
} finally {
window.showWarningMessage = warning;
}
console.log(
"Runa actor directories, live config, natural state/mailbox edits, logs, temporary behaviors and source operations passed",
);
}
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(
`Document did not refresh: ${document.uri.toString()} (closed=${document.isClosed}): ${document.getText()}`,
),
);
}, 5000);
});
}