Complete training workflow and acceptance hardening

This commit is contained in:
andreysk0304 2026-09-26 18:12:27 +03:00 • committed by gglamer
commit 7237265833
243 changed files with 17014 additions and 1500 deletions

View file

@ -0,0 +1,110 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { transform } from "esbuild";
const source = await readFile(new URL("../src/shared/api/ws.ts", import.meta.url), "utf8");
const { code } = await transform(source, { loader: "ts", format: "esm" });
const moduleUrl = `data:text/javascript;base64,${Buffer.from(code).toString("base64")}`;
const { callChannel, stationChannel } = await import(moduleUrl);
const mergeSource = await readFile(new URL("../src/features/kio-card/merge.ts", import.meta.url), "utf8");
const { code: mergeCode } = await transform(mergeSource, { loader: "ts", format: "esm" });
const { applyPatch, applyState, edit, empty } = await import(
`data:text/javascript;base64,${Buffer.from(mergeCode).toString("base64")}`,
);
class FakeWebSocket {
static OPEN = 1;
static instances = [];
readyState = 0;
sent = [];
constructor(url) {
this.url = url;
FakeWebSocket.instances.push(this);
}
send(value) { this.sent.push(JSON.parse(value)); }
open() { this.readyState = FakeWebSocket.OPEN; this.onopen(); }
drop() { this.readyState = 3; this.onclose(); }
receive(event) { this.onmessage({ data: JSON.stringify(event) }); }
}
globalThis.WebSocket = FakeWebSocket;
globalThis.location = { protocol: "http:", host: "localhost" };
const reconnectTimers = [];
globalThis.setTimeout = (callback) => (reconnectTimers.push(callback), reconnectTimers.length);
globalThis.clearTimeout = () => {};
function reconnect(socket) {
socket.drop();
reconnectTimers.shift()();
const next = FakeWebSocket.instances.at(-1);
next.open();
return next;
}
const call = callChannel("session-1").connect();
let socket = FakeWebSocket.instances.at(-1);
socket.open();
const patch = {
type: "kio.patch",
fields: {
address: "ул. Тестовая, 1",
notify: ["101", "СМП"],
coords: { latitude: 55.7, longitude: 37.6 },
},
};
assert.equal(call.send(patch), true);
const sentPatch = socket.sent[0];
assert.equal(sentPatch.type, patch.type);
assert.deepEqual(sentPatch.fields, patch.fields);
assert.equal(typeof sentPatch._command_id, "string");
socket = reconnect(socket);
assert.deepEqual(socket.sent, [sentPatch], "unacknowledged KIO patch is replayed after reconnect");
socket.receive({ type: "kio.patch", fields: patch.fields, source: "auto" });
socket = reconnect(socket);
assert.deepEqual(socket.sent, [sentPatch], "unrelated automatic patch does not acknowledge operator input");
socket.receive({ type: "kio.patch", fields: { address: "ул. Другая, 2" }, source: "operator" });
socket = reconnect(socket);
assert.deepEqual(socket.sent, [sentPatch], "different operator state does not acknowledge the pending patch");
socket.receive({ type: "kio.patch", fields: {
address: patch.fields.address,
notify: ["101", "СМП"],
coords: { longitude: 37.6, latitude: 55.7 },
}, source: "operator" });
socket = reconnect(socket);
assert.deepEqual(socket.sent, [], "matching checkpoint echo clears the KIO patch");
const edited = edit(empty, "coords", { latitude: 55.7, longitude: 37.6 });
assert.deepEqual(applyPatch(edited, { coords: { longitude: 37.6, latitude: 55.7 } }, "operator").pending, {});
assert.deepEqual(applyState(edited, { coords: { longitude: 37.6, latitude: 55.7 } }).pending, {});
const dds = stationChannel("session-1", "dds").connect();
socket = FakeWebSocket.instances.at(-1);
socket.open();
const crewSelect = { type: "crew.select", crew: "ПСЧ-1" };
assert.equal(dds.send(crewSelect), true);
const sentCrewSelect = socket.sent[0];
assert.equal(sentCrewSelect.type, crewSelect.type);
assert.equal(sentCrewSelect.crew, crewSelect.crew);
assert.equal(typeof sentCrewSelect._command_id, "string");
socket = reconnect(socket);
assert.deepEqual(socket.sent, [sentCrewSelect], "station command is replayed with its stable ID");
socket.receive({ type: "command.ack", command_id: sentCrewSelect._command_id });
socket.sent = [];
const reply = { type: "card.reply", card_id: "card-1", text: "Принято" };
assert.equal(dds.send(reply), true);
const sentReply = socket.sent[0];
assert.equal(sentReply.type, reply.type);
assert.equal(sentReply.card_id, reply.card_id);
assert.equal(sentReply.text, reply.text);
socket = reconnect(socket);
assert.deepEqual(socket.sent, [sentReply], "unacknowledged reply is replayed after reconnect");
socket.receive({ type: "station.state", snapshot: { card_id: "card-2", reply_text: "Принято" } });
socket = reconnect(socket);
assert.deepEqual(socket.sent, [sentReply], "reply for another card does not acknowledge this card");
socket.receive({ type: "station.state", snapshot: { card_id: "card-1", reply_text: "Принято" } });
socket = reconnect(socket);
assert.deepEqual(socket.sent, [], "matching persisted station snapshot clears the reply");
console.log("WebSocket durable outbox replay/ack tests passed");