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.
Files changed (42) hide show
  1. package/dist/adapter/AcodeSdkAdapter.d.ts.map +1 -1
  2. package/dist/adapter/AcodeSdkAdapter.js +6 -98
  3. package/dist/adapter/AcodeSdkAdapter.test.js +2 -313
  4. package/dist/adapter/OpencodeSdkAdapter.js +1 -1
  5. package/dist/adapter/types.d.ts.map +1 -1
  6. package/dist/adapter/types.js +3 -7
  7. package/dist/config.d.ts +3 -2
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +13 -36
  10. package/dist/config.test.js +20 -24
  11. package/dist/routes/ai-sources.js +1 -5
  12. package/dist/routes/ai-sources.test.js +0 -21
  13. package/dist/routes/instance.d.ts.map +1 -1
  14. package/dist/routes/instance.js +14 -31
  15. package/dist/routes/instance.test.js +224 -215
  16. package/dist/routes/runtime-binding.d.ts.map +1 -1
  17. package/dist/routes/runtime-binding.js +1 -36
  18. package/dist/routes/terminal.d.ts +0 -2
  19. package/dist/routes/terminal.d.ts.map +1 -1
  20. package/dist/routes/terminal.js +7 -109
  21. package/dist/routes/terminal.test.js +1 -118
  22. package/dist/services/mcp-launch-binding-queue.d.ts +0 -7
  23. package/dist/services/mcp-launch-binding-queue.d.ts.map +1 -1
  24. package/dist/services/mcp-launch-binding-queue.js +0 -25
  25. package/dist/services/session-output.d.ts +3 -7
  26. package/dist/services/session-output.d.ts.map +1 -1
  27. package/package/acode/dist/runtime.d.ts +0 -1
  28. package/package/acode/dist/runtime.d.ts.map +1 -1
  29. package/package/acode/dist/runtime.js +31 -265
  30. package/package/acode/dist/types.d.ts +1 -1
  31. package/package/acode/dist/types.d.ts.map +1 -1
  32. package/package/aws-client-agent-mcp/dist/config.d.ts +0 -4
  33. package/package/aws-client-agent-mcp/dist/config.d.ts.map +1 -1
  34. package/package/aws-client-agent-mcp/dist/config.js +0 -15
  35. package/package/aws-client-agent-mcp/dist/config.js.map +1 -1
  36. package/package/aws-client-agent-mcp/dist/http-client.d.ts +0 -10
  37. package/package/aws-client-agent-mcp/dist/http-client.d.ts.map +1 -1
  38. package/package/aws-client-agent-mcp/dist/http-client.js +1 -79
  39. package/package/aws-client-agent-mcp/dist/http-client.js.map +1 -1
  40. package/package/aws-client-agent-mcp/dist/http-client.test.js +265 -129
  41. package/package/aws-client-agent-mcp/dist/http-client.test.js.map +1 -1
  42. package/package.json +1 -1
@@ -1,63 +1,61 @@
1
1
  /**
2
2
  * Instance 路由单元测试
3
3
  */
