mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
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:
committed by
GitHub
parent
0a848362fd
commit
f658c09a05
@@ -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 () => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user