aws-runtime-bridge 1.8.3 → 1.8.5

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.
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
package/dist/config.js CHANGED
@@ -41,6 +41,19 @@ function normalizeSchedulerHttpBaseUrl(value) {
41
41
  return repaired.replace(/\/+$/, "");
42
42
  }
43
43
  }
44
+ function isLoopbackSchedulerBaseUrl(value) {
45
+ try {
46
+ const url = new URL(value);
47
+ const hostname = url.hostname.toLowerCase();
48
+ return (hostname === "localhost" ||
49
+ hostname === "127.0.0.1" ||
50
+ hostname === "::1" ||
51
+ hostname === "[::1]");
52
+ }
53
+ catch {
54
+ return /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?(?:\/|$)/i.test(value);
55
+ }
56
+ }
44
57
  function getAutoRegisterConfigFilePath() {
45
58
  const testHomeDir = String(process.env.AWS_TEST_HOME || "").trim();
46
59
  return path.join(testHomeDir || homedir(), ".aws-bridge", "config.json");
@@ -84,14 +97,26 @@ export function resolveSchedulerBaseUrl() {
84
97
  */
85
98
  export function resolveSchedulerBaseUrlFrom(requestSchedulerBaseUrl) {
86
99
  const normalizedRequestSchedulerBaseUrl = normalizeSchedulerHttpBaseUrl(requestSchedulerBaseUrl);
100
+ const envSchedulerBaseUrl = normalizeSchedulerHttpBaseUrl(process.env.AWS_RUNTIME_SCHEDULER_BASE_URL);
101
+ const autoRegisterTargetUrls = readAutoRegisterTargetUrls();
102
+ const singleAutoRegisterTargetUrl = autoRegisterTargetUrls.length === 1 ? autoRegisterTargetUrls[0] : undefined;
87
103
  if (normalizedRequestSchedulerBaseUrl) {
104
+ const localSchedulerBaseUrl = envSchedulerBaseUrl || singleAutoRegisterTargetUrl;
105
+ if (localSchedulerBaseUrl &&
106
+ isLoopbackSchedulerBaseUrl(normalizedRequestSchedulerBaseUrl) &&
107
+ !isLoopbackSchedulerBaseUrl(localSchedulerBaseUrl)) {
108
+ return {
109
+ url: localSchedulerBaseUrl,
110
+ source: envSchedulerBaseUrl ? "env" : "single-auto-register-target",
111
+ ambiguousTargetUrls: [],
112
+ };
113
+ }
88
114
  return {
89
115
  url: normalizedRequestSchedulerBaseUrl,
90
116
  source: "request",
91
117
  ambiguousTargetUrls: [],
92
118
  };
93
119
  }
94
- const envSchedulerBaseUrl = normalizeSchedulerHttpBaseUrl(process.env.AWS_RUNTIME_SCHEDULER_BASE_URL);
95
120
  if (envSchedulerBaseUrl) {
96
121
  return {
97
122
  url: envSchedulerBaseUrl,
@@ -99,10 +124,9 @@ export function resolveSchedulerBaseUrlFrom(requestSchedulerBaseUrl) {
99
124
  ambiguousTargetUrls: [],
100
125
  };
101
126
  }
102
- const autoRegisterTargetUrls = readAutoRegisterTargetUrls();
103
- if (autoRegisterTargetUrls.length === 1) {
127
+ if (singleAutoRegisterTargetUrl) {
104
128
  return {
105
- url: autoRegisterTargetUrls[0] || DEFAULT_SCHEDULER_BASE_URL,
129
+ url: singleAutoRegisterTargetUrl || DEFAULT_SCHEDULER_BASE_URL,
106
130
  source: "single-auto-register-target",
107
131
  ambiguousTargetUrls: [],
108
132
  };
@@ -57,7 +57,8 @@ describe("scheduler base URL resolution", () => {
57
57
  it("keeps explicit AWS_RUNTIME_SCHEDULER_BASE_URL as the highest priority", async () => {
58
58
  const runtimeHome = createRuntimeHome();
59
59
  useRuntimeHome(runtimeHome);
60
- process.env.AWS_RUNTIME_SCHEDULER_BASE_URL = "http://explicit.local:7380/path";
60
+ process.env.AWS_RUNTIME_SCHEDULER_BASE_URL =
61
+ "http://explicit.local:7380/path";
61
62
  writeBridgeConfig(runtimeHome, [{ serverUrl: "http://target.local:7380" }]);
62
63
  const { resolveSchedulerBaseUrl } = await import("./config.js");
63
64
  expect(resolveSchedulerBaseUrl()).toEqual({
@@ -66,6 +67,30 @@ describe("scheduler base URL resolution", () => {
66
67
  ambiguousTargetUrls: [],
67
68
  });
68
69
  });
70
+ it("ignores a server-supplied loopback URL when bridge has an explicit external scheduler URL", async () => {
71
+ const runtimeHome = createRuntimeHome();
72
+ useRuntimeHome(runtimeHome);
73
+ process.env.AWS_RUNTIME_SCHEDULER_BASE_URL = "http://101.42.200.84:7380";
74
+ const { resolveSchedulerBaseUrlFrom } = await import("./config.js");
75
+ expect(resolveSchedulerBaseUrlFrom("http://127.0.0.1:7380/api")).toEqual({
76
+ url: "http://101.42.200.84:7380",
77
+ source: "env",
78
+ ambiguousTargetUrls: [],
79
+ });
80
+ });
81
+ it("uses a single external auto-register target instead of a server-supplied loopback URL", async () => {
82
+ const runtimeHome = createRuntimeHome();
83
+ useRuntimeHome(runtimeHome);
84
+ writeBridgeConfig(runtimeHome, [
85
+ { serverUrl: "http://101.42.200.84:7380" },
86
+ ]);
87
+ const { resolveSchedulerBaseUrlFrom } = await import("./config.js");
88
+ expect(resolveSchedulerBaseUrlFrom("http://127.0.0.1:7380/api")).toEqual({
89
+ url: "http://101.42.200.84:7380",
90
+ source: "single-auto-register-target",
91
+ ambiguousTargetUrls: [],
92
+ });
93
+ });
69
94
  it("uses a single auto-register target serverUrl when scheduler env is absent", async () => {
70
95
  const runtimeHome = createRuntimeHome();
71
96
  useRuntimeHome(runtimeHome);
@@ -1 +1 @@
1
- {"version":3,"file":"instance.d.ts","sourceRoot":"","sources":["../../src/routes/instance.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAqBrD,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,GAC/D,IAAI,CAEN;AAED,eAAO,MAAM,cAAc,4CAAW,CAAC;AAsJvC,wBAAgB,4BAA4B,CAAC,gBAAgB,EAAE,MAAM,GAAG;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE;QACJ,EAAE,EAAE,OAAO,CAAC;QACZ,aAAa,CAAC,EAAE,SAAS,CAAC;QAC1B,oBAAoB,EAAE,OAAO,CAAC;QAC9B,qBAAqB,EAAE,OAAO,CAAC;QAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;CACH,CAqCA;AAED,MAAM,WAAW,4BAA4B;IAC3C,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,gBAAgB,CAAC;IAC/B,aAAa,EAAE,SAAS,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,iCAAiC;IACzC,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,8BAA8B,CAAC;IACtC,YAAY,EAAE,gBAAgB,CAAC;IAC/B,aAAa,EAAE,SAAS,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,sBAAsB,EAAE,MAAM,EAAE,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,wBAAgB,iCAAiC,CAC/C,KAAK,EAAE,KAAK,EACZ,0BAA0B,EAAE,MAAM,GACjC,4BAA4B,CAiB9B;AAoDD;;;GAGG;AACH,wBAAgB,sCAAsC,CACpD,UAAU,EAAE,MAAM,EAAE,GACnB,iCAAiC,CAUnC;AAED;;;GAGG;AACH,wBAAgB,+BAA+B,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAQhF;AAED;;GAEG;AACH,wBAAgB,4BAA4B,CAC1C,cAAc,EAAE,MAAM,EAAE,EACxB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,GAC5C,MAAM,CAaR;AAED,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,GAC5C,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,iBAAiB,EAAE,WAAW,GAAG,SAAS,GAAG,YAAY,GAAG,OAAO,CAAC,CAAC,CAY3F"}
1
+ {"version":3,"file":"instance.d.ts","sourceRoot":"","sources":["../../src/routes/instance.ts"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AA0BrD,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,GAC/D,IAAI,CAEN;AAED,eAAO,MAAM,cAAc,4CAAW,CAAC;AAwJvC,wBAAgB,4BAA4B,CAAC,gBAAgB,EAAE,MAAM,GAAG;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE;QACJ,EAAE,EAAE,OAAO,CAAC;QACZ,aAAa,CAAC,EAAE,SAAS,CAAC;QAC1B,oBAAoB,EAAE,OAAO,CAAC;QAC9B,qBAAqB,EAAE,OAAO,CAAC;QAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;CACH,CAqCA;AAED,MAAM,WAAW,4BAA4B;IAC3C,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,gBAAgB,CAAC;IAC/B,aAAa,EAAE,SAAS,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,iCAAiC;IACzC,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,8BAA8B,CAAC;IACtC,YAAY,EAAE,gBAAgB,CAAC;IAC/B,aAAa,EAAE,SAAS,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,sBAAsB,EAAE,MAAM,EAAE,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,wBAAgB,iCAAiC,CAC/C,KAAK,EAAE,KAAK,EACZ,0BAA0B,EAAE,MAAM,GACjC,4BAA4B,CAgB9B;AAmDD;;;GAGG;AACH,wBAAgB,sCAAsC,CACpD,UAAU,EAAE,MAAM,EAAE,GACnB,iCAAiC,CAUnC;AAED;;;GAGG;AACH,wBAAgB,+BAA+B,CAC7C,YAAY,EAAE,MAAM,EAAE,GACrB,MAAM,EAAE,CAYV;AAED;;GAEG;AACH,wBAAgB,4BAA4B,CAC1C,cAAc,EAAE,MAAM,EAAE,EACxB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,GAC5C,MAAM,CAgBR;AAED,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,GAC5C,MAAM,CACP,MAAM,EACN,IAAI,CAAC,iBAAiB,EAAE,WAAW,GAAG,SAAS,GAAG,YAAY,GAAG,OAAO,CAAC,CAC1E,CAYA"}
@@ -4,16 +4,21 @@ import { spawn } from "node:child_process";
4
4
  import { createHash, timingSafeEqual } from "node:crypto";
5
5
  import { resolveSchedulerBaseUrlFrom } from "../config.js";
6
6
  import { validateToken } from "../middleware/auth.js";
7
- import { getConfiguredConnectionKeys, requestRuntimeAccessTokenRefreshForServer } from "../services/auto-register.js";
8
- import { discoverCcSwitchConfiguredItems, loadCcSwitchSdk } from "../services/cc-switch-sdk.js";
9
- import { initInstance, syncLegacyStateFromSdk } from "../services/instance-init-service.js";
7
+ import { getConfiguredConnectionKeys, requestRuntimeAccessTokenRefreshForServer, } from "../services/auto-register.js";
8
+ import { discoverCcSwitchConfiguredItems, loadCcSwitchSdk, } from "../services/cc-switch-sdk.js";
9
+ import { initInstance, syncLegacyStateFromSdk, } from "../services/instance-init-service.js";
10
10
  import { createBridgeLifecycleOperation, deliverPendingBridgeLifecycleNotifications, upsertBridgeLifecycleOperation, } from "../services/lifecycle-state.js";
11
11
  import { loadInstanceState, saveInstanceState, } from "../services/instance-state.js";
12
12
  import { getRuntimeAccessToken } from "../services/runtime-binding.js";
13
13
  import { detectToolStatuses, ensureToolsInstalled, SUPPORTED_INSTALLABLE_TOOLS, SUPPORTED_UNINSTALLABLE_TOOLS, uninstallTools, } from "../services/tool-installer.js";
14
14
  import { createLogger } from "../utils/logger.js";
15
15
  const log = createLogger("instance");
16
- const PANEL_TOOL_STATUS_KEYS = ["acode", "claude", "opencode", "codex"];
16
+ const PANEL_TOOL_STATUS_KEYS = [
17
+ "acode",
18
+ "claude",
19
+ "opencode",
20
+ "codex",
21
+ ];
17
22
  // ★★★ 导出 gracefulShutdown 的触发函数
18
23
  // 用于 aws-mcp-server 通知 bridge 保留会话状态关闭
19
24
  let gracefulShutdownFn = null;
@@ -249,7 +254,9 @@ export function buildAmbiguousSchedulerBaseUrlResponse(targetUrls) {
249
254
  */
250
255
  export function buildToolStatusDetectionTargets(enabledTools) {
251
256
  return Array.from(new Set([...enabledTools, ...PANEL_TOOL_STATUS_KEYS]
252
- .map((tool) => String(tool || "").trim().toLowerCase())
257
+ .map((tool) => String(tool || "")
258
+ .trim()
259
+ .toLowerCase())
253
260
  .filter(Boolean)));
254
261
  }
255
262
  /**
@@ -300,17 +307,25 @@ instanceRouter.get("/ping", validateToken, async (req, res) => {
300
307
  }
301
308
  const schedulerBaseUrl = schedulerBaseUrlResolution.url;
302
309
  try {
303
- const runtimeAccessToken = getRuntimeAccessToken(undefined, schedulerBaseUrl);
310
+ let runtimeAccessToken = getRuntimeAccessToken(undefined, schedulerBaseUrl);
311
+ let tokenRefreshed = false;
304
312
  if (!runtimeAccessToken) {
305
- res.status(401).json({ ok: false, error: "runtime_access_token_required" });
306
- return;
313
+ const refreshResult = await requestRuntimeAccessTokenRefreshForServer(schedulerBaseUrl);
314
+ if (!refreshResult.success || !refreshResult.runtimeAccessToken) {
315
+ res
316
+ .status(401)
317
+ .json({ ok: false, error: "runtime_access_token_required" });
318
+ return;
319
+ }
320
+ runtimeAccessToken = refreshResult.runtimeAccessToken;
321
+ tokenRefreshed = true;
307
322
  }
308
323
  const schedulerPing = await pingSchedulerWithToken(schedulerBaseUrl, runtimeAccessToken);
309
324
  res.json({
310
325
  ok: true,
311
326
  runtimeBridge: "healthy",
312
327
  schedulerReachable: schedulerPing.schedulerReachable,
313
- tokenRefreshed: schedulerPing.tokenRefreshed,
328
+ tokenRefreshed: tokenRefreshed || schedulerPing.tokenRefreshed,
314
329
  schedulerBaseUrl,
315
330
  });
316
331
  }
@@ -371,7 +386,9 @@ instanceRouter.post("/cc-switch/state", validateToken, async (req, res) => {
371
386
  return;
372
387
  }
373
388
  try {
374
- log.info("[cc-switch/state] Request received", { agentId: String(agentId) });
389
+ log.info("[cc-switch/state] Request received", {
390
+ agentId: String(agentId),
391
+ });
375
392
  const sdk = await loadCcSwitchSdk(agentId);
376
393
  const imported = await discoverCcSwitchConfiguredItems(sdk);
377
394
  const synced = await syncLegacyStateFromSdk(agentId, sdk);
@@ -438,9 +455,7 @@ instanceRouter.post("/cc-switch/install-tools", validateToken, async (req, res)
438
455
  agentId: String(agentId),
439
456
  requestedTools,
440
457
  });
441
- res
442
- .status(400)
443
- .json({
458
+ res.status(400).json({
444
459
  error: `tools must include ${SUPPORTED_INSTALLABLE_TOOLS.join(", ")}`,
445
460
  });
446
461
  return;
@@ -533,7 +548,9 @@ instanceRouter.post("/cc-switch/uninstall-tools", validateToken, async (req, res
533
548
  try {
534
549
  const state = await loadInstanceState(agentId);
535
550
  const uninstallableToolSet = new Set(uninstallableTools);
536
- const enabledTools = (state.enabledTools || []).filter((tool) => !uninstallableToolSet.has(String(tool || "").trim().toLowerCase()));
551
+ const enabledTools = (state.enabledTools || []).filter((tool) => !uninstallableToolSet.has(String(tool || "")
552
+ .trim()
553
+ .toLowerCase()));
537
554
  const uninstalledToolStatus = await uninstallTools(uninstallableTools);
538
555
  const refreshedToolStatus = await detectToolStatuses([
539
556
  ...enabledTools,
@@ -1,39 +1,43 @@
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(() => ({ status: 'paired', userId: 'user-1', schedulerBaseUrl: 'http://server.local:7380' })),
20
- 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(() => ({
20
+ status: "paired",
21
+ userId: "user-1",
22
+ schedulerBaseUrl: "http://server.local:7380",
23
+ })),
24
+ normalizeSchedulerBaseUrl: vi.fn((value) => value ? String(value).replace(/\/+$/, "") : undefined),
21
25
  }));
22
- vi.mock('../services/lifecycle-state.js', () => ({
26
+ vi.mock("../services/lifecycle-state.js", () => ({
23
27
  createBridgeLifecycleOperation: vi.fn((input) => ({
24
- operationId: 'operation-1',
25
- requestedAt: '2026-01-01T00:00:00.000Z',
26
- updatedAt: '2026-01-01T00:00:00.000Z',
28
+ operationId: "operation-1",
29
+ requestedAt: "2026-01-01T00:00:00.000Z",
30
+ updatedAt: "2026-01-01T00:00:00.000Z",
27
31
  ...input,
28
32
  })),
29
33
  deliverPendingBridgeLifecycleNotifications: vi.fn(),
30
34
  upsertBridgeLifecycleOperation: vi.fn(),
31
35
  }));
32
- vi.mock('../config.js', async (importOriginal) => ({
36
+ vi.mock("../config.js", async (importOriginal) => ({
33
37
  ...(await importOriginal()),
34
38
  getRuntimeHomeDir: vi.fn(() => process.cwd()),
35
39
  }));
36
- vi.mock('node:child_process', async (importOriginal) => ({
40
+ vi.mock("node:child_process", async (importOriginal) => ({
37
41
  ...(await importOriginal()),
38
42
  spawn: spawnMock,
39
43
  }));
@@ -41,19 +45,19 @@ afterEach(() => {
41
45
  vi.useRealTimers();
42
46
  vi.clearAllMocks();
43
47
  });
44
- function mockSpawnResult(exitCode, stdoutText = '', stderrText = '') {
48
+ function mockSpawnResult(exitCode, stdoutText = "", stderrText = "") {
45
49
  spawnMock.mockImplementationOnce(() => {
46
50
  const child = new EventEmitter();
47
51
  child.stdout = new EventEmitter();
48
52
  child.stderr = new EventEmitter();
49
53
  process.nextTick(() => {
50
54
  if (stdoutText) {
51
- child.stdout.emit('data', Buffer.from(stdoutText));
55
+ child.stdout.emit("data", Buffer.from(stdoutText));
52
56
  }
53
57
  if (stderrText) {
54
- child.stderr.emit('data', Buffer.from(stderrText));
58
+ child.stderr.emit("data", Buffer.from(stderrText));
55
59
  }
56
- child.emit('close', exitCode);
60
+ child.emit("close", exitCode);
57
61
  });
58
62
  return child;
59
63
  });
@@ -63,30 +67,33 @@ function createMockResponse() {
63
67
  const status = vi.fn(() => ({ json }));
64
68
  return { json, status };
65
69
  }
66
- describe('instance route validation', () => {
67
- it('builds correct ping response when healthy', () => {
70
+ describe("instance route validation", () => {
71
+ it("builds correct ping response when healthy", () => {
68
72
  const buildPingResponse = (schedulerStatus, schedulerBaseUrl) => ({
69
73
  ok: true,
70
- runtimeBridge: 'healthy',
74
+ runtimeBridge: "healthy",
71
75
  schedulerReachable: schedulerStatus >= 200 && schedulerStatus < 300,
72
76
  schedulerBaseUrl,
73
77
  });
74
- const healthyResponse = buildPingResponse(200, 'http://localhost:8080');
78
+ const healthyResponse = buildPingResponse(200, "http://localhost:8080");
75
79
  expect(healthyResponse.ok).toBe(true);
76
80
  expect(healthyResponse.schedulerReachable).toBe(true);
77
- const unhealthyResponse = buildPingResponse(502, 'http://localhost:8080');
81
+ const unhealthyResponse = buildPingResponse(502, "http://localhost:8080");
78
82
  expect(unhealthyResponse.schedulerReachable).toBe(false);
79
83
  });
80
- it('requires agentId and workspacePath', () => {
84
+ it("requires agentId and workspacePath", () => {
81
85
  const validateInitRequest = (body) => {
82
86
  if (!body.agentId || !body.workspacePath)
83
- return { valid: false, error: 'agentId and workspacePath are required' };
87
+ return {
88
+ valid: false,
89
+ error: "agentId and workspacePath are required",
90
+ };
84
91
  return { valid: true };
85
92
  };
86
93
  expect(validateInitRequest({}).valid).toBe(false);
87
- expect(validateInitRequest({ agentId: 'agent-1', workspacePath: '/path' }).valid).toBe(true);
94
+ expect(validateInitRequest({ agentId: "agent-1", workspacePath: "/path" }).valid).toBe(true);
88
95
  });
89
- it('builds correct init options', () => {
96
+ it("builds correct init options", () => {
90
97
  const buildInitOptions = (body) => ({
91
98
  skillEnabled: body.skillEnabled,
92
99
  mcpEnabled: body.mcpEnabled,
@@ -95,87 +102,91 @@ describe('instance route validation', () => {
95
102
  const options = buildInitOptions({
96
103
  skillEnabled: true,
97
104
  mcpEnabled: false,
98
- ccSwitchEnabledTools: ['claude', 'opencode'],
105
+ ccSwitchEnabledTools: ["claude", "opencode"],
99
106
  });
100
107
  expect(options.skillEnabled).toBe(true);
101
108
  expect(options.mcpEnabled).toBe(false);
102
- expect(options.ccSwitchEnabledTools).toEqual(['claude', 'opencode']);
109
+ expect(options.ccSwitchEnabledTools).toEqual(["claude", "opencode"]);
103
110
  });
104
- it('requires agentId for state query', () => {
111
+ it("requires agentId for state query", () => {
105
112
  const validateStateRequest = (body) => {
106
113
  if (!body.agentId)
107
- return { valid: false, error: 'agentId is required' };
114
+ return { valid: false, error: "agentId is required" };
108
115
  return { valid: true };
109
116
  };
110
117
  expect(validateStateRequest({}).valid).toBe(false);
111
- expect(validateStateRequest({ agentId: 'agent-1' }).valid).toBe(true);
118
+ expect(validateStateRequest({ agentId: "agent-1" }).valid).toBe(true);
112
119
  });
113
- it('always detects panel tool statuses even when legacy enabled tools omit codex', async () => {
114
- const { buildToolStatusDetectionTargets } = await import('./instance.js');
115
- expect(buildToolStatusDetectionTargets(['claude', 'opencode'])).toEqual([
116
- 'claude',
117
- 'opencode',
118
- 'acode',
119
- 'codex',
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",
120
127
  ]);
121
128
  }, 15_000);
122
- it('accepts codex for cc-switch tool installation validation', async () => {
123
- const { SUPPORTED_INSTALLABLE_TOOLS } = await import('../services/tool-installer.js');
129
+ it("accepts codex for cc-switch tool installation validation", async () => {
130
+ const { SUPPORTED_INSTALLABLE_TOOLS } = await import("../services/tool-installer.js");
124
131
  const normalizeInstallableTools = (tools) => {
125
132
  const supported = new Set(SUPPORTED_INSTALLABLE_TOOLS);
126
133
  const requestedTools = Array.isArray(tools)
127
- ? tools.map((tool) => String(tool || '').trim().toLowerCase()).filter(Boolean)
134
+ ? tools
135
+ .map((tool) => String(tool || "")
136
+ .trim()
137
+ .toLowerCase())
138
+ .filter(Boolean)
128
139
  : [];
129
140
  return requestedTools.filter((tool) => supported.has(tool));
130
141
  };
131
- expect(normalizeInstallableTools(['Codex'])).toEqual(['codex']);
132
- expect(SUPPORTED_INSTALLABLE_TOOLS).toContain('codex');
142
+ expect(normalizeInstallableTools(["Codex"])).toEqual(["codex"]);
143
+ expect(SUPPORTED_INSTALLABLE_TOOLS).toContain("codex");
133
144
  });
134
- it('accepts panel tools for cc-switch tool uninstallation validation', async () => {
135
- const { SUPPORTED_UNINSTALLABLE_TOOLS } = await import('../services/tool-installer.js');
145
+ it("accepts panel tools for cc-switch tool uninstallation validation", async () => {
146
+ const { SUPPORTED_UNINSTALLABLE_TOOLS } = await import("../services/tool-installer.js");
136
147
  const normalizeUninstallableTools = (tools) => {
137
148
  const supported = new Set(SUPPORTED_UNINSTALLABLE_TOOLS);
138
149
  const requestedTools = Array.isArray(tools)
139
- ? tools.map((tool) => String(tool || '').trim().toLowerCase()).filter(Boolean)
150
+ ? tools
151
+ .map((tool) => String(tool || "")
152
+ .trim()
153
+ .toLowerCase())
154
+ .filter(Boolean)
140
155
  : [];
141
156
  return requestedTools.filter((tool) => supported.has(tool));
142
157
  };
143
- expect(normalizeUninstallableTools(['Claude', 'OpenCode', 'Codex'])).toEqual([
144
- 'claude',
145
- 'opencode',
146
- 'codex',
147
- ]);
148
- expect(normalizeUninstallableTools(['unknown-tool'])).toEqual([]);
158
+ expect(normalizeUninstallableTools(["Claude", "OpenCode", "Codex"])).toEqual(["claude", "opencode", "codex"]);
159
+ expect(normalizeUninstallableTools(["unknown-tool"])).toEqual([]);
149
160
  });
150
- it('reports uninstall failure when a requested tool remains installed', async () => {
151
- const { buildUninstallFailureMessage } = await import('./instance.js');
152
- expect(buildUninstallFailureMessage(['opencode'], {
161
+ it("reports uninstall failure when a requested tool remains installed", async () => {
162
+ const { buildUninstallFailureMessage } = await import("./instance.js");
163
+ expect(buildUninstallFailureMessage(["opencode"], {
153
164
  opencode: {
154
- tool: 'opencode',
165
+ tool: "opencode",
155
166
  installed: true,
156
- executable: '/home/user/.opencode/bin/opencode',
157
- version: '1.14.39',
167
+ executable: "/home/user/.opencode/bin/opencode",
168
+ version: "1.14.39",
158
169
  installing: false,
159
- error: 'uninstall completed but command is still available',
170
+ error: "uninstall completed but command is still available",
160
171
  },
161
- })).toContain('opencode: uninstall completed but command is still available');
172
+ })).toContain("opencode: uninstall completed but command is still available");
162
173
  });
163
- it('summarizes tool status for route diagnostics', async () => {
164
- const { summarizeToolStatus } = await import('./instance.js');
174
+ it("summarizes tool status for route diagnostics", async () => {
175
+ const { summarizeToolStatus } = await import("./instance.js");
165
176
  expect(summarizeToolStatus({
166
177
  opencode: {
167
- tool: 'opencode',
178
+ tool: "opencode",
168
179
  installed: false,
169
180
  executable: null,
170
181
  version: null,
171
182
  installing: false,
172
- error: 'SDK package @opencode-ai/sdk is not installed in aws-runtime-bridge',
183
+ error: "SDK package @opencode-ai/sdk is not installed in aws-runtime-bridge",
173
184
  },
174
185
  claude: {
175
- tool: 'claude',
186
+ tool: "claude",
176
187
  installed: true,
177
- executable: '@anthropic-ai/claude-agent-sdk',
178
- version: '0.2.87',
188
+ executable: "@anthropic-ai/claude-agent-sdk",
189
+ version: "0.2.87",
179
190
  installing: false,
180
191
  error: null,
181
192
  },
@@ -184,202 +195,251 @@ describe('instance route validation', () => {
184
195
  installed: false,
185
196
  executable: null,
186
197
  version: null,
187
- error: 'SDK package @opencode-ai/sdk is not installed in aws-runtime-bridge',
198
+ error: "SDK package @opencode-ai/sdk is not installed in aws-runtime-bridge",
188
199
  },
189
200
  claude: {
190
201
  installed: true,
191
- executable: '@anthropic-ai/claude-agent-sdk',
192
- version: '0.2.87',
202
+ executable: "@anthropic-ai/claude-agent-sdk",
203
+ version: "0.2.87",
193
204
  error: null,
194
205
  },
195
206
  });
196
207
  });
197
- it('does not report uninstall failure when requested tools are no longer installed', async () => {
198
- const { buildUninstallFailureMessage } = await import('./instance.js');
199
- expect(buildUninstallFailureMessage(['claude'], {
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"], {
200
211
  claude: {
201
- tool: 'claude',
212
+ tool: "claude",
202
213
  installed: false,
203
214
  executable: null,
204
215
  version: null,
205
216
  installing: false,
206
- error: 'command not installed',
217
+ error: "command not installed",
207
218
  },
208
- })).toBe('');
219
+ })).toBe("");
209
220
  });
210
- it('allows connection check when no connection key is configured', async () => {
211
- const { getConfiguredConnectionKeys } = await import('../services/auto-register.js');
221
+ it("allows connection check when no connection key is configured", async () => {
222
+ const { getConfiguredConnectionKeys } = await import("../services/auto-register.js");
212
223
  vi.mocked(getConfiguredConnectionKeys).mockReturnValue([]);
213
- const { buildConnectionCheckResponse } = await import('./instance.js');
214
- expect(buildConnectionCheckResponse('')).toEqual({
224
+ const { buildConnectionCheckResponse } = await import("./instance.js");
225
+ expect(buildConnectionCheckResponse("")).toEqual({
215
226
  status: 200,
216
227
  body: {
217
228
  ok: true,
218
- runtimeBridge: 'healthy',
229
+ runtimeBridge: "healthy",
219
230
  connectionKeyMatched: true,
220
231
  connectionKeyRequired: false,
221
232
  },
222
233
  });
223
234
  });
224
- it('requires matching connection key digest when a connection key is configured', async () => {
225
- const { createHash } = await import('node:crypto');
226
- const { getConfiguredConnectionKeys } = await import('../services/auto-register.js');
227
- vi.mocked(getConfiguredConnectionKeys).mockReturnValue(['secret-key']);
228
- const { buildConnectionCheckResponse } = await import('./instance.js');
229
- const digest = createHash('md5').update('secret-key', 'utf8').digest('hex');
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");
230
241
  expect(buildConnectionCheckResponse(digest).status).toBe(200);
231
- expect(buildConnectionCheckResponse('00000000000000000000000000000000')).toEqual({
242
+ expect(buildConnectionCheckResponse("00000000000000000000000000000000")).toEqual({
232
243
  status: 401,
233
244
  body: {
234
245
  ok: false,
235
- error: 'connection_key_mismatch',
246
+ error: "connection_key_mismatch",
236
247
  connectionKeyMatched: false,
237
248
  connectionKeyRequired: true,
238
249
  },
239
250
  });
240
251
  });
241
- it('explains localhost scheduler ping failures with environment variable guidance', async () => {
242
- const { buildSchedulerPingFailureResponse } = await import('./instance.js');
243
- const response = buildSchedulerPingFailureResponse(new Error('connect ECONNREFUSED 127.0.0.1:8080'), 'http://localhost:8080');
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");
244
255
  expect(response).toEqual({
245
256
  ok: false,
246
- error: 'connect ECONNREFUSED 127.0.0.1:8080',
247
- failureStage: 'scheduler_ping',
248
- runtimeBridge: 'healthy',
249
- schedulerBaseUrl: 'http://localhost:8080',
250
- hint: expect.stringContaining('AWS_RUNTIME_SCHEDULER_BASE_URL'),
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"),
251
262
  });
252
- expect(response.hint).toContain('http://<server-host>:7380');
263
+ expect(response.hint).toContain("http://<server-host>:7380");
253
264
  });
254
- it('explains non-localhost scheduler ping failures as reachability or token issues', async () => {
255
- const { buildSchedulerPingFailureResponse } = await import('./instance.js');
256
- const response = buildSchedulerPingFailureResponse(new Error('Request failed with status code 401'), 'http://10.0.0.8:7380');
257
- expect(response.schedulerBaseUrl).toBe('http://10.0.0.8:7380');
258
- expect(response.failureStage).toBe('scheduler_ping');
259
- expect(response.runtimeBridge).toBe('healthy');
260
- expect(response.hint).toContain('运行时访问令牌仍有效');
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("运行时访问令牌仍有效");
261
272
  });
262
- it('refreshes runtime token and retries scheduler ping after 401', async () => {
263
- process.env.AWS_RUNTIME_SCHEDULER_BASE_URL = 'http://localhost:8080';
264
- const { default: axios } = await import('axios');
265
- const { requestRuntimeAccessTokenRefreshForServer } = await import('../services/auto-register.js');
266
- const { instanceRouter } = await import('./instance.js');
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");
267
278
  vi.mocked(requestRuntimeAccessTokenRefreshForServer).mockResolvedValue({
268
279
  success: true,
269
- runtimeAccessToken: 'fresh-runtime-token-123456',
280
+ runtimeAccessToken: "fresh-runtime-token-123456",
270
281
  updated: true,
271
282
  });
272
283
  vi.mocked(axios.get)
273
- .mockRejectedValueOnce({ response: { status: 401 }, message: 'Request failed with status code 401' })
284
+ .mockRejectedValueOnce({
285
+ response: { status: 401 },
286
+ message: "Request failed with status code 401",
287
+ })
274
288
  .mockResolvedValueOnce({ status: 200 });
275
- const handler = instanceRouter.stack.find((layer) => layer.route?.path === '/ping')?.route?.stack[1]?.handle;
276
- expect(handler).toBeTypeOf('function');
289
+ const handler = instanceRouter.stack.find((layer) => layer.route?.path === "/ping")?.route?.stack[1]?.handle;
290
+ expect(handler).toBeTypeOf("function");
277
291
  const json = vi.fn();
278
292
  const status = vi.fn(() => ({ json }));
279
- await handler?.({ header: vi.fn(() => '') }, { json, status }, vi.fn());
280
- expect(requestRuntimeAccessTokenRefreshForServer).toHaveBeenCalledWith('http://localhost:8080');
281
- expect(axios.get).toHaveBeenNthCalledWith(1, 'http://localhost:8080/api/runtime/ping', {
282
- headers: { 'X-Runtime-Token': 'stale-runtime-token-123456' },
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" },
283
297
  });
284
- expect(axios.get).toHaveBeenNthCalledWith(2, 'http://localhost:8080/api/runtime/ping', {
285
- headers: { 'X-Runtime-Token': 'fresh-runtime-token-123456' },
298
+ expect(axios.get).toHaveBeenNthCalledWith(2, "http://localhost:8080/api/runtime/ping", {
299
+ headers: { "X-Runtime-Token": "fresh-runtime-token-123456" },
286
300
  });
287
301
  expect(json).toHaveBeenCalledWith({
288
302
  ok: true,
289
- runtimeBridge: 'healthy',
303
+ runtimeBridge: "healthy",
290
304
  schedulerReachable: true,
291
305
  tokenRefreshed: true,
292
- schedulerBaseUrl: 'http://localhost:8080',
306
+ schedulerBaseUrl: "http://localhost:8080",
293
307
  });
294
308
  expect(status).not.toHaveBeenCalled();
295
309
  });
296
- it('uses scheduler base URL supplied by server ping request before local bridge config', async () => {
297
- process.env.AWS_RUNTIME_SCHEDULER_BASE_URL = 'http://localhost:8080';
298
- const { default: axios } = await import('axios');
299
- const { getRuntimeAccessToken } = await import('../services/runtime-binding.js');
300
- const { instanceRouter } = await import('./instance.js');
301
- vi.mocked(getRuntimeAccessToken).mockReturnValue('stale-runtime-token-123456');
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();
302
318
  vi.mocked(axios.get).mockReset();
303
319
  vi.mocked(axios.get).mockResolvedValueOnce({ status: 200 });
304
- const handler = instanceRouter.stack.find((layer) => layer.route?.path === '/ping')?.route?.stack[1]?.handle;
305
- expect(handler).toBeTypeOf('function');
320
+ const handler = instanceRouter.stack.find((layer) => layer.route?.path === "/ping")?.route?.stack[1]?.handle;
321
+ expect(handler).toBeTypeOf("function");
306
322
  const json = vi.fn();
307
323
  const status = vi.fn(() => ({ json }));
308
- const header = vi.fn((name) => (name === 'X-Scheduler-Base-Url' ? 'http://server.local:7380/api' : ''));
324
+ const header = vi.fn((name) => name === "X-Scheduler-Base-Url" ? "http://server.local:7380/api" : "");
309
325
  await handler?.({ header }, { json, status }, vi.fn());
310
- expect(getRuntimeAccessToken).toHaveBeenCalledWith(undefined, 'http://server.local:7380');
311
- expect(axios.get).toHaveBeenCalledWith('http://server.local:7380/api/runtime/ping', {
312
- headers: { 'X-Runtime-Token': 'stale-runtime-token-123456' },
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" },
313
330
  });
314
331
  expect(json).toHaveBeenCalledWith({
315
332
  ok: true,
316
- runtimeBridge: 'healthy',
333
+ runtimeBridge: "healthy",
317
334
  schedulerReachable: true,
318
335
  tokenRefreshed: false,
319
- schedulerBaseUrl: 'http://server.local:7380',
336
+ schedulerBaseUrl: "http://server.local:7380",
337
+ });
338
+ expect(status).not.toHaveBeenCalled();
339
+ });
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,
350
+ });
351
+ vi.mocked(axios.get).mockReset();
352
+ 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");
355
+ const json = vi.fn();
356
+ const status = vi.fn(() => ({ json }));
357
+ const header = vi.fn((name) => name === "X-Scheduler-Base-Url" ? "http://server.local:7380/api" : "");
358
+ 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" },
363
+ });
364
+ expect(json).toHaveBeenCalledWith({
365
+ ok: true,
366
+ runtimeBridge: "healthy",
367
+ schedulerReachable: true,
368
+ tokenRefreshed: true,
369
+ schedulerBaseUrl: "http://server.local:7380",
320
370
  });
321
371
  expect(status).not.toHaveBeenCalled();
322
372
  });
323
- it('explains ambiguous auto-register scheduler targets without picking the first one', async () => {
324
- const { buildAmbiguousSchedulerBaseUrlResponse } = await import('./instance.js');
373
+ it("explains ambiguous auto-register scheduler targets without picking the first one", async () => {
374
+ const { buildAmbiguousSchedulerBaseUrlResponse } = await import("./instance.js");
325
375
  const response = buildAmbiguousSchedulerBaseUrlResponse([
326
- 'http://scheduler-a.local:7380',
327
- 'http://scheduler-b.local:7380',
376
+ "http://scheduler-a.local:7380",
377
+ "http://scheduler-b.local:7380",
328
378
  ]);
329
379
  expect(response).toEqual({
330
380
  ok: false,
331
- error: 'ambiguous_scheduler_base_url',
332
- failureStage: 'scheduler_ping',
333
- runtimeBridge: 'healthy',
334
- schedulerBaseUrl: '',
381
+ error: "ambiguous_scheduler_base_url",
382
+ failureStage: "scheduler_ping",
383
+ runtimeBridge: "healthy",
384
+ schedulerBaseUrl: "",
335
385
  autoRegisterTargetUrls: [
336
- 'http://scheduler-a.local:7380',
337
- 'http://scheduler-b.local:7380',
386
+ "http://scheduler-a.local:7380",
387
+ "http://scheduler-b.local:7380",
338
388
  ],
339
- hint: expect.stringContaining('AWS_RUNTIME_SCHEDULER_BASE_URL'),
389
+ hint: expect.stringContaining("AWS_RUNTIME_SCHEDULER_BASE_URL"),
340
390
  });
341
391
  });
342
- it('starts bridge package update in background and returns operation id', async () => {
343
- const { instanceRouter } = await import('./instance.js');
344
- mockSpawnResult(0, 'updated');
345
- const handler = instanceRouter.stack.find((layer) => layer.route?.path === '/update-bridge')?.route?.stack[1]?.handle;
346
- expect(handler).toBeTypeOf('function');
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");
347
397
  const { json, status } = createMockResponse();
348
- await handler?.({ body: { requestedByUserId: 'user-1' }, get: () => 'bridge.local:18081', protocol: 'http' }, { json, status }, vi.fn());
398
+ await handler?.({
399
+ body: { requestedByUserId: "user-1" },
400
+ get: () => "bridge.local:18081",
401
+ protocol: "http",
402
+ }, { json, status }, vi.fn());
349
403
  expect(status).not.toHaveBeenCalled();
350
404
  expect(json).toHaveBeenCalledWith(expect.objectContaining({
351
405
  ok: true,
352
- message: 'Bridge 更新已在后台开始,完成后会通过消息中心通知结果。',
406
+ message: "Bridge 更新已在后台开始,完成后会通过消息中心通知结果。",
353
407
  }));
354
408
  await new Promise((resolve) => setImmediate(resolve));
355
- expect(spawnMock).toHaveBeenCalledWith(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['install', '-g', 'aws-runtime-bridge@latest'], { shell: false, windowsHide: true });
409
+ expect(spawnMock).toHaveBeenCalledWith(process.platform === "win32" ? "npm.cmd" : "npm", ["install", "-g", "aws-runtime-bridge@latest"], { shell: false, windowsHide: true });
356
410
  });
357
- it('rejects background bridge update without requester user id', async () => {
358
- const { instanceRouter } = await import('./instance.js');
359
- const handler = instanceRouter.stack.find((layer) => layer.route?.path === '/update-bridge')?.route?.stack[1]?.handle;
360
- expect(handler).toBeTypeOf('function');
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");
361
415
  const { json, status } = createMockResponse();
362
416
  await handler?.({}, { json, status }, vi.fn());
363
417
  expect(status).toHaveBeenCalledWith(400);
364
- expect(json).toHaveBeenCalledWith({ error: 'requestedByUserId is required' });
418
+ expect(json).toHaveBeenCalledWith({
419
+ error: "requestedByUserId is required",
420
+ });
365
421
  });
366
- it('responds before triggering graceful bridge restart', async () => {
422
+ it("responds before triggering graceful bridge restart", async () => {
367
423
  vi.useFakeTimers();
368
- const { instanceRouter, setGracefulShutdownFn } = await import('./instance.js');
424
+ const { instanceRouter, setGracefulShutdownFn } = await import("./instance.js");
369
425
  const gracefulShutdown = vi.fn().mockResolvedValue(undefined);
370
426
  setGracefulShutdownFn(gracefulShutdown);
371
- const handler = instanceRouter.stack.find((layer) => layer.route?.path === '/restart-bridge')?.route?.stack[1]?.handle;
372
- expect(handler).toBeTypeOf('function');
427
+ const handler = instanceRouter.stack.find((layer) => layer.route?.path === "/restart-bridge")?.route?.stack[1]?.handle;
428
+ expect(handler).toBeTypeOf("function");
373
429
  const { json, status } = createMockResponse();
374
- await handler?.({ body: { preserveSessions: true, requestedByUserId: 'user-1' }, get: () => 'bridge.local:18081', protocol: 'http' }, { json, status }, vi.fn());
430
+ await handler?.({
431
+ body: { preserveSessions: true, requestedByUserId: "user-1" },
432
+ get: () => "bridge.local:18081",
433
+ protocol: "http",
434
+ }, { json, status }, vi.fn());
375
435
  expect(status).not.toHaveBeenCalled();
376
436
  expect(json).toHaveBeenCalledWith(expect.objectContaining({
377
437
  ok: true,
378
438
  preserveSessions: true,
379
- message: 'Bridge 正在优雅退出;下次启动后会通过消息中心通知结果。若未由守护进程管理,请手动重新启动。',
439
+ message: "Bridge 正在优雅退出;下次启动后会通过消息中心通知结果。若未由守护进程管理,请手动重新启动。",
380
440
  }));
381
441
  expect(gracefulShutdown).not.toHaveBeenCalled();
382
442
  await vi.advanceTimersByTimeAsync(250);
383
- expect(gracefulShutdown).toHaveBeenCalledWith('PANEL_RESTART', true);
443
+ expect(gracefulShutdown).toHaveBeenCalledWith("PANEL_RESTART", true);
384
444
  });
385
445
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aws-runtime-bridge",
3
- "version": "1.8.3",
3
+ "version": "1.8.5",
4
4
  "description": "AgentsWorkStudio runtime bridge service for machine-level agent runtime integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",