@axiom-lattice/gateway 3.0.2 → 3.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/dist/index.js +31 -9
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.mjs +31 -9
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +9 -6
  6. package/.eslintrc.json +0 -22
  7. package/.turbo/turbo-build.log +0 -46
  8. package/AGENTS.md +0 -112
  9. package/CHANGELOG.md +0 -1190
  10. package/LOGGER_CONFIG.md +0 -135
  11. package/RESUME_STREAM_CONTENT_BASED.md +0 -326
  12. package/RESUME_STREAM_README.md +0 -388
  13. package/jest.config.js +0 -72
  14. package/public/sdk/README.md +0 -695
  15. package/public/sdk/dashboard-engine-skill.md +0 -1122
  16. package/public/sdk/dashboard-single-file-spec.md +0 -357
  17. package/public/sdk/data-query-sdk-skill.md +0 -307
  18. package/public/sdk/data-query-sdk.d.ts +0 -252
  19. package/public/sdk/data-query-sdk.js +0 -970
  20. package/public/sdk/occupancy-dashboard.html +0 -363
  21. package/public/sdk/test-dashboard.html +0 -690
  22. package/scripts/seed-lark-installation.ts +0 -54
  23. package/src/__tests__/__mocks__/e2b.ts +0 -1
  24. package/src/__tests__/a2a.test.ts +0 -428
  25. package/src/__tests__/channel-installations.test.ts +0 -199
  26. package/src/__tests__/data-query.test.ts +0 -77
  27. package/src/__tests__/sandbox-provider-registration.test.ts +0 -74
  28. package/src/__tests__/setup.ts +0 -5
  29. package/src/__tests__/workspace.test.ts +0 -400
  30. package/src/channels/__tests__/routes.test.ts +0 -50
  31. package/src/channels/lark/LarkChannelAdapter.ts +0 -217
  32. package/src/channels/lark/README.md +0 -187
  33. package/src/channels/lark/__tests__/controller.test.ts +0 -136
  34. package/src/channels/lark/__tests__/mapping-service.test.ts +0 -118
  35. package/src/channels/lark/__tests__/parser.test.ts +0 -72
  36. package/src/channels/lark/__tests__/sender.test.ts +0 -37
  37. package/src/channels/lark/__tests__/verification.test.ts +0 -157
  38. package/src/channels/lark/controller.ts +0 -48
  39. package/src/channels/lark/mapping-service.ts +0 -138
  40. package/src/channels/lark/parser.ts +0 -68
  41. package/src/channels/lark/routes.ts +0 -22
  42. package/src/channels/lark/sender.ts +0 -58
  43. package/src/channels/lark/types.ts +0 -20
  44. package/src/channels/lark/verification.ts +0 -32
  45. package/src/channels/registry.ts +0 -20
  46. package/src/channels/routes.ts +0 -30
  47. package/src/channels/wechat/WechatChannelAdapter.ts +0 -294
  48. package/src/channels/wechat/__tests__/context-store.test.ts +0 -148
  49. package/src/channels/wechat/context-store.ts +0 -54
  50. package/src/channels/wechat/controller.ts +0 -59
  51. package/src/channels/wechat/routes.ts +0 -7
  52. package/src/channels/wechat/types.ts +0 -62
  53. package/src/channels/wechat/wechat-client.ts +0 -162
  54. package/src/config/local-a2a-templates.ts +0 -59
  55. package/src/config.ts +0 -124
  56. package/src/controllers/__tests__/assistant-local-a2a.test.ts +0 -147
  57. package/src/controllers/__tests__/local-a2a-templates.test.ts +0 -112
  58. package/src/controllers/__tests__/mime.test.ts +0 -74
  59. package/src/controllers/__tests__/resources.test.ts +0 -122
  60. package/src/controllers/__tests__/run.test.ts +0 -115
  61. package/src/controllers/__tests__/tasks.test.ts +0 -215
  62. package/src/controllers/__tests__/workflow-tracking.test.ts +0 -113
  63. package/src/controllers/agent_task.ts +0 -72
  64. package/src/controllers/assistant.ts +0 -490
  65. package/src/controllers/auth.ts +0 -524
  66. package/src/controllers/channel-bindings.ts +0 -135
  67. package/src/controllers/channel-installations.ts +0 -409
  68. package/src/controllers/collections.ts +0 -176
  69. package/src/controllers/config.ts +0 -126
  70. package/src/controllers/connections.ts +0 -181
  71. package/src/controllers/data-query.ts +0 -236
  72. package/src/controllers/database-configs.ts +0 -440
  73. package/src/controllers/embeddings.ts +0 -22
  74. package/src/controllers/entries.ts +0 -167
  75. package/src/controllers/eval.ts +0 -513
  76. package/src/controllers/export-import.ts +0 -166
  77. package/src/controllers/health.ts +0 -36
  78. package/src/controllers/local-a2a-templates.ts +0 -18
  79. package/src/controllers/local-a2a.ts +0 -149
  80. package/src/controllers/mcp-configs.ts +0 -673
  81. package/src/controllers/memory.ts +0 -257
  82. package/src/controllers/menu-items.ts +0 -114
  83. package/src/controllers/metrics-configs.ts +0 -1061
  84. package/src/controllers/middleware-types.ts +0 -15
  85. package/src/controllers/models.ts +0 -199
  86. package/src/controllers/personal-assistant.ts +0 -191
  87. package/src/controllers/resources.ts +0 -477
  88. package/src/controllers/run.ts +0 -338
  89. package/src/controllers/sandbox.ts +0 -255
  90. package/src/controllers/schedules.ts +0 -336
  91. package/src/controllers/skills.ts +0 -256
  92. package/src/controllers/tasks.ts +0 -213
  93. package/src/controllers/tenants.ts +0 -121
  94. package/src/controllers/thread_status.ts +0 -66
  95. package/src/controllers/threads.ts +0 -260
  96. package/src/controllers/tools.ts +0 -139
  97. package/src/controllers/users.ts +0 -135
  98. package/src/controllers/workflow-tracking.ts +0 -693
  99. package/src/controllers/workspace.ts +0 -879
  100. package/src/export_registrations/a2a-api-key.registration.ts +0 -89
  101. package/src/export_registrations/agent.registration.ts +0 -80
  102. package/src/export_registrations/binding.registration.ts +0 -106
  103. package/src/export_registrations/channel-installation.registration.ts +0 -88
  104. package/src/export_registrations/collection.registration.ts +0 -85
  105. package/src/export_registrations/connection.registration.ts +0 -97
  106. package/src/export_registrations/database-config.registration.ts +0 -94
  107. package/src/export_registrations/eval.registration.ts +0 -334
  108. package/src/export_registrations/index.ts +0 -31
  109. package/src/export_registrations/mcp-config.registration.ts +0 -97
  110. package/src/export_registrations/menu.registration.ts +0 -91
  111. package/src/export_registrations/metrics-config.registration.ts +0 -94
  112. package/src/export_registrations/skill.registration.ts +0 -88
  113. package/src/export_registrations/task.registration.ts +0 -99
  114. package/src/index.ts +0 -497
  115. package/src/logger/Logger.ts +0 -186
  116. package/src/router/MessageContext.ts +0 -14
  117. package/src/router/MessageRouter.ts +0 -490
  118. package/src/router/middlewares/auditLogger.ts +0 -34
  119. package/src/router/middlewares/deduplication.ts +0 -38
  120. package/src/router/middlewares/index.ts +0 -3
  121. package/src/router/middlewares/rateLimit.ts +0 -46
  122. package/src/routes/__tests__/a2a-test-connection.test.ts +0 -177
  123. package/src/routes/a2a-bridge.ts +0 -266
  124. package/src/routes/a2a-test-connection.ts +0 -126
  125. package/src/routes/a2a.ts +0 -851
  126. package/src/routes/channel-bindings.ts +0 -11
  127. package/src/routes/channel-installations.ts +0 -33
  128. package/src/routes/index.ts +0 -640
  129. package/src/routes/menu-items.ts +0 -10
  130. package/src/routes/resource-routes.ts +0 -35
  131. package/src/routes/stt.ts +0 -102
  132. package/src/schemas/data-query.ts +0 -69
  133. package/src/schemas/index.ts +0 -852
  134. package/src/services/ExportImportService.ts +0 -296
  135. package/src/services/__tests__/ExportImportService.test.ts +0 -130
  136. package/src/services/__tests__/eval-runner.test.ts +0 -131
  137. package/src/services/__tests__/local-a2a.test.ts +0 -124
  138. package/src/services/agent_task_consumer.ts +0 -348
  139. package/src/services/collection_service.ts +0 -258
  140. package/src/services/eval-runner.ts +0 -283
  141. package/src/services/local-a2a.ts +0 -354
  142. package/src/services/queue_service.ts +0 -104
  143. package/src/services/sandbox_service.ts +0 -223
  144. package/src/services/skill_service.ts +0 -97
  145. package/src/services/supabase.ts +0 -60
  146. package/src/swagger.ts +0 -77
  147. package/src/types/index.ts +0 -118
  148. package/src/types/koa.d.ts +0 -7
  149. package/src/utils/__tests__/local-a2a-runtime.test.ts +0 -77
  150. package/src/utils/__tests__/path-containment.test.ts +0 -35
  151. package/src/utils/a2a-url.ts +0 -20
  152. package/src/utils/local-a2a-runtime.ts +0 -107
  153. package/src/utils/mime.ts +0 -44
  154. package/src/utils/path-containment.ts +0 -38
  155. package/tsconfig.json +0 -26
