mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-13 15:47:28 +00:00
fix(slack): wake interactive reply sessions (#79836)
Merged via squash.
Prepared head SHA: 2bc9182d0f
Co-authored-by: velvet-shark <126378+velvet-shark@users.noreply.github.com>
Co-authored-by: velvet-shark <126378+velvet-shark@users.noreply.github.com>
Reviewed-by: @velvet-shark
This commit is contained in:
committed by
GitHub
parent
95a87f2f21
commit
59326c8e3b
@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Feishu: auto-thread `message(action="send")` replies inside the topic when the active session is group_topic or group_topic_sender, and propagate `replyInThread` through text, card, and media outbound adapters so topic-scoped sessions no longer post at the group root. Fixes #74903. (#77151) Thanks @ai-hpc.
|
- Feishu: auto-thread `message(action="send")` replies inside the topic when the active session is group_topic or group_topic_sender, and propagate `replyInThread` through text, card, and media outbound adapters so topic-scoped sessions no longer post at the group root. Fixes #74903. (#77151) Thanks @ai-hpc.
|
||||||
- WhatsApp: pass routing context into voice-note transcript echo preflight so echoed transcripts can deliver to the originating chat. Fixes #79778. (#79788) Thanks @hclsys.
|
- WhatsApp: pass routing context into voice-note transcript echo preflight so echoed transcripts can deliver to the originating chat. Fixes #79778. (#79788) Thanks @hclsys.
|
||||||
- Cron/failover: classify structured OpenAI-compatible `server_error` payloads as `server_error`, expose that reason in cron state, and let one-shot cron retry policy honor `retryOn: ["server_error"]` without requiring raw `5xx` text. (#45594) Thanks @clovericbot.
|
- Cron/failover: classify structured OpenAI-compatible `server_error` payloads as `server_error`, expose that reason in cron state, and let one-shot cron retry policy honor `retryOn: ["server_error"]` without requiring raw `5xx` text. (#45594) Thanks @clovericbot.
|
||||||
|
- Slack: wake the resolved thread session after interactive reply button/select clicks and carry Slack delivery context through the queued interaction event, so clicks continue the visible conversation. Fixes #79676 and #61502. (#79836) Thanks @velvet-shark, @tianxiaochannel-oss88, and @Saicheg.
|
||||||
|
|
||||||
## 2026.5.9
|
## 2026.5.9
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
1aa92e012261d21474fde2aea8b1724bea369b5bc730e9fb943ff82de73f38de plugin-sdk-api-baseline.json
|
fce2653618d6a41bd46fd4503b66f8ad912b9abee111cda24661ea1629c293b2 plugin-sdk-api-baseline.json
|
||||||
f076fb0cc1d09b5d50502741428e88a52725d73db950745801ae4fe782d709e2 plugin-sdk-api-baseline.jsonl
|
5d73209fe60e9ab89296242d1921d43d6b385d113155c17d31bf0bf174ad964c plugin-sdk-api-baseline.jsonl
|
||||||
|
|||||||
@@ -427,7 +427,7 @@ releases.
|
|||||||
| Need | Import |
|
| Need | Import |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| System event queue helpers | `openclaw/plugin-sdk/system-event-runtime` |
|
| System event queue helpers | `openclaw/plugin-sdk/system-event-runtime` |
|
||||||
| Heartbeat event and visibility helpers | `openclaw/plugin-sdk/heartbeat-runtime` |
|
| Heartbeat wake, event, and visibility helpers | `openclaw/plugin-sdk/heartbeat-runtime` |
|
||||||
| Pending delivery queue drain | `openclaw/plugin-sdk/delivery-queue-runtime` |
|
| Pending delivery queue drain | `openclaw/plugin-sdk/delivery-queue-runtime` |
|
||||||
| Channel activity telemetry | `openclaw/plugin-sdk/channel-activity-runtime` |
|
| Channel activity telemetry | `openclaw/plugin-sdk/channel-activity-runtime` |
|
||||||
| In-memory dedupe caches | `openclaw/plugin-sdk/dedupe-runtime` |
|
| In-memory dedupe caches | `openclaw/plugin-sdk/dedupe-runtime` |
|
||||||
@@ -545,7 +545,7 @@ releases.
|
|||||||
| `plugin-sdk/ssrf-policy` | SSRF policy helpers | Host allowlist and private-network policy helpers |
|
| `plugin-sdk/ssrf-policy` | SSRF policy helpers | Host allowlist and private-network policy helpers |
|
||||||
| `plugin-sdk/ssrf-runtime` | SSRF runtime helpers | Pinned-dispatcher, guarded fetch, SSRF policy helpers |
|
| `plugin-sdk/ssrf-runtime` | SSRF runtime helpers | Pinned-dispatcher, guarded fetch, SSRF policy helpers |
|
||||||
| `plugin-sdk/system-event-runtime` | System event helpers | `enqueueSystemEvent`, `peekSystemEventEntries` |
|
| `plugin-sdk/system-event-runtime` | System event helpers | `enqueueSystemEvent`, `peekSystemEventEntries` |
|
||||||
| `plugin-sdk/heartbeat-runtime` | Heartbeat helpers | Heartbeat event and visibility helpers |
|
| `plugin-sdk/heartbeat-runtime` | Heartbeat helpers | Heartbeat wake, event, and visibility helpers |
|
||||||
| `plugin-sdk/delivery-queue-runtime` | Delivery queue helpers | `drainPendingDeliveries` |
|
| `plugin-sdk/delivery-queue-runtime` | Delivery queue helpers | `drainPendingDeliveries` |
|
||||||
| `plugin-sdk/channel-activity-runtime` | Channel activity helpers | `recordChannelActivity` |
|
| `plugin-sdk/channel-activity-runtime` | Channel activity helpers | `recordChannelActivity` |
|
||||||
| `plugin-sdk/dedupe-runtime` | Dedupe helpers | In-memory dedupe caches |
|
| `plugin-sdk/dedupe-runtime` | Dedupe helpers | In-memory dedupe caches |
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview)
|
|||||||
| `plugin-sdk/dedupe-runtime` | In-memory dedupe cache helpers |
|
| `plugin-sdk/dedupe-runtime` | In-memory dedupe cache helpers |
|
||||||
| `plugin-sdk/delivery-queue-runtime` | Outbound pending-delivery drain helper |
|
| `plugin-sdk/delivery-queue-runtime` | Outbound pending-delivery drain helper |
|
||||||
| `plugin-sdk/file-access-runtime` | Safe local-file and media-source path helpers |
|
| `plugin-sdk/file-access-runtime` | Safe local-file and media-source path helpers |
|
||||||
| `plugin-sdk/heartbeat-runtime` | Heartbeat event and visibility helpers |
|
| `plugin-sdk/heartbeat-runtime` | Heartbeat wake, event, and visibility helpers |
|
||||||
| `plugin-sdk/number-runtime` | Numeric coercion helper |
|
| `plugin-sdk/number-runtime` | Numeric coercion helper |
|
||||||
| `plugin-sdk/secure-random-runtime` | Secure token/UUID helpers |
|
| `plugin-sdk/secure-random-runtime` | Secure token/UUID helpers |
|
||||||
| `plugin-sdk/system-event-runtime` | System event queue helpers |
|
| `plugin-sdk/system-event-runtime` | System event queue helpers |
|
||||||
|
|||||||
@@ -83,3 +83,18 @@ describe("createSlackMonitorContext shouldDropMismatchedSlackEvent", () => {
|
|||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("createSlackMonitorContext resolveSlackSystemEventSessionKey", () => {
|
||||||
|
it("routes threaded interaction events to the Slack thread session", () => {
|
||||||
|
const ctx = createTestContext();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
ctx.resolveSlackSystemEventSessionKey({
|
||||||
|
channelId: "C_THREAD",
|
||||||
|
channelType: "channel",
|
||||||
|
senderId: "U_CLICKER",
|
||||||
|
threadTs: "1712345678.123456",
|
||||||
|
}),
|
||||||
|
).toBe("agent:main:slack:channel:c_thread:thread:1712345678.123456");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ import type {
|
|||||||
} from "openclaw/plugin-sdk/config-types";
|
} from "openclaw/plugin-sdk/config-types";
|
||||||
import type { SessionScope } from "openclaw/plugin-sdk/config-types";
|
import type { SessionScope } from "openclaw/plugin-sdk/config-types";
|
||||||
import type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk/config-types";
|
import type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk/config-types";
|
||||||
|
import { resolveRuntimeConversationBindingRoute } from "openclaw/plugin-sdk/conversation-runtime";
|
||||||
import { createDedupeCache } from "openclaw/plugin-sdk/dedupe-runtime";
|
import { createDedupeCache } from "openclaw/plugin-sdk/dedupe-runtime";
|
||||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||||
import type { HistoryEntry } from "openclaw/plugin-sdk/reply-history";
|
import type { HistoryEntry } from "openclaw/plugin-sdk/reply-history";
|
||||||
import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
|
import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
|
||||||
|
import { resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing";
|
||||||
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||||
import { getChildLogger } from "openclaw/plugin-sdk/runtime-env";
|
import { getChildLogger } from "openclaw/plugin-sdk/runtime-env";
|
||||||
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
|
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
|
||||||
@@ -78,6 +80,7 @@ export type SlackMonitorContext = {
|
|||||||
channelId?: string | null;
|
channelId?: string | null;
|
||||||
channelType?: string | null;
|
channelType?: string | null;
|
||||||
senderId?: string | null;
|
senderId?: string | null;
|
||||||
|
threadTs?: string | null;
|
||||||
}) => string;
|
}) => string;
|
||||||
isChannelAllowed: (params: {
|
isChannelAllowed: (params: {
|
||||||
channelId?: string;
|
channelId?: string;
|
||||||
@@ -178,6 +181,7 @@ export function createSlackMonitorContext(params: {
|
|||||||
channelId?: string | null;
|
channelId?: string | null;
|
||||||
channelType?: string | null;
|
channelType?: string | null;
|
||||||
senderId?: string | null;
|
senderId?: string | null;
|
||||||
|
threadTs?: string | null;
|
||||||
}) => {
|
}) => {
|
||||||
const channelId = normalizeOptionalString(p.channelId) ?? "";
|
const channelId = normalizeOptionalString(p.channelId) ?? "";
|
||||||
if (!channelId) {
|
if (!channelId) {
|
||||||
@@ -207,18 +211,58 @@ export function createSlackMonitorContext(params: {
|
|||||||
teamId: params.teamId,
|
teamId: params.teamId,
|
||||||
peer: { kind: peerKind, id: peerId },
|
peer: { kind: peerKind, id: peerId },
|
||||||
});
|
});
|
||||||
return route.sessionKey;
|
const threadTs = normalizeOptionalString(p.threadTs);
|
||||||
|
const baseConversationId = isDirectMessage ? `user:${senderId}` : channelId;
|
||||||
|
const threadBindingRoute = threadTs
|
||||||
|
? resolveRuntimeConversationBindingRoute({
|
||||||
|
route,
|
||||||
|
conversation: {
|
||||||
|
channel: "slack",
|
||||||
|
accountId: params.accountId,
|
||||||
|
conversationId: threadTs,
|
||||||
|
parentConversationId: baseConversationId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
const runtimeRoute =
|
||||||
|
threadBindingRoute?.boundSessionKey || threadBindingRoute?.bindingRecord
|
||||||
|
? threadBindingRoute
|
||||||
|
: resolveRuntimeConversationBindingRoute({
|
||||||
|
route,
|
||||||
|
conversation: {
|
||||||
|
channel: "slack",
|
||||||
|
accountId: params.accountId,
|
||||||
|
conversationId: baseConversationId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (runtimeRoute.boundSessionKey) {
|
||||||
|
return runtimeRoute.route.sessionKey;
|
||||||
|
}
|
||||||
|
return resolveThreadSessionKeys({
|
||||||
|
baseSessionKey: runtimeRoute.route.sessionKey,
|
||||||
|
threadId: threadTs,
|
||||||
|
parentSessionKey:
|
||||||
|
threadTs && params.threadInheritParent ? runtimeRoute.route.sessionKey : undefined,
|
||||||
|
}).sessionKey;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Fall through to legacy key derivation.
|
// Fall through to legacy key derivation.
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolveSessionKey(
|
const legacySessionKey = resolveSessionKey(
|
||||||
params.sessionScope,
|
params.sessionScope,
|
||||||
{ From: from, ChatType: chatType, Provider: "slack" },
|
{ From: from, ChatType: chatType, Provider: "slack" },
|
||||||
params.mainKey,
|
params.mainKey,
|
||||||
resolveDefaultAgentId(params.cfg),
|
resolveDefaultAgentId(params.cfg),
|
||||||
);
|
);
|
||||||
|
return resolveThreadSessionKeys({
|
||||||
|
baseSessionKey: legacySessionKey,
|
||||||
|
threadId: normalizeOptionalString(p.threadTs),
|
||||||
|
parentSessionKey:
|
||||||
|
normalizeOptionalString(p.threadTs) && params.threadInheritParent
|
||||||
|
? legacySessionKey
|
||||||
|
: undefined,
|
||||||
|
}).sessionKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
const resolveChannelName = async (channelId: string) => {
|
const resolveChannelName = async (channelId: string) => {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
resolveCommandAuthorization,
|
resolveCommandAuthorization,
|
||||||
resolveCommandAuthorizedFromAuthorizers,
|
resolveCommandAuthorizedFromAuthorizers,
|
||||||
} from "openclaw/plugin-sdk/command-auth-native";
|
} from "openclaw/plugin-sdk/command-auth-native";
|
||||||
|
import { requestHeartbeat } from "openclaw/plugin-sdk/heartbeat-runtime";
|
||||||
import { enqueueSystemEvent } from "openclaw/plugin-sdk/system-event-runtime";
|
import { enqueueSystemEvent } from "openclaw/plugin-sdk/system-event-runtime";
|
||||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||||
import {
|
import {
|
||||||
@@ -778,6 +779,7 @@ function enqueueSlackBlockActionEvent(params: {
|
|||||||
channelId: params.parsed.channelId,
|
channelId: params.parsed.channelId,
|
||||||
channelType: params.auth.channelType,
|
channelType: params.auth.channelType,
|
||||||
senderId: params.parsed.userId,
|
senderId: params.parsed.userId,
|
||||||
|
threadTs: params.parsed.threadTs,
|
||||||
});
|
});
|
||||||
const contextParts = [
|
const contextParts = [
|
||||||
"slack:interaction",
|
"slack:interaction",
|
||||||
@@ -785,10 +787,31 @@ function enqueueSlackBlockActionEvent(params: {
|
|||||||
params.parsed.messageTs,
|
params.parsed.messageTs,
|
||||||
params.parsed.actionId,
|
params.parsed.actionId,
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
enqueueSystemEvent(params.formatSystemEvent(eventPayload), {
|
const queued = enqueueSystemEvent(params.formatSystemEvent(eventPayload), {
|
||||||
sessionKey,
|
sessionKey,
|
||||||
contextKey: contextParts.join(":"),
|
contextKey: contextParts.join(":"),
|
||||||
|
deliveryContext: {
|
||||||
|
channel: "slack",
|
||||||
|
to:
|
||||||
|
params.auth.channelType === "im"
|
||||||
|
? `user:${params.parsed.userId}`
|
||||||
|
: params.parsed.channelId
|
||||||
|
? `channel:${params.parsed.channelId}`
|
||||||
|
: undefined,
|
||||||
|
accountId: params.ctx.accountId,
|
||||||
|
threadId: params.parsed.threadTs,
|
||||||
|
},
|
||||||
|
trusted: false,
|
||||||
});
|
});
|
||||||
|
if (queued) {
|
||||||
|
requestHeartbeat({
|
||||||
|
source: "hook",
|
||||||
|
intent: "immediate",
|
||||||
|
reason: "hook:slack-interaction",
|
||||||
|
sessionKey,
|
||||||
|
heartbeat: { target: "last" },
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSlackConfirmationBlocks(params: {
|
function buildSlackConfirmationBlocks(params: {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
const enqueueSystemEventMock = vi.hoisted(() => vi.fn());
|
const enqueueSystemEventMock = vi.hoisted(() => vi.fn());
|
||||||
|
const requestHeartbeatMock = vi.hoisted(() => vi.fn());
|
||||||
type DispatchPluginInteractiveHandlerResult = {
|
type DispatchPluginInteractiveHandlerResult = {
|
||||||
matched: boolean;
|
matched: boolean;
|
||||||
handled: boolean;
|
handled: boolean;
|
||||||
@@ -29,6 +30,14 @@ vi.mock("openclaw/plugin-sdk/system-event-runtime", async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mock("openclaw/plugin-sdk/heartbeat-runtime", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/heartbeat-runtime")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
requestHeartbeat: (...args: unknown[]) => requestHeartbeatMock(...args),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("openclaw/plugin-sdk/approval-gateway-runtime", () => ({
|
vi.mock("openclaw/plugin-sdk/approval-gateway-runtime", () => ({
|
||||||
resolveApprovalOverGateway: (arg: unknown) => resolveApprovalOverGatewayMock(arg),
|
resolveApprovalOverGateway: (arg: unknown) => resolveApprovalOverGatewayMock(arg),
|
||||||
}));
|
}));
|
||||||
@@ -307,7 +316,9 @@ describe("registerSlackInteractionEvents", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
enqueueSystemEventMock.mockClear();
|
enqueueSystemEventMock.mockReset();
|
||||||
|
enqueueSystemEventMock.mockReturnValue(true);
|
||||||
|
requestHeartbeatMock.mockClear();
|
||||||
dispatchPluginInteractiveHandlerMock.mockClear();
|
dispatchPluginInteractiveHandlerMock.mockClear();
|
||||||
resolvePluginConversationBindingApprovalMock.mockClear();
|
resolvePluginConversationBindingApprovalMock.mockClear();
|
||||||
resolvePluginConversationBindingApprovalMock.mockResolvedValue({ status: "expired" });
|
resolvePluginConversationBindingApprovalMock.mockResolvedValue({ status: "expired" });
|
||||||
@@ -394,6 +405,7 @@ describe("registerSlackInteractionEvents", () => {
|
|||||||
channelId: "C1",
|
channelId: "C1",
|
||||||
channelType: "channel",
|
channelType: "channel",
|
||||||
senderId: "U123",
|
senderId: "U123",
|
||||||
|
threadTs: "100.100",
|
||||||
});
|
});
|
||||||
expect(trackEvent).toHaveBeenCalledTimes(1);
|
expect(trackEvent).toHaveBeenCalledTimes(1);
|
||||||
expect(app.client.chat.update).toHaveBeenCalledTimes(1);
|
expect(app.client.chat.update).toHaveBeenCalledTimes(1);
|
||||||
@@ -642,7 +654,7 @@ describe("registerSlackInteractionEvents", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("treats Slack reply buttons as plain interaction events instead of plugin dispatch", async () => {
|
it("treats Slack reply buttons as plain interaction events instead of plugin dispatch", async () => {
|
||||||
const { ctx, app, getHandler } = createContext();
|
const { ctx, app, getHandler, resolveSessionKey } = createContext();
|
||||||
registerSlackInteractionEvents({ ctx: ctx as never });
|
registerSlackInteractionEvents({ ctx: ctx as never });
|
||||||
|
|
||||||
const handler = getHandler();
|
const handler = getHandler();
|
||||||
@@ -679,8 +691,31 @@ describe("registerSlackInteractionEvents", () => {
|
|||||||
expect(dispatchPluginInteractiveHandlerMock).not.toHaveBeenCalled();
|
expect(dispatchPluginInteractiveHandlerMock).not.toHaveBeenCalled();
|
||||||
expect(enqueueSystemEventMock).toHaveBeenCalledWith(
|
expect(enqueueSystemEventMock).toHaveBeenCalledWith(
|
||||||
expect.stringContaining('"actionId":"openclaw:reply_button"'),
|
expect.stringContaining('"actionId":"openclaw:reply_button"'),
|
||||||
expect.any(Object),
|
expect.objectContaining({
|
||||||
|
contextKey: "slack:interaction:C1:100.200:openclaw:reply_button",
|
||||||
|
deliveryContext: {
|
||||||
|
accountId: "default",
|
||||||
|
channel: "slack",
|
||||||
|
threadId: "100.100",
|
||||||
|
to: "channel:C1",
|
||||||
|
},
|
||||||
|
sessionKey: "agent:ops:slack:channel:C1",
|
||||||
|
trusted: false,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
expect(resolveSessionKey).toHaveBeenCalledWith({
|
||||||
|
channelId: "C1",
|
||||||
|
channelType: "channel",
|
||||||
|
senderId: "U123",
|
||||||
|
threadTs: "100.100",
|
||||||
|
});
|
||||||
|
expect(requestHeartbeatMock).toHaveBeenCalledWith({
|
||||||
|
source: "hook",
|
||||||
|
intent: "immediate",
|
||||||
|
reason: "hook:slack-interaction",
|
||||||
|
sessionKey: "agent:ops:slack:channel:C1",
|
||||||
|
heartbeat: { target: "last" },
|
||||||
|
});
|
||||||
expect(app.client.chat.update).toHaveBeenCalledTimes(1);
|
expect(app.client.chat.update).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1459,6 +1494,7 @@ describe("registerSlackInteractionEvents", () => {
|
|||||||
channelId: "C222",
|
channelId: "C222",
|
||||||
channelType: "channel",
|
channelType: "channel",
|
||||||
senderId: "U111",
|
senderId: "U111",
|
||||||
|
threadTs: "222.111",
|
||||||
});
|
});
|
||||||
expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1);
|
expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1);
|
||||||
const [eventText] = enqueueSystemEventMock.mock.calls[0] as [string];
|
const [eventText] = enqueueSystemEventMock.mock.calls[0] as [string];
|
||||||
|
|||||||
@@ -102,23 +102,39 @@ mutation($input: CreateCommitOnBranchInput!) {
|
|||||||
GRAPHQL
|
GRAPHQL
|
||||||
)
|
)
|
||||||
|
|
||||||
|
local additions_file deletions_file
|
||||||
|
additions_file=$(mktemp)
|
||||||
|
deletions_file=$(mktemp)
|
||||||
|
printf '%s\n' "$additions" >"$additions_file"
|
||||||
|
printf '%s\n' "$deletions" >"$deletions_file"
|
||||||
|
|
||||||
local variables
|
local variables
|
||||||
variables=$(jq -n \
|
variables=$(jq -n \
|
||||||
--arg nwo "$repo_nwo" \
|
--arg nwo "$repo_nwo" \
|
||||||
--arg branch "$branch" \
|
--arg branch "$branch" \
|
||||||
--arg oid "$expected_oid" \
|
--arg oid "$expected_oid" \
|
||||||
--arg headline "$commit_headline" \
|
--arg headline "$commit_headline" \
|
||||||
--argjson additions "$additions" \
|
--slurpfile additions "$additions_file" \
|
||||||
--argjson deletions "$deletions" \
|
--slurpfile deletions "$deletions_file" \
|
||||||
'{input: {
|
'{input: {
|
||||||
branch: { repositoryNameWithOwner: $nwo, branchName: $branch },
|
branch: { repositoryNameWithOwner: $nwo, branchName: $branch },
|
||||||
message: { headline: $headline },
|
message: { headline: $headline },
|
||||||
fileChanges: { additions: $additions, deletions: $deletions },
|
fileChanges: { additions: $additions[0], deletions: $deletions[0] },
|
||||||
expectedHeadOid: $oid
|
expectedHeadOid: $oid
|
||||||
}}')
|
}}')
|
||||||
|
rm -f "$additions_file" "$deletions_file"
|
||||||
|
|
||||||
|
local variables_file
|
||||||
|
variables_file=$(mktemp)
|
||||||
|
printf '%s\n' "$variables" >"$variables_file"
|
||||||
|
|
||||||
|
local payload
|
||||||
|
payload=$(jq -n --arg query "$query" --slurpfile variables "$variables_file" \
|
||||||
|
'{query: $query, variables: $variables[0]}')
|
||||||
|
rm -f "$variables_file"
|
||||||
|
|
||||||
local result
|
local result
|
||||||
result=$(gh api graphql -f query="$query" --input - <<< "$variables" 2>&1) || {
|
result=$(gh api graphql --input - <<< "$payload" 2>&1) || {
|
||||||
echo "GraphQL push failed: $result" >&2
|
echo "GraphQL push failed: $result" >&2
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -382,14 +382,17 @@ describe("buildInboundUserContextPrefix", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("includes formatted timestamp in conversation info when provided", () => {
|
it("includes formatted timestamp in conversation info when provided", () => {
|
||||||
const text = buildInboundUserContextPrefix({
|
const text = buildInboundUserContextPrefix(
|
||||||
ChatType: "group",
|
{
|
||||||
MessageSid: "msg-with-ts",
|
ChatType: "group",
|
||||||
Timestamp: Date.UTC(2026, 1, 15, 13, 35),
|
MessageSid: "msg-with-ts",
|
||||||
} as TemplateContext);
|
Timestamp: Date.UTC(2026, 1, 15, 13, 35),
|
||||||
|
} as TemplateContext,
|
||||||
|
{ timezone: "utc" },
|
||||||
|
);
|
||||||
|
|
||||||
const conversationInfo = parseConversationInfoPayload(text);
|
const conversationInfo = parseConversationInfoPayload(text);
|
||||||
expect(conversationInfo["timestamp"]).toMatch(/^Sun 2026-02-15 13:35 (?:GMT|UTC)$/);
|
expect(conversationInfo["timestamp"]).toBe("Sun 2026-02-15T13:35Z");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("honors envelope user timezone for conversation timestamps", () => {
|
it("honors envelope user timezone for conversation timestamps", () => {
|
||||||
|
|||||||
@@ -2,3 +2,4 @@
|
|||||||
|
|
||||||
export * from "../infra/heartbeat-events.js";
|
export * from "../infra/heartbeat-events.js";
|
||||||
export * from "../infra/heartbeat-visibility.js";
|
export * from "../infra/heartbeat-visibility.js";
|
||||||
|
export { requestHeartbeat } from "../infra/heartbeat-wake.js";
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ describe("version resolution", () => {
|
|||||||
expect(resolveUsableRuntimeVersion(" 2026.3.2 ")).toBe("2026.3.2");
|
expect(resolveUsableRuntimeVersion(" 2026.3.2 ")).toBe("2026.3.2");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prefers runtime VERSION over service/package markers and ignores blank env values", () => {
|
it("prefers runtime VERSION over service/package markers and ignores unusable env values", () => {
|
||||||
expect(
|
expect(
|
||||||
resolveRuntimeServiceVersion({
|
resolveRuntimeServiceVersion({
|
||||||
OPENCLAW_VERSION: " ",
|
OPENCLAW_VERSION: " ",
|
||||||
@@ -231,5 +231,13 @@ describe("version resolution", () => {
|
|||||||
"fallback",
|
"fallback",
|
||||||
),
|
),
|
||||||
).toBe(VERSION);
|
).toBe(VERSION);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
resolveRuntimeServiceVersion({
|
||||||
|
OPENCLAW_VERSION: "undefined",
|
||||||
|
OPENCLAW_SERVICE_VERSION: "null",
|
||||||
|
npm_package_version: "1.0.0-package",
|
||||||
|
}),
|
||||||
|
).toBe(VERSION);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ function readVersionFromJsonCandidates(
|
|||||||
function firstNonEmpty(...values: Array<string | undefined>): string | undefined {
|
function firstNonEmpty(...values: Array<string | undefined>): string | undefined {
|
||||||
for (const value of values) {
|
for (const value of values) {
|
||||||
const trimmed = normalizeOptionalString(value);
|
const trimmed = normalizeOptionalString(value);
|
||||||
if (trimmed) {
|
if (trimmed && trimmed.toLowerCase() !== "undefined" && trimmed.toLowerCase() !== "null") {
|
||||||
return trimmed;
|
return trimmed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user