4
- import { describe, it, expect, vi, afterEach } from "vitest";
5
- import { EventEmitter } from "node:events";
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("axios", () => ({
7
+ vi.mock('axios', () => ({
8
8
  default: {
9
9
  get: vi.fn(),
10
10
  post: vi.fn(),
11
11
  },
12
12
  }));
13
- vi.mock("../services/auto-register.js", () => ({
13
+ vi.mock('../services/auto-register.js', () => ({
14
14
  getConfiguredConnectionKeys: vi.fn(() => []),
15
15
  requestRuntimeAccessTokenRefreshForServer: vi.fn(),
16
16
  }));
17
- vi.mock("../services/runtime-binding.js", () => ({
18
- getRuntimeAccessToken: vi.fn(() => "stale-runtime-token-123456"),
19
- loadRuntimeBinding: vi.fn(() => ({
20
- status: "paired",
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("../services/lifecycle-state.js", () => ({
22
+ vi.mock('../services/lifecycle-state.js', () => ({
27
23
  createBridgeLifecycleOperation: vi.fn((input) => ({
28
- operationId: "operation-1",
29
- requestedAt: "2026-01-01T00:00:00.000Z",
30
- updatedAt: "2026-01-01T00:00:00.000Z",
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("../config.js", async (importOriginal) => ({
32
+ vi.mock('../config.js', async (importOriginal) => ({
37
33
  ...(await importOriginal()),
38
34
  getRuntimeHomeDir: vi.fn(() => process.cwd()),
39
35
  }));
40
- vi.mock("node:child_process", async (importOriginal) => ({
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 = "", stderrText = "") {
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("data", Buffer.from(stdoutText));
53
+ child.stdout.emit('data', Buffer.from(stdoutText));
56
54
  }
57
55
  if (stderrText) {
58
- child.stderr.emit("data", Buffer.from(stderrText));
56
+ child.stderr.emit('data', Buffer.from(stderrText));
59
57
  }
60
- child.emit("close", exitCode);
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("instance route validation", () => {
71
- it("builds correct ping response when healthy", () => {
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: "healthy",
72
+ runtimeBridge: 'healthy',
75
73
  schedulerReachable: schedulerStatus >= 200 && schedulerStatus < 300,
76
74
  schedulerBaseUrl,
77
75
  });
78
- const healthyResponse = buildPingResponse(200, "http://localhost:8080");
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, "http://localhost:8080");
79
+ const unhealthyResponse = buildPingResponse(502, 'http://localhost:8080');
82
80
  expect(unhealthyResponse.schedulerReachable).toBe(false);
83
81
  });
84
- it("requires agentId and workspacePath", () => {
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: "agent-1", workspacePath: "/path" }).valid).toBe(true);
89
+ expect(validateInitRequest({ agentId: 'agent-1', workspacePath: '/path' }).valid).toBe(true);
95
90
  });
96
- it("builds correct init options", () => {
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: ["claude", "opencode"],
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(["claude", "opencode"]);
104
+ expect(options.ccSwitchEnabledTools).toEqual(['claude', 'opencode']);
110
105
  });
111
- it("requires agentId for state query", () => {
106
+ it('requires agentId for state query', () => {
112
107
  const validateStateRequest = (body) => {
113
108
  if (!body.agentId)
114
- return { valid: false, error: "agentId is required" };
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: "agent-1" }).valid).toBe(true);
113
+ expect(validateStateRequest({ agentId: 'agent-1' }).valid).toBe(true);
119
114
  });
120
- it("always detects panel tool statuses even when legacy enabled tools omit codex", async () => {
121
- const { buildToolStatusDetectionTargets } = await import("./instance.js");
122
- expect(buildToolStatusDetectionTargets(["claude", "opencode"])).toEqual([
123
- "claude",
124
- "opencode",
125
- "acode",
126
- "codex",
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("accepts codex for cc-switch tool installation validation", async () => {
130
- const { SUPPORTED_INSTALLABLE_TOOLS } = await import("../services/tool-installer.js");
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(["Codex"])).toEqual(["codex"]);
143
- expect(SUPPORTED_INSTALLABLE_TOOLS).toContain("codex");
133
+ expect(normalizeInstallableTools(['Codex'])).toEqual(['codex']);
134
+ expect(SUPPORTED_INSTALLABLE_TOOLS).toContain('codex');
144
135
  });
145
- it("accepts panel tools for cc-switch tool uninstallation validation", async () => {
146
- const { SUPPORTED_UNINSTALLABLE_TOOLS } = await import("../services/tool-installer.js");
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(["Claude", "OpenCode", "Codex"])).toEqual(["claude", "opencode", "codex"]);
159
- expect(normalizeUninstallableTools(["unknown-tool"])).toEqual([]);
145
+ expect(normalizeUninstallableTools(['Claude', 'OpenCode', 'Codex'])).toEqual([
146
+ 'claude',
147
+ 'opencode',
148
+ 'codex',
149
+ ]);
150
+ expect(normalizeUninstallableTools(['unknown-tool'])).toEqual([]);
160
151
  });
161
- it("reports uninstall failure when a requested tool remains installed", async () => {
162
- const { buildUninstallFailureMessage } = await import("./instance.js");
163
- expect(buildUninstallFailureMessage(["opencode"], {
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: "opencode",
156
+ tool: 'opencode',
166
157
  installed: true,
167
- executable: "/home/user/.opencode/bin/opencode",
168
- version: "1.14.39",
158
+ executable: '/home/user/.opencode/bin/opencode',
159
+ version: '1.14.39',
169
160
  installing: false,
170
- error: "uninstall completed but command is still available",
161
+ error: 'uninstall completed but command is still available',
171
162
  },
172
- })).toContain("opencode: uninstall completed but command is still available");
163
+ })).toContain('opencode: uninstall completed but command is still available');
173
164
  });
174
- it("summarizes tool status for route diagnostics", async () => {
175
- const { summarizeToolStatus } = await import("./instance.js");
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: "opencode",
169
+ tool: 'opencode',
179
170
  installed: false,
180
171
  executable: null,
181
172
  version: null,
182
173
  installing: false,
183
- error: "SDK package @opencode-ai/sdk is not installed in aws-runtime-bridge",
174
+ error: 'SDK package @opencode-ai/sdk is not installed in aws-runtime-bridge',
184
175
  },
185
176
  claude: {
186
- tool: "claude",
177
+ tool: 'claude',
187
178
  installed: true,
188
- executable: "@anthropic-ai/claude-agent-sdk",
189
- version: "0.2.87",
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: "SDK package @opencode-ai/sdk is not installed in aws-runtime-bridge",
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: "@anthropic-ai/claude-agent-sdk",
203
- version: "0.2.87",
193
+ executable: '@anthropic-ai/claude-agent-sdk',
194
+ version: '0.2.87',
204
195
  error: null,
205
196
  },
206
197
  });
207
198
  });
208
- it("does not report uninstall failure when requested tools are no longer installed", async () => {
209
- const { buildUninstallFailureMessage } = await import("./instance.js");
210
- expect(buildUninstallFailureMessage(["claude"], {
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: "claude",
203
+ tool: 'claude',
213
204
  installed: false,
214
205
  executable: null,
215
206
  version: null,
216
207
  installing: false,
217
- error: "command not installed",
208
+ error: 'command not installed',
218
209
  },
219
- })).toBe("");
210
+ })).toBe('');
220
211
  });
221
- it("allows connection check when no connection key is configured", async () => {
222
- const { getConfiguredConnectionKeys } = await import("../services/auto-register.js");
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("./instance.js");
225
- expect(buildConnectionCheckResponse("")).toEqual({
215
+ const { buildConnectionCheckResponse } = await import('./instance.js');
216
+ expect(buildConnectionCheckResponse('')).toEqual({
226
217
  status: 200,
227
218
  body: {
228
219
  ok: true,
229
- runtimeBridge: "healthy",
220
+ runtimeBridge: 'healthy',
230
221
  connectionKeyMatched: true,
231
222
  connectionKeyRequired: false,
232
223
  },
233
224
  });
234
225
  });
235
- it("requires matching connection key digest when a connection key is configured", async () => {
236
- const { createHash } = await import("node:crypto");
237
- const { getConfiguredConnectionKeys } = await import("../services/auto-register.js");
238
- vi.mocked(getConfiguredConnectionKeys).mockReturnValue(["secret-key"]);
239
- const { buildConnectionCheckResponse } = await import("./instance.js");
240
- const digest = createHash("md5").update("secret-key", "utf8").digest("hex");
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("00000000000000000000000000000000")).toEqual({
233
+ expect(buildConnectionCheckResponse('00000000000000000000000000000000')).toEqual({
243
234
  status: 401,
244
235
  body: {
245
236
  ok: false,
246
- error: "connection_key_mismatch",
237
+ error: 'connection_key_mismatch',
247
238
  connectionKeyMatched: false,
248
239
  connectionKeyRequired: true,
249
240
  },
250
241
  });
251
242
  });
252
- it("explains localhost scheduler ping failures with environment variable guidance", async () => {
253
- const { buildSchedulerPingFailureResponse } = await import("./instance.js");
254
- const response = buildSchedulerPingFailureResponse(new Error("connect ECONNREFUSED 127.0.0.1:8080"), "http://localhost:8080");
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: "connect ECONNREFUSED 127.0.0.1:8080",
258
- failureStage: "scheduler_ping",
259
- runtimeBridge: "healthy",
260
- schedulerBaseUrl: "http://localhost:8080",
261
- hint: expect.stringContaining("AWS_RUNTIME_SCHEDULER_BASE_URL"),
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("http://<server-host>:7380");
254
+ expect(response.hint).toContain('http://<server-host>:7380');
264
255
  });
265
- it("explains non-localhost scheduler ping failures as reachability or token issues", async () => {
266
- const { buildSchedulerPingFailureResponse } = await import("./instance.js");
267
- const response = buildSchedulerPingFailureResponse(new Error("Request failed with status code 401"), "http://10.0.0.8:7380");
268
- expect(response.schedulerBaseUrl).toBe("http://10.0.0.8:7380");
269
- expect(response.failureStage).toBe("scheduler_ping");
270
- expect(response.runtimeBridge).toBe("healthy");
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("refreshes runtime token and retries scheduler ping after 401", async () => {
274
- process.env.AWS_RUNTIME_SCHEDULER_BASE_URL = "http://localhost:8080";
275
- const { default: axios } = await import("axios");
276
- const { requestRuntimeAccessTokenRefreshForServer } = await import("../services/auto-register.js");
277
- const { instanceRouter } = await import("./instance.js");
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: "fresh-runtime-token-123456",
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 === "/ping")?.route?.stack[1]?.handle;
290
- expect(handler).toBeTypeOf("function");
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(() => "") }, { json, status }, vi.fn());
294
- expect(requestRuntimeAccessTokenRefreshForServer).toHaveBeenCalledWith("http://localhost:8080");
295
- expect(axios.get).toHaveBeenNthCalledWith(1, "http://localhost:8080/api/runtime/ping", {
296
- headers: { "X-Runtime-Token": "stale-runtime-token-123456" },
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, "http://localhost:8080/api/runtime/ping", {
299
- headers: { "X-Runtime-Token": "fresh-runtime-token-123456" },
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: "healthy",
291
+ runtimeBridge: 'healthy',
304
292
  schedulerReachable: true,
305
293
  tokenRefreshed: true,
306
- schedulerBaseUrl: "http://localhost:8080",
294
+ schedulerBaseUrl: 'http://localhost:8080',
307
295
  });
308
296
  expect(status).not.toHaveBeenCalled();
309
297
  });
310
- it("uses scheduler base URL supplied by server ping request before local bridge config", async () => {
311
- process.env.AWS_RUNTIME_SCHEDULER_BASE_URL = "http://localhost:8080";
312
- const { default: axios } = await import("axios");
313
- const { requestRuntimeAccessTokenRefreshForServer } = await import("../services/auto-register.js");
314
- const { getRuntimeAccessToken } = await import("../services/runtime-binding.js");
315
- const { instanceRouter } = await import("./instance.js");
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 === "/ping")?.route?.stack[1]?.handle;
321
- expect(handler).toBeTypeOf("function");
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 === "X-Scheduler-Base-Url" ? "http://server.local:7380/api" : "");
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, "http://server.local:7380");
327
- expect(requestRuntimeAccessTokenRefreshForServer).not.toHaveBeenCalled();
328
- expect(axios.get).toHaveBeenCalledWith("http://server.local:7380/api/runtime/ping", {
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: "healthy",
318
+ runtimeBridge: 'healthy',
334
319
  schedulerReachable: true,
335
320
  tokenRefreshed: false,
336
- schedulerBaseUrl: "http://server.local:7380",
321
+ schedulerBaseUrl: 'http://localhost:8080',
337
322
  });
338
323
  expect(status).not.toHaveBeenCalled();
339
324
  });
340
- it("refreshes runtime access token when bridge ping has no local scheduler token", async () => {
341
- const { default: axios } = await import("axios");
342
- const { requestRuntimeAccessTokenRefreshForServer } = await import("../services/auto-register.js");
343
- const { getRuntimeAccessToken } = await import("../services/runtime-binding.js");
344
- const { instanceRouter } = await import("./instance.js");
345
- vi.mocked(getRuntimeAccessToken).mockReturnValue(undefined);
346
- vi.mocked(requestRuntimeAccessTokenRefreshForServer).mockResolvedValueOnce({
347
- success: true,
348
- runtimeAccessToken: "fresh-runtime-token-123456",
349
- updated: true,
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 === "/ping")?.route?.stack[1]?.handle;
354
- expect(handler).toBeTypeOf("function");
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 === "X-Scheduler-Base-Url" ? "http://server.local:7380/api" : "");
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, "http://server.local:7380");
360
- expect(requestRuntimeAccessTokenRefreshForServer).toHaveBeenCalledWith("http://server.local:7380");
361
- expect(axios.get).toHaveBeenCalledWith("http://server.local:7380/api/runtime/ping", {
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: "healthy",
385
+ runtimeBridge: 'healthy',
367
386
  schedulerReachable: true,
368
- tokenRefreshed: true,
369
- schedulerBaseUrl: "http://server.local:7380",
387
+ tokenRefreshed: false,
388
+ schedulerBaseUrl: 'http://bridge-config.local:7380',
370
389
  });
371
390
  expect(status).not.toHaveBeenCalled();
372
391
  });
373
- it("explains ambiguous auto-register scheduler targets without picking the first one", async () => {
374
- const { buildAmbiguousSchedulerBaseUrlResponse } = await import("./instance.js");
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
- "http://scheduler-a.local:7380",
377
- "http://scheduler-b.local:7380",
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: "ambiguous_scheduler_base_url",
382
- failureStage: "scheduler_ping",
383
- runtimeBridge: "healthy",
384
- schedulerBaseUrl: "",
400
+ error: 'ambiguous_scheduler_base_url',
401
+ failureStage: 'scheduler_ping',
402
+ runtimeBridge: 'healthy',
403
+ schedulerBaseUrl: '',
385
404
  autoRegisterTargetUrls: [
386
- "http://scheduler-a.local:7380",
387
- "http://scheduler-b.local:7380",
405
+ 'http://scheduler-a.local:7380',
406
+ 'http://scheduler-b.local:7380',
388
407
  ],
389
- hint: expect.stringContaining("AWS_RUNTIME_SCHEDULER_BASE_URL"),
408
+ hint: expect.stringContaining('AWS_RUNTIME_SCHEDULER_BASE_URL'),
390
409
  });
391
410
  });
392
- it("starts bridge package update in background and returns operation id", async () => {
393
- const { instanceRouter } = await import("./instance.js");
394
- mockSpawnResult(0, "updated");
395
- const handler = instanceRouter.stack.find((layer) => layer.route?.path === "/update-bridge")?.route?.stack[1]?.handle;
396
- expect(handler).toBeTypeOf("function");
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: "Bridge 更新已在后台开始,完成后会通过消息中心通知结果。",
421
+ message: 'Bridge 更新已在后台开始,完成后会通过消息中心通知结果。',
407
422
  }));
408
423
  await new Promise((resolve) => setImmediate(resolve));
409
- expect(spawnMock).toHaveBeenCalledWith(process.platform === "win32" ? "npm.cmd" : "npm", ["install", "-g", "aws-runtime-bridge@latest"], { shell: false, windowsHide: true });
424
+ expect(spawnMock).toHaveBeenCalledWith(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['install', '-g', 'aws-runtime-bridge@latest'], { shell: false, windowsHide: true });
410
425
  });
411
- it("rejects background bridge update without requester user id", async () => {
412
- const { instanceRouter } = await import("./instance.js");
413
- const handler = instanceRouter.stack.find((layer) => layer.route?.path === "/update-bridge")?.route?.stack[1]?.handle;
414
- expect(handler).toBeTypeOf("function");
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("responds before triggering graceful bridge restart", async () => {
435
+ it('responds before triggering graceful bridge restart', async () => {
423
436
  vi.useFakeTimers();
424
- const { instanceRouter, setGracefulShutdownFn } = await import("./instance.js");
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 === "/restart-bridge")?.route?.stack[1]?.handle;
428
- expect(handler).toBeTypeOf("function");
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: "Bridge 正在优雅退出;下次启动后会通过消息中心通知结果。若未由守护进程管理,请手动重新启动。",
448
+ message: 'Bridge 正在优雅退出;下次启动后会通过消息中心通知结果。若未由守护进程管理,请手动重新启动。',
440
449
  }));
441
450
  expect(gracefulShutdown).not.toHaveBeenCalled();
442
451
  await vi.advanceTimersByTimeAsync(250);
443
- expect(gracefulShutdown).toHaveBeenCalledWith("PANEL_RESTART", true);
452
+ expect(gracefulShutdown).toHaveBeenCalledWith('PANEL_RESTART', true);
444
453
  });
445
454
  });