Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
448
extensions/line/src/bot-message-context.test.ts
Normal file
448
extensions/line/src/bot-message-context.test.ts
Normal file
@@ -0,0 +1,448 @@
|
||||
// Line tests cover bot message context plugin behavior.
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { webhook } from "@line/bot-sdk";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { getSessionBindingService } from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import { testing as sessionBindingTesting } from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import {
|
||||
createTestRegistry,
|
||||
setActivePluginRegistry,
|
||||
} from "openclaw/plugin-sdk/plugin-test-runtime";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { lineBindingsAdapter } from "./bindings.js";
|
||||
import { buildLineMessageContext, buildLinePostbackContext } from "./bot-message-context.js";
|
||||
import type { ResolvedLineAccount } from "./types.js";
|
||||
|
||||
type MessageEvent = webhook.MessageEvent;
|
||||
type PostbackEvent = webhook.PostbackEvent;
|
||||
|
||||
const lineBindingsPlugin = {
|
||||
id: "line",
|
||||
bindings: lineBindingsAdapter,
|
||||
conversationBindings: {
|
||||
defaultTopLevelPlacement: "current",
|
||||
supportsCurrentConversationBinding: true,
|
||||
},
|
||||
};
|
||||
|
||||
describe("buildLineMessageContext", () => {
|
||||
let tmpDir: string;
|
||||
let storePath: string;
|
||||
let cfg: OpenClawConfig;
|
||||
const account: ResolvedLineAccount = {
|
||||
accountId: "default",
|
||||
enabled: true,
|
||||
channelAccessToken: "token",
|
||||
channelSecret: "secret",
|
||||
tokenSource: "config",
|
||||
config: {},
|
||||
};
|
||||
|
||||
const createMessageEvent = (
|
||||
source: MessageEvent["source"],
|
||||
overrides?: Partial<MessageEvent>,
|
||||
): MessageEvent =>
|
||||
({
|
||||
type: "message",
|
||||
message: { id: "1", type: "text", text: "hello" },
|
||||
replyToken: "reply-token",
|
||||
timestamp: Date.now(),
|
||||
source,
|
||||
mode: "active",
|
||||
webhookEventId: "evt-1",
|
||||
deliveryContext: { isRedelivery: false },
|
||||
...overrides,
|
||||
}) as MessageEvent;
|
||||
|
||||
const createPostbackEvent = (
|
||||
source: PostbackEvent["source"],
|
||||
overrides?: Partial<PostbackEvent>,
|
||||
): PostbackEvent =>
|
||||
({
|
||||
type: "postback",
|
||||
postback: { data: "action=select" },
|
||||
replyToken: "reply-token",
|
||||
timestamp: Date.now(),
|
||||
source,
|
||||
mode: "active",
|
||||
webhookEventId: "evt-2",
|
||||
deliveryContext: { isRedelivery: false },
|
||||
...overrides,
|
||||
}) as PostbackEvent;
|
||||
|
||||
beforeEach(async () => {
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([
|
||||
{
|
||||
pluginId: lineBindingsPlugin.id,
|
||||
plugin: lineBindingsPlugin,
|
||||
source: "test",
|
||||
},
|
||||
]),
|
||||
);
|
||||
sessionBindingTesting.resetSessionBindingAdaptersForTests();
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-line-context-"));
|
||||
storePath = path.join(tmpDir, "sessions.json");
|
||||
cfg = { session: { store: storePath } };
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
sessionBindingTesting.resetSessionBindingAdaptersForTests();
|
||||
await fs.rm(tmpDir, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
maxRetries: 3,
|
||||
retryDelay: 50,
|
||||
});
|
||||
});
|
||||
|
||||
it("routes group message replies to the group id", async () => {
|
||||
const event = createMessageEvent({ type: "group", groupId: "group-1", userId: "user-1" });
|
||||
|
||||
const context = await buildLineMessageContext({
|
||||
event,
|
||||
allMedia: [],
|
||||
cfg,
|
||||
account,
|
||||
commandAuthorized: true,
|
||||
});
|
||||
|
||||
expect(context?.ctxPayload.OriginatingTo).toBe("line:group:group-1");
|
||||
expect(context?.ctxPayload.To).toBe("line:group:group-1");
|
||||
});
|
||||
|
||||
it("replaces a failed media placeholder with an unavailable notice", async () => {
|
||||
const event = createMessageEvent({ type: "user", userId: "user-image" }, {
|
||||
message: {
|
||||
id: "image-1",
|
||||
type: "image",
|
||||
contentProvider: { type: "line" },
|
||||
},
|
||||
} as Partial<MessageEvent>);
|
||||
|
||||
const context = await buildLineMessageContext({
|
||||
event,
|
||||
allMedia: [],
|
||||
mediaUnavailable: true,
|
||||
cfg,
|
||||
account,
|
||||
commandAuthorized: true,
|
||||
});
|
||||
|
||||
expect(context?.ctxPayload.RawBody).toBe("<media:image>");
|
||||
expect(context?.ctxPayload.CommandBody).toBe("<media:image>");
|
||||
expect(context?.ctxPayload.BodyForAgent).toBe("[line attachment unavailable]");
|
||||
expect(context?.ctxPayload.MediaPath).toBeUndefined();
|
||||
});
|
||||
|
||||
it("routes group postback replies to the group id", async () => {
|
||||
const event = createPostbackEvent({ type: "group", groupId: "group-2", userId: "user-2" });
|
||||
|
||||
const context = await buildLinePostbackContext({
|
||||
event,
|
||||
cfg,
|
||||
account,
|
||||
commandAuthorized: true,
|
||||
});
|
||||
|
||||
expect(context?.ctxPayload.OriginatingTo).toBe("line:group:group-2");
|
||||
expect(context?.ctxPayload.To).toBe("line:group:group-2");
|
||||
});
|
||||
|
||||
it("routes room postback replies to the room id", async () => {
|
||||
const event = createPostbackEvent({ type: "room", roomId: "room-1", userId: "user-3" });
|
||||
|
||||
const context = await buildLinePostbackContext({
|
||||
event,
|
||||
cfg,
|
||||
account,
|
||||
commandAuthorized: true,
|
||||
});
|
||||
|
||||
expect(context?.ctxPayload.OriginatingTo).toBe("line:room:room-1");
|
||||
expect(context?.ctxPayload.To).toBe("line:room:room-1");
|
||||
});
|
||||
|
||||
it("resolves prefixed-only group config through the inbound message context", async () => {
|
||||
const event = createMessageEvent({ type: "group", groupId: "group-1", userId: "user-1" });
|
||||
|
||||
const context = await buildLineMessageContext({
|
||||
event,
|
||||
allMedia: [],
|
||||
cfg,
|
||||
account: {
|
||||
...account,
|
||||
config: {
|
||||
groups: {
|
||||
"group:group-1": {
|
||||
systemPrompt: "Use the prefixed group config",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
commandAuthorized: true,
|
||||
});
|
||||
|
||||
expect(context?.ctxPayload.GroupSystemPrompt).toBe("Use the prefixed group config");
|
||||
});
|
||||
|
||||
it("resolves prefixed-only room config through the inbound message context", async () => {
|
||||
const event = createMessageEvent({ type: "room", roomId: "room-1", userId: "user-1" });
|
||||
|
||||
const context = await buildLineMessageContext({
|
||||
event,
|
||||
allMedia: [],
|
||||
cfg,
|
||||
account: {
|
||||
...account,
|
||||
config: {
|
||||
groups: {
|
||||
"room:room-1": {
|
||||
systemPrompt: "Use the prefixed room config",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
commandAuthorized: true,
|
||||
});
|
||||
|
||||
expect(context?.ctxPayload.GroupSystemPrompt).toBe("Use the prefixed room config");
|
||||
});
|
||||
|
||||
it("keeps non-text message contexts fail-closed for command auth", async () => {
|
||||
const event = createMessageEvent(
|
||||
{ type: "user", userId: "user-audio" },
|
||||
{
|
||||
message: { id: "audio-1", type: "audio", duration: 1000 } as MessageEvent["message"],
|
||||
},
|
||||
);
|
||||
|
||||
const context = await buildLineMessageContext({
|
||||
event,
|
||||
allMedia: [],
|
||||
cfg,
|
||||
account,
|
||||
commandAuthorized: false,
|
||||
});
|
||||
|
||||
expect(context?.ctxPayload.CommandAuthorized).toBe(false);
|
||||
});
|
||||
|
||||
it("sets CommandAuthorized=true when authorized", async () => {
|
||||
const event = createMessageEvent({ type: "user", userId: "user-auth" });
|
||||
|
||||
const context = await buildLineMessageContext({
|
||||
event,
|
||||
allMedia: [],
|
||||
cfg,
|
||||
account,
|
||||
commandAuthorized: true,
|
||||
});
|
||||
|
||||
expect(context?.ctxPayload.CommandAuthorized).toBe(true);
|
||||
});
|
||||
|
||||
it("sets CommandAuthorized=false when not authorized", async () => {
|
||||
const event = createMessageEvent({ type: "user", userId: "user-noauth" });
|
||||
|
||||
const context = await buildLineMessageContext({
|
||||
event,
|
||||
allMedia: [],
|
||||
cfg,
|
||||
account,
|
||||
commandAuthorized: false,
|
||||
});
|
||||
|
||||
expect(context?.ctxPayload.CommandAuthorized).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps per-channel-peer direct-message last-route writes on the isolated session", async () => {
|
||||
const event = createMessageEvent({ type: "user", userId: "user-1" });
|
||||
const directCfg: OpenClawConfig = {
|
||||
session: { store: storePath, dmScope: "per-channel-peer" },
|
||||
};
|
||||
|
||||
const context = await buildLineMessageContext({
|
||||
event,
|
||||
allMedia: [],
|
||||
cfg: directCfg,
|
||||
account: {
|
||||
...account,
|
||||
config: { allowFrom: ["user-1"] },
|
||||
},
|
||||
commandAuthorized: true,
|
||||
});
|
||||
|
||||
expect(context?.route.sessionKey).toBe("agent:main:line:direct:user-1");
|
||||
const updateLastRoute = context?.turn.record.updateLastRoute;
|
||||
expect(updateLastRoute?.sessionKey).toBe(context?.route.sessionKey);
|
||||
expect(updateLastRoute?.sessionKey).not.toBe("agent:main:main");
|
||||
expect(updateLastRoute?.channel).toBe("line");
|
||||
expect(updateLastRoute?.to).toBe("user-1");
|
||||
expect(updateLastRoute?.mainDmOwnerPin).toBeUndefined();
|
||||
});
|
||||
|
||||
it("sets CommandAuthorized on postback context", async () => {
|
||||
const event = createPostbackEvent({ type: "user", userId: "user-pb" });
|
||||
|
||||
const context = await buildLinePostbackContext({
|
||||
event,
|
||||
cfg,
|
||||
account,
|
||||
commandAuthorized: true,
|
||||
});
|
||||
|
||||
expect(context?.ctxPayload.CommandAuthorized).toBe(true);
|
||||
});
|
||||
|
||||
it("group peer binding matches raw groupId without prefix (#21907)", async () => {
|
||||
const groupId = "Cc7e3bece1234567890abcdef"; // pragma: allowlist secret
|
||||
const bindingCfg: OpenClawConfig = {
|
||||
session: { store: storePath },
|
||||
agents: {
|
||||
list: [{ id: "main" }, { id: "line-group-agent" }],
|
||||
},
|
||||
bindings: [
|
||||
{
|
||||
agentId: "line-group-agent",
|
||||
match: { channel: "line", peer: { kind: "group", id: groupId } },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const event = {
|
||||
type: "message",
|
||||
message: { id: "msg-1", type: "text", text: "hello" },
|
||||
replyToken: "reply-token",
|
||||
timestamp: Date.now(),
|
||||
source: { type: "group", groupId, userId: "user-1" },
|
||||
mode: "active",
|
||||
webhookEventId: "evt-1",
|
||||
deliveryContext: { isRedelivery: false },
|
||||
} as MessageEvent;
|
||||
|
||||
const context = await buildLineMessageContext({
|
||||
event,
|
||||
allMedia: [],
|
||||
cfg: bindingCfg,
|
||||
account,
|
||||
commandAuthorized: true,
|
||||
});
|
||||
expect(context?.route.agentId).toBe("line-group-agent");
|
||||
expect(context?.route.matchedBy).toBe("binding.peer");
|
||||
});
|
||||
|
||||
it("room peer binding matches raw roomId without prefix (#21907)", async () => {
|
||||
const roomId = "Rr1234567890abcdef";
|
||||
const bindingCfg: OpenClawConfig = {
|
||||
session: { store: storePath },
|
||||
agents: {
|
||||
list: [{ id: "main" }, { id: "line-room-agent" }],
|
||||
},
|
||||
bindings: [
|
||||
{
|
||||
agentId: "line-room-agent",
|
||||
match: { channel: "line", peer: { kind: "group", id: roomId } },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const event = {
|
||||
type: "message",
|
||||
message: { id: "msg-2", type: "text", text: "hello" },
|
||||
replyToken: "reply-token",
|
||||
timestamp: Date.now(),
|
||||
source: { type: "room", roomId, userId: "user-2" },
|
||||
mode: "active",
|
||||
webhookEventId: "evt-2",
|
||||
deliveryContext: { isRedelivery: false },
|
||||
} as MessageEvent;
|
||||
|
||||
const context = await buildLineMessageContext({
|
||||
event,
|
||||
allMedia: [],
|
||||
cfg: bindingCfg,
|
||||
account,
|
||||
commandAuthorized: true,
|
||||
});
|
||||
expect(context?.route.agentId).toBe("line-room-agent");
|
||||
expect(context?.route.matchedBy).toBe("binding.peer");
|
||||
});
|
||||
|
||||
it("normalizes LINE ACP binding conversation ids through the plugin bindings surface", () => {
|
||||
const compiled = lineBindingsAdapter.compileConfiguredBinding({
|
||||
conversationId: "line:user:U1234567890abcdef1234567890abcdef",
|
||||
});
|
||||
|
||||
expect(compiled).toEqual({
|
||||
conversationId: "U1234567890abcdef1234567890abcdef",
|
||||
});
|
||||
expect(
|
||||
lineBindingsAdapter.matchInboundConversation({
|
||||
compiledBinding: compiled!,
|
||||
conversationId: "U1234567890abcdef1234567890abcdef",
|
||||
}),
|
||||
).toEqual({
|
||||
conversationId: "U1234567890abcdef1234567890abcdef",
|
||||
matchPriority: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes canonical LINE targets through the plugin bindings surface", () => {
|
||||
const compiled = lineBindingsAdapter.compileConfiguredBinding({
|
||||
conversationId: "line:U1234567890abcdef1234567890abcdef",
|
||||
});
|
||||
|
||||
expect(compiled).toEqual({
|
||||
conversationId: "U1234567890abcdef1234567890abcdef",
|
||||
});
|
||||
expect(
|
||||
lineBindingsAdapter.resolveCommandConversation({
|
||||
originatingTo: "line:U1234567890abcdef1234567890abcdef",
|
||||
}),
|
||||
).toEqual({
|
||||
conversationId: "U1234567890abcdef1234567890abcdef",
|
||||
});
|
||||
expect(
|
||||
lineBindingsAdapter.matchInboundConversation({
|
||||
compiledBinding: compiled!,
|
||||
conversationId: "U1234567890abcdef1234567890abcdef",
|
||||
}),
|
||||
).toEqual({
|
||||
conversationId: "U1234567890abcdef1234567890abcdef",
|
||||
matchPriority: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it("routes LINE conversations through active ACP session bindings", async () => {
|
||||
const userId = "U1234567890abcdef1234567890abcdef";
|
||||
await getSessionBindingService().bind({
|
||||
targetSessionKey: "agent:codex:acp:binding:line:default:test123",
|
||||
targetKind: "session",
|
||||
conversation: {
|
||||
channel: "line",
|
||||
accountId: "default",
|
||||
conversationId: userId,
|
||||
},
|
||||
placement: "current",
|
||||
metadata: {
|
||||
agentId: "codex",
|
||||
},
|
||||
});
|
||||
|
||||
const event = createMessageEvent({ type: "user", userId });
|
||||
const context = await buildLineMessageContext({
|
||||
event,
|
||||
allMedia: [],
|
||||
cfg,
|
||||
account,
|
||||
commandAuthorized: true,
|
||||
});
|
||||
|
||||
expect(context?.route.agentId).toBe("codex");
|
||||
expect(context?.route.sessionKey).toBe("agent:codex:acp:binding:line:default:test123");
|
||||
expect(context?.route.matchedBy).toBe("binding.channel");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user