runa/vscode/integration/sources.test.ts

173 lines
7.3 KiB
TypeScript
Raw Normal View History

import assert from "node:assert/strict";
import test from "node:test";
import { ImageClient } from "../src/client";
import { ProtocolError } from "../src/protocol";
import { parseValue } from "../src/values";
import { startImage } from "./server";
const conflict = (error: unknown) => error instanceof ProtocolError && error.code === "conflict";
test(
"actor-local behaviors, source rename guards and log files use the image protocol",
{ timeout: 10000 },
async (t) => {
const image = await startImage();
const client = await ImageClient.connect(image.address, (error) =>
assert.equal(error, undefined),
);
t.after(async () => {
await client.close();
await image.stop();
});
const source = await client.writeSource(
"tests/behavior",
null,
'pub fn handle(ctx, state, sender, message) { ctx.info("old"); state + 1 }',
);
const actor = await client.spawn(source.path, parseValue("0"));
const other = await client.spawn(source.path, parseValue("0"));
const commit = await client.commitSource(source.path);
const saved = await client.writeSource(
source.path,
source.source_id,
source.source.replace("state + 1", "state + 2"),
);
assert.equal((await client.actorBehavior(actor)).source, source.source);
const temporary = await client.writeActorBehavior(
actor,
source.source_id,
source.source.replace('"old"', '"temporary"').replace("state + 1", "state + 10"),
);
assert.equal((await client.inspectActor(actor)).temporary_behavior, true);
assert.equal((await client.inspectActor(other)).temporary_behavior, false);
await assert.rejects(
client.writeActorBehavior(actor, source.source_id, source.source),
conflict,
);
await assert.rejects(
client.updateBehaviorSource(actor, source.source_id, temporary.source_id, source.path),
conflict,
);
const updated = await client.updateBehaviorSource(
actor,
saved.source_id,
temporary.source_id,
source.path,
);
assert.equal(updated.source, temporary.source);
assert.deepEqual(await client.sourceHistory(source.path), [commit]);
await client.send(actor, parseValue("null"));
await client.send(other, parseValue("null"));
await client.tick(20);
assert.deepEqual((await client.inspectActor(actor)).state, parseValue("10"));
assert.deepEqual((await client.inspectActor(other)).state, parseValue("1"));
const logs = await client.logs(actor);
assert.equal(logs.length, 1);
assert.equal(logs[0]?.message, "temporary");
assert.equal(logs[0]?.level, "info");
assert.equal(logs[0]?.actor_ref, actor);
await assert.rejects(
client.renameSource(source.path, "moved/behavior", updated.source_id, false),
/in use/,
);
await assert.rejects(
client.deleteSource(source.path, updated.source_id),
/stop the actors first/,
);
const renamed = await client.renameSource(
source.path,
"moved/behavior",
updated.source_id,
true,
);
assert.equal((await client.inspectActor(actor)).behavior_path, renamed.path);
assert.equal((await client.inspectActor(other)).behavior_path, renamed.path);
await assert.rejects(
client.updateBehaviorSource(actor, updated.source_id, temporary.source_id, source.path),
conflict,
);
assert.deepEqual(await client.sourceHistory(renamed.path), [commit]);
await assert.rejects(client.readSource(source.path), ProtocolError);
await client.system(actor, { kind: "reload" });
assert.equal((await client.inspectActor(actor)).temporary_behavior, false);
await client.system(actor, { kind: "kill" });
await client.system(other, { kind: "kill" });
await client.deleteSource(renamed.path, renamed.source_id);
await assert.rejects(client.readSource(renamed.path), ProtocolError);
},
);
test(
"image sources are saved with compare-and-swap, spawned uncommitted, committed, and rolled back",
{ timeout: 10000 },
async (t) => {
const image = await startImage();
let client: ImageClient | undefined;
t.after(async () => {
client?.close();
await image.stop();
});
client = await ImageClient.connect(image.address, (error) => assert.equal(error, undefined));
const [counter, ...others] = await client.listSources();
assert.ok(counter);
assert.deepEqual(others, []);
assert.equal(counter.path, "actors/counter");
assert.match(counter.source, /pub fn handle\(ctx, state, sender, message\)/);
assert.deepEqual(await client.readSource("actors/counter"), counter);
assert.deepEqual(await client.sourceHistory("actors/counter"), []);
await assert.rejects(client.readSource("actors/missing"), ProtocolError);
const edited = "pub fn handle(ctx, state, sender, message) {\n state + 2 // 雪\n}\n";
await assert.rejects(client.writeSource("actors/counter", null, edited), conflict);
await assert.rejects(client.writeSource("actors/counter", "stale", edited), conflict);
const saved = await client.writeSource("actors/counter", counter.source_id, edited);
assert.equal(saved.source, edited);
assert.notEqual(saved.source_id, counter.source_id);
await assert.rejects(
client.writeSource("actors/counter", counter.source_id, counter.source),
conflict,
);
assert.deepEqual(await client.sourceHistory("actors/counter"), []);
// Spawn uses the latest saved working source, independent of history.
const actor = await client.spawn("actors/counter", parseValue("0"));
await client.send(actor, parseValue("null"));
await client.tick(100);
assert.deepEqual((await client.inspectActor(actor)).state, parseValue("2"));
const commit = await client.commitSource("actors/counter");
assert.match(commit.revision, /^[A-Za-z0-9]{1,64}$/);
assert.equal(commit.source_id, saved.source_id);
assert.deepEqual(await client.sourceHistory("actors/counter"), [commit]);
assert.equal(await client.revisionSource("actors/counter", commit.revision), edited);
await assert.rejects(client.revisionSource("actors/counter", "missing0"), ProtocolError);
const later = await client.writeSource("actors/counter", saved.source_id, counter.source);
assert.equal((await client.readSource("actors/counter")).source, counter.source);
const rolled = await client.rollbackSource("actors/counter", commit.revision);
assert.deepEqual(rolled, {
path: "actors/counter",
source_id: saved.source_id,
source: edited,
});
assert.notEqual(rolled.source_id, later.source_id);
assert.deepEqual(await client.sourceHistory("actors/counter"), [commit]);
assert.equal(await client.revisionSource("actors/counter", commit.revision), edited);
const created = await client.writeSource(
"actors/echo",
null,
"pub fn handle(ctx, state, sender, message) { message }\n",
);
assert.deepEqual((await client.listSources()).map((file) => file.path).sort(), [
"actors/counter",
"actors/echo",
]);
assert.deepEqual(await client.sourceHistory("actors/echo"), []);
await client.system(actor, { kind: "become", path: created.path });
await client.send(actor, parseValue('"hello"'));
await client.tick(100);
assert.deepEqual((await client.inspectActor(actor)).state, parseValue('"hello"'));
},
);