aws-runtime-bridge 1.8.5 → 1.8.6
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/adapter/AcodeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/AcodeSdkAdapter.js +6 -98
- package/dist/adapter/AcodeSdkAdapter.test.js +2 -313
- package/dist/adapter/OpencodeSdkAdapter.js +1 -1
- package/dist/adapter/types.d.ts.map +1 -1
- package/dist/adapter/types.js +3 -7
- package/dist/config.d.ts +3 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +13 -36
- package/dist/config.test.js +20 -24
- package/dist/routes/ai-sources.js +1 -5
- package/dist/routes/ai-sources.test.js +0 -21
- package/dist/routes/instance.d.ts.map +1 -1
- package/dist/routes/instance.js +14 -31
- package/dist/routes/instance.test.js +224 -215
- package/dist/routes/runtime-binding.d.ts.map +1 -1
- package/dist/routes/runtime-binding.js +1 -36
- package/dist/routes/terminal.d.ts +0 -2
- package/dist/routes/terminal.d.ts.map +1 -1
- package/dist/routes/terminal.js +7 -109
- package/dist/routes/terminal.test.js +1 -118
- package/dist/services/mcp-launch-binding-queue.d.ts +0 -7
- package/dist/services/mcp-launch-binding-queue.d.ts.map +1 -1
- package/dist/services/mcp-launch-binding-queue.js +0 -25
- package/dist/services/session-output.d.ts +3 -7
- package/dist/services/session-output.d.ts.map +1 -1
- package/package/acode/dist/runtime.d.ts +0 -1
- package/package/acode/dist/runtime.d.ts.map +1 -1
- package/package/acode/dist/runtime.js +31 -265
- package/package/acode/dist/types.d.ts +1 -1
- package/package/acode/dist/types.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/config.d.ts +0 -4
- package/package/aws-client-agent-mcp/dist/config.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/config.js +0 -15
- package/package/aws-client-agent-mcp/dist/config.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/http-client.d.ts +0 -10
- package/package/aws-client-agent-mcp/dist/http-client.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/http-client.js +1 -79
- package/package/aws-client-agent-mcp/dist/http-client.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/http-client.test.js +265 -129
- package/package/aws-client-agent-mcp/dist/http-client.test.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,63 +1,61 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Instance 路由单元测试
|
|
3
3
|
*/
|
|
4
|
-
import { describe, it, expect, vi, afterEach } from
|
|
5
|
-
import { EventEmitter } from
|
|
4
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
5
|
+
import { EventEmitter } from 'node:events';
|
|
6
6
|
const spawnMock = vi.hoisted(() => vi.fn());
|
|
7
|
-
vi.mock(
|
|
7
|
+
vi.mock('axios', () => ({
|
|
8
8
|
default: {
|
|
9
9
|
get: vi.fn(),
|
|
10
10
|
post: vi.fn(),
|
|
11
11
|
},
|
|
12
12
|
}));
|
|
13
|
-
vi.mock(
|
|
13
|
+
vi.mock('../services/auto-register.js', () => ({
|
|
14
14
|
getConfiguredConnectionKeys: vi.fn(() => []),
|
|
15
15
|
requestRuntimeAccessTokenRefreshForServer: vi.fn(),
|
|
16
16
|
}));
|
|
17
|
-
vi.mock(
|
|
18
|
-
getRuntimeAccessToken: vi.fn(() =>
|
|
19
|
-
loadRuntimeBinding: vi.fn(() => ({
|
|
20
|
-
|
|
21
|
-
userId: "user-1",
|
|
22
|
-
schedulerBaseUrl: "http://server.local:7380",
|
|
23
|
-
})),
|
|
24
|
-
normalizeSchedulerBaseUrl: vi.fn((value) => value ? String(value).replace(/\/+$/, "") : undefined),
|
|
17
|
+
vi.mock('../services/runtime-binding.js', () => ({
|
|
18
|
+
getRuntimeAccessToken: vi.fn(() => 'stale-runtime-token-123456'),
|
|
19
|
+
loadRuntimeBinding: vi.fn(() => ({ status: 'paired', userId: 'user-1', schedulerBaseUrl: 'http://server.local:7380' })),
|
|
20
|
+
normalizeSchedulerBaseUrl: vi.fn((value) => (value ? String(value).replace(/\/+$/, '') : undefined)),
|
|
25
21
|
}));
|
|
26
|
-
vi.mock(
|
|
22
|
+
vi.mock('../services/lifecycle-state.js', () => ({
|
|
27
23
|
createBridgeLifecycleOperation: vi.fn((input) => ({
|
|
28
|
-
operationId:
|
|
29
|
-
requestedAt:
|
|
30
|
-
updatedAt:
|
|
24
|
+
operationId: 'operation-1',
|
|
25
|
+
requestedAt: '2026-01-01T00:00:00.000Z',
|
|
26
|
+
updatedAt: '2026-01-01T00:00:00.000Z',
|
|
31
27
|
...input,
|
|
32
28
|
})),
|
|
33
29
|
deliverPendingBridgeLifecycleNotifications: vi.fn(),
|
|
34
30
|
upsertBridgeLifecycleOperation: vi.fn(),
|
|
35
31
|
}));
|
|
36
|
-
vi.mock(
|
|
32
|
+
vi.mock('../config.js', async (importOriginal) => ({
|
|
37
33
|
...(await importOriginal()),
|
|
38
34
|
getRuntimeHomeDir: vi.fn(() => process.cwd()),
|
|
39
35
|
}));
|
|
40
|
-
vi.mock(
|
|
36
|
+
vi.mock('node:child_process', async (importOriginal) => ({
|
|
41
37
|
...(await importOriginal()),
|
|
42
38
|
spawn: spawnMock,
|
|
43
39
|
}));
|
|
44
40
|
afterEach(() => {
|
|
45
41
|
vi.useRealTimers();
|
|
46
42
|
vi.clearAllMocks();
|
|
43
|
+
delete process.env.AWS_RUNTIME_SCHEDULER_BASE_URL;
|
|
44
|
+
delete process.env.AWS_TEST_HOME;
|
|
47
45
|
});
|
|
48
|
-
function mockSpawnResult(exitCode, stdoutText =
|
|
46
|
+
function mockSpawnResult(exitCode, stdoutText = '', stderrText = '') {
|
|
49
47
|
spawnMock.mockImplementationOnce(() => {
|
|
50
48
|
const child = new EventEmitter();
|
|
51
49
|
child.stdout = new EventEmitter();
|
|
52
50
|
child.stderr = new EventEmitter();
|
|
53
51
|
process.nextTick(() => {
|
|
54
52
|
if (stdoutText) {
|
|
55
|
-
child.stdout.emit(
|
|
53
|
+
child.stdout.emit('data', Buffer.from(stdoutText));
|
|
56
54
|
}
|
|
57
55
|
if (stderrText) {
|
|
58
|
-
child.stderr.emit(
|
|
56
|
+
child.stderr.emit('data', Buffer.from(stderrText));
|
|
59
57
|
}
|
|
60
|
-
child.emit(
|
|
58
|
+
child.emit('close', exitCode);
|
|
61
59
|
});
|
|
62
60
|
return child;
|
|
63
61
|
});
|
|
@@ -67,33 +65,30 @@ function createMockResponse() {
|
|
|
67
65
|
const status = vi.fn(() => ({ json }));
|
|
68
66
|
return { json, status };
|
|
69
67
|
}
|
|
70
|
-
describe(
|
|
71
|
-
it(
|
|
68
|
+
describe('instance route validation', () => {
|
|
69
|
+
it('builds correct ping response when healthy', () => {
|
|
72
70
|
const buildPingResponse = (schedulerStatus, schedulerBaseUrl) => ({
|
|
73
71
|
ok: true,
|
|
74
|
-
runtimeBridge:
|
|
72
|
+
runtimeBridge: 'healthy',
|
|
75
73
|
schedulerReachable: schedulerStatus >= 200 && schedulerStatus < 300,
|
|
76
74
|
schedulerBaseUrl,
|
|
77
75
|
});
|
|
78
|
-
const healthyResponse = buildPingResponse(200,
|
|
76
|
+
const healthyResponse = buildPingResponse(200, 'http://localhost:8080');
|
|
79
77
|
expect(healthyResponse.ok).toBe(true);
|
|
80
78
|
expect(healthyResponse.schedulerReachable).toBe(true);
|
|
81
|
-
const unhealthyResponse = buildPingResponse(502,
|
|
79
|
+
const unhealthyResponse = buildPingResponse(502, 'http://localhost:8080');
|
|
82
80
|
expect(unhealthyResponse.schedulerReachable).toBe(false);
|
|
83
81
|
});
|
|
84
|
-
it(
|
|
82
|
+
it('requires agentId and workspacePath', () => {
|
|
85
83
|
const validateInitRequest = (body) => {
|
|
86
84
|
if (!body.agentId || !body.workspacePath)
|
|
87
|
-
return {
|
|
88
|
-
valid: false,
|
|
89
|
-
error: "agentId and workspacePath are required",
|
|
90
|
-
};
|
|
85
|
+
return { valid: false, error: 'agentId and workspacePath are required' };
|
|
91
86
|
return { valid: true };
|
|
92
87
|
};
|
|
93
88
|
expect(validateInitRequest({}).valid).toBe(false);
|
|
94
|
-
expect(validateInitRequest({ agentId:
|
|
89
|
+
expect(validateInitRequest({ agentId: 'agent-1', workspacePath: '/path' }).valid).toBe(true);
|
|
95
90
|
});
|
|
96
|
-
it(
|
|
91
|
+
it('builds correct init options', () => {
|
|
97
92
|
const buildInitOptions = (body) => ({
|
|
98
93
|
skillEnabled: body.skillEnabled,
|
|
99
94
|
mcpEnabled: body.mcpEnabled,
|
|
@@ -102,91 +97,87 @@ describe("instance route validation", () => {
|
|
|
102
97
|
const options = buildInitOptions({
|
|
103
98
|
skillEnabled: true,
|
|
104
99
|
mcpEnabled: false,
|
|
105
|
-
ccSwitchEnabledTools: [
|
|
100
|
+
ccSwitchEnabledTools: ['claude', 'opencode'],
|
|
106
101
|
});
|
|
107
102
|
expect(options.skillEnabled).toBe(true);
|
|
108
103
|
expect(options.mcpEnabled).toBe(false);
|
|
109
|
-
expect(options.ccSwitchEnabledTools).toEqual([
|
|
104
|
+
expect(options.ccSwitchEnabledTools).toEqual(['claude', 'opencode']);
|
|
110
105
|
});
|
|
111
|
-
it(
|
|
106
|
+
it('requires agentId for state query', () => {
|
|
112
107
|
const validateStateRequest = (body) => {
|
|
113
108
|
if (!body.agentId)
|
|
114
|
-
return { valid: false, error:
|
|
109
|
+
return { valid: false, error: 'agentId is required' };
|
|
115
110
|
return { valid: true };
|
|
116
111
|
};
|
|
117
112
|
expect(validateStateRequest({}).valid).toBe(false);
|
|
118
|
-
expect(validateStateRequest({ agentId:
|
|
113
|
+
expect(validateStateRequest({ agentId: 'agent-1' }).valid).toBe(true);
|
|
119
114
|
});
|
|
120
|
-
it(
|
|
121
|
-
const { buildToolStatusDetectionTargets } = await import(
|
|
122
|
-
expect(buildToolStatusDetectionTargets([
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
115
|
+
it('always detects panel tool statuses even when legacy enabled tools omit codex', async () => {
|
|
116
|
+
const { buildToolStatusDetectionTargets } = await import('./instance.js');
|
|
117
|
+
expect(buildToolStatusDetectionTargets(['claude', 'opencode'])).toEqual([
|
|
118
|
+
'claude',
|
|
119
|
+
'opencode',
|
|
120
|
+
'acode',
|
|
121
|
+
'codex',
|
|
127
122
|
]);
|
|
128
123
|
}, 15_000);
|
|
129
|
-
it(
|
|
130
|
-
const { SUPPORTED_INSTALLABLE_TOOLS } = await import(
|
|
124
|
+
it('accepts codex for cc-switch tool installation validation', async () => {
|
|
125
|
+
const { SUPPORTED_INSTALLABLE_TOOLS } = await import('../services/tool-installer.js');
|
|
131
126
|
const normalizeInstallableTools = (tools) => {
|
|
132
127
|
const supported = new Set(SUPPORTED_INSTALLABLE_TOOLS);
|
|
133
128
|
const requestedTools = Array.isArray(tools)
|
|
134
|
-
? tools
|
|
135
|
-
.map((tool) => String(tool || "")
|
|
136
|
-
.trim()
|
|
137
|
-
.toLowerCase())
|
|
138
|
-
.filter(Boolean)
|
|
129
|
+
? tools.map((tool) => String(tool || '').trim().toLowerCase()).filter(Boolean)
|
|
139
130
|
: [];
|
|
140
131
|
return requestedTools.filter((tool) => supported.has(tool));
|
|
141
132
|
};
|
|
142
|
-
expect(normalizeInstallableTools([
|
|
143
|
-
expect(SUPPORTED_INSTALLABLE_TOOLS).toContain(
|
|
133
|
+
expect(normalizeInstallableTools(['Codex'])).toEqual(['codex']);
|
|
134
|
+
expect(SUPPORTED_INSTALLABLE_TOOLS).toContain('codex');
|
|
144
135
|
});
|
|
145
|
-
it(
|
|
146
|
-
const { SUPPORTED_UNINSTALLABLE_TOOLS } = await import(
|
|
136
|
+
it('accepts panel tools for cc-switch tool uninstallation validation', async () => {
|
|
137
|
+
const { SUPPORTED_UNINSTALLABLE_TOOLS } = await import('../services/tool-installer.js');
|
|
147
138
|
const normalizeUninstallableTools = (tools) => {
|
|
148
139
|
const supported = new Set(SUPPORTED_UNINSTALLABLE_TOOLS);
|
|
149
140
|
const requestedTools = Array.isArray(tools)
|
|
150
|
-
? tools
|
|
151
|
-
.map((tool) => String(tool || "")
|
|
152
|
-
.trim()
|
|
153
|
-
.toLowerCase())
|
|
154
|
-
.filter(Boolean)
|
|
141
|
+
? tools.map((tool) => String(tool || '').trim().toLowerCase()).filter(Boolean)
|
|
155
142
|
: [];
|
|
156
143
|
return requestedTools.filter((tool) => supported.has(tool));
|
|
157
144
|
};
|
|
158
|
-
expect(normalizeUninstallableTools([
|
|
159
|
-
|
|
145
|
+
expect(normalizeUninstallableTools(['Claude', 'OpenCode', 'Codex'])).toEqual([
|
|
146
|
+
'claude',
|
|
147
|
+
'opencode',
|
|
148
|
+
'codex',
|
|
149
|
+
]);
|
|
150
|
+
expect(normalizeUninstallableTools(['unknown-tool'])).toEqual([]);
|
|
160
151
|
});
|
|
161
|
-
it(
|
|
162
|
-
const { buildUninstallFailureMessage } = await import(
|
|
163
|
-
expect(buildUninstallFailureMessage([
|
|
152
|
+
it('reports uninstall failure when a requested tool remains installed', async () => {
|
|
153
|
+
const { buildUninstallFailureMessage } = await import('./instance.js');
|
|
154
|
+
expect(buildUninstallFailureMessage(['opencode'], {
|
|
164
155
|
opencode: {
|
|
165
|
-
tool:
|
|
156
|
+
tool: 'opencode',
|
|
166
157
|
installed: true,
|
|
167
|
-
executable:
|
|
168
|
-
version:
|
|
158
|
+
executable: '/home/user/.opencode/bin/opencode',
|
|
159
|
+
version: '1.14.39',
|
|
169
160
|
installing: false,
|
|
170
|
-
error:
|
|
161
|
+
error: 'uninstall completed but command is still available',
|
|
171
162
|
},
|
|
172
|
-
})).toContain(
|
|
163
|
+
})).toContain('opencode: uninstall completed but command is still available');
|
|
173
164
|
});
|
|
174
|
-
it(
|
|
175
|
-
const { summarizeToolStatus } = await import(
|
|
165
|
+
it('summarizes tool status for route diagnostics', async () => {
|
|
166
|
+
const { summarizeToolStatus } = await import('./instance.js');
|
|
176
167
|
expect(summarizeToolStatus({
|
|
177
168
|
opencode: {
|
|
178
|
-
tool:
|
|
169
|
+
tool: 'opencode',
|
|
179
170
|
installed: false,
|
|
180
171
|
executable: null,
|
|
181
172
|
version: null,
|
|
182
173
|
installing: false,
|
|
183
|
-
error:
|
|
174
|
+
error: 'SDK package @opencode-ai/sdk is not installed in aws-runtime-bridge',
|
|
184
175
|
},
|
|
185
176
|
claude: {
|
|
186
|
-
tool:
|
|
177
|
+
tool: 'claude',
|
|
187
178
|
installed: true,
|
|
188
|
-
executable:
|
|
189
|
-
version:
|
|
179
|
+
executable: '@anthropic-ai/claude-agent-sdk',
|
|
180
|
+
version: '0.2.87',
|
|
190
181
|
installing: false,
|
|
191
182
|
error: null,
|
|
192
183
|
},
|
|
@@ -195,251 +186,269 @@ describe("instance route validation", () => {
|
|
|
195
186
|
installed: false,
|
|
196
187
|
executable: null,
|
|
197
188
|
version: null,
|
|
198
|
-
error:
|
|
189
|
+
error: 'SDK package @opencode-ai/sdk is not installed in aws-runtime-bridge',
|
|
199
190
|
},
|
|
200
191
|
claude: {
|
|
201
192
|
installed: true,
|
|
202
|
-
executable:
|
|
203
|
-
version:
|
|
193
|
+
executable: '@anthropic-ai/claude-agent-sdk',
|
|
194
|
+
version: '0.2.87',
|
|
204
195
|
error: null,
|
|
205
196
|
},
|
|
206
197
|
});
|
|
207
198
|
});
|
|
208
|
-
it(
|
|
209
|
-
const { buildUninstallFailureMessage } = await import(
|
|
210
|
-
expect(buildUninstallFailureMessage([
|
|
199
|
+
it('does not report uninstall failure when requested tools are no longer installed', async () => {
|
|
200
|
+
const { buildUninstallFailureMessage } = await import('./instance.js');
|
|
201
|
+
expect(buildUninstallFailureMessage(['claude'], {
|
|
211
202
|
claude: {
|
|
212
|
-
tool:
|
|
203
|
+
tool: 'claude',
|
|
213
204
|
installed: false,
|
|
214
205
|
executable: null,
|
|
215
206
|
version: null,
|
|
216
207
|
installing: false,
|
|
217
|
-
error:
|
|
208
|
+
error: 'command not installed',
|
|
218
209
|
},
|
|
219
|
-
})).toBe(
|
|
210
|
+
})).toBe('');
|
|
220
211
|
});
|
|
221
|
-
it(
|
|
222
|
-
const { getConfiguredConnectionKeys } = await import(
|
|
212
|
+
it('allows connection check when no connection key is configured', async () => {
|
|
213
|
+
const { getConfiguredConnectionKeys } = await import('../services/auto-register.js');
|
|
223
214
|
vi.mocked(getConfiguredConnectionKeys).mockReturnValue([]);
|
|
224
|
-
const { buildConnectionCheckResponse } = await import(
|
|
225
|
-
expect(buildConnectionCheckResponse(
|
|
215
|
+
const { buildConnectionCheckResponse } = await import('./instance.js');
|
|
216
|
+
expect(buildConnectionCheckResponse('')).toEqual({
|
|
226
217
|
status: 200,
|
|
227
218
|
body: {
|
|
228
219
|
ok: true,
|
|
229
|
-
runtimeBridge:
|
|
220
|
+
runtimeBridge: 'healthy',
|
|
230
221
|
connectionKeyMatched: true,
|
|
231
222
|
connectionKeyRequired: false,
|
|
232
223
|
},
|
|
233
224
|
});
|
|
234
225
|
});
|
|
235
|
-
it(
|
|
236
|
-
const { createHash } = await import(
|
|
237
|
-
const { getConfiguredConnectionKeys } = await import(
|
|
238
|
-
vi.mocked(getConfiguredConnectionKeys).mockReturnValue([
|
|
239
|
-
const { buildConnectionCheckResponse } = await import(
|
|
240
|
-
const digest = createHash(
|
|
226
|
+
it('requires matching connection key digest when a connection key is configured', async () => {
|
|
227
|
+
const { createHash } = await import('node:crypto');
|
|
228
|
+
const { getConfiguredConnectionKeys } = await import('../services/auto-register.js');
|
|
229
|
+
vi.mocked(getConfiguredConnectionKeys).mockReturnValue(['secret-key']);
|
|
230
|
+
const { buildConnectionCheckResponse } = await import('./instance.js');
|
|
231
|
+
const digest = createHash('md5').update('secret-key', 'utf8').digest('hex');
|
|
241
232
|
expect(buildConnectionCheckResponse(digest).status).toBe(200);
|
|
242
|
-
expect(buildConnectionCheckResponse(
|
|
233
|
+
expect(buildConnectionCheckResponse('00000000000000000000000000000000')).toEqual({
|
|
243
234
|
status: 401,
|
|
244
235
|
body: {
|
|
245
236
|
ok: false,
|
|
246
|
-
error:
|
|
237
|
+
error: 'connection_key_mismatch',
|
|
247
238
|
connectionKeyMatched: false,
|
|
248
239
|
connectionKeyRequired: true,
|
|
249
240
|
},
|
|
250
241
|
});
|
|
251
242
|
});
|
|
252
|
-
it(
|
|
253
|
-
const { buildSchedulerPingFailureResponse } = await import(
|
|
254
|
-
const response = buildSchedulerPingFailureResponse(new Error(
|
|
243
|
+
it('explains localhost scheduler ping failures with environment variable guidance', async () => {
|
|
244
|
+
const { buildSchedulerPingFailureResponse } = await import('./instance.js');
|
|
245
|
+
const response = buildSchedulerPingFailureResponse(new Error('connect ECONNREFUSED 127.0.0.1:8080'), 'http://localhost:8080');
|
|
255
246
|
expect(response).toEqual({
|
|
256
247
|
ok: false,
|
|
257
|
-
error:
|
|
258
|
-
failureStage:
|
|
259
|
-
runtimeBridge:
|
|
260
|
-
schedulerBaseUrl:
|
|
261
|
-
hint: expect.stringContaining(
|
|
248
|
+
error: 'connect ECONNREFUSED 127.0.0.1:8080',
|
|
249
|
+
failureStage: 'scheduler_ping',
|
|
250
|
+
runtimeBridge: 'healthy',
|
|
251
|
+
schedulerBaseUrl: 'http://localhost:8080',
|
|
252
|
+
hint: expect.stringContaining('AWS_RUNTIME_SCHEDULER_BASE_URL'),
|
|
262
253
|
});
|
|
263
|
-
expect(response.hint).toContain(
|
|
254
|
+
expect(response.hint).toContain('http://<server-host>:7380');
|
|
264
255
|
});
|
|
265
|
-
it(
|
|
266
|
-
const { buildSchedulerPingFailureResponse } = await import(
|
|
267
|
-
const response = buildSchedulerPingFailureResponse(new Error(
|
|
268
|
-
expect(response.schedulerBaseUrl).toBe(
|
|
269
|
-
expect(response.failureStage).toBe(
|
|
270
|
-
expect(response.runtimeBridge).toBe(
|
|
271
|
-
expect(response.hint).toContain(
|
|
256
|
+
it('explains non-localhost scheduler ping failures as reachability or token issues', async () => {
|
|
257
|
+
const { buildSchedulerPingFailureResponse } = await import('./instance.js');
|
|
258
|
+
const response = buildSchedulerPingFailureResponse(new Error('Request failed with status code 401'), 'http://10.0.0.8:7380');
|
|
259
|
+
expect(response.schedulerBaseUrl).toBe('http://10.0.0.8:7380');
|
|
260
|
+
expect(response.failureStage).toBe('scheduler_ping');
|
|
261
|
+
expect(response.runtimeBridge).toBe('healthy');
|
|
262
|
+
expect(response.hint).toContain('运行时访问令牌仍有效');
|
|
272
263
|
});
|
|
273
|
-
it(
|
|
274
|
-
process.env.AWS_RUNTIME_SCHEDULER_BASE_URL =
|
|
275
|
-
const { default: axios } = await import(
|
|
276
|
-
const { requestRuntimeAccessTokenRefreshForServer } = await import(
|
|
277
|
-
const { instanceRouter } = await import(
|
|
264
|
+
it('refreshes runtime token and retries scheduler ping after 401', async () => {
|
|
265
|
+
process.env.AWS_RUNTIME_SCHEDULER_BASE_URL = 'http://localhost:8080';
|
|
266
|
+
const { default: axios } = await import('axios');
|
|
267
|
+
const { requestRuntimeAccessTokenRefreshForServer } = await import('../services/auto-register.js');
|
|
268
|
+
const { instanceRouter } = await import('./instance.js');
|
|
278
269
|
vi.mocked(requestRuntimeAccessTokenRefreshForServer).mockResolvedValue({
|
|
279
270
|
success: true,
|
|
280
|
-
runtimeAccessToken:
|
|
271
|
+
runtimeAccessToken: 'fresh-runtime-token-123456',
|
|
281
272
|
updated: true,
|
|
282
273
|
});
|
|
283
274
|
vi.mocked(axios.get)
|
|
284
|
-
.mockRejectedValueOnce({
|
|
285
|
-
response: { status: 401 },
|
|
286
|
-
message: "Request failed with status code 401",
|
|
287
|
-
})
|
|
275
|
+
.mockRejectedValueOnce({ response: { status: 401 }, message: 'Request failed with status code 401' })
|
|
288
276
|
.mockResolvedValueOnce({ status: 200 });
|
|
289
|
-
const handler = instanceRouter.stack.find((layer) => layer.route?.path ===
|
|
290
|
-
expect(handler).toBeTypeOf(
|
|
277
|
+
const handler = instanceRouter.stack.find((layer) => layer.route?.path === '/ping')?.route?.stack[1]?.handle;
|
|
278
|
+
expect(handler).toBeTypeOf('function');
|
|
291
279
|
const json = vi.fn();
|
|
292
280
|
const status = vi.fn(() => ({ json }));
|
|
293
|
-
await handler?.({ header: vi.fn(() =>
|
|
294
|
-
expect(requestRuntimeAccessTokenRefreshForServer).toHaveBeenCalledWith(
|
|
295
|
-
expect(axios.get).toHaveBeenNthCalledWith(1,
|
|
296
|
-
headers: {
|
|
281
|
+
await handler?.({ header: vi.fn(() => '') }, { json, status }, vi.fn());
|
|
282
|
+
expect(requestRuntimeAccessTokenRefreshForServer).toHaveBeenCalledWith('http://localhost:8080');
|
|
283
|
+
expect(axios.get).toHaveBeenNthCalledWith(1, 'http://localhost:8080/api/runtime/ping', {
|
|
284
|
+
headers: { 'X-Runtime-Token': 'stale-runtime-token-123456' },
|
|
297
285
|
});
|
|
298
|
-
expect(axios.get).toHaveBeenNthCalledWith(2,
|
|
299
|
-
headers: {
|
|
286
|
+
expect(axios.get).toHaveBeenNthCalledWith(2, 'http://localhost:8080/api/runtime/ping', {
|
|
287
|
+
headers: { 'X-Runtime-Token': 'fresh-runtime-token-123456' },
|
|
300
288
|
});
|
|
301
289
|
expect(json).toHaveBeenCalledWith({
|
|
302
290
|
ok: true,
|
|
303
|
-
runtimeBridge:
|
|
291
|
+
runtimeBridge: 'healthy',
|
|
304
292
|
schedulerReachable: true,
|
|
305
293
|
tokenRefreshed: true,
|
|
306
|
-
schedulerBaseUrl:
|
|
294
|
+
schedulerBaseUrl: 'http://localhost:8080',
|
|
307
295
|
});
|
|
308
296
|
expect(status).not.toHaveBeenCalled();
|
|
309
297
|
});
|
|
310
|
-
it(
|
|
311
|
-
process.env.AWS_RUNTIME_SCHEDULER_BASE_URL =
|
|
312
|
-
const { default: axios } = await import(
|
|
313
|
-
const {
|
|
314
|
-
const {
|
|
315
|
-
|
|
316
|
-
vi.mocked(getRuntimeAccessToken).mockReturnValue("stale-runtime-token-123456");
|
|
317
|
-
vi.mocked(requestRuntimeAccessTokenRefreshForServer).mockReset();
|
|
298
|
+
it('keeps explicit local scheduler config before server ping request header', async () => {
|
|
299
|
+
process.env.AWS_RUNTIME_SCHEDULER_BASE_URL = 'http://localhost:8080';
|
|
300
|
+
const { default: axios } = await import('axios');
|
|
301
|
+
const { getRuntimeAccessToken } = await import('../services/runtime-binding.js');
|
|
302
|
+
const { instanceRouter } = await import('./instance.js');
|
|
303
|
+
vi.mocked(getRuntimeAccessToken).mockReturnValue('stale-runtime-token-123456');
|
|
318
304
|
vi.mocked(axios.get).mockReset();
|
|
319
305
|
vi.mocked(axios.get).mockResolvedValueOnce({ status: 200 });
|
|
320
|
-
const handler = instanceRouter.stack.find((layer) => layer.route?.path ===
|
|
321
|
-
expect(handler).toBeTypeOf(
|
|
306
|
+
const handler = instanceRouter.stack.find((layer) => layer.route?.path === '/ping')?.route?.stack[1]?.handle;
|
|
307
|
+
expect(handler).toBeTypeOf('function');
|
|
322
308
|
const json = vi.fn();
|
|
323
309
|
const status = vi.fn(() => ({ json }));
|
|
324
|
-
const header = vi.fn((name) => name ===
|
|
310
|
+
const header = vi.fn((name) => (name === 'X-Scheduler-Base-Url' ? 'http://server.local:7380/api' : ''));
|
|
325
311
|
await handler?.({ header }, { json, status }, vi.fn());
|
|
326
|
-
expect(getRuntimeAccessToken).toHaveBeenCalledWith(undefined,
|
|
327
|
-
expect(
|
|
328
|
-
|
|
329
|
-
headers: { "X-Runtime-Token": "stale-runtime-token-123456" },
|
|
312
|
+
expect(getRuntimeAccessToken).toHaveBeenCalledWith(undefined, 'http://localhost:8080');
|
|
313
|
+
expect(axios.get).toHaveBeenCalledWith('http://localhost:8080/api/runtime/ping', {
|
|
314
|
+
headers: { 'X-Runtime-Token': 'stale-runtime-token-123456' },
|
|
330
315
|
});
|
|
331
316
|
expect(json).toHaveBeenCalledWith({
|
|
332
317
|
ok: true,
|
|
333
|
-
runtimeBridge:
|
|
318
|
+
runtimeBridge: 'healthy',
|
|
334
319
|
schedulerReachable: true,
|
|
335
320
|
tokenRefreshed: false,
|
|
336
|
-
schedulerBaseUrl:
|
|
321
|
+
schedulerBaseUrl: 'http://localhost:8080',
|
|
337
322
|
});
|
|
338
323
|
expect(status).not.toHaveBeenCalled();
|
|
339
324
|
});
|
|
340
|
-
it(
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const {
|
|
344
|
-
const {
|
|
345
|
-
|
|
346
|
-
vi.mocked(
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
325
|
+
it('uses scheduler base URL supplied by server ping request when bridge has no local scheduler config', async () => {
|
|
326
|
+
delete process.env.AWS_RUNTIME_SCHEDULER_BASE_URL;
|
|
327
|
+
process.env.AWS_TEST_HOME = `${process.cwd()}/.vitest-empty-bridge-home`;
|
|
328
|
+
const { default: axios } = await import('axios');
|
|
329
|
+
const { getRuntimeAccessToken } = await import('../services/runtime-binding.js');
|
|
330
|
+
const { instanceRouter } = await import('./instance.js');
|
|
331
|
+
vi.mocked(getRuntimeAccessToken).mockReturnValue('stale-runtime-token-123456');
|
|
332
|
+
vi.mocked(axios.get).mockReset();
|
|
333
|
+
vi.mocked(axios.get).mockResolvedValueOnce({ status: 200 });
|
|
334
|
+
const handler = instanceRouter.stack.find((layer) => layer.route?.path === '/ping')?.route?.stack[1]?.handle;
|
|
335
|
+
expect(handler).toBeTypeOf('function');
|
|
336
|
+
const json = vi.fn();
|
|
337
|
+
const status = vi.fn(() => ({ json }));
|
|
338
|
+
const header = vi.fn((name) => (name === 'X-Scheduler-Base-Url' ? 'http://server.local:7380/api' : ''));
|
|
339
|
+
await handler?.({ header }, { json, status }, vi.fn());
|
|
340
|
+
expect(getRuntimeAccessToken).toHaveBeenCalledWith(undefined, 'http://server.local:7380');
|
|
341
|
+
expect(axios.get).toHaveBeenCalledWith('http://server.local:7380/api/runtime/ping', {
|
|
342
|
+
headers: { 'X-Runtime-Token': 'stale-runtime-token-123456' },
|
|
350
343
|
});
|
|
344
|
+
expect(json).toHaveBeenCalledWith({
|
|
345
|
+
ok: true,
|
|
346
|
+
runtimeBridge: 'healthy',
|
|
347
|
+
schedulerReachable: true,
|
|
348
|
+
tokenRefreshed: false,
|
|
349
|
+
schedulerBaseUrl: 'http://server.local:7380',
|
|
350
|
+
});
|
|
351
|
+
expect(status).not.toHaveBeenCalled();
|
|
352
|
+
});
|
|
353
|
+
it('keeps single auto-register scheduler config before server ping request header', async () => {
|
|
354
|
+
delete process.env.AWS_RUNTIME_SCHEDULER_BASE_URL;
|
|
355
|
+
const fs = await import('node:fs');
|
|
356
|
+
const path = await import('node:path');
|
|
357
|
+
const testHome = path.join(process.cwd(), '.vitest-single-target-bridge-home');
|
|
358
|
+
const configDir = path.join(testHome, '.aws-bridge');
|
|
359
|
+
fs.rmSync(testHome, { recursive: true, force: true });
|
|
360
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
361
|
+
fs.writeFileSync(path.join(configDir, 'config.json'), JSON.stringify({
|
|
362
|
+
autoRegisterTargets: [
|
|
363
|
+
{ serverUrl: 'http://bridge-config.local:7380/api' },
|
|
364
|
+
],
|
|
365
|
+
}), 'utf-8');
|
|
366
|
+
process.env.AWS_TEST_HOME = testHome;
|
|
367
|
+
const { default: axios } = await import('axios');
|
|
368
|
+
const { getRuntimeAccessToken } = await import('../services/runtime-binding.js');
|
|
369
|
+
const { instanceRouter } = await import('./instance.js');
|
|
370
|
+
vi.mocked(getRuntimeAccessToken).mockReturnValue('stale-runtime-token-123456');
|
|
351
371
|
vi.mocked(axios.get).mockReset();
|
|
352
372
|
vi.mocked(axios.get).mockResolvedValueOnce({ status: 200 });
|
|
353
|
-
const handler = instanceRouter.stack.find((layer) => layer.route?.path ===
|
|
354
|
-
expect(handler).toBeTypeOf(
|
|
373
|
+
const handler = instanceRouter.stack.find((layer) => layer.route?.path === '/ping')?.route?.stack[1]?.handle;
|
|
374
|
+
expect(handler).toBeTypeOf('function');
|
|
355
375
|
const json = vi.fn();
|
|
356
376
|
const status = vi.fn(() => ({ json }));
|
|
357
|
-
const header = vi.fn((name) => name ===
|
|
377
|
+
const header = vi.fn((name) => (name === 'X-Scheduler-Base-Url' ? 'http://server.local:7380/api' : ''));
|
|
358
378
|
await handler?.({ header }, { json, status }, vi.fn());
|
|
359
|
-
expect(getRuntimeAccessToken).toHaveBeenCalledWith(undefined,
|
|
360
|
-
expect(
|
|
361
|
-
|
|
362
|
-
headers: { "X-Runtime-Token": "fresh-runtime-token-123456" },
|
|
379
|
+
expect(getRuntimeAccessToken).toHaveBeenCalledWith(undefined, 'http://bridge-config.local:7380');
|
|
380
|
+
expect(axios.get).toHaveBeenCalledWith('http://bridge-config.local:7380/api/runtime/ping', {
|
|
381
|
+
headers: { 'X-Runtime-Token': 'stale-runtime-token-123456' },
|
|
363
382
|
});
|
|
364
383
|
expect(json).toHaveBeenCalledWith({
|
|
365
384
|
ok: true,
|
|
366
|
-
runtimeBridge:
|
|
385
|
+
runtimeBridge: 'healthy',
|
|
367
386
|
schedulerReachable: true,
|
|
368
|
-
tokenRefreshed:
|
|
369
|
-
schedulerBaseUrl:
|
|
387
|
+
tokenRefreshed: false,
|
|
388
|
+
schedulerBaseUrl: 'http://bridge-config.local:7380',
|
|
370
389
|
});
|
|
371
390
|
expect(status).not.toHaveBeenCalled();
|
|
372
391
|
});
|
|
373
|
-
it(
|
|
374
|
-
const { buildAmbiguousSchedulerBaseUrlResponse } = await import(
|
|
392
|
+
it('explains ambiguous auto-register scheduler targets without picking the first one', async () => {
|
|
393
|
+
const { buildAmbiguousSchedulerBaseUrlResponse } = await import('./instance.js');
|
|
375
394
|
const response = buildAmbiguousSchedulerBaseUrlResponse([
|
|
376
|
-
|
|
377
|
-
|
|
395
|
+
'http://scheduler-a.local:7380',
|
|
396
|
+
'http://scheduler-b.local:7380',
|
|
378
397
|
]);
|
|
379
398
|
expect(response).toEqual({
|
|
380
399
|
ok: false,
|
|
381
|
-
error:
|
|
382
|
-
failureStage:
|
|
383
|
-
runtimeBridge:
|
|
384
|
-
schedulerBaseUrl:
|
|
400
|
+
error: 'ambiguous_scheduler_base_url',
|
|
401
|
+
failureStage: 'scheduler_ping',
|
|
402
|
+
runtimeBridge: 'healthy',
|
|
403
|
+
schedulerBaseUrl: '',
|
|
385
404
|
autoRegisterTargetUrls: [
|
|
386
|
-
|
|
387
|
-
|
|
405
|
+
'http://scheduler-a.local:7380',
|
|
406
|
+
'http://scheduler-b.local:7380',
|
|
388
407
|
],
|
|
389
|
-
hint: expect.stringContaining(
|
|
408
|
+
hint: expect.stringContaining('AWS_RUNTIME_SCHEDULER_BASE_URL'),
|
|
390
409
|
});
|
|
391
410
|
});
|
|
392
|
-
it(
|
|
393
|
-
const { instanceRouter } = await import(
|
|
394
|
-
mockSpawnResult(0,
|
|
395
|
-
const handler = instanceRouter.stack.find((layer) => layer.route?.path ===
|
|
396
|
-
expect(handler).toBeTypeOf(
|
|
411
|
+
it('starts bridge package update in background and returns operation id', async () => {
|
|
412
|
+
const { instanceRouter } = await import('./instance.js');
|
|
413
|
+
mockSpawnResult(0, 'updated');
|
|
414
|
+
const handler = instanceRouter.stack.find((layer) => layer.route?.path === '/update-bridge')?.route?.stack[1]?.handle;
|
|
415
|
+
expect(handler).toBeTypeOf('function');
|
|
397
416
|
const { json, status } = createMockResponse();
|
|
398
|
-
await handler?.({
|
|
399
|
-
body: { requestedByUserId: "user-1" },
|
|
400
|
-
get: () => "bridge.local:18081",
|
|
401
|
-
protocol: "http",
|
|
402
|
-
}, { json, status }, vi.fn());
|
|
417
|
+
await handler?.({ body: { requestedByUserId: 'user-1' }, get: () => 'bridge.local:18081', protocol: 'http' }, { json, status }, vi.fn());
|
|
403
418
|
expect(status).not.toHaveBeenCalled();
|
|
404
419
|
expect(json).toHaveBeenCalledWith(expect.objectContaining({
|
|
405
420
|
ok: true,
|
|
406
|
-
message:
|
|
421
|
+
message: 'Bridge 更新已在后台开始,完成后会通过消息中心通知结果。',
|
|
407
422
|
}));
|
|
408
423
|
await new Promise((resolve) => setImmediate(resolve));
|
|
409
|
-
expect(spawnMock).toHaveBeenCalledWith(process.platform ===
|
|
424
|
+
expect(spawnMock).toHaveBeenCalledWith(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['install', '-g', 'aws-runtime-bridge@latest'], { shell: false, windowsHide: true });
|
|
410
425
|
});
|
|
411
|
-
it(
|
|
412
|
-
const { instanceRouter } = await import(
|
|
413
|
-
const handler = instanceRouter.stack.find((layer) => layer.route?.path ===
|
|
414
|
-
expect(handler).toBeTypeOf(
|
|
426
|
+
it('rejects background bridge update without requester user id', async () => {
|
|
427
|
+
const { instanceRouter } = await import('./instance.js');
|
|
428
|
+
const handler = instanceRouter.stack.find((layer) => layer.route?.path === '/update-bridge')?.route?.stack[1]?.handle;
|
|
429
|
+
expect(handler).toBeTypeOf('function');
|
|
415
430
|
const { json, status } = createMockResponse();
|
|
416
431
|
await handler?.({}, { json, status }, vi.fn());
|
|
417
432
|
expect(status).toHaveBeenCalledWith(400);
|
|
418
|
-
expect(json).toHaveBeenCalledWith({
|
|
419
|
-
error: "requestedByUserId is required",
|
|
420
|
-
});
|
|
433
|
+
expect(json).toHaveBeenCalledWith({ error: 'requestedByUserId is required' });
|
|
421
434
|
});
|
|
422
|
-
it(
|
|
435
|
+
it('responds before triggering graceful bridge restart', async () => {
|
|
423
436
|
vi.useFakeTimers();
|
|
424
|
-
const { instanceRouter, setGracefulShutdownFn } = await import(
|
|
437
|
+
const { instanceRouter, setGracefulShutdownFn } = await import('./instance.js');
|
|
425
438
|
const gracefulShutdown = vi.fn().mockResolvedValue(undefined);
|
|
426
439
|
setGracefulShutdownFn(gracefulShutdown);
|
|
427
|
-
const handler = instanceRouter.stack.find((layer) => layer.route?.path ===
|
|
428
|
-
expect(handler).toBeTypeOf(
|
|
440
|
+
const handler = instanceRouter.stack.find((layer) => layer.route?.path === '/restart-bridge')?.route?.stack[1]?.handle;
|
|
441
|
+
expect(handler).toBeTypeOf('function');
|
|
429
442
|
const { json, status } = createMockResponse();
|
|
430
|
-
await handler?.({
|
|
431
|
-
body: { preserveSessions: true, requestedByUserId: "user-1" },
|
|
432
|
-
get: () => "bridge.local:18081",
|
|
433
|
-
protocol: "http",
|
|
434
|
-
}, { json, status }, vi.fn());
|
|
443
|
+
await handler?.({ body: { preserveSessions: true, requestedByUserId: 'user-1' }, get: () => 'bridge.local:18081', protocol: 'http' }, { json, status }, vi.fn());
|
|
435
444
|
expect(status).not.toHaveBeenCalled();
|
|
436
445
|
expect(json).toHaveBeenCalledWith(expect.objectContaining({
|
|
437
446
|
ok: true,
|
|
438
447
|
preserveSessions: true,
|
|
439
|
-
message:
|
|
448
|
+
message: 'Bridge 正在优雅退出;下次启动后会通过消息中心通知结果。若未由守护进程管理,请手动重新启动。',
|
|
440
449
|
}));
|
|
441
450
|
expect(gracefulShutdown).not.toHaveBeenCalled();
|
|
442
451
|
await vi.advanceTimersByTimeAsync(250);
|
|
443
|
-
expect(gracefulShutdown).toHaveBeenCalledWith(
|
|
452
|
+
expect(gracefulShutdown).toHaveBeenCalledWith('PANEL_RESTART', true);
|
|
444
453
|
});
|
|
445
454
|
});
|