fix(release): repair beta.2 plugin prerelease shards (#121946)

* fix(release): repair beta.2 plugin prerelease shards

Restore coordinator-owned approval ambiguity by removing the selector-level channel guard.

Align release-only fixtures with privacy-safe progress, canonical Telegram targets and grammY updates, production-shaped auth snapshots, and the real Codex message schema.

* test(approvals): document out-of-band route ownership

Native approval clients are deliberate out-of-band surfaces; configured approvers plus Gateway account custody are the boundary. The regression proves the sole eligible cross-channel account and fails with the candidate-only guard.
This commit is contained in:
Peter Steinberger
2026-08-11 02:39:31 -07:00
committed by GitHub
parent 0a848362fd
commit f658c09a05
18 changed files with 168 additions and 137 deletions
@@ -2095,35 +2095,28 @@ describe("Codex app-server dynamic tool build", () => {
).toBe(false);
});
it("exposes the final delivery control only on Codex message-tool-only schemas", async () => {
it("preserves the core final delivery control only on message-tool-only schemas", async () => {
const workspaceDir = path.join(tempDir, "workspace");
const params = createParams(path.join(tempDir, "session.jsonl"), workspaceDir);
params.disableTools = false;
params.runtimePlan = createCodexRuntimePlanFixture();
// Mirror production createOpenClawCodingTools: attempt-fresh tool instances
// per build, never a shared object reused across delivery modes.
setOpenClawCodingToolsFactoryForTests(() => [
{
...createRuntimeDynamicTool("message"),
parameters: {
type: "object",
properties: { message: { type: "string" } },
additionalProperties: false,
},
},
]);
setOpenClawCodingToolsFactoryForTests((options) =>
createOpenClawCodingTools(options).filter((tool) => tool.name === "message"),
);
params.sourceReplyDeliveryMode = "message_tool_only";
const sourceReplyTools = await buildDynamicToolsForTest(params, workspaceDir);
const sourceReplySchema = sourceReplyTools[0]?.parameters as {
properties?: Record<string, unknown>;
additionalProperties?: unknown;
};
expect(sourceReplySchema.properties).toMatchObject({
final: { type: "boolean" },
final: {
type: "boolean",
description:
"Set false for progress. Set true, or omit, for the completed current-source reply.",
},
});
expect(sourceReplySchema.additionalProperties).toBe(false);
params.sourceReplyDeliveryMode = "automatic";
const automaticTools = await buildDynamicToolsForTest(params, workspaceDir);
@@ -376,7 +376,7 @@ describe("CodexAppServerEventProjector native tool finalization", () => {
expect(toolResult.status).toBe("completed");
expect(toolResult.isError).toBe(false);
expect(onToolResult).toHaveBeenCalledWith({
text: "🛠️ `run tests (workspace)`",
text: "🛠️ Bash",
});
expect(trajectoryRecorder.recordEvent).toHaveBeenCalledWith("tool.call", {
threadId: THREAD_ID,
@@ -23,7 +23,7 @@ describe("CodexAppServerEventProjector tool progress echo filtering", () => {
const onToolResult = vi.fn();
const projector = await createProjector({
...(await createParams()),
verboseLevel: "on",
verboseLevel: "full",
onToolResult,
});
@@ -71,7 +71,7 @@ describe("CodexAppServerEventProjector tool progress echo filtering", () => {
const onToolResult = vi.fn();
const projector = await createProjector({
...(await createParams()),
verboseLevel: "on",
verboseLevel: "full",
onToolResult,
});
const command = "pnpm test";
@@ -327,7 +327,7 @@ describe("CodexAppServerEventProjector replay safety and progress projection", (
const onToolResult = vi.fn();
const projector = await createProjector({
...(await createParams()),
verboseLevel: "on",
verboseLevel: "full",
onAgentEvent,
onToolResult,
});
@@ -1037,7 +1037,7 @@ describe("Codex app-server native code mode config", () => {
expect(instructions).toContain("## Skill Workshop");
expect(instructions).toContain("Durable reusable skill/playbook/workflow work");
expect(instructions).toContain("`skill_workshop`");
expect(instructions).toContain("Generated = pending proposal");
expect(instructions).toContain("Other generated work = pending proposal");
expect(instructions).toContain("only explicit user ask");
});
@@ -1072,7 +1072,7 @@ describe("Codex app-server native code mode config", () => {
});
expect(instructions).toContain("For progress, set `final=false`.");
expect(instructions).toContain("set `final=true`");
expect(instructions).toContain("Set `final=true`");
});
it("keeps durable dynamic tool fingerprints scoped to loading mode", () => {
@@ -2780,7 +2780,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
domain: "feishu",
config: {
renderMode: "card",
streaming: { mode: "partial" },
streaming: { mode: "partial", preview: { commandText: "raw" } },
},
});
@@ -3414,7 +3414,10 @@ describe("matrix monitor handler draft streaming", () => {
streaming: "progress",
previewToolProgressEnabled: true,
accountConfig: {
streaming: { mode: "progress", progress: { label: "Working" } },
streaming: {
mode: "progress",
progress: { label: "Working", commandText: "raw" },
},
} as never,
});
const { opts, finish } = await dispatch();
@@ -1879,7 +1879,10 @@ describe("mattermost inbound user posts", () => {
chatmode: "onmessage",
dmPolicy: "open",
groupPolicy: "open",
streaming: { mode: "block", preview: { toolProgress: true } },
streaming: {
mode: "block",
preview: { toolProgress: true, commandText: "raw" },
},
},
},
};
@@ -311,11 +311,17 @@ describe("memory-lancedb provider lifecycle", () => {
},
...Object.values(agentDirs).map((agentDir) => ({
agentDir,
store: { version: 1, profiles: {} },
store: {
version: 1,
profiles: {
[profileId]: { type: "api_key" as const, provider: "openai", key: credential },
},
runtimeLocalProfileIds: [],
},
})),
]);
};
const closeProvider = vi.fn(async () => {});
const closedProviderKeys: string[] = [];
const createProvider = vi.fn(async (options: { agentDir?: string }) => {
const agentDir = expectDefined(options.agentDir, "inherited agent owner");
const profile = ensureAuthProfileStore(agentDir, {
@@ -336,7 +342,9 @@ describe("memory-lancedb provider lifecycle", () => {
return [0.1, 0.2, 0.3];
}),
embedBatch: vi.fn(async () => [[0.1, 0.2, 0.3]]),
close: closeProvider,
close: vi.fn(async () => {
closedProviderKeys.push(`${agentDir}:${credential}`);
}),
},
};
});
@@ -369,7 +377,19 @@ describe("memory-lancedb provider lifecycle", () => {
]);
expect(createProvider).toHaveBeenCalledTimes(4);
expect(closeProvider).toHaveBeenCalledTimes(2);
expect(closedProviderKeys).toHaveLength(2);
expect(closedProviderKeys).toEqual(
expect.arrayContaining([
`${agentDirs.private}:fixture-inherited-old`,
`${agentDirs.secondary}:fixture-inherited-old`,
]),
);
expect(closedProviderKeys).not.toEqual(
expect.arrayContaining([
`${agentDirs.private}:fixture-inherited-new`,
`${agentDirs.secondary}:fixture-inherited-new`,
]),
);
expect(requests).toEqual(
expect.arrayContaining([
{
@@ -398,6 +418,15 @@ describe("memory-lancedb provider lifecycle", () => {
await embeddings.close?.();
clearRuntimeAuthProfileStoreSnapshots();
}
expect(closedProviderKeys.toSorted()).toEqual(
[
`${agentDirs.private}:fixture-inherited-old`,
`${agentDirs.private}:fixture-inherited-new`,
`${agentDirs.secondary}:fixture-inherited-old`,
`${agentDirs.secondary}:fixture-inherited-new`,
].toSorted(),
);
});
it("retires cached agent providers and fails closed after runtime config replacement", async () => {
+2 -2
View File
@@ -239,7 +239,7 @@ describe("slack exec approvals", () => {
).toBe(false);
});
it("rejects requests bound to another channel or Slack account", () => {
it("reports a foreign-channel candidate but rejects another Slack account", () => {
const cfg = buildConfig({
enabled: true,
approvers: ["U123"],
@@ -261,7 +261,7 @@ describe("slack exec approvals", () => {
expiresAtMs: 1000,
},
}),
).toBe(false);
).toBe(true);
expect(
slackApprovalCapability.nativeRuntime?.availability.shouldHandle({
@@ -586,7 +586,7 @@ describeTelegramDispatch("dispatchTelegramMessage context-recovery", () => {
const outbound = expectRecordFields(mockCallArg(deliverInboundReplyWithMessageSendContext), {
threadId: 1,
to: "-100555",
to: "telegram:-100555",
});
expectRecordFields(outbound.ctxPayload, {
From: "telegram:group:-100555:topic:1",
@@ -77,7 +77,7 @@ describeTelegramDispatch("dispatchTelegramMessage delivery-basics", () => {
const outbound = expectRecordFields(mockCallArg(deliverInboundReplyWithMessageSendContext), {
channel: "telegram",
to: "123",
to: "telegram:123",
accountId: "default",
info: { kind: "final" },
replyToMode: "first",
@@ -512,7 +512,12 @@ describeTelegramDispatch("dispatchTelegramMessage draft-failures-progress", () =
await dispatchWithContext({
context: createContext(),
streamMode: "progress",
telegramCfg: { streaming: { mode: "progress", progress: { label: "Cracking" } } },
telegramCfg: {
streaming: {
mode: "progress",
progress: { label: "Cracking", commandText: "raw" },
},
},
});
expect(answerDraftStream.updatePreview).toHaveBeenCalledWith(
+32 -38
View File
@@ -584,6 +584,8 @@ async function waitForApiMiddleware(
type TestTelegramUpdate = {
update_id: number;
message: {
message_id: number;
date: number;
text: string;
chat: { id: number; type: "private" | "supergroup"; is_forum?: boolean };
message_thread_id?: number;
@@ -595,6 +597,8 @@ function topicUpdate(updateId: number, threadId: number, text: string): TestTele
return {
update_id: updateId,
message: {
message_id: updateId,
date: 1_700_000_000 + updateId,
text,
message_thread_id: threadId,
is_topic_message: true,
@@ -607,6 +611,8 @@ function directUpdate(updateId: number, chatId: number, text: string): TestTeleg
return {
update_id: updateId,
message: {
message_id: updateId,
date: 1_700_000_000 + updateId,
text,
chat: { id: chatId, type: chatId < 0 ? "supergroup" : "private" },
},
@@ -1290,9 +1296,10 @@ describe("TelegramPollingSession", () => {
const abort = new AbortController();
const handleUpdate = vi.fn(async () => undefined);
const init = vi.fn(async () => undefined);
const update = directUpdate(42, 123, "hello");
await writeTelegramSpooledUpdate({
spoolDir: tempDir,
update: { update_id: 42, message: { text: "hello" } },
update,
});
const { createWorker, runPromise } = startIsolatedIngressSession({
@@ -1330,7 +1337,7 @@ describe("TelegramPollingSession", () => {
persistenceFloorUpdateId: null,
});
expect(init).toHaveBeenCalledBefore(handleUpdate);
expect(handleUpdate).toHaveBeenCalledWith({ update_id: 42, message: { text: "hello" } });
expect(handleUpdate).toHaveBeenCalledWith(update);
});
});
@@ -1345,12 +1352,13 @@ describe("TelegramPollingSession", () => {
handleUpdate,
createWorker: worker.createWorker,
});
const update = directUpdate(42, 123, "hello");
try {
await waitForTelegramTestState(() => expect(worker.hasListener()).toBe(true));
worker.emit({
type: "update",
requestId: "write-1",
update: { update_id: 42, message: { text: "hello" } },
update,
queued: 1,
});
await waitForTelegramTestState(() =>
@@ -1359,9 +1367,7 @@ describe("TelegramPollingSession", () => {
updateId: 42,
}),
);
await waitForTelegramTestState(() =>
expect(handleUpdate).toHaveBeenCalledWith({ update_id: 42, message: { text: "hello" } }),
);
await waitForTelegramTestState(() => expect(handleUpdate).toHaveBeenCalledWith(update));
await waitForTelegramTestState(async () =>
expect(await pendingUpdateIds(tempDir, "all")).toEqual([]),
);
@@ -1423,19 +1429,14 @@ describe("TelegramPollingSession", () => {
},
});
const runPromise = session.runUntilAbort();
const update = directUpdate(143, 1234, "installed bot capability snapshot");
update.message.message_thread_id = 42;
try {
await waitForTelegramTestState(() => expect(worker.hasListener()).toBe(true));
worker.emit({
type: "update",
requestId: "topic-capability-1",
update: {
update_id: 143,
message: {
chat: { id: 1234, type: "private" },
message_thread_id: 42,
text: "installed bot capability snapshot",
},
},
update,
queued: 1,
});
await waitForTelegramTestState(() =>
@@ -1487,7 +1488,7 @@ describe("TelegramPollingSession", () => {
worker.emit({
type: "update",
requestId: "offset-gap",
update: { update_id: 42, message: { text: "hello" } },
update: directUpdate(42, 123, "hello"),
queued: 1,
});
await waitForTelegramTestState(() =>
@@ -1528,7 +1529,7 @@ describe("TelegramPollingSession", () => {
worker.emit({
type: "update",
requestId: "offset-failure",
update: { update_id: 43, message: { text: "hello" } },
update: directUpdate(43, 123, "hello"),
queued: 1,
});
await waitForTelegramTestState(() =>
@@ -1566,7 +1567,7 @@ describe("TelegramPollingSession", () => {
worker.emit({
type: "update",
requestId: "offset-catching-up",
update: { update_id: 44, message: { text: "hello" } },
update: directUpdate(44, 123, "hello"),
queued: 1,
});
await waitForTelegramTestState(() =>
@@ -1614,7 +1615,7 @@ describe("TelegramPollingSession", () => {
firstWorker.emit({
type: "update",
requestId: "first-delivery",
update: { update_id: 42, message: { text: "hello" } },
update: directUpdate(42, 123, "hello"),
queued: 1,
});
await waitForTelegramTestState(() =>
@@ -1661,7 +1662,7 @@ describe("TelegramPollingSession", () => {
restartWorker.emit({
type: "update",
requestId: "restart-replay",
update: { update_id: 42, message: { text: "hello" } },
update: directUpdate(42, 123, "hello"),
queued: 1,
});
await waitForTelegramTestState(() =>
@@ -1726,12 +1727,13 @@ describe("TelegramPollingSession", () => {
createWorker: worker.createWorker,
drainIntervalMs: 60_000,
});
const update = directUpdate(42, 123, "hello");
try {
await waitForTelegramTestState(() => expect(worker.hasListener()).toBe(true));
worker.emit({
type: "update",
requestId: "write-1",
update: { update_id: 42, message: { text: "hello" } },
update,
queued: 1,
});
await waitForTelegramTestState(() =>
@@ -1741,9 +1743,7 @@ describe("TelegramPollingSession", () => {
}),
);
worker.emit({ type: "spooled", updateId: 42, queued: 1 });
await waitForTelegramTestState(() =>
expect(handleUpdate).toHaveBeenCalledWith({ update_id: 42, message: { text: "hello" } }),
);
await waitForTelegramTestState(() => expect(handleUpdate).toHaveBeenCalledWith(update));
await waitForTelegramTestState(async () =>
expect(await pendingUpdateIds(tempDir, "all")).toEqual([]),
);
@@ -1788,9 +1788,11 @@ describe("TelegramPollingSession", () => {
},
} as TelegramRuntime);
const preSeededUpdate = directUpdate(1, 123, "pre-seeded");
const duringDrainUpdate = directUpdate(2, 123, "during-drain");
await writeTelegramSpooledUpdate({
spoolDir: tempDir,
update: { update_id: 1, message: { text: "pre-seeded" } },
update: preSeededUpdate,
});
const handleUpdate = vi.fn(async () => undefined);
const worker = createListeningIngressWorker();
@@ -1809,7 +1811,7 @@ describe("TelegramPollingSession", () => {
worker.emit({
type: "update",
requestId: "write-2",
update: { update_id: 2, message: { text: "during-drain" } },
update: duringDrainUpdate,
queued: 1,
});
expect(worker.ackSpooledUpdate).not.toHaveBeenCalledWith("write-2", expect.anything());
@@ -1824,16 +1826,10 @@ describe("TelegramPollingSession", () => {
worker.emit({ type: "spooled", updateId: 2, queued: 1 });
await waitForTelegramTestState(() =>
expect(handleUpdate).toHaveBeenCalledWith({
update_id: 1,
message: { text: "pre-seeded" },
}),
expect(handleUpdate).toHaveBeenCalledWith(preSeededUpdate),
);
await waitForTelegramTestState(() =>
expect(handleUpdate).toHaveBeenCalledWith({
update_id: 2,
message: { text: "during-drain" },
}),
expect(handleUpdate).toHaveBeenCalledWith(duringDrainUpdate),
);
await waitForTelegramTestState(async () =>
expect(await pendingUpdateIds(tempDir, "all")).toEqual([]),
@@ -1850,9 +1846,10 @@ describe("TelegramPollingSession", () => {
await withTempSpool(async (tempDir) => {
const abort = new AbortController();
const handleUpdate = vi.fn(async () => undefined);
const update = directUpdate(42, 123, "pre-upgrade pending");
await writeTelegramSpooledUpdate({
spoolDir: tempDir,
update: { update_id: 42, message: { text: "pre-upgrade pending" } },
update,
});
const { createWorker, runPromise } = startIsolatedIngressSession({
@@ -1883,10 +1880,7 @@ describe("TelegramPollingSession", () => {
lastUpdateId: null,
persistenceFloorUpdateId: 42,
});
expect(handleUpdate).toHaveBeenCalledWith({
update_id: 42,
message: { text: "pre-upgrade pending" },
});
expect(handleUpdate).toHaveBeenCalledWith(update);
});
});
@@ -5,13 +5,16 @@ import { describe, expect, it } from "vitest";
import { renderTelegramProgressDraftPreview } from "./progress-draft-preview.js";
function renderToolLine(name: string) {
const line = buildChannelProgressDraftLine({
event: "tool",
toolCallId: "call-1",
name,
phase: "start",
args: { command: "echo alpha", description: "print text" },
});
const line = buildChannelProgressDraftLine(
{
event: "tool",
toolCallId: "call-1",
name,
phase: "start",
args: { command: "echo alpha", description: "print text" },
},
{ commandText: "raw" },
);
if (!line) {
throw new Error(`expected a progress line for ${name}`);
}
+54 -51
View File
@@ -191,6 +191,26 @@ function requireWebhookSpoolDir(): string {
return webhookSpoolDir;
}
function telegramTextUpdate(
updateId: number,
text: string,
options: { chatId?: number; messageThreadId?: number } = {},
) {
const chatId = options.chatId ?? 1234;
return {
update_id: updateId,
message: {
message_id: updateId,
date: 1_700_000_000 + updateId,
chat: { id: chatId, type: chatId < 0 ? ("supergroup" as const) : ("private" as const) },
...(options.messageThreadId === undefined
? {}
: { message_thread_id: options.messageThreadId }),
text,
},
};
}
function createTelegramPrivateTopicCallback(updateId: number) {
return {
id: `callback-${updateId}`,
@@ -486,16 +506,13 @@ async function postWebhookPayloadWithChunkPlan(params: {
function createNearLimitTelegramPayload(): { payload: string; sizeBytes: number } {
const maxBytes = 1_024 * 1_024;
const targetBytes = maxBytes - 4_096;
const shell = { update_id: 77_777, message: { text: "" } };
const shell = telegramTextUpdate(77_777, "");
const shellSize = Buffer.byteLength(JSON.stringify(shell), "utf-8");
const textLength = Math.max(1, targetBytes - shellSize);
const pattern = "the quick brown fox jumps over the lazy dog ";
const repeats = Math.ceil(textLength / pattern.length);
const text = pattern.repeat(repeats).slice(0, textLength);
const payload = JSON.stringify({
update_id: 77_777,
message: { text },
});
const payload = JSON.stringify(telegramTextUpdate(77_777, text));
return { payload, sizeBytes: Buffer.byteLength(payload, "utf-8") };
}
@@ -1044,7 +1061,7 @@ describe("startTelegramWebhook", () => {
);
expect(botParams.accountId).toBe("opie");
expect(requireRecord(botParams.config, "telegram config").bindings).toEqual([]);
const payload = JSON.stringify({ update_id: 1, message: { text: "hello" } });
const payload = JSON.stringify(telegramTextUpdate(1, "hello"));
const response = await postWebhookJson({
url: webhookUrl(port, TELEGRAM_WEBHOOK_PATH),
payload,
@@ -1061,8 +1078,9 @@ describe("startTelegramWebhook", () => {
let finishWork: (() => void) | undefined;
let workStarted = false;
let workFinished = false;
handleUpdateSpy.mockImplementationOnce(async (update: unknown) => {
expect(update).toEqual({ update_id: 2, message: { text: "slow" } });
const update = telegramTextUpdate(2, "slow");
handleUpdateSpy.mockImplementationOnce(async (receivedUpdate: unknown) => {
expect(receivedUpdate).toEqual(update);
workStarted = true;
await new Promise<void>((resolve) => {
finishWork = resolve;
@@ -1078,7 +1096,7 @@ describe("startTelegramWebhook", () => {
async ({ port }) => {
const response = await postWebhookJson({
url: webhookUrl(port, TELEGRAM_WEBHOOK_PATH),
payload: JSON.stringify({ update_id: 2, message: { text: "slow" } }),
payload: JSON.stringify(update),
secret: TELEGRAM_SECRET,
timeoutMs: 1_000,
});
@@ -1115,7 +1133,7 @@ describe("startTelegramWebhook", () => {
try {
const response = await postWebhookJson({
url: webhookUrl(getServerPort(started.server), TELEGRAM_WEBHOOK_PATH),
payload: JSON.stringify({ update_id: 3, message: { text: "stuck" } }),
payload: JSON.stringify(telegramTextUpdate(3, "stuck")),
secret: TELEGRAM_SECRET,
});
expect(response.status).toBe(200);
@@ -1195,7 +1213,7 @@ describe("startTelegramWebhook", () => {
let responseSettled = false;
const responseTask = postWebhookJson({
url: webhookUrl(port, TELEGRAM_WEBHOOK_PATH),
payload: JSON.stringify({ update_id: 4, message: { text: "commit gate" } }),
payload: JSON.stringify(telegramTextUpdate(4, "commit gate")),
secret: TELEGRAM_SECRET,
}).then((response) => {
responseSettled = true;
@@ -1229,7 +1247,7 @@ describe("startTelegramWebhook", () => {
throw new Error("agent turn failed");
}
});
const payload = JSON.stringify({ update_id: 3, message: { text: "boom" } });
const payload = JSON.stringify(telegramTextUpdate(3, "boom"));
try {
await withStartedWebhook(
@@ -1295,7 +1313,7 @@ describe("startTelegramWebhook", () => {
} = {};
await writeTelegramSpooledUpdate({
spoolDir: requireWebhookSpoolDir(),
update: { update_id: 39, message: { chat: { id: 123 }, text: "stalled" } },
update: telegramTextUpdate(39, "stalled", { chatId: 123 }),
});
handleUpdateSpy.mockImplementationOnce(async () => {
active.dispatchStartedAt = Date.now();
@@ -1337,8 +1355,8 @@ describe("startTelegramWebhook", () => {
try {
let finishFirstUpdate: (() => void) | undefined;
const seenUpdateIds: number[] = [];
const firstUpdate = { update_id: 40, message: { chat: { id: 123 }, text: "slow" } };
const secondUpdate = { update_id: 41, message: { chat: { id: 123 }, text: "blocked" } };
const firstUpdate = telegramTextUpdate(40, "slow", { chatId: 123 });
const secondUpdate = telegramTextUpdate(41, "blocked", { chatId: 123 });
await writeTelegramSpooledUpdate({
spoolDir: requireWebhookSpoolDir(),
update: firstUpdate,
@@ -1384,7 +1402,7 @@ describe("startTelegramWebhook", () => {
it("holds buffered timeout settlement behind durable webhook adoption", async () => {
vi.useFakeTimers({ toFake: ["Date", "setTimeout", "clearTimeout"] });
try {
const update = { update_id: 42, message: { chat: { id: 123 }, text: "held adoption" } };
const update = telegramTextUpdate(42, "held adoption", { chatId: 123 });
await writeTelegramSpooledUpdate({
spoolDir: requireWebhookSpoolDir(),
update,
@@ -1435,7 +1453,7 @@ describe("startTelegramWebhook", () => {
});
it("drains spooled webhook updates left by a previous process on startup", async () => {
const update = { update_id: 30, message: { text: "leftover" } };
const update = telegramTextUpdate(30, "leftover");
await writeTelegramSpooledUpdate({
spoolDir: requireWebhookSpoolDir(),
update,
@@ -2185,8 +2203,8 @@ describe("startTelegramWebhook", () => {
},
},
} as TelegramRuntime);
const firstUpdate = { update_id: 50, message: { chat: { id: 123 }, text: "first" } };
const secondUpdate = { update_id: 51, message: { chat: { id: 123 }, text: "second" } };
const firstUpdate = telegramTextUpdate(50, "first", { chatId: 123 });
const secondUpdate = telegramTextUpdate(51, "second", { chatId: 123 });
await writeTelegramSpooledUpdate({
spoolDir: requireWebhookSpoolDir(),
update: firstUpdate,
@@ -2264,7 +2282,7 @@ describe("startTelegramWebhook", () => {
} as unknown as TelegramRuntime);
await writeTelegramSpooledUpdate({
spoolDir: requireWebhookSpoolDir(),
update: { update_id: 52, message: { chat: { id: 123 }, text: "stop retry" } },
update: telegramTextUpdate(52, "stop retry", { chatId: 123 }),
});
const runtimeLog = vi.fn();
const started = await startTelegramWebhook({
@@ -2294,7 +2312,7 @@ describe("startTelegramWebhook", () => {
try {
vi.setSystemTime(10_000_000);
const runtimeLog = vi.fn();
const update = { update_id: 31, message: { text: "young poison" } };
const update = telegramTextUpdate(31, "young poison");
await writeTelegramSpooledUpdate({
spoolDir: requireWebhookSpoolDir(),
update,
@@ -2334,7 +2352,7 @@ describe("startTelegramWebhook", () => {
try {
vi.setSystemTime(10_000_000);
const runtimeLog = vi.fn();
const update = { update_id: 32, message: { text: "old poison" } };
const update = telegramTextUpdate(32, "old poison");
await writeTelegramSpooledUpdate({
spoolDir: requireWebhookSpoolDir(),
update,
@@ -2426,7 +2444,7 @@ describe("startTelegramWebhook", () => {
for (let i = 0; i < TELEGRAM_WEBHOOK_RATE_LIMIT_BURST; i += 1) {
const response = await postWebhookJson({
url: webhookUrl(port, TELEGRAM_WEBHOOK_PATH),
payload: JSON.stringify({ update_id: i, message: { text: `guess ${i}` } }),
payload: JSON.stringify(telegramTextUpdate(i, `guess ${i}`)),
secret: `wrong-secret-${String(i).padStart(3, "0")}`,
});
@@ -2446,7 +2464,7 @@ describe("startTelegramWebhook", () => {
const validResponse = await postWebhookJson({
url: webhookUrl(port, TELEGRAM_WEBHOOK_PATH),
payload: JSON.stringify({ update_id: 999, message: { text: "hello" } }),
payload: JSON.stringify(telegramTextUpdate(999, "hello")),
secret: TELEGRAM_SECRET,
});
expect(validResponse.status).toBe(200);
@@ -2468,7 +2486,7 @@ describe("startTelegramWebhook", () => {
for (let i = 0; i < TELEGRAM_WEBHOOK_RATE_LIMIT_BURST; i += 1) {
const response = await postWebhookJson({
url: webhookUrl(port, TELEGRAM_WEBHOOK_PATH),
payload: JSON.stringify({ update_id: 10_000 + i, message: { text: `valid ${i}` } }),
payload: JSON.stringify(telegramTextUpdate(10_000 + i, `valid ${i}`)),
secret: TELEGRAM_SECRET,
});
expect(response.status).toBe(200);
@@ -2501,7 +2519,7 @@ describe("startTelegramWebhook", () => {
"x-forwarded-for": "198.51.100.10",
"x-telegram-bot-api-secret-token": `wrong-secret-${String(i).padStart(3, "0")}`,
},
body: JSON.stringify({ update_id: i, message: { text: `guess ${i}` } }),
body: JSON.stringify(telegramTextUpdate(i, `guess ${i}`)),
},
5_000,
);
@@ -2520,7 +2538,7 @@ describe("startTelegramWebhook", () => {
"x-forwarded-for": "203.0.113.20",
"x-telegram-bot-api-secret-token": TELEGRAM_SECRET,
},
body: JSON.stringify({ update_id: 201, message: { text: "hello" } }),
body: JSON.stringify(telegramTextUpdate(201, "hello")),
},
5_000,
);
@@ -2559,7 +2577,7 @@ describe("startTelegramWebhook", () => {
for (let i = 0; i < TELEGRAM_WEBHOOK_RATE_LIMIT_BURST; i += 1) {
const response = await postWebhookJson({
url: webhookUrl(firstPort, TELEGRAM_WEBHOOK_PATH),
payload: JSON.stringify({ update_id: i, message: { text: `guess ${i}` } }),
payload: JSON.stringify(telegramTextUpdate(i, `guess ${i}`)),
secret: `wrong-secret-${String(i).padStart(3, "0")}`,
});
if (response.status === 429) {
@@ -2569,7 +2587,7 @@ describe("startTelegramWebhook", () => {
const secondResponse = await postWebhookJson({
url: webhookUrl(secondPort, TELEGRAM_WEBHOOK_PATH),
payload: JSON.stringify({ update_id: 301, message: { text: "hello" } }),
payload: JSON.stringify(telegramTextUpdate(301, "hello")),
secret: TELEGRAM_SECRET,
});
@@ -2628,7 +2646,7 @@ describe("startTelegramWebhook", () => {
path: TELEGRAM_WEBHOOK_PATH,
},
async ({ port }) => {
const payload = JSON.stringify({ update_id: 1, message: { text: "hello" } });
const payload = JSON.stringify(telegramTextUpdate(1, "hello"));
const res = await postWebhookJson({
url: webhookUrl(port, TELEGRAM_WEBHOOK_PATH),
payload,
@@ -2668,23 +2686,8 @@ describe("startTelegramWebhook", () => {
},
async ({ port }) => {
const url = webhookUrl(port, TELEGRAM_WEBHOOK_PATH);
const firstUpdate = {
update_id: 100,
message: {
chat: { id: 1234, type: "private" },
message_id: 1,
text: "first",
},
};
const secondUpdate = {
update_id: 101,
message: {
chat: { id: 1234, type: "private" },
message_id: 2,
message_thread_id: 42,
text: "second",
},
};
const firstUpdate = telegramTextUpdate(100, "first");
const secondUpdate = telegramTextUpdate(101, "second", { messageThreadId: 42 });
try {
const firstResponse = await postWebhookJson({
@@ -2733,8 +2736,8 @@ describe("startTelegramWebhook", () => {
},
async ({ port }) => {
const payloads = [
JSON.stringify({ update_id: 1, message: { text: "first" } }),
JSON.stringify({ update_id: 2, message: { text: "second" } }),
JSON.stringify(telegramTextUpdate(1, "first")),
JSON.stringify(telegramTextUpdate(2, "second")),
];
for (const payload of payloads) {
@@ -2768,8 +2771,8 @@ describe("startTelegramWebhook", () => {
path: TELEGRAM_WEBHOOK_PATH,
},
async ({ port }) => {
const firstPayload = JSON.stringify({ update_id: 100, message: { text: "first" } });
const secondPayload = JSON.stringify({ update_id: 101, message: { text: "second" } });
const firstPayload = JSON.stringify(telegramTextUpdate(100, "first"));
const secondPayload = JSON.stringify(telegramTextUpdate(101, "second"));
const firstResponse = await postWebhookPayloadWithChunkPlan({
port,
path: TELEGRAM_WEBHOOK_PATH,
@@ -250,11 +250,6 @@ export function doesApprovalRequestSelectChannelAccount(params: {
if (!accountId) {
return false;
}
const expectedChannel = normalizeOptionalChannel(params.channel);
const turnSourceChannel = normalizeOptionalChannel(params.request.request.turnSourceChannel);
if (!expectedChannel || (turnSourceChannel && turnSourceChannel !== expectedChannel)) {
return false;
}
const boundAccountId = resolveApprovalRequestChannelAccountId(params);
if (accountId === normalizeOptionalAccountId(boundAccountId)) {
return true;
@@ -266,6 +261,9 @@ export function doesApprovalRequestSelectChannelAccount(params: {
if (boundAccountId || forwardAccountIds.length > 0) {
return false;
}
// Native approval clients are intentional out-of-band surfaces: a sole eligible account may
// own an unbound request from another source channel. Configured approvers plus Gateway account
// custody are the authorization boundary.
const eligibleAccountIds = params.eligibleAccountIds
.map(normalizeOptionalAccountId)
.filter((candidate): candidate is string => Boolean(candidate));
@@ -67,7 +67,7 @@ const baseRequest: ExecApprovalRequest = {
};
describe("native approval account selection", () => {
it("does not let a conflicting turn-source channel fall through to the sole account", () => {
it("allows the sole eligible out-of-band approval account across channels", () => {
const discordRequest = buildRequest({
turnSourceChannel: "discord",
turnSourceAccountId: "default",
@@ -81,7 +81,7 @@ describe("native approval account selection", () => {
defaultAccountId: "default",
eligibleAccountIds: ["default"],
}),
).toBe(false);
).toBe(true);
});
it("selects only the sole eligible account when no owner is recorded", () => {