Fork of upstream llblab/[email protected].0 (SHA afe09c5) repackaged as a drop-in extension for can1357/oh-my-pi (omp >= 16.5.2). Changes vs upstream: - Rename package: @llblab/pi-telegram -> @evandrodevbr/oh-my-pi-telegram - Bump version: 0.22.0 -> 0.22.0-evandro.1 - Update repository/homepage/bugs URLs to evandrodevbr - Add omp field in package.json (displayName + compatibleWith) - README: prepend 'Oh My Pi (omp) Compatibility' section with install commands, verified-components table, upstream lineage, and rationale - CHANGELOG: add '0.22.0-evandro.1' entry under Unreleased - docs/: rename @llblab/pi-telegram imports to @evandrodevbr/oh-my-pi-telegram - tests/public-api.test.ts: rename package import paths Gates re-run on the fork: - npm run typecheck -> tsc --noEmit (success) - npm test -> 1229 pass / 0 fail / 1 platform-only skip - npm run audit -> found 0 vulnerabilities - npm run pack:check -> evandrodevbr-oh-my-pi-telegram-0.22.0-evandro.1.tgz Verified: - omp install --link ./pi-telegram -> Linked OK - omp plugin list -> @evandrodevbr/[email protected].0-evandro.1 Upstream lineage: badlogic/pi-telegram -> llblab/pi-telegram (0.22.0) -> evandrodevbr/oh-my-pi-telegram (this fork)
753 lines
23 KiB
TypeScript
753 lines
23 KiB
TypeScript
/**
|
|
* Telegram delivery API domain regressions
|
|
* Zones: telegram delivery, extension API, runtime binding
|
|
* Mirrors lib/delivery.ts and protects public operation delegation, generation fencing, and fail-soft availability
|
|
*/
|
|
|
|
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import {
|
|
bindTelegramDeliveryRuntime,
|
|
clearTelegramDeliveryRuntime,
|
|
createTelegramBridgeDeliveryRuntime,
|
|
createTelegramDeliveryLifecycleHooks,
|
|
createTelegramDeliveryTargetPolicyRuntime,
|
|
createTelegramDeliveryRuntime as createConcreteDeliveryRuntime,
|
|
deleteTelegramView,
|
|
editTelegramView,
|
|
isTelegramDeliveryExplicitTargetAuthorized,
|
|
resolveTelegramDeliveryAggregateTarget,
|
|
resolveTelegramDeliveryInstanceTarget,
|
|
sendTelegramChatAction,
|
|
sendTelegramView,
|
|
type TelegramDeliveryHandle,
|
|
type TelegramDeliveryRuntime,
|
|
type TelegramDeliveryRuntimeDeps,
|
|
} from "../lib/delivery.ts";
|
|
import { TelegramApiCommitUnknownError } from "../lib/telegram-api.ts";
|
|
|
|
const target = { chatId: 42, threadId: 7 };
|
|
|
|
function createRuntime(
|
|
generation: string,
|
|
calls: string[] = [],
|
|
): TelegramDeliveryRuntime {
|
|
return {
|
|
generation,
|
|
shutdown() {},
|
|
async sendView() {
|
|
calls.push("send");
|
|
return {
|
|
ok: true,
|
|
value: { target, messageIds: [11], generation },
|
|
};
|
|
},
|
|
async editView() {
|
|
calls.push("edit");
|
|
return {
|
|
ok: true,
|
|
value: { target, messageIds: [11], generation },
|
|
};
|
|
},
|
|
async deleteView() {
|
|
calls.push("delete");
|
|
return { ok: true, value: undefined };
|
|
},
|
|
async sendChatAction(action) {
|
|
calls.push(`action:${action}`);
|
|
return { ok: true, value: undefined };
|
|
},
|
|
};
|
|
}
|
|
|
|
function handle(generation: string): TelegramDeliveryHandle {
|
|
return { target, messageIds: [11], generation };
|
|
}
|
|
|
|
test.afterEach(() => clearTelegramDeliveryRuntime());
|
|
|
|
test("Delivery API fails softly while no runtime is bound", async () => {
|
|
const result = await sendTelegramView(
|
|
{ text: "hello" },
|
|
{ scope: { kind: "instance" } },
|
|
);
|
|
assert.deepEqual(result, {
|
|
ok: false,
|
|
reason: "runtime-unavailable",
|
|
message: "Telegram delivery runtime is unavailable in this Pi session.",
|
|
});
|
|
});
|
|
|
|
test("Delivery API delegates operations to the current runtime", async () => {
|
|
const calls: string[] = [];
|
|
bindTelegramDeliveryRuntime(createRuntime("one", calls));
|
|
const sent = await sendTelegramView(
|
|
{ text: "hello", parseMode: "html" },
|
|
{ scope: { kind: "active-turn" }, replyToMessageId: 5 },
|
|
);
|
|
assert.equal(sent.ok, true);
|
|
await editTelegramView(handle("one"), { text: "updated" });
|
|
await deleteTelegramView(handle("one"));
|
|
await sendTelegramChatAction("typing", {
|
|
scope: { kind: "aggregate" },
|
|
});
|
|
assert.deepEqual(calls, ["send", "edit", "delete", "action:typing"]);
|
|
});
|
|
|
|
test("Delivery API rejects empty views before transport", async () => {
|
|
const calls: string[] = [];
|
|
bindTelegramDeliveryRuntime(createRuntime("one", calls));
|
|
const result = await sendTelegramView(
|
|
{ text: " " },
|
|
{ scope: { kind: "instance" } },
|
|
);
|
|
assert.equal(result.ok, false);
|
|
if (!result.ok) assert.equal(result.reason, "invalid-view");
|
|
assert.deepEqual(calls, []);
|
|
});
|
|
|
|
test("Delivery API fences handles from older runtime generations", async () => {
|
|
const calls: string[] = [];
|
|
bindTelegramDeliveryRuntime(createRuntime("new", calls));
|
|
const edited = await editTelegramView(handle("old"), { text: "updated" });
|
|
const deleted = await deleteTelegramView(handle("old"));
|
|
assert.equal(edited.ok, false);
|
|
assert.equal(deleted.ok, false);
|
|
if (!edited.ok) assert.equal(edited.reason, "stale-handle");
|
|
if (!deleted.ok) assert.equal(deleted.reason, "stale-handle");
|
|
assert.deepEqual(calls, []);
|
|
});
|
|
|
|
test("A stale runtime disposer cannot clear its replacement", async () => {
|
|
const firstCalls: string[] = [];
|
|
const secondCalls: string[] = [];
|
|
const disposeFirst = bindTelegramDeliveryRuntime(
|
|
createRuntime("first", firstCalls),
|
|
);
|
|
bindTelegramDeliveryRuntime(createRuntime("second", secondCalls));
|
|
disposeFirst();
|
|
await sendTelegramView(
|
|
{ text: "hello" },
|
|
{ scope: { kind: "instance" } },
|
|
);
|
|
assert.deepEqual(firstCalls, []);
|
|
assert.deepEqual(secondCalls, ["send"]);
|
|
});
|
|
|
|
test("Delivery lifecycle mints fresh generations and invalidates on shutdown", async () => {
|
|
const calls: string[] = [];
|
|
let sequence = 0;
|
|
const lifecycle = createTelegramDeliveryLifecycleHooks(() =>
|
|
createRuntime(`generation-${++sequence}`, calls),
|
|
);
|
|
await lifecycle.onSessionStart();
|
|
const active = await sendTelegramView(
|
|
{ text: "hello" },
|
|
{ scope: { kind: "instance" } },
|
|
);
|
|
assert.equal(active.ok, true);
|
|
if (!active.ok) return;
|
|
|
|
await lifecycle.onSessionShutdown();
|
|
const stopped = await sendTelegramView(
|
|
{ text: "hello" },
|
|
{ scope: { kind: "instance" } },
|
|
);
|
|
assert.equal(stopped.ok, false);
|
|
if (!stopped.ok) assert.equal(stopped.reason, "runtime-unavailable");
|
|
|
|
await lifecycle.onSessionStart();
|
|
const replacement = await sendTelegramView(
|
|
{ text: "replacement" },
|
|
{ scope: { kind: "instance" } },
|
|
);
|
|
assert.equal(replacement.ok, true);
|
|
if (replacement.ok) {
|
|
assert.notEqual(replacement.value.generation, active.value.generation);
|
|
}
|
|
const staleEdit = await editTelegramView(active.value, { text: "stale" });
|
|
const staleDelete = await deleteTelegramView(active.value);
|
|
assert.equal(staleEdit.ok, false);
|
|
assert.equal(staleDelete.ok, false);
|
|
if (!staleEdit.ok) assert.equal(staleEdit.reason, "stale-handle");
|
|
if (!staleDelete.ok) assert.equal(staleDelete.reason, "stale-handle");
|
|
assert.deepEqual(calls, ["send", "send"]);
|
|
});
|
|
|
|
test("Delivery shutdown fences an in-flight chunk sequence and queued work", async () => {
|
|
const transportCalls: string[] = [];
|
|
let releaseFirstChunk: (() => void) | undefined;
|
|
let markFirstChunkStarted: (() => void) | undefined;
|
|
const firstChunkGate = new Promise<void>((resolve) => {
|
|
releaseFirstChunk = resolve;
|
|
});
|
|
const firstChunkStarted = new Promise<void>((resolve) => {
|
|
markFirstChunkStarted = resolve;
|
|
});
|
|
const runtime = createConcreteDeliveryRuntime({
|
|
generation: "generation-one",
|
|
getActiveTurnTarget: () => target,
|
|
getInstanceTarget: () => target,
|
|
getAggregateTarget: () => ({ chatId: target.chatId }),
|
|
isExplicitTargetAuthorized: () => true,
|
|
renderView: () => [
|
|
{ text: "first", parseMode: "html" },
|
|
{ text: "second", parseMode: "html" },
|
|
],
|
|
async sendChunk() {
|
|
transportCalls.push("send");
|
|
if (transportCalls.length === 1) {
|
|
markFirstChunkStarted?.();
|
|
await firstChunkGate;
|
|
}
|
|
return 100 + transportCalls.length;
|
|
},
|
|
async editChunk() {
|
|
transportCalls.push("edit");
|
|
},
|
|
async deleteMessage() {
|
|
transportCalls.push("delete");
|
|
},
|
|
async sendChatAction() {
|
|
transportCalls.push("action");
|
|
},
|
|
});
|
|
|
|
const inFlight = runtime.sendView(
|
|
{ text: "two chunks" },
|
|
{ scope: { kind: "instance" } },
|
|
);
|
|
await firstChunkStarted;
|
|
const queued = runtime.sendChatAction("typing", { kind: "instance" });
|
|
runtime.shutdown();
|
|
releaseFirstChunk?.();
|
|
|
|
const [inFlightResult, queuedResult] = await Promise.all([inFlight, queued]);
|
|
assert.equal(inFlightResult.ok, false);
|
|
assert.equal(queuedResult.ok, false);
|
|
if (!inFlightResult.ok) {
|
|
assert.equal(inFlightResult.reason, "runtime-unavailable");
|
|
}
|
|
if (!queuedResult.ok) assert.equal(queuedResult.reason, "runtime-unavailable");
|
|
assert.deepEqual(transportCalls, ["send"]);
|
|
});
|
|
|
|
test("Delivery API converts unexpected runtime errors into transport failures", async () => {
|
|
const runtime = createRuntime("one");
|
|
runtime.sendView = async () => {
|
|
throw new Error("secret transport detail");
|
|
};
|
|
bindTelegramDeliveryRuntime(runtime);
|
|
const result = await sendTelegramView(
|
|
{ text: "hello" },
|
|
{ scope: { kind: "instance" } },
|
|
);
|
|
assert.deepEqual(result, {
|
|
ok: false,
|
|
reason: "transport-failed",
|
|
message: "Telegram delivery failed.",
|
|
});
|
|
});
|
|
|
|
test("Delivery target policy runtime projects live authority and excludes Guest turns", () => {
|
|
let ownsDirect = false;
|
|
let followerRegistered = true;
|
|
let guestQueryId: string | undefined;
|
|
const runtime = createTelegramDeliveryTargetPolicyRuntime({
|
|
ownsDirect: () => ownsDirect,
|
|
isFollowerRegistered: () => followerRegistered,
|
|
getAllowedChatId: () => 42,
|
|
getFollowerTarget: () => ({ chatId: 42, threadId: 7 }),
|
|
getLeaderTarget: () => ({ chatId: 42, threadId: 8 }),
|
|
listThreadRecords: () => [
|
|
{ target: { chatId: 42, threadId: 7 } },
|
|
{ target: { chatId: 42, threadId: 8 } },
|
|
],
|
|
getActiveTurnTarget: () => ({ chatId: 42, threadId: 7 }),
|
|
getActiveGuestQueryId: () => guestQueryId,
|
|
});
|
|
|
|
assert.deepEqual(runtime.getTargetPolicyView(), {
|
|
canDeliver: true,
|
|
ownsDirect: false,
|
|
allowedChatId: 42,
|
|
followerTarget: { chatId: 42, threadId: 7 },
|
|
leaderTarget: { chatId: 42, threadId: 8 },
|
|
liveTargets: [
|
|
{ chatId: 42, threadId: 7 },
|
|
{ chatId: 42, threadId: 8 },
|
|
],
|
|
});
|
|
assert.deepEqual(runtime.getActiveTurnTarget(), {
|
|
chatId: 42,
|
|
threadId: 7,
|
|
});
|
|
guestQueryId = "guest";
|
|
assert.equal(runtime.getActiveTurnTarget(), undefined);
|
|
followerRegistered = false;
|
|
ownsDirect = false;
|
|
assert.equal(runtime.getTargetPolicyView().canDeliver, false);
|
|
});
|
|
|
|
test("Delivery target policy resolves classic, leader, and follower instance surfaces", () => {
|
|
assert.deepEqual(
|
|
resolveTelegramDeliveryInstanceTarget({
|
|
canDeliver: true,
|
|
ownsDirect: true,
|
|
allowedChatId: 42,
|
|
}),
|
|
{ chatId: 42 },
|
|
);
|
|
assert.deepEqual(
|
|
resolveTelegramDeliveryInstanceTarget({
|
|
canDeliver: true,
|
|
ownsDirect: true,
|
|
allowedChatId: 42,
|
|
leaderTarget: { chatId: 42, threadId: 3 },
|
|
}),
|
|
{ chatId: 42, threadId: 3 },
|
|
);
|
|
assert.deepEqual(
|
|
resolveTelegramDeliveryInstanceTarget({
|
|
canDeliver: true,
|
|
ownsDirect: false,
|
|
allowedChatId: 42,
|
|
followerTarget: { chatId: 42, threadId: 7 },
|
|
}),
|
|
{ chatId: 42, threadId: 7 },
|
|
);
|
|
assert.deepEqual(
|
|
resolveTelegramDeliveryAggregateTarget({
|
|
canDeliver: true,
|
|
ownsDirect: false,
|
|
allowedChatId: 42,
|
|
}),
|
|
{ chatId: 42 },
|
|
);
|
|
});
|
|
|
|
test("Delivery target policy restricts follower and leader explicit targets", () => {
|
|
const followerView = {
|
|
canDeliver: true,
|
|
ownsDirect: false,
|
|
allowedChatId: 42,
|
|
followerTarget: { chatId: 42, threadId: 7 },
|
|
};
|
|
assert.equal(
|
|
isTelegramDeliveryExplicitTargetAuthorized(
|
|
{ chatId: 42, threadId: 7 },
|
|
followerView,
|
|
),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
isTelegramDeliveryExplicitTargetAuthorized(
|
|
{ chatId: 42, threadId: 8 },
|
|
followerView,
|
|
),
|
|
false,
|
|
);
|
|
const leaderView = {
|
|
canDeliver: true,
|
|
ownsDirect: true,
|
|
allowedChatId: 42,
|
|
leaderTarget: { chatId: 42, threadId: 3 },
|
|
liveTargets: [{ chatId: 42, threadId: 8 }],
|
|
};
|
|
assert.equal(
|
|
isTelegramDeliveryExplicitTargetAuthorized(
|
|
{ chatId: 42, threadId: 8 },
|
|
leaderView,
|
|
),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
isTelegramDeliveryExplicitTargetAuthorized({ chatId: 99 }, leaderView),
|
|
false,
|
|
);
|
|
});
|
|
|
|
function createConcreteRuntimeHarness(
|
|
overrides: Partial<TelegramDeliveryRuntimeDeps> = {},
|
|
) {
|
|
let nextMessageId = 100;
|
|
const events: Array<Record<string, unknown>> = [];
|
|
const deps: TelegramDeliveryRuntimeDeps = {
|
|
generation: "generation-one",
|
|
getActiveTurnTarget: () => target,
|
|
getInstanceTarget: () => target,
|
|
getAggregateTarget: () => ({ chatId: target.chatId }),
|
|
isExplicitTargetAuthorized: (candidate) => candidate.chatId === target.chatId,
|
|
renderView: (view) =>
|
|
view.text.split("|").map((text) => ({
|
|
text,
|
|
parseMode: view.parseMode ?? "plain",
|
|
})),
|
|
async sendChunk(deliveryTarget, chunk, options) {
|
|
nextMessageId += 1;
|
|
events.push({ type: "send", deliveryTarget, chunk, options, messageId: nextMessageId });
|
|
return nextMessageId;
|
|
},
|
|
async editChunk(deliveryTarget, messageId, chunk, options) {
|
|
events.push({ type: "edit", deliveryTarget, messageId, chunk, options });
|
|
},
|
|
async deleteMessage(deliveryTarget, messageId) {
|
|
events.push({ type: "delete", deliveryTarget, messageId });
|
|
},
|
|
async sendChatAction(deliveryTarget, action) {
|
|
events.push({ type: "action", deliveryTarget, action });
|
|
},
|
|
...overrides,
|
|
};
|
|
return { runtime: createConcreteDeliveryRuntime(deps), events };
|
|
}
|
|
|
|
test("Bridge delivery runtime owns rendering and bus-aware transport adaptation", async () => {
|
|
const sentBodies: Array<Record<string, unknown>> = [];
|
|
const ownership: Array<Record<string, unknown>> = [];
|
|
const runtime = createTelegramBridgeDeliveryRuntime({
|
|
generation: "generation-one",
|
|
getTargetPolicyView: () => ({
|
|
canDeliver: true,
|
|
ownsDirect: true,
|
|
allowedChatId: 42,
|
|
leaderTarget: target,
|
|
}),
|
|
getActiveTurnTarget: () => target,
|
|
api: {
|
|
async sendMessage(body) {
|
|
sentBodies.push(body);
|
|
return { message_id: 101 };
|
|
},
|
|
async editMessageText() {
|
|
return "edited";
|
|
},
|
|
async deleteMessage() {},
|
|
async sendChatAction() {
|
|
return true;
|
|
},
|
|
},
|
|
recordOwnership(input) {
|
|
ownership.push(input);
|
|
},
|
|
});
|
|
|
|
const result = await runtime.sendView(
|
|
{ text: "<b>Working</b>", parseMode: "html" },
|
|
{ scope: { kind: "active-turn" }, replyToMessageId: 9 },
|
|
);
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(sentBodies, [
|
|
{
|
|
chat_id: 42,
|
|
text: "<b>Working</b>",
|
|
parse_mode: "HTML",
|
|
message_thread_id: 7,
|
|
reply_parameters: {
|
|
message_id: 9,
|
|
allow_sending_without_reply: true,
|
|
},
|
|
},
|
|
]);
|
|
assert.deepEqual(ownership, [
|
|
{ chatId: 42, messageId: 101, target },
|
|
]);
|
|
});
|
|
|
|
test("Bridge delivery runtime rejects work after transport generation replacement", async () => {
|
|
let transportActive = true;
|
|
let apiCalls = 0;
|
|
const runtime = createTelegramBridgeDeliveryRuntime({
|
|
generation: "generation-one",
|
|
isTransportActive: () => transportActive,
|
|
getTargetPolicyView: () => ({
|
|
canDeliver: true,
|
|
ownsDirect: true,
|
|
allowedChatId: 42,
|
|
leaderTarget: target,
|
|
}),
|
|
getActiveTurnTarget: () => target,
|
|
api: {
|
|
async sendMessage() {
|
|
apiCalls += 1;
|
|
return { message_id: 101 };
|
|
},
|
|
async editMessageText() {
|
|
apiCalls += 1;
|
|
return "edited";
|
|
},
|
|
async deleteMessage() {
|
|
apiCalls += 1;
|
|
},
|
|
async sendChatAction() {
|
|
apiCalls += 1;
|
|
return true;
|
|
},
|
|
},
|
|
recordOwnership() {},
|
|
});
|
|
transportActive = false;
|
|
|
|
const result = await runtime.sendView(
|
|
{ text: "stale" },
|
|
{ scope: { kind: "instance" } },
|
|
);
|
|
|
|
assert.equal(result.ok, false);
|
|
if (!result.ok) assert.equal(result.reason, "runtime-unavailable");
|
|
assert.equal(apiCalls, 0);
|
|
});
|
|
|
|
test("Concrete delivery runtime resolves scopes and rejects unauthorized targets", async () => {
|
|
const { runtime, events } = createConcreteRuntimeHarness();
|
|
const sent = await runtime.sendView(
|
|
{ text: "hello" },
|
|
{ scope: { kind: "instance" } },
|
|
);
|
|
const denied = await runtime.sendView(
|
|
{ text: "hello" },
|
|
{ scope: { kind: "target", target: { chatId: 99 } } },
|
|
);
|
|
assert.equal(sent.ok, true);
|
|
assert.equal(denied.ok, false);
|
|
if (!denied.ok) assert.equal(denied.reason, "target-unauthorized");
|
|
assert.equal(events.length, 1);
|
|
});
|
|
|
|
test("Concrete delivery runtime keeps active-turn, instance, and aggregate scopes distinct", async () => {
|
|
const activeTurnTarget = { chatId: 42, threadId: 7 };
|
|
const instanceTarget = { chatId: 42, threadId: 3 };
|
|
const aggregateTarget = { chatId: 42 };
|
|
const { runtime, events } = createConcreteRuntimeHarness({
|
|
getActiveTurnTarget: () => activeTurnTarget,
|
|
getInstanceTarget: () => instanceTarget,
|
|
getAggregateTarget: () => aggregateTarget,
|
|
});
|
|
|
|
await runtime.sendView(
|
|
{ text: "turn" },
|
|
{ scope: { kind: "active-turn" } },
|
|
);
|
|
await runtime.sendView(
|
|
{ text: "instance" },
|
|
{ scope: { kind: "instance" } },
|
|
);
|
|
await runtime.sendView(
|
|
{ text: "all" },
|
|
{ scope: { kind: "aggregate" } },
|
|
);
|
|
|
|
assert.deepEqual(
|
|
events.map((event) => event.deliveryTarget),
|
|
[activeTurnTarget, instanceTarget, aggregateTarget],
|
|
);
|
|
});
|
|
|
|
test("Concrete delivery runtime anchors first chunk and keyboard on last chunk", async () => {
|
|
const { runtime, events } = createConcreteRuntimeHarness();
|
|
const replyMarkup = {
|
|
inline_keyboard: [[{ text: "Run", callback_data: "demo:run" }]],
|
|
};
|
|
const result = await runtime.sendView(
|
|
{ text: "one|two", parseMode: "html", replyMarkup },
|
|
{ scope: { kind: "active-turn" }, replyToMessageId: 9 },
|
|
);
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(
|
|
events.map((event) => event.options),
|
|
[{ replyToMessageId: 9 }, { replyMarkup }],
|
|
);
|
|
if (result.ok) assert.deepEqual(result.value.messageIds, [101, 102]);
|
|
});
|
|
|
|
test("Concrete delivery runtime keeps handles opaque and deeply immutable", async () => {
|
|
const { runtime } = createConcreteRuntimeHarness();
|
|
const sent = await runtime.sendView(
|
|
{ text: "hello" },
|
|
{ scope: { kind: "instance" } },
|
|
);
|
|
assert.equal(sent.ok, true);
|
|
if (!sent.ok) return;
|
|
assert.equal(Object.isFrozen(sent.value), true);
|
|
assert.equal(Object.isFrozen(sent.value.target), true);
|
|
assert.equal(Object.isFrozen(sent.value.messageIds), true);
|
|
assert.throws(() => {
|
|
(sent.value.target as { threadId?: number }).threadId = 999;
|
|
}, TypeError);
|
|
const forged = {
|
|
...sent.value,
|
|
target: { ...sent.value.target },
|
|
messageIds: [...sent.value.messageIds],
|
|
};
|
|
const edited = await runtime.editView(forged, { text: "forged" });
|
|
assert.equal(edited.ok, false);
|
|
if (!edited.ok) assert.equal(edited.reason, "stale-handle");
|
|
});
|
|
|
|
test("Concrete delivery runtime exposes non-idempotent commit-unknown outcomes", async () => {
|
|
const { runtime } = createConcreteRuntimeHarness({
|
|
async sendChunk() {
|
|
throw new TelegramApiCommitUnknownError(
|
|
"sendMessage",
|
|
new TypeError("fetch failed"),
|
|
);
|
|
},
|
|
});
|
|
const result = await runtime.sendView(
|
|
{ text: "hello" },
|
|
{ scope: { kind: "target", target } },
|
|
);
|
|
assert.deepEqual(result, {
|
|
ok: false,
|
|
reason: "commit-unknown",
|
|
message: "Telegram delivery send may have committed before transport failed.",
|
|
});
|
|
});
|
|
|
|
test("Concrete delivery runtime returns a recoverable handle after partial send failure", async () => {
|
|
let sendCount = 0;
|
|
const deleted: number[] = [];
|
|
const { runtime } = createConcreteRuntimeHarness({
|
|
async sendChunk() {
|
|
sendCount += 1;
|
|
if (sendCount === 2) throw new Error("second chunk failed");
|
|
return 101;
|
|
},
|
|
async deleteMessage(_deliveryTarget, messageId) {
|
|
deleted.push(messageId);
|
|
},
|
|
});
|
|
|
|
const result = await runtime.sendView(
|
|
{ text: "one|two" },
|
|
{ scope: { kind: "instance" } },
|
|
);
|
|
assert.equal(result.ok, false);
|
|
if (result.ok) return;
|
|
assert.equal(result.reason, "transport-failed");
|
|
assert.deepEqual(result.partial?.messageIds, [101]);
|
|
assert.equal(result.partial?.generation, "generation-one");
|
|
assert.deepEqual(result.partial?.target, target);
|
|
assert.ok(result.partial);
|
|
const cleanup = await runtime.deleteView(result.partial);
|
|
assert.equal(cleanup.ok, true);
|
|
assert.deepEqual(deleted, [101]);
|
|
});
|
|
|
|
test("Concrete delivery runtime returns all visible ids after edit growth failure", async () => {
|
|
let growthSendCount = 0;
|
|
const deleted: number[] = [];
|
|
const { runtime } = createConcreteRuntimeHarness({
|
|
async sendChunk() {
|
|
growthSendCount += 1;
|
|
if (growthSendCount === 3) throw new Error("growth chunk failed");
|
|
return growthSendCount === 1 ? 11 : 12;
|
|
},
|
|
async deleteMessage(_deliveryTarget, messageId) {
|
|
deleted.push(messageId);
|
|
},
|
|
});
|
|
const initial = await runtime.sendView(
|
|
{ text: "one" },
|
|
{ scope: { kind: "instance" } },
|
|
);
|
|
assert.equal(initial.ok, true);
|
|
if (!initial.ok) return;
|
|
const result = await runtime.editView(initial.value, {
|
|
text: "one|two|three",
|
|
});
|
|
assert.equal(result.ok, false);
|
|
if (result.ok) return;
|
|
assert.equal(result.reason, "transport-failed");
|
|
assert.deepEqual(result.partial?.messageIds, [11, 12]);
|
|
assert.ok(result.partial);
|
|
const cleanup = await runtime.deleteView(result.partial);
|
|
assert.equal(cleanup.ok, true);
|
|
assert.deepEqual(deleted, [11, 12]);
|
|
});
|
|
|
|
test("Concrete delivery runtime removes already-deleted ids from partial edit recovery", async () => {
|
|
const deleteCalls: number[] = [];
|
|
let failedOnce = false;
|
|
const { runtime } = createConcreteRuntimeHarness({
|
|
async deleteMessage(_deliveryTarget, messageId) {
|
|
deleteCalls.push(messageId);
|
|
if (messageId === 103 && !failedOnce) {
|
|
failedOnce = true;
|
|
throw new Error("shrink delete failed");
|
|
}
|
|
},
|
|
});
|
|
const initial = await runtime.sendView(
|
|
{ text: "one|two|three" },
|
|
{ scope: { kind: "instance" } },
|
|
);
|
|
assert.equal(initial.ok, true);
|
|
if (!initial.ok) return;
|
|
const result = await runtime.editView(initial.value, { text: "one" });
|
|
assert.equal(result.ok, false);
|
|
if (result.ok) return;
|
|
assert.deepEqual(result.partial?.messageIds, [101, 103]);
|
|
assert.ok(result.partial);
|
|
const cleanup = await runtime.deleteView(result.partial);
|
|
assert.equal(cleanup.ok, true);
|
|
assert.deepEqual(deleteCalls, [102, 103, 101, 103]);
|
|
});
|
|
|
|
test("Concrete delivery runtime reconciles logical view growth and shrink", async () => {
|
|
const { runtime, events } = createConcreteRuntimeHarness();
|
|
const initial = await runtime.sendView(
|
|
{ text: "one" },
|
|
{ scope: { kind: "instance" } },
|
|
);
|
|
assert.equal(initial.ok, true);
|
|
if (!initial.ok) return;
|
|
events.length = 0;
|
|
const grown = await runtime.editView(initial.value, { text: "one|two" });
|
|
assert.equal(grown.ok, true);
|
|
assert.deepEqual(
|
|
events.map((event) => event.type),
|
|
["edit", "send"],
|
|
);
|
|
assert.deepEqual(events[0]?.options, { replyMarkup: null });
|
|
if (!grown.ok) return;
|
|
events.length = 0;
|
|
const shrunk = await runtime.editView(grown.value, { text: "one" });
|
|
assert.equal(shrunk.ok, true);
|
|
assert.deepEqual(
|
|
events.map((event) => event.type),
|
|
["edit", "delete"],
|
|
);
|
|
assert.deepEqual(events[0]?.options, { replyMarkup: null });
|
|
});
|
|
|
|
test("Concrete delivery runtime serializes operations per target", async () => {
|
|
let releaseFirst: (() => void) | undefined;
|
|
const order: string[] = [];
|
|
const { runtime } = createConcreteRuntimeHarness({
|
|
async sendChunk() {
|
|
order.push("first-start");
|
|
await new Promise<void>((resolve) => {
|
|
releaseFirst = resolve;
|
|
});
|
|
order.push("first-end");
|
|
return 201;
|
|
},
|
|
async sendChatAction() {
|
|
order.push("action");
|
|
},
|
|
});
|
|
const sending = runtime.sendView(
|
|
{ text: "one" },
|
|
{ scope: { kind: "instance" } },
|
|
);
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
const action = runtime.sendChatAction("typing", { kind: "instance" });
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
assert.deepEqual(order, ["first-start"]);
|
|
releaseFirst?.();
|
|
await Promise.all([sending, action]);
|
|
assert.deepEqual(order, ["first-start", "first-end", "action"]);
|
|
});
|