@axiom-lattice/gateway 3.0.2 → 3.0.4
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.
- package/dist/index.js +37 -9
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +37 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -6
- package/.eslintrc.json +0 -22
- package/.turbo/turbo-build.log +0 -46
- package/AGENTS.md +0 -112
- package/CHANGELOG.md +0 -1190
- package/LOGGER_CONFIG.md +0 -135
- package/RESUME_STREAM_CONTENT_BASED.md +0 -326
- package/RESUME_STREAM_README.md +0 -388
- package/jest.config.js +0 -72
- package/public/sdk/README.md +0 -695
- package/public/sdk/dashboard-engine-skill.md +0 -1122
- package/public/sdk/dashboard-single-file-spec.md +0 -357
- package/public/sdk/data-query-sdk-skill.md +0 -307
- package/public/sdk/data-query-sdk.d.ts +0 -252
- package/public/sdk/data-query-sdk.js +0 -970
- package/public/sdk/occupancy-dashboard.html +0 -363
- package/public/sdk/test-dashboard.html +0 -690
- package/scripts/seed-lark-installation.ts +0 -54
- package/src/__tests__/__mocks__/e2b.ts +0 -1
- package/src/__tests__/a2a.test.ts +0 -428
- package/src/__tests__/channel-installations.test.ts +0 -199
- package/src/__tests__/data-query.test.ts +0 -77
- package/src/__tests__/sandbox-provider-registration.test.ts +0 -74
- package/src/__tests__/setup.ts +0 -5
- package/src/__tests__/workspace.test.ts +0 -400
- package/src/channels/__tests__/routes.test.ts +0 -50
- package/src/channels/lark/LarkChannelAdapter.ts +0 -217
- package/src/channels/lark/README.md +0 -187
- package/src/channels/lark/__tests__/controller.test.ts +0 -136
- package/src/channels/lark/__tests__/mapping-service.test.ts +0 -118
- package/src/channels/lark/__tests__/parser.test.ts +0 -72
- package/src/channels/lark/__tests__/sender.test.ts +0 -37
- package/src/channels/lark/__tests__/verification.test.ts +0 -157
- package/src/channels/lark/controller.ts +0 -48
- package/src/channels/lark/mapping-service.ts +0 -138
- package/src/channels/lark/parser.ts +0 -68
- package/src/channels/lark/routes.ts +0 -22
- package/src/channels/lark/sender.ts +0 -58
- package/src/channels/lark/types.ts +0 -20
- package/src/channels/lark/verification.ts +0 -32
- package/src/channels/registry.ts +0 -20
- package/src/channels/routes.ts +0 -30
- package/src/channels/wechat/WechatChannelAdapter.ts +0 -294
- package/src/channels/wechat/__tests__/context-store.test.ts +0 -148
- package/src/channels/wechat/context-store.ts +0 -54
- package/src/channels/wechat/controller.ts +0 -59
- package/src/channels/wechat/routes.ts +0 -7
- package/src/channels/wechat/types.ts +0 -62
- package/src/channels/wechat/wechat-client.ts +0 -162
- package/src/config/local-a2a-templates.ts +0 -59
- package/src/config.ts +0 -124
- package/src/controllers/__tests__/assistant-local-a2a.test.ts +0 -147
- package/src/controllers/__tests__/local-a2a-templates.test.ts +0 -112
- package/src/controllers/__tests__/mime.test.ts +0 -74
- package/src/controllers/__tests__/resources.test.ts +0 -122
- package/src/controllers/__tests__/run.test.ts +0 -115
- package/src/controllers/__tests__/tasks.test.ts +0 -215
- package/src/controllers/__tests__/workflow-tracking.test.ts +0 -113
- package/src/controllers/agent_task.ts +0 -72
- package/src/controllers/assistant.ts +0 -490
- package/src/controllers/auth.ts +0 -524
- package/src/controllers/channel-bindings.ts +0 -135
- package/src/controllers/channel-installations.ts +0 -409
- package/src/controllers/collections.ts +0 -176
- package/src/controllers/config.ts +0 -126
- package/src/controllers/connections.ts +0 -181
- package/src/controllers/data-query.ts +0 -236
- package/src/controllers/database-configs.ts +0 -440
- package/src/controllers/embeddings.ts +0 -22
- package/src/controllers/entries.ts +0 -167
- package/src/controllers/eval.ts +0 -513
- package/src/controllers/export-import.ts +0 -166
- package/src/controllers/health.ts +0 -36
- package/src/controllers/local-a2a-templates.ts +0 -18
- package/src/controllers/local-a2a.ts +0 -149
- package/src/controllers/mcp-configs.ts +0 -673
- package/src/controllers/memory.ts +0 -257
- package/src/controllers/menu-items.ts +0 -114
- package/src/controllers/metrics-configs.ts +0 -1061
- package/src/controllers/middleware-types.ts +0 -15
- package/src/controllers/models.ts +0 -199
- package/src/controllers/personal-assistant.ts +0 -191
- package/src/controllers/resources.ts +0 -477
- package/src/controllers/run.ts +0 -338
- package/src/controllers/sandbox.ts +0 -255
- package/src/controllers/schedules.ts +0 -336
- package/src/controllers/skills.ts +0 -256
- package/src/controllers/tasks.ts +0 -213
- package/src/controllers/tenants.ts +0 -121
- package/src/controllers/thread_status.ts +0 -66
- package/src/controllers/threads.ts +0 -260
- package/src/controllers/tools.ts +0 -139
- package/src/controllers/users.ts +0 -135
- package/src/controllers/workflow-tracking.ts +0 -693
- package/src/controllers/workspace.ts +0 -879
- package/src/export_registrations/a2a-api-key.registration.ts +0 -89
- package/src/export_registrations/agent.registration.ts +0 -80
- package/src/export_registrations/binding.registration.ts +0 -106
- package/src/export_registrations/channel-installation.registration.ts +0 -88
- package/src/export_registrations/collection.registration.ts +0 -85
- package/src/export_registrations/connection.registration.ts +0 -97
- package/src/export_registrations/database-config.registration.ts +0 -94
- package/src/export_registrations/eval.registration.ts +0 -334
- package/src/export_registrations/index.ts +0 -31
- package/src/export_registrations/mcp-config.registration.ts +0 -97
- package/src/export_registrations/menu.registration.ts +0 -91
- package/src/export_registrations/metrics-config.registration.ts +0 -94
- package/src/export_registrations/skill.registration.ts +0 -88
- package/src/export_registrations/task.registration.ts +0 -99
- package/src/index.ts +0 -497
- package/src/logger/Logger.ts +0 -186
- package/src/router/MessageContext.ts +0 -14
- package/src/router/MessageRouter.ts +0 -490
- package/src/router/middlewares/auditLogger.ts +0 -34
- package/src/router/middlewares/deduplication.ts +0 -38
- package/src/router/middlewares/index.ts +0 -3
- package/src/router/middlewares/rateLimit.ts +0 -46
- package/src/routes/__tests__/a2a-test-connection.test.ts +0 -177
- package/src/routes/a2a-bridge.ts +0 -266
- package/src/routes/a2a-test-connection.ts +0 -126
- package/src/routes/a2a.ts +0 -851
- package/src/routes/channel-bindings.ts +0 -11
- package/src/routes/channel-installations.ts +0 -33
- package/src/routes/index.ts +0 -640
- package/src/routes/menu-items.ts +0 -10
- package/src/routes/resource-routes.ts +0 -35
- package/src/routes/stt.ts +0 -102
- package/src/schemas/data-query.ts +0 -69
- package/src/schemas/index.ts +0 -852
- package/src/services/ExportImportService.ts +0 -296
- package/src/services/__tests__/ExportImportService.test.ts +0 -130
- package/src/services/__tests__/eval-runner.test.ts +0 -131
- package/src/services/__tests__/local-a2a.test.ts +0 -124
- package/src/services/agent_task_consumer.ts +0 -348
- package/src/services/collection_service.ts +0 -258
- package/src/services/eval-runner.ts +0 -283
- package/src/services/local-a2a.ts +0 -354
- package/src/services/queue_service.ts +0 -104
- package/src/services/sandbox_service.ts +0 -223
- package/src/services/skill_service.ts +0 -97
- package/src/services/supabase.ts +0 -60
- package/src/swagger.ts +0 -77
- package/src/types/index.ts +0 -118
- package/src/types/koa.d.ts +0 -7
- package/src/utils/__tests__/local-a2a-runtime.test.ts +0 -77
- package/src/utils/__tests__/path-containment.test.ts +0 -35
- package/src/utils/a2a-url.ts +0 -20
- package/src/utils/local-a2a-runtime.ts +0 -107
- package/src/utils/mime.ts +0 -44
- package/src/utils/path-containment.ts +0 -38
- package/tsconfig.json +0 -26
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Seed script: creates a Lark channel installation in the local SQLite store.
|
|
3
|
-
*
|
|
4
|
-
* Usage:
|
|
5
|
-
* pnpm --filter @axiom-lattice/gateway exec tsx scripts/seed-lark-installation.ts
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
initDatabase,
|
|
10
|
-
getDatabase,
|
|
11
|
-
LocalChannelInstallationStore,
|
|
12
|
-
closeDatabase,
|
|
13
|
-
} from "@axiom-lattice/local-stores";
|
|
14
|
-
|
|
15
|
-
const APP_ID = "cli_a92dc36def309cca";
|
|
16
|
-
const APP_SECRET = "hNTWhEfti4Y7JIq8JA46Af7WNPcAclGa";
|
|
17
|
-
const INSTALLATION_ID = "lark-main";
|
|
18
|
-
const TENANT_ID = "default";
|
|
19
|
-
|
|
20
|
-
async function main(): Promise<void> {
|
|
21
|
-
const dbPath = process.env.LOCAL_DB_PATH || "~/.axiom/lattice.db";
|
|
22
|
-
|
|
23
|
-
console.log(`[seed] Initializing SQLite at ${dbPath}`);
|
|
24
|
-
await initDatabase({ dbPath });
|
|
25
|
-
|
|
26
|
-
const db = getDatabase();
|
|
27
|
-
const store = new LocalChannelInstallationStore(db);
|
|
28
|
-
|
|
29
|
-
console.log(`[seed] Creating Lark installation: ${INSTALLATION_ID}`);
|
|
30
|
-
const installation = await store.createInstallation(
|
|
31
|
-
TENANT_ID,
|
|
32
|
-
INSTALLATION_ID,
|
|
33
|
-
{
|
|
34
|
-
channel: "lark",
|
|
35
|
-
config: {
|
|
36
|
-
appId: APP_ID,
|
|
37
|
-
appSecret: APP_SECRET,
|
|
38
|
-
},
|
|
39
|
-
enabled: true,
|
|
40
|
-
rejectWhenNoBinding: false,
|
|
41
|
-
},
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
console.log(`[seed] Installation created:`);
|
|
45
|
-
console.log(JSON.stringify(installation, null, 2));
|
|
46
|
-
|
|
47
|
-
await closeDatabase();
|
|
48
|
-
console.log(`[seed] Done. Installation persisted to ${dbPath}`);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
main().catch((err) => {
|
|
52
|
-
console.error("[seed] Failed:", err);
|
|
53
|
-
process.exit(1);
|
|
54
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const Sandbox = jest.fn();
|
|
@@ -1,428 +0,0 @@
|
|
|
1
|
-
import { FastifyInstance } from "fastify";
|
|
2
|
-
import fastify from "fastify";
|
|
3
|
-
import { registerA2ARoutes } from "../routes/a2a";
|
|
4
|
-
|
|
5
|
-
const mockChunkStream = jest.fn();
|
|
6
|
-
const mockAddMessage = jest.fn();
|
|
7
|
-
const mockAbort = jest.fn();
|
|
8
|
-
|
|
9
|
-
const mockAgent = {
|
|
10
|
-
addMessage: mockAddMessage,
|
|
11
|
-
chunkStream: mockChunkStream,
|
|
12
|
-
abort: mockAbort,
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const mockGetAgent = jest.fn(() => mockAgent);
|
|
16
|
-
|
|
17
|
-
jest.mock("@axiom-lattice/core", () => ({
|
|
18
|
-
agentInstanceManager: {
|
|
19
|
-
getAgent: (...args: unknown[]) => mockGetAgent(...args),
|
|
20
|
-
},
|
|
21
|
-
}));
|
|
22
|
-
|
|
23
|
-
describe("A2A Routes", () => {
|
|
24
|
-
let app: FastifyInstance;
|
|
25
|
-
|
|
26
|
-
beforeEach(() => {
|
|
27
|
-
mockGetAgent.mockClear();
|
|
28
|
-
mockAddMessage.mockClear();
|
|
29
|
-
mockChunkStream.mockClear();
|
|
30
|
-
mockAbort.mockClear();
|
|
31
|
-
mockGetAgent.mockReturnValue(mockAgent);
|
|
32
|
-
|
|
33
|
-
process.env.A2A_API_KEYS = "valid-key:tenant-a";
|
|
34
|
-
process.env.A2A_DEFAULT_AGENT_ID = "agent-1";
|
|
35
|
-
process.env.A2A_DEFAULT_TENANT_ID = "default-tenant";
|
|
36
|
-
delete process.env.A2A_DEFAULT_PROJECT_ID;
|
|
37
|
-
delete process.env.A2A_DEFAULT_WORKSPACE_ID;
|
|
38
|
-
|
|
39
|
-
app = fastify();
|
|
40
|
-
registerA2ARoutes(app);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
afterEach(async () => {
|
|
44
|
-
await app.close();
|
|
45
|
-
delete process.env.A2A_API_KEYS;
|
|
46
|
-
delete process.env.A2A_DEFAULT_AGENT_ID;
|
|
47
|
-
delete process.env.A2A_DEFAULT_TENANT_ID;
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// ─── Agent Card ──────────────────────────────────────────────────────
|
|
51
|
-
|
|
52
|
-
describe("GET /api/a2a/.well-known/agent.json", () => {
|
|
53
|
-
it("should return agent card without auth", async () => {
|
|
54
|
-
const response = await app.inject({
|
|
55
|
-
method: "GET",
|
|
56
|
-
url: "/api/a2a/.well-known/agent.json",
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
expect(response.statusCode).toBe(200);
|
|
60
|
-
const body = JSON.parse(response.body);
|
|
61
|
-
expect(body).toHaveProperty("name", "Axiom Lattice Agent");
|
|
62
|
-
expect(body).toHaveProperty("capabilities");
|
|
63
|
-
expect(body.capabilities).toHaveProperty("streaming", true);
|
|
64
|
-
expect(body).toHaveProperty("defaultInputModes");
|
|
65
|
-
expect(body).toHaveProperty("defaultOutputModes");
|
|
66
|
-
expect(body).toHaveProperty("url");
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
describe("GET /api/a2a/.well-known/agent-card.json", () => {
|
|
71
|
-
it("should support alternate path", async () => {
|
|
72
|
-
const response = await app.inject({
|
|
73
|
-
method: "GET",
|
|
74
|
-
url: "/api/a2a/.well-known/agent-card.json",
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
expect(response.statusCode).toBe(200);
|
|
78
|
-
const body = JSON.parse(response.body);
|
|
79
|
-
expect(body).toHaveProperty("name", "Axiom Lattice Agent");
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// ─── Agent Card with env override ────────────────────────────────────
|
|
84
|
-
|
|
85
|
-
describe("Agent Card with custom env", () => {
|
|
86
|
-
it("should reflect custom agent name from env", async () => {
|
|
87
|
-
process.env.A2A_AGENT_NAME = "Custom Bot";
|
|
88
|
-
process.env.A2A_ORGANIZATION = "Custom Org";
|
|
89
|
-
process.env.A2A_VERSION = "2.0.0";
|
|
90
|
-
|
|
91
|
-
const response = await app.inject({
|
|
92
|
-
method: "GET",
|
|
93
|
-
url: "/api/a2a/.well-known/agent.json",
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
expect(response.statusCode).toBe(200);
|
|
97
|
-
const body = JSON.parse(response.body);
|
|
98
|
-
expect(body.name).toBe("Custom Bot");
|
|
99
|
-
expect(body.provider.organization).toBe("Custom Org");
|
|
100
|
-
expect(body.version).toBe("2.0.0");
|
|
101
|
-
|
|
102
|
-
delete process.env.A2A_AGENT_NAME;
|
|
103
|
-
delete process.env.A2A_ORGANIZATION;
|
|
104
|
-
delete process.env.A2A_VERSION;
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
// ─── Task Send ───────────────────────────────────────────────────────
|
|
109
|
-
|
|
110
|
-
describe("POST /api/a2a/tasks/send", () => {
|
|
111
|
-
it("should return 400 when message.parts is missing", async () => {
|
|
112
|
-
const response = await app.inject({
|
|
113
|
-
method: "POST",
|
|
114
|
-
url: "/api/a2a/tasks/send",
|
|
115
|
-
headers: {
|
|
116
|
-
authorization: "Bearer valid-key",
|
|
117
|
-
"content-type": "application/json",
|
|
118
|
-
},
|
|
119
|
-
payload: { message: {} },
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
expect(response.statusCode).toBe(400);
|
|
123
|
-
const body = JSON.parse(response.body);
|
|
124
|
-
expect(body.error).toBe("message.parts is required");
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("should return 400 when message text is empty", async () => {
|
|
128
|
-
const response = await app.inject({
|
|
129
|
-
method: "POST",
|
|
130
|
-
url: "/api/a2a/tasks/send",
|
|
131
|
-
headers: {
|
|
132
|
-
authorization: "Bearer valid-key",
|
|
133
|
-
"content-type": "application/json",
|
|
134
|
-
},
|
|
135
|
-
payload: {
|
|
136
|
-
message: {
|
|
137
|
-
role: "user",
|
|
138
|
-
parts: [{ type: "text", text: "" }],
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
expect(response.statusCode).toBe(400);
|
|
144
|
-
const body = JSON.parse(response.body);
|
|
145
|
-
expect(body.error).toBe("message must contain text content");
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("should return 500 when no agent is configured", async () => {
|
|
149
|
-
delete process.env.A2A_DEFAULT_AGENT_ID;
|
|
150
|
-
|
|
151
|
-
const response = await app.inject({
|
|
152
|
-
method: "POST",
|
|
153
|
-
url: "/api/a2a/tasks/send",
|
|
154
|
-
headers: {
|
|
155
|
-
authorization: "Bearer valid-key",
|
|
156
|
-
"content-type": "application/json",
|
|
157
|
-
},
|
|
158
|
-
payload: {
|
|
159
|
-
message: {
|
|
160
|
-
role: "user",
|
|
161
|
-
parts: [{ type: "text", text: "Hello" }],
|
|
162
|
-
},
|
|
163
|
-
},
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
expect(response.statusCode).toBe(500);
|
|
167
|
-
const body = JSON.parse(response.body);
|
|
168
|
-
expect(body.error).toBe("No agent configured");
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it("should return 401 with invalid API key", async () => {
|
|
172
|
-
const response = await app.inject({
|
|
173
|
-
method: "POST",
|
|
174
|
-
url: "/api/a2a/tasks/send",
|
|
175
|
-
headers: {
|
|
176
|
-
authorization: "Bearer wrong-key",
|
|
177
|
-
"content-type": "application/json",
|
|
178
|
-
},
|
|
179
|
-
payload: {
|
|
180
|
-
message: {
|
|
181
|
-
role: "user",
|
|
182
|
-
parts: [{ type: "text", text: "Hello" }],
|
|
183
|
-
},
|
|
184
|
-
},
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
expect(response.statusCode).toBe(401);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it("should stream task result with valid key and message", async () => {
|
|
191
|
-
mockAddMessage.mockResolvedValueOnce({ messageId: "msg-1" });
|
|
192
|
-
|
|
193
|
-
const mockStream = (async function* () {
|
|
194
|
-
yield {
|
|
195
|
-
type: "ai",
|
|
196
|
-
data: { id: "c1", content: "Hello " },
|
|
197
|
-
};
|
|
198
|
-
yield {
|
|
199
|
-
type: "ai",
|
|
200
|
-
data: { id: "c2", content: "World" },
|
|
201
|
-
};
|
|
202
|
-
})();
|
|
203
|
-
mockChunkStream.mockReturnValueOnce(mockStream);
|
|
204
|
-
|
|
205
|
-
const response = await app.inject({
|
|
206
|
-
method: "POST",
|
|
207
|
-
url: "/api/a2a/tasks/send",
|
|
208
|
-
headers: {
|
|
209
|
-
authorization: "Bearer valid-key",
|
|
210
|
-
"content-type": "application/json",
|
|
211
|
-
},
|
|
212
|
-
payload: {
|
|
213
|
-
message: {
|
|
214
|
-
role: "user",
|
|
215
|
-
parts: [{ type: "text", text: "Hello World" }],
|
|
216
|
-
},
|
|
217
|
-
},
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
expect(response.statusCode).toBe(200);
|
|
221
|
-
expect(response.headers["content-type"]).toBe("text/event-stream");
|
|
222
|
-
|
|
223
|
-
// Verify SSE events were emitted
|
|
224
|
-
const body = response.body;
|
|
225
|
-
expect(body).toContain("event: status-update");
|
|
226
|
-
expect(body).toContain('"state":"working"');
|
|
227
|
-
expect(body).toContain("Hello ");
|
|
228
|
-
expect(body).toContain("World");
|
|
229
|
-
expect(body).toContain('"state":"completed"');
|
|
230
|
-
expect(body).toContain('"final":true');
|
|
231
|
-
|
|
232
|
-
// Verify agent was called with correct scope from API key
|
|
233
|
-
expect(mockGetAgent).toHaveBeenCalledWith(
|
|
234
|
-
expect.objectContaining({
|
|
235
|
-
assistant_id: "agent-1",
|
|
236
|
-
tenant_id: "tenant-a",
|
|
237
|
-
}),
|
|
238
|
-
);
|
|
239
|
-
|
|
240
|
-
// Verify agent was called with workspace/project from API key
|
|
241
|
-
// tenant-a has no project/workspace, so they should be undefined
|
|
242
|
-
const getAgentCall = mockGetAgent.mock.calls[0][0];
|
|
243
|
-
expect(getAgentCall.project_id).toBeUndefined();
|
|
244
|
-
expect(getAgentCall.workspace_id).toBeUndefined();
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it("should propagate project scope from API key", async () => {
|
|
248
|
-
process.env.A2A_API_KEYS = "scoped-key:tenant-a:proj-x:ws-y";
|
|
249
|
-
|
|
250
|
-
mockAddMessage.mockResolvedValueOnce({ messageId: "msg-1" });
|
|
251
|
-
const mockStream = (async function* () {
|
|
252
|
-
yield { type: "ai", data: { id: "c1", content: "ok" } };
|
|
253
|
-
})();
|
|
254
|
-
mockChunkStream.mockReturnValueOnce(mockStream);
|
|
255
|
-
|
|
256
|
-
const response = await app.inject({
|
|
257
|
-
method: "POST",
|
|
258
|
-
url: "/api/a2a/tasks/send",
|
|
259
|
-
headers: {
|
|
260
|
-
authorization: "Bearer scoped-key",
|
|
261
|
-
"content-type": "application/json",
|
|
262
|
-
},
|
|
263
|
-
payload: {
|
|
264
|
-
message: {
|
|
265
|
-
role: "user",
|
|
266
|
-
parts: [{ type: "text", text: "Hello" }],
|
|
267
|
-
},
|
|
268
|
-
},
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
expect(response.statusCode).toBe(200);
|
|
272
|
-
|
|
273
|
-
const getAgentCall = mockGetAgent.mock.calls[0][0];
|
|
274
|
-
expect(getAgentCall.tenant_id).toBe("tenant-a");
|
|
275
|
-
expect(getAgentCall.project_id).toBe("proj-x");
|
|
276
|
-
expect(getAgentCall.workspace_id).toBe("ws-y");
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
it("should create fresh internal threads for each new task with same contextId", async () => {
|
|
280
|
-
const contextId = "ctx-multi-turn";
|
|
281
|
-
|
|
282
|
-
mockAddMessage.mockResolvedValue({ messageId: "msg-1" });
|
|
283
|
-
const shortStream = (async function* () {
|
|
284
|
-
yield { type: "ai", data: { id: "c1", content: "response" } };
|
|
285
|
-
})();
|
|
286
|
-
mockChunkStream.mockReturnValue(shortStream);
|
|
287
|
-
|
|
288
|
-
// First request
|
|
289
|
-
await app.inject({
|
|
290
|
-
method: "POST",
|
|
291
|
-
url: "/api/a2a/tasks/send",
|
|
292
|
-
headers: {
|
|
293
|
-
authorization: "Bearer valid-key",
|
|
294
|
-
"content-type": "application/json",
|
|
295
|
-
},
|
|
296
|
-
payload: {
|
|
297
|
-
message: {
|
|
298
|
-
role: "user",
|
|
299
|
-
parts: [{ type: "text", text: "First message" }],
|
|
300
|
-
contextId,
|
|
301
|
-
},
|
|
302
|
-
},
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
const threadId1 = mockGetAgent.mock.calls[0][0].thread_id;
|
|
306
|
-
|
|
307
|
-
mockChunkStream.mockReturnValue(shortStream);
|
|
308
|
-
|
|
309
|
-
// Second request — same contextId, should get a different thread
|
|
310
|
-
await app.inject({
|
|
311
|
-
method: "POST",
|
|
312
|
-
url: "/api/a2a/tasks/send",
|
|
313
|
-
headers: {
|
|
314
|
-
authorization: "Bearer valid-key",
|
|
315
|
-
"content-type": "application/json",
|
|
316
|
-
},
|
|
317
|
-
payload: {
|
|
318
|
-
message: {
|
|
319
|
-
role: "user",
|
|
320
|
-
parts: [{ type: "text", text: "Follow up" }],
|
|
321
|
-
contextId,
|
|
322
|
-
},
|
|
323
|
-
},
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
const threadId2 = mockGetAgent.mock.calls[1][0].thread_id;
|
|
327
|
-
|
|
328
|
-
expect(threadId1).toBeDefined();
|
|
329
|
-
expect(threadId2).toBeDefined();
|
|
330
|
-
expect(threadId1).not.toBe(threadId2);
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
it("should accept referenceTaskIds in the message without error", async () => {
|
|
334
|
-
mockAddMessage.mockResolvedValueOnce({ messageId: "msg-1" });
|
|
335
|
-
const mockStream = (async function* () {
|
|
336
|
-
yield { type: "ai", data: { id: "c1", content: "ok" } };
|
|
337
|
-
})();
|
|
338
|
-
mockChunkStream.mockReturnValueOnce(mockStream);
|
|
339
|
-
|
|
340
|
-
const response = await app.inject({
|
|
341
|
-
method: "POST",
|
|
342
|
-
url: "/api/a2a/tasks/send",
|
|
343
|
-
headers: {
|
|
344
|
-
authorization: "Bearer valid-key",
|
|
345
|
-
"content-type": "application/json",
|
|
346
|
-
},
|
|
347
|
-
payload: {
|
|
348
|
-
message: {
|
|
349
|
-
role: "user",
|
|
350
|
-
parts: [{ type: "text", text: "Follow up" }],
|
|
351
|
-
referenceTaskIds: ["task-prev-1"],
|
|
352
|
-
},
|
|
353
|
-
},
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
expect(response.statusCode).toBe(200);
|
|
357
|
-
expect(response.headers["content-type"]).toBe("text/event-stream");
|
|
358
|
-
expect(response.body).toContain('"state":"completed"');
|
|
359
|
-
});
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
// ─── Task Cancel ─────────────────────────────────────────────────────
|
|
363
|
-
|
|
364
|
-
describe("POST /api/a2a/tasks/:taskId/cancel", () => {
|
|
365
|
-
it("should return 404 for unknown task", async () => {
|
|
366
|
-
const response = await app.inject({
|
|
367
|
-
method: "POST",
|
|
368
|
-
url: "/api/a2a/tasks/nonexistent/cancel",
|
|
369
|
-
headers: {
|
|
370
|
-
authorization: "Bearer valid-key",
|
|
371
|
-
"content-type": "application/json",
|
|
372
|
-
},
|
|
373
|
-
payload: {},
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
expect(response.statusCode).toBe(404);
|
|
377
|
-
});
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
// ─── Task Get ────────────────────────────────────────────────────────
|
|
381
|
-
|
|
382
|
-
describe("GET /api/a2a/tasks/:taskId", () => {
|
|
383
|
-
it("should return 404 for unknown task", async () => {
|
|
384
|
-
const response = await app.inject({
|
|
385
|
-
method: "GET",
|
|
386
|
-
url: "/api/a2a/tasks/nonexistent",
|
|
387
|
-
headers: {
|
|
388
|
-
authorization: "Bearer valid-key",
|
|
389
|
-
"content-type": "application/json",
|
|
390
|
-
},
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
expect(response.statusCode).toBe(404);
|
|
394
|
-
});
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
// ─── No auth mode ────────────────────────────────────────────────────
|
|
398
|
-
|
|
399
|
-
describe("when no API keys configured", () => {
|
|
400
|
-
beforeEach(() => {
|
|
401
|
-
delete process.env.A2A_API_KEYS;
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
it("should accept requests without auth", async () => {
|
|
405
|
-
mockAddMessage.mockResolvedValueOnce({ messageId: "msg-1" });
|
|
406
|
-
const mockStream = (async function* () {
|
|
407
|
-
yield { type: "ai", data: { id: "c1", content: "Hi" } };
|
|
408
|
-
})();
|
|
409
|
-
mockChunkStream.mockReturnValueOnce(mockStream);
|
|
410
|
-
|
|
411
|
-
const response = await app.inject({
|
|
412
|
-
method: "POST",
|
|
413
|
-
url: "/api/a2a/tasks/send",
|
|
414
|
-
headers: {
|
|
415
|
-
"content-type": "application/json",
|
|
416
|
-
},
|
|
417
|
-
payload: {
|
|
418
|
-
message: {
|
|
419
|
-
role: "user",
|
|
420
|
-
parts: [{ type: "text", text: "Hello" }],
|
|
421
|
-
},
|
|
422
|
-
},
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
expect(response.statusCode).toBe(200);
|
|
426
|
-
});
|
|
427
|
-
});
|
|
428
|
-
});
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
import { FastifyInstance } from "fastify";
|
|
2
|
-
import fastify from "fastify";
|
|
3
|
-
import { registerChannelInstallationRoutes } from "../routes/channel-installations";
|
|
4
|
-
|
|
5
|
-
// Set DATABASE_URL before importing controller
|
|
6
|
-
process.env.DATABASE_URL = "postgres://test:test@localhost:5432/test";
|
|
7
|
-
|
|
8
|
-
// Mock the store
|
|
9
|
-
const mockGetInstallationsByTenant = jest.fn();
|
|
10
|
-
const mockGetInstallationById = jest.fn();
|
|
11
|
-
const mockCreateInstallation = jest.fn();
|
|
12
|
-
const mockUpdateInstallation = jest.fn();
|
|
13
|
-
const mockDeleteInstallation = jest.fn();
|
|
14
|
-
|
|
15
|
-
jest.mock("@axiom-lattice/pg-stores", () => ({
|
|
16
|
-
PostgreSQLChannelInstallationStore: jest.fn().mockImplementation(() => ({
|
|
17
|
-
getInstallationsByTenant: mockGetInstallationsByTenant,
|
|
18
|
-
getInstallationById: mockGetInstallationById,
|
|
19
|
-
createInstallation: mockCreateInstallation,
|
|
20
|
-
updateInstallation: mockUpdateInstallation,
|
|
21
|
-
deleteInstallation: mockDeleteInstallation,
|
|
22
|
-
})),
|
|
23
|
-
}));
|
|
24
|
-
|
|
25
|
-
describe("Channel Installation API", () => {
|
|
26
|
-
let app: FastifyInstance;
|
|
27
|
-
|
|
28
|
-
beforeEach(() => {
|
|
29
|
-
// Reset mocks
|
|
30
|
-
mockGetInstallationsByTenant.mockReset();
|
|
31
|
-
mockGetInstallationById.mockReset();
|
|
32
|
-
mockCreateInstallation.mockReset();
|
|
33
|
-
mockUpdateInstallation.mockReset();
|
|
34
|
-
mockDeleteInstallation.mockReset();
|
|
35
|
-
|
|
36
|
-
app = fastify();
|
|
37
|
-
registerChannelInstallationRoutes(app);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
afterEach(async () => {
|
|
41
|
-
await app.close();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
describe("GET /api/channel-installations", () => {
|
|
45
|
-
it("should return list of installations for tenant", async () => {
|
|
46
|
-
mockGetInstallationsByTenant.mockResolvedValueOnce([
|
|
47
|
-
{
|
|
48
|
-
id: "install-1",
|
|
49
|
-
tenantId: "tenant-a",
|
|
50
|
-
channel: "lark",
|
|
51
|
-
name: "Test Installation",
|
|
52
|
-
config: {
|
|
53
|
-
appId: "cli_test",
|
|
54
|
-
appSecret: "secret",
|
|
55
|
-
assistantId: "assistant-1",
|
|
56
|
-
mappingMode: "hybrid",
|
|
57
|
-
},
|
|
58
|
-
createdAt: new Date(),
|
|
59
|
-
updatedAt: new Date(),
|
|
60
|
-
},
|
|
61
|
-
]);
|
|
62
|
-
|
|
63
|
-
const response = await app.inject({
|
|
64
|
-
method: "GET",
|
|
65
|
-
url: "/api/channel-installations",
|
|
66
|
-
headers: {
|
|
67
|
-
"x-tenant-id": "tenant-a",
|
|
68
|
-
},
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
expect(response.statusCode).toBe(200);
|
|
72
|
-
const body = JSON.parse(response.body);
|
|
73
|
-
expect(body.success).toBe(true);
|
|
74
|
-
expect(body.data).toHaveProperty("records");
|
|
75
|
-
expect(body.data).toHaveProperty("total");
|
|
76
|
-
expect(body.data.records).toHaveLength(1);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("should filter by channel type", async () => {
|
|
80
|
-
const response = await app.inject({
|
|
81
|
-
method: "GET",
|
|
82
|
-
url: "/api/channel-installations?channel=lark",
|
|
83
|
-
headers: {
|
|
84
|
-
"x-tenant-id": "tenant-a",
|
|
85
|
-
},
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
expect(response.statusCode).toBe(200);
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
describe("GET /api/channel-installations/:installationId", () => {
|
|
93
|
-
it("should return installation by id", async () => {
|
|
94
|
-
const response = await app.inject({
|
|
95
|
-
method: "GET",
|
|
96
|
-
url: "/api/channel-installations/install-1",
|
|
97
|
-
headers: {
|
|
98
|
-
"x-tenant-id": "tenant-a",
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
// Will return 404 since mock returns null, but route is accessible
|
|
103
|
-
expect([200, 404]).toContain(response.statusCode);
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
describe("POST /api/channel-installations", () => {
|
|
108
|
-
it("should create new installation", async () => {
|
|
109
|
-
mockCreateInstallation.mockResolvedValueOnce({
|
|
110
|
-
id: "new-install-id",
|
|
111
|
-
tenantId: "tenant-a",
|
|
112
|
-
channel: "lark",
|
|
113
|
-
name: "Test Lark Installation",
|
|
114
|
-
config: {
|
|
115
|
-
appId: "cli_test",
|
|
116
|
-
appSecret: "secret",
|
|
117
|
-
assistantId: "assistant-1",
|
|
118
|
-
mappingMode: "hybrid",
|
|
119
|
-
},
|
|
120
|
-
createdAt: new Date(),
|
|
121
|
-
updatedAt: new Date(),
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
const response = await app.inject({
|
|
125
|
-
method: "POST",
|
|
126
|
-
url: "/api/channel-installations",
|
|
127
|
-
headers: {
|
|
128
|
-
"x-tenant-id": "tenant-a",
|
|
129
|
-
"content-type": "application/json",
|
|
130
|
-
},
|
|
131
|
-
payload: {
|
|
132
|
-
channel: "lark",
|
|
133
|
-
name: "Test Lark Installation",
|
|
134
|
-
config: {
|
|
135
|
-
appId: "cli_test",
|
|
136
|
-
appSecret: "secret",
|
|
137
|
-
assistantId: "assistant-1",
|
|
138
|
-
mappingMode: "hybrid",
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
expect(response.statusCode).toBe(201);
|
|
144
|
-
const body = JSON.parse(response.body);
|
|
145
|
-
expect(body.success).toBe(true);
|
|
146
|
-
expect(body.data).toHaveProperty("id");
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("should validate required fields", async () => {
|
|
150
|
-
const response = await app.inject({
|
|
151
|
-
method: "POST",
|
|
152
|
-
url: "/api/channel-installations",
|
|
153
|
-
headers: {
|
|
154
|
-
"x-tenant-id": "tenant-a",
|
|
155
|
-
"content-type": "application/json",
|
|
156
|
-
},
|
|
157
|
-
payload: {
|
|
158
|
-
// Missing channel and config
|
|
159
|
-
},
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
expect(response.statusCode).toBe(400);
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
describe("PUT /api/channel-installations/:installationId", () => {
|
|
167
|
-
it("should update installation", async () => {
|
|
168
|
-
const response = await app.inject({
|
|
169
|
-
method: "PUT",
|
|
170
|
-
url: "/api/channel-installations/install-1",
|
|
171
|
-
headers: {
|
|
172
|
-
"x-tenant-id": "tenant-a",
|
|
173
|
-
"content-type": "application/json",
|
|
174
|
-
},
|
|
175
|
-
payload: {
|
|
176
|
-
name: "Updated Name",
|
|
177
|
-
},
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
// Will return 404 since mock returns null, but route is accessible
|
|
181
|
-
expect([200, 404]).toContain(response.statusCode);
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
describe("DELETE /api/channel-installations/:installationId", () => {
|
|
186
|
-
it("should delete installation", async () => {
|
|
187
|
-
const response = await app.inject({
|
|
188
|
-
method: "DELETE",
|
|
189
|
-
url: "/api/channel-installations/install-1",
|
|
190
|
-
headers: {
|
|
191
|
-
"x-tenant-id": "tenant-a",
|
|
192
|
-
},
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
// Will return 404 since mock returns null, but route is accessible
|
|
196
|
-
expect([200, 404]).toContain(response.statusCode);
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
});
|