@@ -1,217 +0,0 @@
1
- import { z } from "zod";
2
- import type {
3
- ChannelAdapter,
4
- InboundMessage,
5
- OutboundMessage,
6
- ReplyTarget,
7
- ChannelInstallation,
8
- } from "@axiom-lattice/protocols";
9
- import type { LarkChannelInstallationConfig } from "@axiom-lattice/protocols";
10
- import { parseLarkMessageEvent } from "./parser";
11
- import { Logger } from "../../logger/Logger";
12
- import * as Lark from "@larksuiteoapi/node-sdk";
13
-
14
- const logger = new Logger({ serviceName: "lattice/gateway/lark" });
15
-
16
- // ─── WS connection state ──────────────────────────────────────────────────
17
-
18
- const activeConnections = new Map<string, Lark.WSClient>();
19
-
20
- // ─── WS message parsing (shared with lark-ws) ─────────────────────────────
21
-
22
- interface LarkWSMessageEvent {
23
- sender?: { sender_id?: { open_id?: string } };
24
- message?: {
25
- message_id?: string;
26
- chat_id?: string;
27
- chat_type?: string;
28
- message_type?: string;
29
- content?: string;
30
- };
31
- }
32
-
33
- function parseTextContent(content: string | undefined): string | null {
34
- if (!content) return null;
35
- try {
36
- const parsed = JSON.parse(content) as { text?: string };
37
- return typeof parsed.text === "string" ? parsed.text : null;
38
- } catch { return null; }
39
- }
40
-
41
- function normalizeChatType(chatType: string | undefined): "direct" | "group" {
42
- return chatType === "p2p" ? "direct" : "group";
43
- }
44
-
45
- function wsEventToInbound(
46
- event: LarkWSMessageEvent,
47
- installationId: string,
48
- tenantId: string,
49
- ): InboundMessage | null {
50
- const messageId = event.message?.message_id;
51
- const chatId = event.message?.chat_id;
52
- const openId = event.sender?.sender_id?.open_id;
53
- if (!messageId || !chatId || !openId) return null;
54
- if (event.message?.message_type !== "text") return null;
55
-
56
- const text = parseTextContent(event.message.content);
57
- if (!text) return null;
58
-
59
- const chatType = normalizeChatType(event.message.chat_type);
60
- return {
61
- channel: "lark",
62
- channelInstallationId: installationId,
63
- tenantId,
64
- sender: { id: openId, displayName: undefined },
65
- content: { text, metadata: { chatId, chatType, messageId } },
66
- conversation: { id: chatId, type: chatType },
67
- replyTarget: {
68
- adapterChannel: "lark",
69
- channelInstallationId: installationId,
70
- rawTarget: { chatId, messageId, chatType },
71
- },
72
- };
73
- }
74
-
75
- // ─── Adapter ──────────────────────────────────────────────────────────────
76
-
77
- export const larkConfigSchema = z.object({
78
- appId: z.string(),
79
- appSecret: z.string(),
80
- verificationToken: z.string().optional(),
81
- encryptKey: z.string().optional(),
82
- });
83
-
84
- export const larkChannelAdapter: ChannelAdapter<LarkChannelInstallationConfig> = {
85
- channel: "lark",
86
-
87
- configSchema: larkConfigSchema,
88
-
89
- async receive(
90
- rawPayload: unknown,
91
- installation: ChannelInstallation<LarkChannelInstallationConfig>,
92
- ): Promise<InboundMessage | null> {
93
- const event = parseLarkMessageEvent(rawPayload);
94
- if (!event) return null;
95
-
96
- return {
97
- channel: "lark",
98
- channelInstallationId: installation.id,
99
- tenantId: installation.tenantId,
100
- sender: { id: event.openId, displayName: undefined },
101
- content: {
102
- text: event.text,
103
- metadata: { chatId: event.chatId, chatType: event.chatType, messageId: event.messageId },
104
- },
105
- conversation: { id: event.chatId, type: event.chatType },
106
- replyTarget: {
107
- adapterChannel: "lark",
108
- channelInstallationId: installation.id,
109
- rawTarget: { chatId: event.chatId, messageId: event.messageId, chatType: event.chatType },
110
- },
111
- };
112
- },
113
-
114
- async sendReply(
115
- replyTarget: ReplyTarget,
116
- message: OutboundMessage,
117
- installation: ChannelInstallation<LarkChannelInstallationConfig>,
118
- ): Promise<void> {
119
- const { createLarkSender } = await import("./sender");
120
- const sender = await createLarkSender(installation.config);
121
- await sender.sendTextReply({
122
- chatId: replyTarget.rawTarget.chatId as string,
123
- text: message.text,
124
- });
125
- },
126
-
127
- resolveThreadId(message: InboundMessage, binding: unknown): string {
128
- const date = new Date().toISOString().split("T")[0];
129
- const chatType = message.conversation?.type === "direct" ? "dm" : "group";
130
- const agentId = (binding as { agentId: string }).agentId;
131
- if (chatType === "dm") {
132
- return `lark:dm:${message.sender.id}:${agentId}:${date}`;
133
- }
134
- return `lark:group:${message.conversation?.id ?? "unknown"}:${agentId}:${date}`;
135
- },
136
-
137
- async connect(
138
- installation: ChannelInstallation<LarkChannelInstallationConfig>,
139
- deps?: unknown,
140
- ): Promise<void> {
141
- const { id: installationId, tenantId, config } = installation;
142
-
143
- if (!config.appId || !config.appSecret) {
144
- logger.warn("Lark installation missing credentials, skipping", { installationId });
145
- return;
146
- }
147
-
148
- if (activeConnections.has(installationId)) {
149
- logger.warn("Lark WS already connected for installation, skipping", { installationId });
150
- return;
151
- }
152
-
153
- logger.info("Lark WS client starting", { installationId, tenantId });
154
-
155
- const router = (deps as { router?: { dispatch(msg: InboundMessage): Promise<unknown> } })?.router;
156
-
157
- const eventDispatcher = new Lark.EventDispatcher({}).register({
158
- "im.message.receive_v1": async (data: LarkWSMessageEvent) => {
159
- try {
160
- const inbound = wsEventToInbound(data, installationId, tenantId);
161
- if (!inbound) return;
162
-
163
- logger.info("Lark WS message received", {
164
- installationId,
165
- senderId: inbound.sender.id,
166
- chatId: data.message?.chat_id,
167
- });
168
-
169
- if (router) {
170
- const result = await router.dispatch(inbound);
171
- if (!(result as { success?: boolean }).success) {
172
- logger.warn("Lark WS dispatch failed", {
173
- installationId,
174
- error: (result as { error?: { message?: string } }).error?.message,
175
- });
176
- }
177
- }
178
- } catch (err) {
179
- logger.error("Lark WS event handler error", {
180
- installationId,
181
- error: err instanceof Error ? err.message : String(err),
182
- });
183
- }
184
- },
185
- });
186
-
187
- const client = new Lark.WSClient({
188
- appId: config.appId,
189
- appSecret: config.appSecret,
190
- loggerLevel: Lark.LoggerLevel.info,
191
- });
192
-
193
- await client.start({ eventDispatcher });
194
- activeConnections.set(installationId, client);
195
- logger.info("Lark WS client connected", { installationId });
196
- },
197
-
198
- async disconnect(installationId: string): Promise<void> {
199
- const client = activeConnections.get(installationId);
200
- if (!client) {
201
- logger.warn("Lark WS not connected, nothing to disconnect", { installationId });
202
- return;
203
- }
204
-
205
- logger.info("Lark WS client disconnecting", { installationId });
206
- try {
207
- client.close({ force: true });
208
- } catch (err) {
209
- logger.error("Lark WS client stop error", {
210
- installationId,
211
- error: err instanceof Error ? err.message : String(err),
212
- });
213
- }
214
- activeConnections.delete(installationId);
215
- logger.info("Lark WS client disconnected", { installationId });
216
- },
217
- };
@@ -1,187 +0,0 @@
1
- # Lark Channel Ingress
2
-
3
- This module adds Feishu/Lark message ingress to the existing `@axiom-lattice/gateway` service.
4
-
5
- ## What It Supports
6
-
7
- - inbound Lark event callbacks
8
- - URL verification challenge
9
- - plain and encrypted callback payloads
10
- - v1 and v2 verification token extraction
11
- - text-message parsing
12
- - thread mapping to existing Axiom threads
13
- - reactive text replies sent through the official Lark SDK
14
-
15
- ## Current Scope
16
-
17
- This is the MVP scope:
18
-
19
- - one gateway deployment can handle multiple tenant-scoped Lark installations
20
- - only text messages are supported
21
- - only reactive replies are supported
22
- - identity mapping is persisted in PostgreSQL
23
-
24
- ## Environment Variables
25
-
26
- Required when `LARK_ENABLED=true`:
27
-
28
- ```bash
29
- LARK_ENABLED=true
30
-
31
- DATABASE_URL=postgresql://user:pass@localhost:5432/axiom_lattice
32
- ```
33
-
34
- Lark app credentials are no longer loaded from global environment variables. They are stored per installation in PostgreSQL.
35
-
36
- ## Route
37
-
38
- The webhook route is mounted on the existing gateway service:
39
-
40
- ```text
41
- POST /api/channels/lark/installations/:installationId/events
42
- ```
43
-
44
- ## Start The Gateway
45
-
46
- Development:
47
-
48
- ```bash
49
- pnpm --filter @axiom-lattice/gateway dev
50
- ```
51
-
52
- Production:
53
-
54
- ```bash
55
- pnpm --filter @axiom-lattice/gateway build
56
- pnpm --filter @axiom-lattice/gateway start
57
- ```
58
-
59
- ## Feishu/Lark Console Setup
60
-
61
- In the Feishu developer console:
62
-
63
- 1. Create or open an enterprise self-built app.
64
- 2. Go to the event subscription / callback configuration page.
65
- 3. Set the request URL to:
66
-
67
- ```text
68
- https://your-domain.com/api/channels/lark/installations/<installationId>/events
69
- ```
70
-
71
- 4. Create a matching installation record in PostgreSQL for that tenant. The installation config must include:
72
-
73
- - appId
74
- - appSecret
75
- - Verification Token
76
- - Encrypt Key
77
- - assistantId
78
- - mappingMode
79
-
80
- 5. Configure the same values in the Feishu console:
81
-
82
- - Verification Token
83
- - Encrypt Key
84
-
85
- 6. Subscribe to the message event:
86
-
87
- ```text
88
- im.message.receive_v1
89
- ```
90
-
91
- ## Callback Handling
92
-
93
- This implementation handles the normal Feishu event callback flow, not the message-card callback signature flow.
94
-
95
- Supported verification behavior:
96
-
97
- - v1 callback token: `body.token`
98
- - v2 callback token: `body.header.token`
99
- - encrypted envelope: `body.encrypt`
100
-
101
- If the callback is encrypted, the gateway decrypts it before:
102
-
103
- - checking the verification token
104
- - handling URL verification
105
- - parsing the event body
106
-
107
- ## Identity Mapping
108
-
109
- The mapping mode controls how Lark conversations map to Axiom threads.
110
-
111
- ### `user`
112
-
113
- - one Lark user maps to one Axiom thread
114
- - useful for personal assistant behavior
115
-
116
- ### `group`
117
-
118
- - one Lark chat maps to one Axiom thread
119
- - useful for shared team assistant behavior
120
-
121
- ### `hybrid`
122
-
123
- - direct chats use user isolation
124
- - group chats use group isolation
125
-
126
- This is the recommended default because it matches normal user expectations.
127
-
128
- Each installation also binds the request to:
129
-
130
- - one tenant
131
- - one default assistant
132
- - optional workspace and project IDs
133
-
134
- ## Message Flow
135
-
136
- ```text
137
- Lark callback
138
- -> gateway route
139
- -> decrypt / verify
140
- -> parse text event
141
- -> claim inbound receipt
142
- -> resolve or create thread mapping
143
- -> run assistant on existing gateway/core pipeline
144
- -> aggregate assistant text
145
- -> send reply through official Lark SDK
146
- ```
147
-
148
- ## Local Debugging
149
-
150
- To test locally, the Feishu callback URL must be reachable from Feishu.
151
-
152
- Typical options:
153
-
154
- - a public dev domain
155
- - an internal network tunnel
156
-
157
- Example callback URL:
158
-
159
- ```text
160
- https://your-tunnel.example.com/api/channels/lark/installations/<installationId>/events
161
- ```
162
-
163
- ## What To Check When It Fails
164
-
165
- 1. `LARK_ENABLED` is set to `true`
166
- 2. `DATABASE_URL` is set and reachable
167
- 3. the installation record exists in `lattice_channel_installations`
168
- 4. the installation `appId` / `appSecret` are correct
169
- 5. the installation `verificationToken` matches the Feishu console value
170
- 6. the installation `encryptKey` matches the Feishu console value
171
- 7. the callback URL is publicly reachable
172
- 8. the Feishu app is subscribed to `im.message.receive_v1`
173
-
174
- ## Data Written
175
-
176
- This module writes to PostgreSQL tables created by the gateway migration path:
177
-
178
- - `lattice_channel_installations`
179
- - `channel_identity_mappings`
180
- - `channel_inbound_message_receipts`
181
-
182
- ## Known Limits
183
-
184
- - text only
185
- - one default assistant per installation
186
- - no proactive outbound messaging workflow yet
187
- - no card/file/image handling yet
@@ -1,136 +0,0 @@
1
- import fastify from "fastify";
2
- import { createLarkEventHandler } from "../controller";
3
-
4
- const installation = {
5
- id: "install-1",
6
- tenantId: "tenant-a",
7
- channel: "lark" as const,
8
- config: { appId: "cli_app_1", appSecret: "secret" },
9
- enabled: true,
10
- rejectWhenNoBinding: false,
11
- createdAt: new Date(),
12
- updatedAt: new Date(),
13
- };
14
-
15
- describe("createLarkEventHandler", () => {
16
- it("returns 404 when installation is not found", async () => {
17
- const app = fastify();
18
- const handler = createLarkEventHandler({
19
- installationStore: {
20
- getInstallationById: jest.fn().mockResolvedValue(null),
21
- } as any,
22
- router: { dispatch: jest.fn() } as any,
23
- });
24
-
25
- app.post(
26
- "/api/channels/lark/installations/:installationId/events",
27
- handler,
28
- );
29
-
30
- const response = await app.inject({
31
- method: "POST",
32
- url: "/api/channels/lark/installations/install-1/events",
33
- payload: {},
34
- });
35
-
36
- expect(response.statusCode).toBe(404);
37
- expect(response.json()).toEqual({ success: false, message: "Installation not found" });
38
-
39
- await app.close();
40
- });
41
-
42
- it("returns 404 when installation channel is not lark", async () => {
43
- const app = fastify();
44
- const handler = createLarkEventHandler({
45
- installationStore: {
46
- getInstallationById: jest.fn().mockResolvedValue({ ...installation, channel: "slack" }),
47
- } as any,
48
- router: { dispatch: jest.fn() } as any,
49
- });
50
-
51
- app.post(
52
- "/api/channels/lark/installations/:installationId/events",
53
- handler,
54
- );
55
-
56
- const response = await app.inject({
57
- method: "POST",
58
- url: "/api/channels/lark/installations/install-1/events",
59
- payload: {},
60
- });
61
-
62
- expect(response.statusCode).toBe(404);
63
-
64
- await app.close();
65
- });
66
-
67
- it("returns 200 for non-message events (e.g. url verification)", async () => {
68
- const app = fastify();
69
- const handler = createLarkEventHandler({
70
- installationStore: {
71
- getInstallationById: jest.fn().mockResolvedValue(installation),
72
- } as any,
73
- router: { dispatch: jest.fn() } as any,
74
- });
75
-
76
- app.post(
77
- "/api/channels/lark/installations/:installationId/events",
78
- handler,
79
- );
80
-
81
- const response = await app.inject({
82
- method: "POST",
83
- url: "/api/channels/lark/installations/install-1/events",
84
- payload: { type: "url_verification", challenge: "challenge-token" },
85
- });
86
-
87
- expect(response.statusCode).toBe(200);
88
-
89
- await app.close();
90
- });
91
-
92
- it("dispatches valid message events via router", async () => {
93
- const app = fastify();
94
- const dispatch = jest.fn().mockResolvedValue({ success: true });
95
-
96
- const handler = createLarkEventHandler({
97
- installationStore: {
98
- getInstallationById: jest.fn().mockResolvedValue(installation),
99
- } as any,
100
- router: { dispatch } as any,
101
- });
102
-
103
- app.post(
104
- "/api/channels/lark/installations/:installationId/events",
105
- handler,
106
- );
107
-
108
- const response = await app.inject({
109
- method: "POST",
110
- url: "/api/channels/lark/installations/install-1/events",
111
- payload: {
112
- header: { event_type: "im.message.receive_v1" },
113
- event: {
114
- sender: { sender_id: { open_id: "ou_1" } },
115
- message: {
116
- message_id: "om_1",
117
- chat_id: "oc_1",
118
- chat_type: "p2p",
119
- message_type: "text",
120
- content: '{"text":"hello"}',
121
- },
122
- },
123
- },
124
- });
125
-
126
- expect(response.statusCode).toBe(200);
127
- expect(dispatch).toHaveBeenCalledTimes(1);
128
- expect(dispatch.mock.calls[0][0]).toMatchObject({
129
- channel: "lark",
130
- channelInstallationId: "install-1",
131
- tenantId: "tenant-a",
132
- });
133
-
134
- await app.close();
135
- });
136
- });
@@ -1,118 +0,0 @@
1
- import type { ThreadStore } from "@axiom-lattice/protocols";
2
- import { createChannelThreadMappingService } from "../mapping-service";
3
-
4
- describe("createChannelThreadMappingService", () => {
5
- it("uses a user subject key for direct chats in hybrid mode", async () => {
6
- const mappingStore = {
7
- getMappingBySubject: jest.fn().mockResolvedValue(null),
8
- createMapping: jest.fn().mockResolvedValue({ threadId: "thread-1" }),
9
- };
10
-
11
- const threadStore: Pick<ThreadStore, "createThread"> = {
12
- createThread: jest.fn().mockResolvedValue({ id: "thread-1" }),
13
- };
14
-
15
- const service = createChannelThreadMappingService({
16
- mappingStore,
17
- threadStore,
18
- uuid: () => "thread-1",
19
- });
20
-
21
- const result = await service.getOrCreateThread({
22
- channel: "lark",
23
- channelAppId: "cli_app_1",
24
- tenantId: "tenant-a",
25
- assistantId: "assistant-a",
26
- mappingMode: "hybrid",
27
- openId: "ou_1",
28
- chatId: "oc_1",
29
- chatType: "direct",
30
- messageId: "om_1",
31
- });
32
-
33
- expect(mappingStore.getMappingBySubject).toHaveBeenCalledWith(
34
- expect.objectContaining({
35
- externalSubjectKey:
36
- "lark:cli_app_1:tenant:tenant-a:assistant:assistant-a:user:ou_1",
37
- }),
38
- );
39
- expect(result.threadId).toBe("thread-1");
40
- });
41
-
42
- it("reuses an existing mapping when one already exists", async () => {
43
- const mappingStore = {
44
- getMappingBySubject: jest.fn().mockResolvedValue({ threadId: "thread-existing" }),
45
- createMapping: jest.fn(),
46
- };
47
-
48
- const threadStore: Pick<ThreadStore, "createThread"> = {
49
- createThread: jest.fn(),
50
- };
51
-
52
- const service = createChannelThreadMappingService({
53
- mappingStore,
54
- threadStore,
55
- uuid: () => "thread-new",
56
- });
57
-
58
- const result = await service.getOrCreateThread({
59
- channel: "lark",
60
- channelAppId: "cli_app_1",
61
- tenantId: "tenant-a",
62
- assistantId: "assistant-a",
63
- mappingMode: "group",
64
- openId: "ou_1",
65
- chatId: "oc_group_1",
66
- chatType: "group",
67
- messageId: "om_2",
68
- });
69
-
70
- expect(result.threadId).toBe("thread-existing");
71
- expect(threadStore.createThread).not.toHaveBeenCalled();
72
- expect(mappingStore.createMapping).not.toHaveBeenCalled();
73
- });
74
-
75
- it("re-reads the canonical mapping when createMapping hits a unique conflict", async () => {
76
- const mappingStore = {
77
- getMappingBySubject: jest
78
- .fn()
79
- .mockResolvedValueOnce(null)
80
- .mockResolvedValueOnce({ threadId: "thread-canonical" }),
81
- createMapping: jest.fn().mockRejectedValue(
82
- Object.assign(new Error("duplicate key value violates unique constraint"), {
83
- code: "23505",
84
- }),
85
- ),
86
- };
87
-
88
- const threadStore: Pick<ThreadStore, "createThread" | "deleteThread"> = {
89
- createThread: jest.fn().mockResolvedValue({ id: "thread-new" }),
90
- deleteThread: jest.fn().mockResolvedValue(true),
91
- };
92
-
93
- const service = createChannelThreadMappingService({
94
- mappingStore,
95
- threadStore,
96
- uuid: () => "thread-new",
97
- });
98
-
99
- const result = await service.getOrCreateThread({
100
- channel: "lark",
101
- channelAppId: "cli_app_1",
102
- tenantId: "tenant-a",
103
- assistantId: "assistant-a",
104
- mappingMode: "hybrid",
105
- openId: "ou_1",
106
- chatId: "oc_1",
107
- chatType: "direct",
108
- messageId: "om_1",
109
- });
110
-
111
- expect(result.threadId).toBe("thread-canonical");
112
- expect(mappingStore.getMappingBySubject).toHaveBeenCalledTimes(2);
113
- expect(threadStore.deleteThread).toHaveBeenCalledWith(
114
- "tenant-a",
115
- "thread-new",
116
- );
117
- });
118
- });