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( "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"')); }, );