mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-13 12:26:05 +00:00
Constraint: Telegram draft previews are mutable until stopped, while OpenClaw block streaming can emit multiple chunks for one assistant message and separate assistant messages around tool calls. Rejected: Finalizing every answer block | same-message chunks would become duplicate standalone Telegram messages before the final payload. Rejected: Exposing full reply payload metadata through the public plugin SDK | Telegram only needs assistant block identity, and broader metadata would make internal dispatch fields a third-party API contract. Rejected: Leaving queued block rotations as FIFO text-only state | delivery hooks can rewrite, cancel, skip, or remove answer text after queueing. Confidence: high Scope-risk: moderate Directive: Keep same-message block chunks in one draft; rotate only at assistant-message/tool-progress boundaries, and expire cancelled, skipped, or non-answer queued blocks without deleting still-pending earlier rotations. Tested: git diff --check origin/main; /Users/alex/PR/projects/openclaw__openclaw/repo/node_modules/oxfmt/bin/oxfmt --check --threads=1 docs/.generated/plugin-sdk-api-baseline.sha256 extensions/telegram/src/bot-message-dispatch.test.ts extensions/telegram/src/bot-message-dispatch.ts extensions/telegram/src/draft-stream.test-helpers.ts extensions/telegram/src/lane-delivery-text-deliverer.ts extensions/telegram/src/lane-delivery.test.ts src/auto-reply/dispatch.test.ts src/auto-reply/dispatch.ts src/auto-reply/reply/before-deliver.test.ts src/auto-reply/reply/dispatch-from-config.test.ts src/auto-reply/reply/dispatch-from-config.ts src/auto-reply/reply/reply-dispatcher.ts src/auto-reply/reply/reply-payload-sending-hook.ts src/plugin-sdk/reply-payload.ts; ./node_modules/.bin/oxlint extensions/telegram/src/bot-message-dispatch.test.ts extensions/telegram/src/bot-message-dispatch.ts extensions/telegram/src/lane-delivery-text-deliverer.ts extensions/telegram/src/lane-delivery.test.ts extensions/telegram/src/draft-stream.test-helpers.ts src/plugin-sdk/reply-payload.ts src/auto-reply/dispatch.ts src/auto-reply/dispatch.test.ts src/auto-reply/reply/before-deliver.test.ts src/auto-reply/reply/dispatch-from-config.ts src/auto-reply/reply/dispatch-from-config.test.ts src/auto-reply/reply/reply-dispatcher.ts src/auto-reply/reply/reply-payload-sending-hook.ts; node --max-old-space-size=8192 --import tsx scripts/generate-plugin-sdk-api-baseline.ts --check; CI=1 NODE_OPTIONS=--max-old-space-size=4096 node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.extensions.test.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/extensions-test.tsbuildinfo; CI=1 NODE_OPTIONS=--max-old-space-size=4096 node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.core.test.non-agents.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/core-test-non-agents.tsbuildinfo; OPENCLAW_HEAVY_CHECK_LOCK_SCOPE=worktree node scripts/run-vitest.mjs extensions/telegram/src/bot-message-dispatch.test.ts; OPENCLAW_HEAVY_CHECK_LOCK_SCOPE=worktree node scripts/run-vitest.mjs extensions/telegram/src/bot-message-dispatch.test.ts extensions/telegram/src/lane-delivery.test.ts extensions/telegram/src/draft-stream.test.ts src/plugin-sdk/reply-payload.test.ts src/plugins/contracts/plugin-sdk-index.test.ts; OPENCLAW_HEAVY_CHECK_LOCK_SCOPE=worktree node scripts/run-vitest.mjs src/auto-reply/reply/before-deliver.test.ts src/auto-reply/dispatch.test.ts; OPENCLAW_HEAVY_CHECK_LOCK_SCOPE=worktree node scripts/run-vitest.mjs src/auto-reply/reply/dispatch-from-config.test.ts -t "forwards payload metadata into onBlockReplyQueued context" Not-tested: Live Telegram bot roundtrip with real credentials. codex review --base origin/main was run twice but did not complete a final verdict after hitting local heavy-check lock, unsupported --runInBand, and .vite-temp EPERM in the review tool sandbox; no actionable P1/P2 finding was emitted before termination.
109 lines
4.1 KiB
TypeScript
109 lines
4.1 KiB
TypeScript
// Telegram helper module supports draft stream helpers behavior.
|
|
import { vi } from "vitest";
|
|
|
|
type TestDraftStream = {
|
|
update: ReturnType<typeof vi.fn<(text: string) => void>>;
|
|
flush: ReturnType<typeof vi.fn<() => Promise<void>>>;
|
|
messageId: ReturnType<typeof vi.fn<() => number | undefined>>;
|
|
visibleSinceMs: ReturnType<typeof vi.fn<() => number | undefined>>;
|
|
previewRevision: ReturnType<typeof vi.fn<() => number>>;
|
|
lastDeliveredText: ReturnType<typeof vi.fn<() => string>>;
|
|
clear: ReturnType<typeof vi.fn<() => Promise<void>>>;
|
|
stop: ReturnType<typeof vi.fn<() => Promise<void>>>;
|
|
discard: ReturnType<typeof vi.fn<() => Promise<void>>>;
|
|
materialize: ReturnType<typeof vi.fn<() => Promise<number | undefined>>>;
|
|
forceNewMessage: ReturnType<typeof vi.fn<() => void>>;
|
|
sendMayHaveLanded: ReturnType<typeof vi.fn<() => boolean>>;
|
|
setMessageId: (value: number | undefined) => void;
|
|
};
|
|
|
|
export function createTestDraftStream(params?: {
|
|
messageId?: number;
|
|
onUpdate?: (text: string) => void;
|
|
onStop?: () => void | Promise<void>;
|
|
onDiscard?: () => void | Promise<void>;
|
|
clearMessageIdOnForceNew?: boolean;
|
|
stopUpdatesOnDiscard?: boolean;
|
|
visibleSinceMs?: number;
|
|
}): TestDraftStream {
|
|
let messageId = params?.messageId;
|
|
let visibleSinceMs = params?.visibleSinceMs;
|
|
let previewRevision = 0;
|
|
let lastDeliveredText = "";
|
|
let stopped = false;
|
|
return {
|
|
update: vi.fn().mockImplementation((text: string) => {
|
|
if (stopped) {
|
|
return;
|
|
}
|
|
previewRevision += 1;
|
|
lastDeliveredText = text.trimEnd();
|
|
params?.onUpdate?.(text);
|
|
}),
|
|
flush: vi.fn().mockResolvedValue(undefined),
|
|
messageId: vi.fn().mockImplementation(() => messageId),
|
|
visibleSinceMs: vi.fn().mockImplementation(() => visibleSinceMs),
|
|
previewRevision: vi.fn().mockImplementation(() => previewRevision),
|
|
lastDeliveredText: vi.fn().mockImplementation(() => lastDeliveredText),
|
|
clear: vi.fn().mockResolvedValue(undefined),
|
|
stop: vi.fn().mockImplementation(async () => {
|
|
await params?.onStop?.();
|
|
}),
|
|
discard: vi.fn().mockImplementation(async () => {
|
|
if (params?.stopUpdatesOnDiscard) {
|
|
stopped = true;
|
|
}
|
|
await params?.onDiscard?.();
|
|
}),
|
|
materialize: vi.fn().mockImplementation(async () => messageId),
|
|
forceNewMessage: vi.fn().mockImplementation(() => {
|
|
stopped = false;
|
|
if (params?.clearMessageIdOnForceNew) {
|
|
messageId = undefined;
|
|
}
|
|
visibleSinceMs = undefined;
|
|
}),
|
|
sendMayHaveLanded: vi.fn().mockReturnValue(false),
|
|
setMessageId: (value: number | undefined) => {
|
|
messageId = value;
|
|
visibleSinceMs = value == null ? undefined : Date.now();
|
|
},
|
|
};
|
|
}
|
|
|
|
export function createSequencedTestDraftStream(startMessageId = 1001): TestDraftStream {
|
|
let activeMessageId: number | undefined;
|
|
let visibleSinceMs: number | undefined;
|
|
let nextMessageId = startMessageId;
|
|
let previewRevision = 0;
|
|
let lastDeliveredText = "";
|
|
return {
|
|
update: vi.fn().mockImplementation((text: string) => {
|
|
if (activeMessageId == null) {
|
|
activeMessageId = nextMessageId++;
|
|
visibleSinceMs = Date.now();
|
|
}
|
|
previewRevision += 1;
|
|
lastDeliveredText = text.trimEnd();
|
|
}),
|
|
flush: vi.fn().mockResolvedValue(undefined),
|
|
messageId: vi.fn().mockImplementation(() => activeMessageId),
|
|
visibleSinceMs: vi.fn().mockImplementation(() => visibleSinceMs),
|
|
previewRevision: vi.fn().mockImplementation(() => previewRevision),
|
|
lastDeliveredText: vi.fn().mockImplementation(() => lastDeliveredText),
|
|
clear: vi.fn().mockResolvedValue(undefined),
|
|
stop: vi.fn().mockResolvedValue(undefined),
|
|
discard: vi.fn().mockResolvedValue(undefined),
|
|
materialize: vi.fn().mockImplementation(async () => activeMessageId),
|
|
forceNewMessage: vi.fn().mockImplementation(() => {
|
|
activeMessageId = undefined;
|
|
visibleSinceMs = undefined;
|
|
}),
|
|
sendMayHaveLanded: vi.fn().mockReturnValue(false),
|
|
setMessageId: (value: number | undefined) => {
|
|
activeMessageId = value;
|
|
visibleSinceMs = value == null ? undefined : Date.now();
|
|
},
|
|
};
|
|
}
|