All checks were successful
Testar e publicar imagem Docker / publish (push) Successful in 44s
688 lines
22 KiB
JavaScript
688 lines
22 KiB
JavaScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { DatabaseSync } from "node:sqlite";
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { Readable } from "node:stream";
|
|
import { createStudio } from "./studio.mjs";
|
|
import { nodeTypes, validateGraph } from "./flow-catalog.mjs";
|
|
import { executeStep, simulatedQuery, exampleClients } from "./flow-steps.mjs";
|
|
import { evaluate, interpolate, timeValues } from "./flow-values.mjs";
|
|
import { compileCron } from "./flow-schedule.mjs";
|
|
import { templates } from "./flow-templates.mjs";
|
|
|
|
const node = (type, config = {}, id = type) => ({
|
|
id,
|
|
type,
|
|
label: type,
|
|
x: 50,
|
|
y: 50,
|
|
config: { ...structuredClone(nodeTypes[type].defaults), ...config },
|
|
});
|
|
const graph = (...nodes) => ({
|
|
nodes,
|
|
edges: nodes
|
|
.slice(1)
|
|
.map((n, i) => ({ from: nodes[i].id, to: n.id, port: "out" })),
|
|
});
|
|
function fixture(options = {}) {
|
|
const directory = mkdtempSync(join(tmpdir(), "botflow-engine-")),
|
|
db = new DatabaseSync(join(directory, "test.sqlite"));
|
|
const calls = [],
|
|
states = [];
|
|
const studio = createStudio(db, directory, {
|
|
scheduler: false,
|
|
transport: async (c, command, params) => {
|
|
calls.push({ command, params });
|
|
return simulatedQuery(command);
|
|
},
|
|
eventFactory: (c, onEvent, onState, settings) => {
|
|
states.push({ c, onEvent, onState, settings });
|
|
onState({ status: "connected", fingerprint: "a".repeat(64) });
|
|
return { close() {}, ready: true };
|
|
},
|
|
...options,
|
|
});
|
|
async function invoke(
|
|
path,
|
|
method = "GET",
|
|
body = {},
|
|
role = "Dono",
|
|
extraHeaders = {},
|
|
) {
|
|
const req = Readable.from(
|
|
method === "GET" ? [] : [Buffer.from(JSON.stringify(body))],
|
|
);
|
|
req.method = method;
|
|
req.headers = { origin: "http://localhost:3080", ...extraHeaders };
|
|
let result;
|
|
const url = new URL(path, "http://localhost:3080");
|
|
if (
|
|
!(await studio.handleWebhook(
|
|
req,
|
|
url,
|
|
(status, data) => (result = { status, data }),
|
|
))
|
|
)
|
|
await studio.route(
|
|
req,
|
|
url,
|
|
{ role, name: "Tester" },
|
|
(status, data) => (result = { status, data }),
|
|
);
|
|
return result;
|
|
}
|
|
const connection = async (extras = {}) =>
|
|
(
|
|
await invoke("/api/connections", "POST", {
|
|
name: "Test",
|
|
url: "http://teamspeak6:10080",
|
|
serverId: 1,
|
|
apiKey: "test-key",
|
|
...extras,
|
|
})
|
|
).data;
|
|
const bot = async (g, connectionId = "", extras = {}) => {
|
|
const created = (
|
|
await invoke("/api/bots", "POST", { import: { name: "Test", graph: g } })
|
|
).data;
|
|
const updated = await invoke(`/api/bots/${created.id}`, "PUT", {
|
|
connectionId,
|
|
...extras,
|
|
});
|
|
assert.equal(updated.status, 200, updated.data.error);
|
|
return updated.data;
|
|
};
|
|
return {
|
|
db,
|
|
directory,
|
|
studio,
|
|
calls,
|
|
states,
|
|
invoke,
|
|
connection,
|
|
bot,
|
|
async close() {
|
|
await studio.close();
|
|
db.close();
|
|
rmSync(directory, { recursive: true, force: true });
|
|
},
|
|
};
|
|
}
|
|
async function until(fn) {
|
|
for (let i = 0; i < 100; i++) {
|
|
const result = await fn();
|
|
if (result) return result;
|
|
await new Promise((r) => setTimeout(r, 10));
|
|
}
|
|
throw new Error("Timed out waiting for execution");
|
|
}
|
|
|
|
test("safe expressions, filters and time support calculations without JavaScript access", () => {
|
|
const context = {
|
|
server: { total: "8", query: "2" },
|
|
clock: timeValues(new Date("2026-10-10T12:34:56Z"), "UTC"),
|
|
text: "olá",
|
|
temp: { list: [1, 2] },
|
|
};
|
|
assert.equal(
|
|
evaluate("round((server.total - server.query) / 4)", context),
|
|
2,
|
|
);
|
|
assert.equal(
|
|
evaluate('server.total > 4 && contains(text, "ol")', context),
|
|
true,
|
|
);
|
|
assert.equal(evaluate("false && missing > 0", context), false);
|
|
assert.equal(evaluate("true || missing > 0", context), true);
|
|
assert.equal(
|
|
interpolate(
|
|
"{{server.total > 4 || false}} · {{text | upper}} · {{time.time}}:{{time.seconds}}",
|
|
context,
|
|
),
|
|
"true · OLÁ · 12:34:56",
|
|
);
|
|
assert.equal(interpolate('{{contains("a|b", "|")}}', context), "true");
|
|
assert.equal(interpolate("{{temp.list | json}}", context), "[1,2]");
|
|
assert.throws(() =>
|
|
evaluate('constructor.constructor("return process")()', context),
|
|
);
|
|
assert.throws(() => evaluate("process.exit()", context));
|
|
assert.throws(() => evaluate("1 / 0", context), /zero/);
|
|
assert.throws(() => evaluate("x = 2", context));
|
|
assert.equal(interpolate("{{server.constructor}}", context), "");
|
|
});
|
|
|
|
test("cron evaluates seconds, ranges, day/week OR and timezone", () => {
|
|
const date = new Date("2026-10-10T12:34:56Z");
|
|
assert.equal(compileCron("* * * * * *", "UTC")(date), true);
|
|
assert.equal(compileCron("* * * * *", "UTC")(date), false);
|
|
assert.equal(
|
|
compileCron("* * * * *", "UTC")(new Date("2026-10-10T12:34:00Z")),
|
|
true,
|
|
);
|
|
assert.equal(compileCron("*/2 30-35 12 10 10 0", "UTC")(date), true);
|
|
assert.equal(compileCron("56 34 13 * * *", "Europe/Lisbon")(date), true);
|
|
assert.throws(() => compileCron("61 * * * * *"), /intervalo/);
|
|
assert.throws(() => compileCron("* * *"), /campos/);
|
|
assert.throws(() => compileCron("* * * * * *", "Invalid/Timezone"));
|
|
});
|
|
|
|
test("every advertised action executes in simulation without network calls", async () => {
|
|
let network = 0;
|
|
for (const [name, type] of Object.entries(nodeTypes)) {
|
|
if (type.trigger || name === "foreach") continue;
|
|
const context = {
|
|
server: simulatedQuery("serverinfo")[0],
|
|
event: {
|
|
clid: 1,
|
|
cid: 21,
|
|
client_database_id: 10,
|
|
client_nickname: "Pessoa",
|
|
},
|
|
clients: structuredClone(exampleClients),
|
|
count: 3,
|
|
temp: {},
|
|
variables: {},
|
|
clock: timeValues(),
|
|
time: "12:34:56",
|
|
text: "Texto",
|
|
top: "Top",
|
|
};
|
|
const n = node(name, {
|
|
banId: 1,
|
|
url:
|
|
name === "discord"
|
|
? "https://discord.com/api/webhooks/1/test"
|
|
: "http://localhost",
|
|
groupId: 9,
|
|
});
|
|
const result = await executeStep(n, context, {
|
|
simulation: true,
|
|
call: () => network++,
|
|
httpTransport: () => network++,
|
|
channelMessage: () => network++,
|
|
persist: () => network++,
|
|
});
|
|
assert.equal(typeof result.message, "string", name);
|
|
}
|
|
assert.equal(network, 0);
|
|
for (const template of templates)
|
|
assert.doesNotThrow(() => validateGraph(template.graph), template.name);
|
|
});
|
|
|
|
test("TeamSpeak actions distinguish session IDs and database IDs; permissions use correct scope", async () => {
|
|
const calls = [],
|
|
context = {
|
|
temp: {},
|
|
event: { clid: "23", client_database_id: "456" },
|
|
variables: {},
|
|
clock: timeValues(),
|
|
};
|
|
const run = async (type, config = {}) =>
|
|
executeStep(node(type, config), context, {
|
|
call: async (command, params) => {
|
|
calls.push({ command, params });
|
|
return [{ sgid: "9" }];
|
|
},
|
|
persist() {},
|
|
signal: new AbortController().signal,
|
|
});
|
|
await run("move", { channelId: 21 });
|
|
await run("groupAdd", { groupId: 9 });
|
|
await run("clientPermissionSet", { value: "-1", skip: "1" });
|
|
await run("channelPermissionSet", { channelId: 21 });
|
|
await run("servergroupPermissionSet", {
|
|
groupId: 9,
|
|
negated: "1",
|
|
skip: "1",
|
|
});
|
|
await run("channelclientPermissionRemove", { channelId: 21 });
|
|
assert.deepEqual(calls[0], {
|
|
command: "clientmove",
|
|
params: { clid: 23, cid: 21 },
|
|
});
|
|
assert.deepEqual(calls[1], {
|
|
command: "servergroupaddclient",
|
|
params: { cldbid: 456, sgid: 9 },
|
|
});
|
|
assert.equal(calls[2].params.cldbid, 456);
|
|
assert.equal(calls[2].params.permvalue, -1);
|
|
assert.equal(calls[2].params.permskip, "1");
|
|
assert.equal(calls[2].params.permnegated, undefined);
|
|
assert.equal(calls[3].params.permskip, undefined);
|
|
assert.equal(calls[3].params.permnegated, undefined);
|
|
assert.equal(calls[4].params.permnegated, "1");
|
|
assert.equal(calls[4].params.permskip, "1");
|
|
assert.deepEqual(calls[5], {
|
|
command: "channelclientdelperm",
|
|
params: { cldbid: 456, cid: 21, permsid: "i_client_talk_power" },
|
|
});
|
|
await run("groups");
|
|
assert.ok(
|
|
Array.isArray(context.temp.groups),
|
|
"single-result lists remain arrays",
|
|
);
|
|
await assert.rejects(() => run("move", { clientId: "not-an-id" }), /inteiro/);
|
|
});
|
|
|
|
test("foreach executes Each sequentially and Done once; simulations never persist variables", async () => {
|
|
const f = fixture();
|
|
try {
|
|
const g = graph(
|
|
node("manual"),
|
|
node("clients"),
|
|
node("foreach"),
|
|
node("variable", {
|
|
name: "total",
|
|
scope: "persistent",
|
|
operation: "increment",
|
|
value: "1",
|
|
}),
|
|
node("template", { text: "Total {{variables.total}}" }, "done"),
|
|
);
|
|
g.edges = [
|
|
{ from: "manual", to: "clients", port: "out" },
|
|
{ from: "clients", to: "foreach", port: "out" },
|
|
{ from: "foreach", to: "variable", port: "each" },
|
|
{ from: "foreach", to: "done", port: "done" },
|
|
];
|
|
const c = await f.connection(),
|
|
b = await f.bot(g, c.id, { variables: { total: 0 } });
|
|
const simulated = (await f.invoke(`/api/bots/${b.id}/simulate`, "POST"))
|
|
.data;
|
|
assert.equal(simulated.output, "Total 3");
|
|
assert.equal(
|
|
simulated.steps.filter((s) => s.type === "variable").length,
|
|
3,
|
|
);
|
|
assert.equal(f.calls.length, 0);
|
|
assert.equal(
|
|
(await f.invoke("/api/dashboard")).data.bots.find((x) => x.id === b.id)
|
|
.variables.total,
|
|
0,
|
|
);
|
|
const real = (await f.invoke(`/api/bots/${b.id}/run`, "POST")).data;
|
|
assert.equal(real.status, "success");
|
|
assert.equal(real.output, "Total 3");
|
|
assert.equal(
|
|
(await f.invoke("/api/dashboard")).data.bots.find((x) => x.id === b.id)
|
|
.variables.total,
|
|
3,
|
|
);
|
|
} finally {
|
|
await f.close();
|
|
}
|
|
});
|
|
|
|
test("retry and error branches retain failures in the execution timeline", async () => {
|
|
let attempts = 0;
|
|
const f = fixture({
|
|
transport: async () => {
|
|
attempts++;
|
|
throw new Error("Query refused");
|
|
},
|
|
});
|
|
try {
|
|
const g = graph(
|
|
node("manual"),
|
|
node("serverinfo", { retries: 1 }),
|
|
node("template", { text: "Tratado: {{temp.error.message}}" }),
|
|
);
|
|
g.edges[1].port = "error";
|
|
const c = await f.connection(),
|
|
b = await f.bot(g, c.id),
|
|
result = (await f.invoke(`/api/bots/${b.id}/run`, "POST")).data;
|
|
assert.equal(result.status, "warning");
|
|
assert.equal(attempts, 2);
|
|
assert.equal(result.steps[1].attempts, 2);
|
|
assert.equal(result.steps[1].status, "error");
|
|
assert.equal(result.output, "Tratado: Query refused");
|
|
} finally {
|
|
await f.close();
|
|
}
|
|
});
|
|
|
|
test("cancellation interrupts waits, blocks overlap, and allows subsequent runs", async () => {
|
|
const f = fixture();
|
|
try {
|
|
const c = await f.connection(),
|
|
b = await f.bot(
|
|
graph(node("manual"), node("delay", { seconds: 100 })),
|
|
c.id,
|
|
);
|
|
const pending = f.invoke(`/api/bots/${b.id}/run`, "POST");
|
|
await until(() => f.studio.running.has(b.id));
|
|
assert.equal((await f.invoke(`/api/bots/${b.id}/run`, "POST")).status, 400);
|
|
assert.equal(
|
|
(await f.invoke(`/api/bots/${b.id}/cancel`, "POST")).status,
|
|
200,
|
|
);
|
|
assert.equal((await pending).data.status, "cancelled");
|
|
assert.equal(f.studio.running.size, 0);
|
|
assert.equal(
|
|
(await f.invoke(`/api/bots/${b.id}/simulate`, "POST")).data.status,
|
|
"success",
|
|
);
|
|
} finally {
|
|
await f.close();
|
|
}
|
|
});
|
|
|
|
test("event filters, group-restricted chat commands, multiple triggers and encrypted secrets", async () => {
|
|
const f = fixture();
|
|
try {
|
|
const c = await f.connection({
|
|
eventsEnabled: true,
|
|
sshUsername: "query",
|
|
sshPassword: "ssh-secret",
|
|
});
|
|
const g = graph(
|
|
node("event", { filters: { client_type: "0" } }),
|
|
node("command", { command: "!info", allowedGroup: "9" }),
|
|
node("template", {
|
|
text: "{{event.client_nickname}} · {{event.command_args}}",
|
|
}),
|
|
);
|
|
g.edges = [
|
|
{ from: "event", to: "template", port: "out" },
|
|
{ from: "command", to: "template", port: "out" },
|
|
];
|
|
const b = await f.bot(g, c.id, { enabled: true });
|
|
assert.equal(f.states.length, 1);
|
|
assert.equal(f.states[0].c.sshPassword, "ssh-secret");
|
|
const stored = f.db
|
|
.prepare("SELECT data FROM studio_connections")
|
|
.get().data;
|
|
assert.ok(!stored.includes("ssh-secret"));
|
|
assert.match(stored, /a{64}/);
|
|
await f.studio.receive(c.id, "notifycliententerview", {
|
|
clid: "1",
|
|
client_type: "1",
|
|
client_nickname: "Query",
|
|
});
|
|
assert.equal((await f.invoke("/api/dashboard")).data.executions.length, 0);
|
|
await f.studio.receive(c.id, "notifycliententerview", {
|
|
clid: "1",
|
|
client_type: "0",
|
|
client_nickname: "Pessoa",
|
|
});
|
|
await until(async () =>
|
|
(await f.invoke("/api/dashboard")).data.executions.some(
|
|
(e) => e.status === "success",
|
|
),
|
|
);
|
|
await f.studio.receive(c.id, "notifytextmessage", {
|
|
invokerid: "1",
|
|
invokername: "Pessoa",
|
|
msg: "!info teste",
|
|
__listenerChannel: "0",
|
|
});
|
|
const runs = await until(async () => {
|
|
const list = (await f.invoke("/api/dashboard")).data.executions;
|
|
return list.filter((e) => e.status === "success").length === 2 && list;
|
|
});
|
|
assert.equal(runs[0].output, "Pessoa A · teste");
|
|
await f.invoke(`/api/bots/${b.id}`, "PUT", {
|
|
graph: {
|
|
...g,
|
|
nodes: g.nodes.map((n) =>
|
|
n.type === "command"
|
|
? { ...n, config: { ...n.config, allowedGroup: "999" } }
|
|
: n,
|
|
),
|
|
},
|
|
});
|
|
await f.studio.receive(c.id, "notifytextmessage", {
|
|
invokerid: "1",
|
|
msg: "!info",
|
|
__listenerChannel: "0",
|
|
});
|
|
assert.equal((await f.invoke("/api/dashboard")).data.executions.length, 2);
|
|
assert.equal(
|
|
(
|
|
await f.invoke(`/api/bots/${b.id}/simulate`, "POST", {
|
|
triggerId: "missing",
|
|
})
|
|
).status,
|
|
400,
|
|
);
|
|
} finally {
|
|
await f.close();
|
|
}
|
|
});
|
|
|
|
test("webhooks authenticate before enqueueing; exports remove secrets and imports remain disabled", async () => {
|
|
const f = fixture();
|
|
try {
|
|
const c = await f.connection(),
|
|
secret = "secret-for-test-12345",
|
|
g = graph(
|
|
node("webhook", { secret, filters: { kind: "update" } }),
|
|
node("http", { headers: { Authorization: "secret-header" } }),
|
|
node("template", { text: "{{event.webhook_body.name}}" }),
|
|
);
|
|
const b = await f.bot(g, c.id, { enabled: true });
|
|
const stored = f.db
|
|
.prepare("SELECT data FROM studio_bots WHERE id=?")
|
|
.get(b.id).data;
|
|
assert.ok(!stored.includes(secret));
|
|
assert.ok(!stored.includes("secret-header"));
|
|
const reader = (
|
|
await f.invoke("/api/dashboard", "GET", {}, "Leitor")
|
|
).data.bots.find((x) => x.id === b.id);
|
|
assert.equal(reader.graph.nodes[0].config.secret, undefined);
|
|
assert.equal(reader.graph.nodes[1].config.headers, undefined);
|
|
assert.equal(
|
|
(
|
|
await f.invoke(
|
|
`/api/hooks/${b.id}`,
|
|
"POST",
|
|
{ kind: "update" },
|
|
{},
|
|
{ "x-webhook-secret": "bad" },
|
|
)
|
|
).status,
|
|
401,
|
|
);
|
|
const exported = (await f.invoke(`/api/bots/${b.id}/export`, "POST")).data;
|
|
assert.equal(exported.format, "ts6-botflow/v1");
|
|
assert.equal(exported.bot.graph.nodes[0].config.secret, undefined);
|
|
assert.equal(exported.bot.connectionId, undefined);
|
|
const imported = (
|
|
await f.invoke("/api/bots", "POST", { import: exported.bot })
|
|
).data;
|
|
assert.equal(imported.enabled, false);
|
|
assert.equal(imported.connectionId, "");
|
|
assert.ok(imported.graph.nodes[0].config.secret.length >= 16);
|
|
await f.invoke(`/api/bots/${b.id}`, "PUT", {
|
|
graph: graph(
|
|
node("webhook", { secret }),
|
|
node("template", { text: "{{event.webhook_body.name}}" }),
|
|
),
|
|
});
|
|
assert.equal(
|
|
(
|
|
await f.invoke(
|
|
`/api/hooks/${b.id}`,
|
|
"POST",
|
|
{ name: "Webhook" },
|
|
"Dono",
|
|
{ "x-webhook-secret": secret },
|
|
)
|
|
).status,
|
|
202,
|
|
);
|
|
const result = await until(async () =>
|
|
(await f.invoke("/api/dashboard")).data.executions.find(
|
|
(e) => e.status === "success",
|
|
),
|
|
);
|
|
assert.equal(result.output, "Webhook");
|
|
const copy = (await f.invoke(`/api/bots/${b.id}/duplicate`, "POST")).data;
|
|
assert.notEqual(copy.id, b.id);
|
|
assert.equal(copy.enabled, false);
|
|
assert.equal(
|
|
(
|
|
await f.invoke("/api/bots", "POST", {
|
|
import: {
|
|
graph: graph(node("manual")),
|
|
variables: { constructor: "bad" },
|
|
},
|
|
})
|
|
).status,
|
|
400,
|
|
);
|
|
} finally {
|
|
await f.close();
|
|
}
|
|
});
|
|
|
|
test("channel messages serialize a temporary Query move and restore the original channel on failure", async () => {
|
|
const calls = [];
|
|
const f = fixture({
|
|
transport: async (c, command, params) => {
|
|
calls.push({ command, params });
|
|
if (command === "whoami")
|
|
return [{ client_id: "50", client_channel_id: "7" }];
|
|
if (command === "sendtextmessage") throw new Error("Message refused");
|
|
return [];
|
|
},
|
|
});
|
|
try {
|
|
const c = await f.connection(),
|
|
b = await f.bot(
|
|
graph(
|
|
node("manual"),
|
|
node("message", { mode: "channel", target: 21, text: "Test" }),
|
|
),
|
|
c.id,
|
|
);
|
|
const result = (await f.invoke(`/api/bots/${b.id}/run`, "POST")).data;
|
|
assert.equal(result.status, "error");
|
|
assert.deepEqual(
|
|
calls.map((c) => c.command),
|
|
["whoami", "clientmove", "sendtextmessage", "clientmove"],
|
|
);
|
|
assert.deepEqual(calls.at(-1).params, { clid: 50, cid: "7" });
|
|
} finally {
|
|
await f.close();
|
|
}
|
|
});
|
|
|
|
test("validation rejects cycles and preserves legitimate multiple-trigger branches", () => {
|
|
const g = graph(node("manual"), node("log"), node("delay"));
|
|
g.edges.push({ from: "delay", to: "log", port: "out" });
|
|
assert.throws(() => validateGraph(g), /ciclo/);
|
|
assert.doesNotThrow(() =>
|
|
validateGraph({
|
|
nodes: [node("manual"), node("timer"), node("log")],
|
|
edges: [
|
|
{ from: "manual", to: "log", port: "out" },
|
|
{ from: "timer", to: "log", port: "out" },
|
|
],
|
|
}),
|
|
);
|
|
assert.throws(
|
|
() => validateGraph(graph(node("manual"), node("timer"))),
|
|
/inválida/,
|
|
);
|
|
});
|
|
|
|
test("legacy graphs, encrypted keys and sessions survive reopening the database", async () => {
|
|
const f = fixture();
|
|
let next;
|
|
try {
|
|
const c = await f.connection(),
|
|
b = await f.bot(templates[0].graph, c.id);
|
|
const legacy = JSON.parse(
|
|
f.db.prepare("SELECT data FROM studio_bots WHERE id=?").get(b.id).data,
|
|
);
|
|
for (const n of legacy.graph.nodes) {
|
|
delete n.secureConfig;
|
|
if (["clients", "serverinfo"].includes(n.type)) n.config = {};
|
|
}
|
|
delete legacy.timeoutSeconds;
|
|
f.db
|
|
.prepare("UPDATE studio_bots SET data=? WHERE id=?")
|
|
.run(JSON.stringify(legacy), b.id);
|
|
const oldConnection = JSON.parse(
|
|
f.db.prepare("SELECT data FROM studio_connections WHERE id=?").get(c.id)
|
|
.data,
|
|
);
|
|
for (const key of Object.keys(oldConnection))
|
|
if (!["id", "name", "url", "serverId", "apiKey"].includes(key))
|
|
delete oldConnection[key];
|
|
f.db
|
|
.prepare("UPDATE studio_connections SET data=? WHERE id=?")
|
|
.run(JSON.stringify(oldConnection), c.id);
|
|
f.db.exec(
|
|
"CREATE TABLE sessions(id TEXT PRIMARY KEY,user TEXT,expires INTEGER)",
|
|
);
|
|
f.db
|
|
.prepare("INSERT INTO sessions VALUES(?,?,?)")
|
|
.run("session-fixture", "owner", Date.now() + 10000);
|
|
await f.studio.close();
|
|
next = createStudio(f.db, f.directory, {
|
|
scheduler: false,
|
|
transport: async (connection, command) => {
|
|
assert.equal(connection.apiKey, "test-key");
|
|
return simulatedQuery(command);
|
|
},
|
|
});
|
|
assert.equal(f.db.prepare("SELECT user FROM sessions").get().user, "owner");
|
|
assert.equal(
|
|
f.db.prepare("SELECT count(*) AS count FROM studio_bots").get().count,
|
|
2,
|
|
);
|
|
const req = Readable.from([Buffer.from("{}")]);
|
|
req.method = "POST";
|
|
req.headers = { origin: "http://localhost:3080" };
|
|
let response;
|
|
await next.route(
|
|
req,
|
|
new URL(`/api/bots/${b.id}/run`, "http://localhost:3080"),
|
|
{ role: "Dono", name: "Test" },
|
|
(status, data) => (response = { status, data }),
|
|
);
|
|
assert.equal(response.status, 200);
|
|
assert.equal(response.data.status, "success");
|
|
assert.equal(response.data.steps.length, 7);
|
|
assert.match(response.data.output, /Online: 3 \/ 32/);
|
|
} finally {
|
|
await next?.close();
|
|
await f.close();
|
|
}
|
|
});
|
|
|
|
test("typed variables support lists and shutdown waits for cancellation before closing storage", async () => {
|
|
const f = fixture();
|
|
try {
|
|
const g = graph(
|
|
node("manual"),
|
|
node("variable", { name: "list", valueType: "json", value: "[1,2,3]" }),
|
|
node("template", { text: "{{length(temp.list)}}" }),
|
|
);
|
|
const c = await f.connection(),
|
|
b = await f.bot(g, c.id),
|
|
simulation = (await f.invoke(`/api/bots/${b.id}/simulate`, "POST")).data;
|
|
assert.equal(simulation.output, "3");
|
|
await f.invoke(`/api/bots/${b.id}`, "PUT", {
|
|
graph: graph(node("manual"), node("delay", { seconds: 100 })),
|
|
});
|
|
const pending = f.invoke(`/api/bots/${b.id}/run`, "POST");
|
|
await until(() => f.studio.running.has(b.id));
|
|
await f.studio.close();
|
|
assert.equal((await pending).data.status, "cancelled");
|
|
assert.equal(f.studio.running.size, 0);
|
|
assert.equal(
|
|
f.db.prepare("SELECT count(*) AS count FROM studio_executions").get()
|
|
.count,
|
|
2,
|
|
);
|
|
} finally {
|
|
await f.close();
|
|
}
|
|
});
|