@axiom-lattice/gateway 2.1.37 → 2.1.39
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 +365 -145
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +412 -191
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/agent_service.test.ts +238 -0
- package/src/__tests__/workspace.test.ts +253 -0
- package/src/controllers/assistant.ts +48 -25
- package/src/controllers/database-configs.ts +17 -9
- package/src/controllers/mcp-configs.ts +9 -1
- package/src/controllers/memory.ts +3 -0
- package/src/controllers/metrics-configs.ts +81 -19
- package/src/controllers/models.ts +39 -0
- package/src/controllers/run.ts +36 -6
- package/src/controllers/sandbox.ts +6 -4
- package/src/controllers/schedules.ts +20 -0
- package/src/controllers/skills.ts +34 -11
- package/src/controllers/threads.ts +32 -12
- package/src/controllers/tools.ts +2 -273
- package/src/controllers/workspace.ts +153 -25
- package/src/index.ts +10 -0
- package/src/services/agent_service.ts +29 -10
- package/src/services/agent_task_consumer.ts +2 -2
- package/src/services/sandbox_service.ts +10 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiom-lattice/gateway",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.39",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"module": "dist/index.mjs",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"pino-roll": "^3.1.0",
|
|
37
37
|
"redis": "^5.0.1",
|
|
38
38
|
"uuid": "^9.0.1",
|
|
39
|
-
"@axiom-lattice/core": "2.1.
|
|
40
|
-
"@axiom-lattice/protocols": "2.1.
|
|
41
|
-
"@axiom-lattice/queue-redis": "1.0.
|
|
39
|
+
"@axiom-lattice/core": "2.1.33",
|
|
40
|
+
"@axiom-lattice/protocols": "2.1.20",
|
|
41
|
+
"@axiom-lattice/queue-redis": "1.0.19"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/jest": "^29.5.14",
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Service Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for agent_service.ts to ensure proper async/await usage
|
|
5
|
+
* and error handling when working with agent clients.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
|
9
|
+
import { agent_state, agent_messages, draw_graph, agent_invoke, agent_stream } from '../services/agent_service';
|
|
10
|
+
import { getAgentClient, agentLatticeManager } from '@axiom-lattice/core';
|
|
11
|
+
|
|
12
|
+
// Mock the dependencies
|
|
13
|
+
jest.mock('@axiom-lattice/core', () => ({
|
|
14
|
+
getAgentClient: jest.fn(),
|
|
15
|
+
agentLatticeManager: {
|
|
16
|
+
getAgentLatticeWithTenant: jest.fn(),
|
|
17
|
+
},
|
|
18
|
+
InMemoryChunkBuffer: jest.fn().mockImplementation(() => ({
|
|
19
|
+
addChunk: jest.fn(),
|
|
20
|
+
completeThread: jest.fn(),
|
|
21
|
+
abortThread: jest.fn(),
|
|
22
|
+
})),
|
|
23
|
+
registerChunkBuffer: jest.fn(),
|
|
24
|
+
getChunkBuffer: jest.fn(),
|
|
25
|
+
hasChunkBuffer: jest.fn().mockReturnValue(false),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
describe('Agent Service - getAgentClient async handling', () => {
|
|
29
|
+
const mockTenantId = 'test-tenant';
|
|
30
|
+
const mockAssistantId = 'test-assistant';
|
|
31
|
+
const mockThreadId = 'test-thread';
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
jest.clearAllMocks();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('agent_state', () => {
|
|
38
|
+
it('should properly await getAgentClient before calling getState', async () => {
|
|
39
|
+
const mockAgent = {
|
|
40
|
+
getState: jest.fn().mockResolvedValue({
|
|
41
|
+
values: { messages: [] },
|
|
42
|
+
next: [],
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Mock getAgentClient to return a promise that resolves to mockAgent
|
|
47
|
+
(getAgentClient as jest.Mock).mockResolvedValue(mockAgent);
|
|
48
|
+
|
|
49
|
+
const result = await agent_state({
|
|
50
|
+
tenant_id: mockTenantId,
|
|
51
|
+
assistant_id: mockAssistantId,
|
|
52
|
+
thread_id: mockThreadId,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Verify getAgentClient was called with correct arguments
|
|
56
|
+
expect(getAgentClient).toHaveBeenCalledWith(mockTenantId, mockAssistantId);
|
|
57
|
+
|
|
58
|
+
// Verify getState was called on the resolved agent (not on a promise)
|
|
59
|
+
expect(mockAgent.getState).toHaveBeenCalledWith({
|
|
60
|
+
configurable: { thread_id: mockThreadId, subgraphs: false },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(result).toEqual({
|
|
64
|
+
values: { messages: [] },
|
|
65
|
+
next: [],
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should throw error when agent is not found', async () => {
|
|
70
|
+
(getAgentClient as jest.Mock).mockResolvedValue(null);
|
|
71
|
+
|
|
72
|
+
await expect(agent_state({
|
|
73
|
+
tenant_id: mockTenantId,
|
|
74
|
+
assistant_id: mockAssistantId,
|
|
75
|
+
thread_id: mockThreadId,
|
|
76
|
+
})).rejects.toThrow(`Agent ${mockAssistantId} not found`);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should throw error when getAgentClient returns undefined', async () => {
|
|
80
|
+
(getAgentClient as jest.Mock).mockResolvedValue(undefined);
|
|
81
|
+
|
|
82
|
+
await expect(agent_state({
|
|
83
|
+
tenant_id: mockTenantId,
|
|
84
|
+
assistant_id: mockAssistantId,
|
|
85
|
+
thread_id: mockThreadId,
|
|
86
|
+
})).rejects.toThrow(`Agent ${mockAssistantId} not found`);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('agent_messages', () => {
|
|
91
|
+
it('should properly await getAgentClient before calling getState', async () => {
|
|
92
|
+
const mockMessages = [
|
|
93
|
+
{ id: '1', getType: () => 'human', content: 'Hello', lc_kwargs: {} },
|
|
94
|
+
{ id: '2', getType: () => 'ai', content: 'Hi there', lc_kwargs: {} },
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
const mockAgent = {
|
|
98
|
+
getState: jest.fn().mockResolvedValue({
|
|
99
|
+
values: { messages: mockMessages },
|
|
100
|
+
}),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
(getAgentClient as jest.Mock).mockResolvedValue(mockAgent);
|
|
104
|
+
|
|
105
|
+
const result = await agent_messages({
|
|
106
|
+
tenant_id: mockTenantId,
|
|
107
|
+
assistant_id: mockAssistantId,
|
|
108
|
+
thread_id: mockThreadId,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(getAgentClient).toHaveBeenCalledWith(mockTenantId, mockAssistantId);
|
|
112
|
+
expect(mockAgent.getState).toHaveBeenCalledWith({
|
|
113
|
+
configurable: { thread_id: mockThreadId, subgraphs: false },
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(result).toHaveLength(2);
|
|
117
|
+
expect(result[0].role).toBe('human');
|
|
118
|
+
expect(result[1].role).toBe('ai');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('draw_graph', () => {
|
|
123
|
+
it('should properly await getAgentClient before calling getGraphAsync', async () => {
|
|
124
|
+
const mockGraph = {
|
|
125
|
+
drawMermaid: jest.fn().mockResolvedValue('graph TD; A-->B;'),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const mockAgent = {
|
|
129
|
+
getGraphAsync: jest.fn().mockResolvedValue(mockGraph),
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
(getAgentClient as jest.Mock).mockResolvedValue(mockAgent);
|
|
133
|
+
|
|
134
|
+
const result = await draw_graph(mockAssistantId, mockTenantId);
|
|
135
|
+
|
|
136
|
+
expect(getAgentClient).toHaveBeenCalledWith(mockTenantId, mockAssistantId);
|
|
137
|
+
expect(mockAgent.getGraphAsync).toHaveBeenCalled();
|
|
138
|
+
expect(mockGraph.drawMermaid).toHaveBeenCalled();
|
|
139
|
+
expect(result).toBe('graph TD; A-->B;');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should throw error when agent is not found', async () => {
|
|
143
|
+
(getAgentClient as jest.Mock).mockResolvedValue(null);
|
|
144
|
+
|
|
145
|
+
await expect(draw_graph(mockAssistantId, mockTenantId))
|
|
146
|
+
.rejects.toThrow(`Agent ${mockAssistantId} not found`);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('agent_stream', () => {
|
|
151
|
+
it('should properly await getAgentClient before calling stream', async () => {
|
|
152
|
+
const mockStream = async function* () {
|
|
153
|
+
yield ['updates', { messages: [{ toDict: () => ({ type: 'ai', content: 'Hello' }) }] }];
|
|
154
|
+
}();
|
|
155
|
+
|
|
156
|
+
const mockAgent = {
|
|
157
|
+
stream: jest.fn().mockResolvedValue(mockStream),
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
(getAgentClient as jest.Mock).mockResolvedValue(mockAgent);
|
|
161
|
+
(agentLatticeManager.getAgentLatticeWithTenant as jest.Mock).mockReturnValue({
|
|
162
|
+
config: { runConfig: {} },
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const result = await agent_stream({
|
|
166
|
+
tenant_id: mockTenantId,
|
|
167
|
+
assistant_id: mockAssistantId,
|
|
168
|
+
thread_id: mockThreadId,
|
|
169
|
+
input: { message: 'Test' },
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
expect(getAgentClient).toHaveBeenCalledWith(mockTenantId, mockAssistantId);
|
|
173
|
+
expect(mockAgent.stream).toHaveBeenCalled();
|
|
174
|
+
expect(typeof result[Symbol.asyncIterator]).toBe('function');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should throw error when agent is not found', async () => {
|
|
178
|
+
(getAgentClient as jest.Mock).mockResolvedValue(null);
|
|
179
|
+
(agentLatticeManager.getAgentLatticeWithTenant as jest.Mock).mockReturnValue({
|
|
180
|
+
config: { runConfig: {} },
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// The function will throw but also try to call chunkBuffer.abortThread
|
|
184
|
+
// which might fail in test environment, so we just check it throws
|
|
185
|
+
await expect(agent_stream({
|
|
186
|
+
tenant_id: mockTenantId,
|
|
187
|
+
assistant_id: mockAssistantId,
|
|
188
|
+
thread_id: mockThreadId,
|
|
189
|
+
input: { message: 'Test' },
|
|
190
|
+
})).rejects.toThrow();
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('agent_invoke', () => {
|
|
195
|
+
it('should use agentLattice.client directly without calling getAgentClient', async () => {
|
|
196
|
+
const mockResult = {
|
|
197
|
+
messages: [
|
|
198
|
+
{ toDict: () => ({ type: 'ai', content: 'Response' }) },
|
|
199
|
+
],
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const mockAgent = {
|
|
203
|
+
invoke: jest.fn().mockResolvedValue(mockResult),
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
(agentLatticeManager.getAgentLatticeWithTenant as jest.Mock).mockReturnValue({
|
|
207
|
+
client: mockAgent,
|
|
208
|
+
config: { runConfig: {} },
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const result = await agent_invoke({
|
|
212
|
+
tenant_id: mockTenantId,
|
|
213
|
+
assistant_id: mockAssistantId,
|
|
214
|
+
thread_id: mockThreadId,
|
|
215
|
+
input: { message: 'Hello' },
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// agent_invoke uses agentLattice.client directly, not getAgentClient
|
|
219
|
+
expect(getAgentClient).not.toHaveBeenCalled();
|
|
220
|
+
expect(mockAgent.invoke).toHaveBeenCalled();
|
|
221
|
+
expect(result.messages).toHaveLength(1);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should throw error when agent lattice client is not found', async () => {
|
|
225
|
+
(agentLatticeManager.getAgentLatticeWithTenant as jest.Mock).mockReturnValue({
|
|
226
|
+
client: null,
|
|
227
|
+
config: { runConfig: {} },
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
await expect(agent_invoke({
|
|
231
|
+
tenant_id: mockTenantId,
|
|
232
|
+
assistant_id: mockAssistantId,
|
|
233
|
+
thread_id: mockThreadId,
|
|
234
|
+
input: { message: 'Hello' },
|
|
235
|
+
})).rejects.toThrow(`Agent ${mockAssistantId} not found`);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
});
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkspaceController Tenant Isolation Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests to verify that file operations in WorkspaceController
|
|
5
|
+
* properly use tenant-specific paths.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeEach, jest } from "@jest/globals";
|
|
9
|
+
import type { FastifyRequest, FastifyReply } from "fastify";
|
|
10
|
+
|
|
11
|
+
// Mock store lattice
|
|
12
|
+
const mockGetWorkspaceById = jest.fn();
|
|
13
|
+
const mockGetAllWorkspaces = jest.fn();
|
|
14
|
+
const mockCreateWorkspace = jest.fn();
|
|
15
|
+
const mockUpdateWorkspace = jest.fn();
|
|
16
|
+
const mockDeleteWorkspace = jest.fn();
|
|
17
|
+
const mockGetProjectsByWorkspace = jest.fn();
|
|
18
|
+
const mockCreateProject = jest.fn();
|
|
19
|
+
const mockGetProjectById = jest.fn();
|
|
20
|
+
const mockUpdateProject = jest.fn();
|
|
21
|
+
const mockDeleteProject = jest.fn();
|
|
22
|
+
|
|
23
|
+
const mockStoreLattice = {
|
|
24
|
+
store: {
|
|
25
|
+
getAllWorkspaces: mockGetAllWorkspaces,
|
|
26
|
+
createWorkspace: mockCreateWorkspace,
|
|
27
|
+
getWorkspaceById: mockGetWorkspaceById,
|
|
28
|
+
updateWorkspace: mockUpdateWorkspace,
|
|
29
|
+
deleteWorkspace: mockDeleteWorkspace,
|
|
30
|
+
getProjectsByWorkspace: mockGetProjectsByWorkspace,
|
|
31
|
+
createProject: mockCreateProject,
|
|
32
|
+
getProjectById: mockGetProjectById,
|
|
33
|
+
updateProject: mockUpdateProject,
|
|
34
|
+
deleteProject: mockDeleteProject,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Mock the dependencies
|
|
39
|
+
jest.mock("@axiom-lattice/core", () => ({
|
|
40
|
+
getStoreLattice: jest.fn().mockReturnValue(mockStoreLattice),
|
|
41
|
+
SandboxFilesystem: jest.fn().mockImplementation(() => ({
|
|
42
|
+
lsInfo: jest.fn(),
|
|
43
|
+
read: jest.fn(),
|
|
44
|
+
})),
|
|
45
|
+
FilesystemBackend: jest.fn().mockImplementation(() => ({
|
|
46
|
+
lsInfo: jest.fn(),
|
|
47
|
+
read: jest.fn(),
|
|
48
|
+
})),
|
|
49
|
+
getSandBoxManager: jest.fn().mockReturnValue({
|
|
50
|
+
createSandbox: jest.fn().mockResolvedValue({
|
|
51
|
+
file: {
|
|
52
|
+
downloadFile: jest.fn(),
|
|
53
|
+
uploadFile: jest.fn(),
|
|
54
|
+
},
|
|
55
|
+
}),
|
|
56
|
+
}),
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
// Import after mocks are set up
|
|
60
|
+
import { WorkspaceController } from "../controllers/workspace";
|
|
61
|
+
|
|
62
|
+
describe("WorkspaceController - Tenant Isolation", () => {
|
|
63
|
+
let controller: WorkspaceController;
|
|
64
|
+
const mockTenantId = "test-tenant-123";
|
|
65
|
+
const mockWorkspaceId = "workspace-456";
|
|
66
|
+
const mockProjectId = "project-789";
|
|
67
|
+
|
|
68
|
+
// Helper to create mock request with tenant
|
|
69
|
+
const createMockRequest = (overrides: any = {}) => ({
|
|
70
|
+
params: { workspaceId: mockWorkspaceId, projectId: mockProjectId },
|
|
71
|
+
query: {},
|
|
72
|
+
headers: { "x-tenant-id": mockTenantId },
|
|
73
|
+
...overrides,
|
|
74
|
+
}) as unknown as FastifyRequest;
|
|
75
|
+
|
|
76
|
+
beforeEach(() => {
|
|
77
|
+
jest.clearAllMocks();
|
|
78
|
+
controller = new WorkspaceController();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("getBackend path construction", () => {
|
|
82
|
+
it("should use tenant-specific path for filesystem storage", async () => {
|
|
83
|
+
const { FilesystemBackend } = await import("@axiom-lattice/core");
|
|
84
|
+
|
|
85
|
+
const mockWorkspace = {
|
|
86
|
+
id: mockWorkspaceId,
|
|
87
|
+
tenantId: mockTenantId,
|
|
88
|
+
storageType: "filesystem",
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
mockGetWorkspaceById.mockResolvedValue(mockWorkspace);
|
|
92
|
+
|
|
93
|
+
// Access private method for testing
|
|
94
|
+
const getBackend = (controller as any).getBackend.bind(controller);
|
|
95
|
+
await getBackend(mockTenantId, mockWorkspaceId, mockProjectId);
|
|
96
|
+
|
|
97
|
+
// Verify FilesystemBackend was called with tenant-specific path
|
|
98
|
+
expect(FilesystemBackend).toHaveBeenCalledWith({
|
|
99
|
+
rootDir: `/lattice_store/tenants/${mockTenantId}/workspaces/${mockWorkspaceId}/${mockProjectId}`,
|
|
100
|
+
virtualMode: true,
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should use tenant-specific path for sandbox storage", async () => {
|
|
105
|
+
const { getSandBoxManager } = await import("@axiom-lattice/core");
|
|
106
|
+
|
|
107
|
+
const mockWorkspace = {
|
|
108
|
+
id: mockWorkspaceId,
|
|
109
|
+
tenantId: mockTenantId,
|
|
110
|
+
storageType: "sandbox",
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
mockGetWorkspaceById.mockResolvedValue(mockWorkspace);
|
|
114
|
+
|
|
115
|
+
const getBackend = (controller as any).getBackend.bind(controller);
|
|
116
|
+
await getBackend(mockTenantId, mockWorkspaceId, mockProjectId);
|
|
117
|
+
|
|
118
|
+
// Verify getSandBoxManager was called (uses default key, sandbox manager is not tenant-specific)
|
|
119
|
+
expect(getSandBoxManager).toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should pass tenantId to workspace store lookup", async () => {
|
|
123
|
+
const mockWorkspace = {
|
|
124
|
+
id: mockWorkspaceId,
|
|
125
|
+
tenantId: mockTenantId,
|
|
126
|
+
storageType: "filesystem",
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
mockGetWorkspaceById.mockResolvedValue(mockWorkspace);
|
|
130
|
+
|
|
131
|
+
const getBackend = (controller as any).getBackend.bind(controller);
|
|
132
|
+
await getBackend(mockTenantId, mockWorkspaceId, mockProjectId);
|
|
133
|
+
|
|
134
|
+
// Verify workspace lookup used correct tenantId
|
|
135
|
+
expect(mockGetWorkspaceById).toHaveBeenCalledWith(mockTenantId, mockWorkspaceId);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("listPath tenant isolation", () => {
|
|
140
|
+
it("should extract tenantId from request headers", async () => {
|
|
141
|
+
const { FilesystemBackend } = await import("@axiom-lattice/core");
|
|
142
|
+
|
|
143
|
+
const mockWorkspace = {
|
|
144
|
+
id: mockWorkspaceId,
|
|
145
|
+
tenantId: mockTenantId,
|
|
146
|
+
storageType: "filesystem",
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
mockGetWorkspaceById.mockResolvedValue(mockWorkspace);
|
|
150
|
+
|
|
151
|
+
const request = createMockRequest();
|
|
152
|
+
await controller.listPath(request);
|
|
153
|
+
|
|
154
|
+
// Verify tenant-specific path was used
|
|
155
|
+
expect(FilesystemBackend).toHaveBeenCalledWith({
|
|
156
|
+
rootDir: `/lattice_store/tenants/${mockTenantId}/workspaces/${mockWorkspaceId}/${mockProjectId}`,
|
|
157
|
+
virtualMode: true,
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("should use different paths for different tenants", async () => {
|
|
162
|
+
const { FilesystemBackend } = await import("@axiom-lattice/core");
|
|
163
|
+
|
|
164
|
+
const tenantA = "tenant-a";
|
|
165
|
+
const tenantB = "tenant-b";
|
|
166
|
+
|
|
167
|
+
const mockWorkspaceA = {
|
|
168
|
+
id: mockWorkspaceId,
|
|
169
|
+
tenantId: tenantA,
|
|
170
|
+
storageType: "filesystem",
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const mockWorkspaceB = {
|
|
174
|
+
id: mockWorkspaceId,
|
|
175
|
+
tenantId: tenantB,
|
|
176
|
+
storageType: "filesystem",
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// First call with tenant A
|
|
180
|
+
mockGetWorkspaceById.mockResolvedValue(mockWorkspaceA);
|
|
181
|
+
await controller.listPath(createMockRequest({ headers: { "x-tenant-id": tenantA } }));
|
|
182
|
+
|
|
183
|
+
expect(FilesystemBackend).toHaveBeenCalledWith({
|
|
184
|
+
rootDir: `/lattice_store/tenants/${tenantA}/workspaces/${mockWorkspaceId}/${mockProjectId}`,
|
|
185
|
+
virtualMode: true,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Second call with tenant B
|
|
189
|
+
mockGetWorkspaceById.mockResolvedValue(mockWorkspaceB);
|
|
190
|
+
await controller.listPath(createMockRequest({ headers: { "x-tenant-id": tenantB } }));
|
|
191
|
+
|
|
192
|
+
expect(FilesystemBackend).toHaveBeenCalledWith({
|
|
193
|
+
rootDir: `/lattice_store/tenants/${tenantB}/workspaces/${mockWorkspaceId}/${mockProjectId}`,
|
|
194
|
+
virtualMode: true,
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("readFile tenant isolation", () => {
|
|
200
|
+
it("should use tenant-specific backend for reading files", async () => {
|
|
201
|
+
const { FilesystemBackend } = await import("@axiom-lattice/core");
|
|
202
|
+
|
|
203
|
+
const mockWorkspace = {
|
|
204
|
+
id: mockWorkspaceId,
|
|
205
|
+
tenantId: mockTenantId,
|
|
206
|
+
storageType: "filesystem",
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
mockGetWorkspaceById.mockResolvedValue(mockWorkspace);
|
|
210
|
+
|
|
211
|
+
const request = createMockRequest({
|
|
212
|
+
query: { path: "/test.txt", offset: 0, limit: 100 },
|
|
213
|
+
});
|
|
214
|
+
await controller.readFile(request);
|
|
215
|
+
|
|
216
|
+
// Verify tenant-specific path was used
|
|
217
|
+
expect(FilesystemBackend).toHaveBeenCalledWith({
|
|
218
|
+
rootDir: `/lattice_store/tenants/${mockTenantId}/workspaces/${mockWorkspaceId}/${mockProjectId}`,
|
|
219
|
+
virtualMode: true,
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("getTenantId helper", () => {
|
|
225
|
+
it("should extract tenantId from user context when available", () => {
|
|
226
|
+
const request = {
|
|
227
|
+
user: { tenantId: "user-context-tenant" },
|
|
228
|
+
headers: {},
|
|
229
|
+
} as unknown as FastifyRequest;
|
|
230
|
+
|
|
231
|
+
const tenantId = (controller as any).getTenantId(request);
|
|
232
|
+
expect(tenantId).toBe("user-context-tenant");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("should fallback to x-tenant-id header when no user context", () => {
|
|
236
|
+
const request = {
|
|
237
|
+
headers: { "x-tenant-id": "header-tenant" },
|
|
238
|
+
} as unknown as FastifyRequest;
|
|
239
|
+
|
|
240
|
+
const tenantId = (controller as any).getTenantId(request);
|
|
241
|
+
expect(tenantId).toBe("header-tenant");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("should use default tenant when no context or header", () => {
|
|
245
|
+
const request = {
|
|
246
|
+
headers: {},
|
|
247
|
+
} as unknown as FastifyRequest;
|
|
248
|
+
|
|
249
|
+
const tenantId = (controller as any).getTenantId(request);
|
|
250
|
+
expect(tenantId).toBe("default");
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|