173 lines
7.3 KiB
TypeScript
173 lines
7.3 KiB
TypeScript
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"'));
|
|
},
|
|
);
|