@axiom-lattice/gateway 2.1.53 → 2.1.55
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/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +19 -0
- package/dist/index.js +1043 -526
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +977 -449
- package/dist/index.mjs.map +1 -1
- package/jest.config.js +5 -0
- package/package.json +6 -5
- package/src/__tests__/__mocks__/e2b.ts +1 -0
- package/src/__tests__/channel-installations.test.ts +199 -0
- package/src/__tests__/sandbox-provider-registration.test.ts +74 -0
- package/src/__tests__/workspace.test.ts +119 -8
- package/src/channels/__tests__/routes.test.ts +71 -0
- package/src/channels/lark/README.md +187 -0
- package/src/channels/lark/__tests__/aggregator.test.ts +23 -0
- package/src/channels/lark/__tests__/controller.test.ts +270 -0
- package/src/channels/lark/__tests__/mapping-service.test.ts +118 -0
- package/src/channels/lark/__tests__/parser.test.ts +72 -0
- package/src/channels/lark/__tests__/sender.test.ts +37 -0
- package/src/channels/lark/__tests__/verification.test.ts +157 -0
- package/src/channels/lark/aggregator.ts +16 -0
- package/src/channels/lark/config.ts +44 -0
- package/src/channels/lark/controller.ts +189 -0
- package/src/channels/lark/mapping-service.ts +138 -0
- package/src/channels/lark/parser.ts +68 -0
- package/src/channels/lark/routes.ts +121 -0
- package/src/channels/lark/runner.ts +37 -0
- package/src/channels/lark/sender.ts +58 -0
- package/src/channels/lark/types.ts +33 -0
- package/src/channels/lark/verification.ts +67 -0
- package/src/channels/routes.ts +25 -0
- package/src/controllers/channel-installations.ts +354 -0
- package/src/controllers/sandbox.ts +30 -80
- package/src/controllers/skills.ts +71 -321
- package/src/controllers/threads.ts +8 -6
- package/src/controllers/workspace.ts +64 -179
- package/src/index.ts +28 -5
- package/src/routes/channel-installations.ts +33 -0
- package/src/routes/index.ts +6 -0
- package/src/schemas/index.ts +2 -2
- package/src/services/sandbox_service.ts +21 -21
- package/src/services/skill_service.ts +97 -0
package/jest.config.js
CHANGED
|
@@ -12,10 +12,15 @@ module.exports = {
|
|
|
12
12
|
],
|
|
13
13
|
},
|
|
14
14
|
moduleNameMapper: {
|
|
15
|
+
"^@axiom-lattice/core$": "<rootDir>/../core/src/index.ts",
|
|
16
|
+
"^@axiom-lattice/protocols$": "<rootDir>/../protocols/src/index.ts",
|
|
17
|
+
"^@axiom-lattice/pg-stores$": "<rootDir>/../pg-stores/src/index.ts",
|
|
18
|
+
"^@axiom-lattice/queue-redis$": "<rootDir>/../queue-redis/src/index.ts",
|
|
15
19
|
"^@/(.*)$": "<rootDir>/src/$1",
|
|
16
20
|
"^@services/(.*)$": "<rootDir>/src/services/$1",
|
|
17
21
|
"^@utils/(.*)$": "<rootDir>/src/utils/$1",
|
|
18
22
|
"^@types$": "<rootDir>/src/types/index.ts",
|
|
23
|
+
"^e2b$": "<rootDir>/src/__tests__/__mocks__/e2b.ts",
|
|
19
24
|
},
|
|
20
25
|
collectCoverageFrom: ["src/**/*.ts", "!src/**/*.d.ts", "!src/__tests__/**/*"],
|
|
21
26
|
coverageReporters: ["text", "lcov", "html"],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiom-lattice/gateway",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.55",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"module": "dist/index.mjs",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"@fastify/swagger": "^9.5.1",
|
|
26
26
|
"@fastify/swagger-ui": "^5.2.3",
|
|
27
27
|
"@fastify/websocket": "^11.0.1",
|
|
28
|
+
"@larksuiteoapi/node-sdk": "^1.47.0",
|
|
28
29
|
"@langchain/core": "1.1.30",
|
|
29
30
|
"@langchain/langgraph": "1.0.4",
|
|
30
31
|
"@supabase/supabase-js": "^2.49.1",
|
|
@@ -38,10 +39,10 @@
|
|
|
38
39
|
"pg": "^8.11.0",
|
|
39
40
|
"redis": "^5.0.1",
|
|
40
41
|
"uuid": "^9.0.1",
|
|
41
|
-
"@axiom-lattice/core": "2.1.
|
|
42
|
-
"@axiom-lattice/pg-stores": "1.0.
|
|
43
|
-
"@axiom-lattice/protocols": "2.1.
|
|
44
|
-
"@axiom-lattice/queue-redis": "1.0.
|
|
42
|
+
"@axiom-lattice/core": "2.1.49",
|
|
43
|
+
"@axiom-lattice/pg-stores": "1.0.39",
|
|
44
|
+
"@axiom-lattice/protocols": "2.1.26",
|
|
45
|
+
"@axiom-lattice/queue-redis": "1.0.25"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
47
48
|
"@types/jest": "^29.5.14",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const Sandbox = jest.fn();
|
|
@@ -0,0 +1,199 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
|
|
2
|
+
|
|
3
|
+
const mockCreateSandboxProvider = jest.fn();
|
|
4
|
+
const mockRegisterSandboxLattice = jest.fn();
|
|
5
|
+
const mockHasSandboxLattice = jest.fn();
|
|
6
|
+
const mockAppListen = jest.fn();
|
|
7
|
+
const mockAppClose = jest.fn();
|
|
8
|
+
const mockProcessExit = jest.spyOn(process, "exit").mockImplementation((() => undefined) as never);
|
|
9
|
+
|
|
10
|
+
jest.mock("../routes", () => ({
|
|
11
|
+
registerLatticeRoutes: jest.fn(),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
jest.mock("@axiom-lattice/core", () => ({
|
|
15
|
+
createSandboxProvider: mockCreateSandboxProvider,
|
|
16
|
+
registerLoggerLattice: jest.fn(),
|
|
17
|
+
getLoggerLattice: jest.fn(() => ({
|
|
18
|
+
client: {
|
|
19
|
+
info: jest.fn(),
|
|
20
|
+
warn: jest.fn(),
|
|
21
|
+
error: jest.fn(),
|
|
22
|
+
},
|
|
23
|
+
updateContext: jest.fn(),
|
|
24
|
+
})),
|
|
25
|
+
loggerLatticeManager: {
|
|
26
|
+
hasLattice: jest.fn(() => false),
|
|
27
|
+
removeLattice: jest.fn(),
|
|
28
|
+
},
|
|
29
|
+
sandboxLatticeManager: {
|
|
30
|
+
hasLattice: mockHasSandboxLattice,
|
|
31
|
+
registerLattice: mockRegisterSandboxLattice,
|
|
32
|
+
},
|
|
33
|
+
sqlDatabaseManager: {
|
|
34
|
+
setConfigStore: jest.fn(),
|
|
35
|
+
},
|
|
36
|
+
getStoreLattice: jest.fn(() => {
|
|
37
|
+
throw new Error("database store unavailable in test");
|
|
38
|
+
}),
|
|
39
|
+
agentInstanceManager: {
|
|
40
|
+
restore: jest.fn().mockResolvedValue({ restored: 0, errors: 0 }),
|
|
41
|
+
},
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
describe("gateway sandbox provider registration", () => {
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
jest.resetModules();
|
|
47
|
+
jest.clearAllMocks();
|
|
48
|
+
mockHasSandboxLattice.mockReturnValue(false);
|
|
49
|
+
mockCreateSandboxProvider.mockReturnValue({ provider: "mock" });
|
|
50
|
+
process.env.SANDBOX_PROVIDER_TYPE = "microsandbox-remote";
|
|
51
|
+
process.env.MICROSANDBOX_SERVICE_BASE_URL = "http://sandbox-host:4002";
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("uses microsandbox-remote when configured", async () => {
|
|
55
|
+
const { LatticeGateway } = await import("../index");
|
|
56
|
+
|
|
57
|
+
jest.spyOn(LatticeGateway.app, "listen").mockImplementation(mockAppListen);
|
|
58
|
+
jest.spyOn(LatticeGateway.app, "close").mockImplementation(mockAppClose);
|
|
59
|
+
|
|
60
|
+
await LatticeGateway.startAsHttpEndpoint({ port: 0 });
|
|
61
|
+
|
|
62
|
+
expect(mockCreateSandboxProvider).toHaveBeenCalledWith(
|
|
63
|
+
expect.objectContaining({
|
|
64
|
+
type: "microsandbox-remote",
|
|
65
|
+
microsandboxServiceBaseURL: "http://sandbox-host:4002",
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
expect(mockRegisterSandboxLattice).toHaveBeenCalledWith("default", {
|
|
69
|
+
provider: "mock",
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await LatticeGateway.app.close();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -35,8 +35,17 @@ const mockStoreLattice = {
|
|
|
35
35
|
},
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
+
const mockSandbox = {
|
|
39
|
+
file: {
|
|
40
|
+
uploadFile: jest.fn(),
|
|
41
|
+
downloadFile: jest.fn(),
|
|
42
|
+
readFile: jest.fn(),
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
38
46
|
// Mock the dependencies
|
|
39
47
|
jest.mock("@axiom-lattice/core", () => ({
|
|
48
|
+
...jest.requireActual("@axiom-lattice/core"),
|
|
40
49
|
getStoreLattice: jest.fn().mockReturnValue(mockStoreLattice),
|
|
41
50
|
SandboxFilesystem: jest.fn().mockImplementation(() => ({
|
|
42
51
|
lsInfo: jest.fn(),
|
|
@@ -46,14 +55,9 @@ jest.mock("@axiom-lattice/core", () => ({
|
|
|
46
55
|
lsInfo: jest.fn(),
|
|
47
56
|
read: jest.fn(),
|
|
48
57
|
})),
|
|
49
|
-
getSandBoxManager: jest.fn()
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
downloadFile: jest.fn(),
|
|
53
|
-
uploadFile: jest.fn(),
|
|
54
|
-
},
|
|
55
|
-
}),
|
|
56
|
-
}),
|
|
58
|
+
getSandBoxManager: jest.fn(() => ({
|
|
59
|
+
getSandboxFromConfig: jest.fn().mockResolvedValue(mockSandbox),
|
|
60
|
+
})),
|
|
57
61
|
}));
|
|
58
62
|
|
|
59
63
|
// Import after mocks are set up
|
|
@@ -221,6 +225,113 @@ describe("WorkspaceController - Tenant Isolation", () => {
|
|
|
221
225
|
});
|
|
222
226
|
});
|
|
223
227
|
|
|
228
|
+
describe("sandbox storage path passthrough", () => {
|
|
229
|
+
const mockWorkspaceSandbox = {
|
|
230
|
+
id: mockWorkspaceId,
|
|
231
|
+
tenantId: mockTenantId,
|
|
232
|
+
storageType: "sandbox",
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
it("uploadFile should pass path directly to sandbox without baseDir prefix", async () => {
|
|
236
|
+
mockGetWorkspaceById.mockResolvedValue(mockWorkspaceSandbox);
|
|
237
|
+
|
|
238
|
+
// Mock Fastify multipart file
|
|
239
|
+
const mockFileData = {
|
|
240
|
+
toBuffer: jest.fn().mockResolvedValue(Buffer.from("test content")),
|
|
241
|
+
filename: "test.txt",
|
|
242
|
+
fields: {
|
|
243
|
+
path: { value: "~/project" },
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const request = createMockRequest({
|
|
248
|
+
file: jest.fn().mockResolvedValue(mockFileData),
|
|
249
|
+
});
|
|
250
|
+
const reply = {
|
|
251
|
+
status: jest.fn().mockReturnThis(),
|
|
252
|
+
send: jest.fn().mockReturnThis(),
|
|
253
|
+
} as unknown as FastifyReply;
|
|
254
|
+
|
|
255
|
+
await controller.uploadFile(request, reply);
|
|
256
|
+
|
|
257
|
+
// Should use frontend-provided path directly, not /tenants/... baseDir
|
|
258
|
+
expect(mockSandbox.file.uploadFile).toHaveBeenCalledWith({
|
|
259
|
+
file: "~/project/test.txt",
|
|
260
|
+
data: expect.any(Buffer),
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("uploadFile should allow ~ in path validation", async () => {
|
|
265
|
+
mockGetWorkspaceById.mockResolvedValue(mockWorkspaceSandbox);
|
|
266
|
+
|
|
267
|
+
const mockFileData = {
|
|
268
|
+
toBuffer: jest.fn().mockResolvedValue(Buffer.from("test content")),
|
|
269
|
+
filename: "test.txt",
|
|
270
|
+
fields: {
|
|
271
|
+
path: { value: "~/project/docs" },
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const request = createMockRequest({
|
|
276
|
+
file: jest.fn().mockResolvedValue(mockFileData),
|
|
277
|
+
});
|
|
278
|
+
const reply = {
|
|
279
|
+
status: jest.fn().mockReturnThis(),
|
|
280
|
+
send: jest.fn().mockReturnThis(),
|
|
281
|
+
} as unknown as FastifyReply;
|
|
282
|
+
|
|
283
|
+
await controller.uploadFile(request, reply);
|
|
284
|
+
|
|
285
|
+
// Should succeed with ~ in path
|
|
286
|
+
expect(mockSandbox.file.uploadFile).toHaveBeenCalledWith({
|
|
287
|
+
file: "~/project/docs/test.txt",
|
|
288
|
+
data: expect.any(Buffer),
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("downloadFile should pass path directly to sandbox without baseDir prefix", async () => {
|
|
293
|
+
mockGetWorkspaceById.mockResolvedValue(mockWorkspaceSandbox);
|
|
294
|
+
mockSandbox.file.downloadFile.mockResolvedValue(Buffer.from("content"));
|
|
295
|
+
|
|
296
|
+
const request = createMockRequest({
|
|
297
|
+
query: { path: "~/agent/AGENTS.md" },
|
|
298
|
+
});
|
|
299
|
+
const reply = {
|
|
300
|
+
status: jest.fn().mockReturnThis(),
|
|
301
|
+
type: jest.fn().mockReturnThis(),
|
|
302
|
+
header: jest.fn().mockReturnThis(),
|
|
303
|
+
send: jest.fn().mockReturnThis(),
|
|
304
|
+
} as unknown as FastifyReply;
|
|
305
|
+
|
|
306
|
+
await controller.downloadFile(request, reply);
|
|
307
|
+
|
|
308
|
+
expect(mockSandbox.file.downloadFile).toHaveBeenCalledWith({
|
|
309
|
+
file: "~/agent/AGENTS.md",
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("viewFile should pass path directly to sandbox without baseDir prefix", async () => {
|
|
314
|
+
mockGetWorkspaceById.mockResolvedValue(mockWorkspaceSandbox);
|
|
315
|
+
mockSandbox.file.downloadFile.mockResolvedValue(Buffer.from("content"));
|
|
316
|
+
|
|
317
|
+
const request = createMockRequest({
|
|
318
|
+
query: { path: "~/project/index.html" },
|
|
319
|
+
});
|
|
320
|
+
const reply = {
|
|
321
|
+
status: jest.fn().mockReturnThis(),
|
|
322
|
+
type: jest.fn().mockReturnThis(),
|
|
323
|
+
header: jest.fn().mockReturnThis(),
|
|
324
|
+
send: jest.fn().mockReturnThis(),
|
|
325
|
+
} as unknown as FastifyReply;
|
|
326
|
+
|
|
327
|
+
await controller.viewFile(request, reply);
|
|
328
|
+
|
|
329
|
+
expect(mockSandbox.file.downloadFile).toHaveBeenCalledWith({
|
|
330
|
+
file: "~/project/index.html",
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
224
335
|
describe("getTenantId helper", () => {
|
|
225
336
|
it("should extract tenantId from user context when available", () => {
|
|
226
337
|
const request = {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fastify from "fastify";
|
|
2
|
+
import { registerChannelRoutes } from "../routes";
|
|
3
|
+
|
|
4
|
+
describe("registerChannelRoutes", () => {
|
|
5
|
+
const envBackup = { ...process.env };
|
|
6
|
+
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
process.env = { ...envBackup };
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("registers the lark route dynamically", async () => {
|
|
12
|
+
const app = fastify();
|
|
13
|
+
registerChannelRoutes(app, {
|
|
14
|
+
lark: {
|
|
15
|
+
getInstallationConfig: jest.fn().mockResolvedValue({
|
|
16
|
+
installationId: "install-1",
|
|
17
|
+
tenantId: "tenant-a",
|
|
18
|
+
assistantId: "assistant-a",
|
|
19
|
+
appId: "cli_app_1",
|
|
20
|
+
appSecret: "secret",
|
|
21
|
+
verificationToken: "token-1",
|
|
22
|
+
mappingMode: "hybrid",
|
|
23
|
+
}),
|
|
24
|
+
parseRequestBody: jest.fn().mockReturnValue({
|
|
25
|
+
type: "url_verification",
|
|
26
|
+
challenge: "challenge-token",
|
|
27
|
+
token: "token-1",
|
|
28
|
+
}),
|
|
29
|
+
verifyParsedBody: jest.fn().mockReturnValue(true),
|
|
30
|
+
parseEvent: jest.fn(),
|
|
31
|
+
claimInboundReceipt: jest.fn(),
|
|
32
|
+
markInboundReceiptCompleted: jest.fn(),
|
|
33
|
+
markInboundReceiptFailed: jest.fn(),
|
|
34
|
+
resolveThread: jest.fn(),
|
|
35
|
+
runAgentAndCollectText: jest.fn(),
|
|
36
|
+
sendTextReply: jest.fn(),
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const response = await app.inject({
|
|
41
|
+
method: "POST",
|
|
42
|
+
url: "/api/channels/lark/installations/install-1/events",
|
|
43
|
+
payload: {
|
|
44
|
+
challenge: "challenge-token",
|
|
45
|
+
type: "url_verification",
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(response.statusCode).toBe(200);
|
|
50
|
+
expect(response.json()).toEqual({ challenge: "challenge-token" });
|
|
51
|
+
|
|
52
|
+
await app.close();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("skips lark route registration when lark config is disabled", async () => {
|
|
56
|
+
process.env.LARK_ENABLED = "false";
|
|
57
|
+
|
|
58
|
+
const app = fastify();
|
|
59
|
+
registerChannelRoutes(app);
|
|
60
|
+
|
|
61
|
+
const response = await app.inject({
|
|
62
|
+
method: "POST",
|
|
63
|
+
url: "/api/channels/lark/installations/install-1/events",
|
|
64
|
+
payload: {},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(response.statusCode).toBe(404);
|
|
68
|
+
|
|
69
|
+
await app.close();
|
|
70
|
+
});
|
|
71
|
+
});
|