aws-runtime-bridge 1.8.5 → 1.8.7

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,19 +41,6 @@ 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
- }
57
44
  function getAutoRegisterConfigFilePath() {
58
45
  const testHomeDir = String(process.env.AWS_TEST_HOME || "").trim();
59
46
  return path.join(testHomeDir || homedir(), ".aws-bridge", "config.json");
@@ -97,26 +84,14 @@ export function resolveSchedulerBaseUrl() {
97
84
  */
98
85
  export function resolveSchedulerBaseUrlFrom(requestSchedulerBaseUrl) {
99
86
  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;
103
87
  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
- }
114
88
  return {
115
89
  url: normalizedRequestSchedulerBaseUrl,
116
90
  source: "request",
117
91
  ambiguousTargetUrls: [],
118
92
  };
119
93
  }
94
+ const envSchedulerBaseUrl = normalizeSchedulerHttpBaseUrl(process.env.AWS_RUNTIME_SCHEDULER_BASE_URL);
120
95
  if (envSchedulerBaseUrl) {
121
96
  return {
122
97
  url: envSchedulerBaseUrl,
@@ -124,9 +99,10 @@ export function resolveSchedulerBaseUrlFrom(requestSchedulerBaseUrl) {
124
99
  ambiguousTargetUrls: [],
125
100
  };
126
101
  }
127
- if (singleAutoRegisterTargetUrl) {
102
+ const autoRegisterTargetUrls = readAutoRegisterTargetUrls();
103
+ if (autoRegisterTargetUrls.length === 1) {
128
104
  return {
129
- url: singleAutoRegisterTargetUrl || DEFAULT_SCHEDULER_BASE_URL,
105
+ url: autoRegisterTargetUrls[0] || DEFAULT_SCHEDULER_BASE_URL,
130
106
  source: "single-auto-register-target",
131
107
  ambiguousTargetUrls: [],
132
108
  };
@@ -57,8 +57,7 @@ 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 =
61
- "http://explicit.local:7380/path";
60
+ process.env.AWS_RUNTIME_SCHEDULER_BASE_URL = "http://explicit.local:7380/path";
62
61
  writeBridgeConfig(runtimeHome, [{ serverUrl: "http://target.local:7380" }]);
63
62
  const { resolveSchedulerBaseUrl } = await import("./config.js");
64
63
  expect(resolveSchedulerBaseUrl()).toEqual({
@@ -67,30 +66,6 @@ describe("scheduler base URL resolution", () => {
67
66
  ambiguousTargetUrls: [],
68
67
  });
69
68
  });
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
- });
94
69
  it("uses a single auto-register target serverUrl when scheduler env is absent", async () => {
95
70
  const runtimeHome = createRuntimeHome();
96
71
  useRuntimeHome(runtimeHome);
@@ -1 +1 @@
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"}
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"}
@@ -4,21 +4,16 @@ 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 = [
17
- "acode",
18
- "claude",
19
- "opencode",
20
- "codex",
21
- ];
16
+ const PANEL_TOOL_STATUS_KEYS = ["acode", "claude", "opencode", "codex"];
22
17
  // ★★★ 导出 gracefulShutdown 的触发函数
23
18
  // 用于 aws-mcp-server 通知 bridge 保留会话状态关闭
24
19
  let gracefulShutdownFn = null;
@@ -254,9 +249,7 @@ export function buildAmbiguousSchedulerBaseUrlResponse(targetUrls) {
254
249
  */
255
250
  export function buildToolStatusDetectionTargets(enabledTools) {
256
251
  return Array.from(new Set([...enabledTools, ...PANEL_TOOL_STATUS_KEYS]
257
- .map((tool) => String(tool || "")
258
- .trim()
259
- .toLowerCase())
252
+ .map((tool) => String(tool || "").trim().toLowerCase())
260
253
  .filter(Boolean)));
261
254
  }
262
255
  /**
@@ -307,25 +300,17 @@ instanceRouter.get("/ping", validateToken, async (req, res) => {
307
300
  }
308
301
  const schedulerBaseUrl = schedulerBaseUrlResolution.url;
309
302
  try {
310
- let runtimeAccessToken = getRuntimeAccessToken(undefined, schedulerBaseUrl);
311
- let tokenRefreshed = false;
303
+ const runtimeAccessToken = getRuntimeAccessToken(undefined, schedulerBaseUrl);
312
304
  if (!runtimeAccessToken) {
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;
305
+ res.status(401).json({ ok: false, error: "runtime_access_token_required" });
306
+ return;
322
307
  }
323
308
  const schedulerPing = await pingSchedulerWithToken(schedulerBaseUrl, runtimeAccessToken);
324
309
  res.json({
325
310
  ok: true,
326
311
  runtimeBridge: "healthy",
327
312
  schedulerReachable: schedulerPing.schedulerReachable,
328
- tokenRefreshed: tokenRefreshed || schedulerPing.tokenRefreshed,
313
+ tokenRefreshed: schedulerPing.tokenRefreshed,
329
314
  schedulerBaseUrl,
330
315
  });
331
316
  }
@@ -386,9 +371,7 @@ instanceRouter.post("/cc-switch/state", validateToken, async (req, res) => {
386
371
  return;
387
372
  }
388
373
  try {
389
- log.info("[cc-switch/state] Request received", {
390
- agentId: String(agentId),
391
- });
374
+ log.info("[cc-switch/state] Request received", { agentId: String(agentId) });
392
375
  const sdk = await loadCcSwitchSdk(agentId);
393
376
  const imported = await discoverCcSwitchConfiguredItems(sdk);
394
377
  const synced = await syncLegacyStateFromSdk(agentId, sdk);
@@ -455,7 +438,9 @@ instanceRouter.post("/cc-switch/install-tools", validateToken, async (req, res)
455
438
  agentId: String(agentId),
456
439
  requestedTools,
457
440
  });
458
- res.status(400).json({
441
+ res
442
+ .status(400)
443
+ .json({
459
444
  error: `tools must include ${SUPPORTED_INSTALLABLE_TOOLS.join(", ")}`,
460
445
  });
461
446
  return;
@@ -548,9 +533,7 @@ instanceRouter.post("/cc-switch/uninstall-tools", validateToken, async (req, res
548
533
  try {
549
534
  const state = await loadInstanceState(agentId);
550
535
  const uninstallableToolSet = new Set(uninstallableTools);
551
- const enabledTools = (state.enabledTools || []).filter((tool) => !uninstallableToolSet.has(String(tool || "")
552
- .trim()
553
- .toLowerCase()));
536
+ const enabledTools = (state.enabledTools || []).filter((tool) => !uninstallableToolSet.has(String(tool || "").trim().toLowerCase()));
554
537
  const uninstalledToolStatus = await uninstallTools(uninstallableTools);
555
538
  const refreshedToolStatus = await detectToolStatuses([
556
539
  ...enabledTools,
@@ -1,43 +1,39 @@
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
  }));
@@ -45,19 +41,19 @@ afterEach(() => {
45
41
  vi.useRealTimers();
46
42
  vi.clearAllMocks();
47
43
  });
48
- function mockSpawnResult(exitCode, stdoutText = "", stderrText = "") {
44
+ function mockSpawnResult(exitCode, stdoutText = '', stderrText = '') {
49
45
  spawnMock.mockImplementationOnce(() => {
50
46
  const child = new EventEmitter();
51
47
  child.stdout = new EventEmitter();
52
48
  child.stderr = new EventEmitter();
53
49
  process.nextTick(() => {
54
50
  if (stdoutText) {
55
- child.stdout.emit("data", Buffer.from(stdoutText));
51
+ child.stdout.emit('data', Buffer.from(stdoutText));
56
52
  }
57
53
  if (stderrText) {
58
- child.stderr.emit("data", Buffer.from(stderrText));
54
+ child.stderr.emit('data', Buffer.from(stderrText));
59
55
  }
60
- child.emit("close", exitCode);
56
+ child.emit('close', exitCode);
61
57
  });
62
58
  return child;
63
59
  });
@@ -67,33 +63,30 @@ function createMockResponse() {
67
63
  const status = vi.fn(() => ({ json }));
68
64
  return { json, status };
69
65
  }
70
- describe("instance route validation", () => {
71
- it("builds correct ping response when healthy", () => {
66
+ describe('instance route validation', () => {
67
+ it('builds correct ping response when healthy', () => {
72
68
  const buildPingResponse = (schedulerStatus, schedulerBaseUrl) => ({
73
69
  ok: true,
74
- runtimeBridge: "healthy",
70
+ runtimeBridge: 'healthy',
75
71
  schedulerReachable: schedulerStatus >= 200 && schedulerStatus < 300,
76
72
  schedulerBaseUrl,
77
73
  });
78
- const healthyResponse = buildPingResponse(200, "http://localhost:8080");
74
+ const healthyResponse = buildPingResponse(200, 'http://localhost:8080');
79
75
  expect(healthyResponse.ok).toBe(true);
80
76
  expect(healthyResponse.schedulerReachable).toBe(true);
81
- const unhealthyResponse = buildPingResponse(502, "http://localhost:8080");
77
+ const unhealthyResponse = buildPingResponse(502, 'http://localhost:8080');
82
78
  expect(unhealthyResponse.schedulerReachable).toBe(false);
83
79
  });
84
- it("requires agentId and workspacePath", () => {
80
+ it('requires agentId and workspacePath', () => {
85
81
  const validateInitRequest = (body) => {
86
82
  if (!body.agentId || !body.workspacePath)
87
- return {
88
- valid: false,
89
- error: "agentId and workspacePath are required",
90
- };
83
+ return { valid: false, error: 'agentId and workspacePath are required' };
91
84
  return { valid: true };
92
85
  };
93
86
  expect(validateInitRequest({}).valid).toBe(false);
94
- expect(validateInitRequest({ agentId: "agent-1", workspacePath: "/path" }).valid).toBe(true);
87
+ expect(validateInitRequest({ agentId: 'agent-1', workspacePath: '/path' }).valid).toBe(true);
95
88
  });
96
- it("builds correct init options", () => {
89
+ it('builds correct init options', () => {
97
90
  const buildInitOptions = (body) => ({
98
91
  skillEnabled: body.skillEnabled,
99
92
  mcpEnabled: body.mcpEnabled,
@@ -102,91 +95,87 @@ describe("instance route validation", () => {
102
95
  const options = buildInitOptions({
103
96
  skillEnabled: true,
104
97
  mcpEnabled: false,
105
- ccSwitchEnabledTools: ["claude", "opencode"],
98
+ ccSwitchEnabledTools: ['claude', 'opencode'],
106
99
  });
107
100
  expect(options.skillEnabled).toBe(true);
108
101
  expect(options.mcpEnabled).toBe(false);
109
- expect(options.ccSwitchEnabledTools).toEqual(["claude", "opencode"]);
102
+ expect(options.ccSwitchEnabledTools).toEqual(['claude', 'opencode']);
110
103
  });
111
- it("requires agentId for state query", () => {
104
+ it('requires agentId for state query', () => {
112
105
  const validateStateRequest = (body) => {
113
106
  if (!body.agentId)
114
- return { valid: false, error: "agentId is required" };
107
+ return { valid: false, error: 'agentId is required' };
115
108
  return { valid: true };
116
109
  };
117
110
  expect(validateStateRequest({}).valid).toBe(false);
118
- expect(validateStateRequest({ agentId: "agent-1" }).valid).toBe(true);
111
+ expect(validateStateRequest({ agentId: 'agent-1' }).valid).toBe(true);
119
112
  });
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",
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',
127
120
  ]);
128
121
  }, 15_000);
129
- it("accepts codex for cc-switch tool installation validation", async () => {
130
- const { SUPPORTED_INSTALLABLE_TOOLS } = await import("../services/tool-installer.js");
122
+ it('accepts codex for cc-switch tool installation validation', async () => {
123
+ const { SUPPORTED_INSTALLABLE_TOOLS } = await import('../services/tool-installer.js');
131
124
  const normalizeInstallableTools = (tools) => {
132
125
  const supported = new Set(SUPPORTED_INSTALLABLE_TOOLS);
133
126
  const requestedTools = Array.isArray(tools)
134
- ? tools
135
- .map((tool) => String(tool || "")
136
- .trim()
137
- .toLowerCase())
138
- .filter(Boolean)
127
+ ? tools.map((tool) => String(tool || '').trim().toLowerCase()).filter(Boolean)
139
128
  : [];
140
129
  return requestedTools.filter((tool) => supported.has(tool));
141
130
  };
142
- expect(normalizeInstallableTools(["Codex"])).toEqual(["codex"]);
143
- expect(SUPPORTED_INSTALLABLE_TOOLS).toContain("codex");
131
+ expect(normalizeInstallableTools(['Codex'])).toEqual(['codex']);
132
+ expect(SUPPORTED_INSTALLABLE_TOOLS).toContain('codex');
144
133
  });
145
- it("accepts panel tools for cc-switch tool uninstallation validation", async () => {
146
- const { SUPPORTED_UNINSTALLABLE_TOOLS } = await import("../services/tool-installer.js");
134
+ it('accepts panel tools for cc-switch tool uninstallation validation', async () => {
135
+ const { SUPPORTED_UNINSTALLABLE_TOOLS } = await import('../services/tool-installer.js');
147
136
  const normalizeUninstallableTools = (tools) => {
148
137
  const supported = new Set(SUPPORTED_UNINSTALLABLE_TOOLS);
149
138
  const requestedTools = Array.isArray(tools)
150
- ? tools
151
- .map((tool) => String(tool || "")
152
- .trim()
153
- .toLowerCase())
154
- .filter(Boolean)
139
+ ? tools.map((tool) => String(tool || '').trim().toLowerCase()).filter(Boolean)
155
140
  : [];
156
141
  return requestedTools.filter((tool) => supported.has(tool));
157
142
  };
158
- expect(normalizeUninstallableTools(["Claude", "OpenCode", "Codex"])).toEqual(["claude", "opencode", "codex"]);
159
- expect(normalizeUninstallableTools(["unknown-tool"])).toEqual([]);
143
+ expect(normalizeUninstallableTools(['Claude', 'OpenCode', 'Codex'])).toEqual([
144
+ 'claude',
145
+ 'opencode',
146
+ 'codex',
147
+ ]);
148
+ expect(normalizeUninstallableTools(['unknown-tool'])).toEqual([]);
160
149
  });
161
- it("reports uninstall failure when a requested tool remains installed", async () => {
162
- const { buildUninstallFailureMessage } = await import("./instance.js");
163
- expect(buildUninstallFailureMessage(["opencode"], {
150
+ it('reports uninstall failure when a requested tool remains installed', async () => {
151
+ const { buildUninstallFailureMessage } = await import('./instance.js');
152
+ expect(buildUninstallFailureMessage(['opencode'], {
164
153
  opencode: {
165
- tool: "opencode",
154
+ tool: 'opencode',
166
155
  installed: true,
167
- executable: "/home/user/.opencode/bin/opencode",
168
- version: "1.14.39",
156
+ executable: '/home/user/.opencode/bin/opencode',
157
+ version: '1.14.39',
169
158
  installing: false,
170
- error: "uninstall completed but command is still available",
159
+ error: 'uninstall completed but command is still available',
171
160
  },
172
- })).toContain("opencode: uninstall completed but command is still available");
161
+ })).toContain('opencode: uninstall completed but command is still available');
173
162
  });
174
- it("summarizes tool status for route diagnostics", async () => {
175
- const { summarizeToolStatus } = await import("./instance.js");
163
+ it('summarizes tool status for route diagnostics', async () => {
164
+ const { summarizeToolStatus } = await import('./instance.js');
176
165
  expect(summarizeToolStatus({
177
166
  opencode: {
178
- tool: "opencode",
167
+ tool: 'opencode',
179
168
  installed: false,
180
169
  executable: null,
181
170
  version: null,
182
171
  installing: false,
183
- error: "SDK package @opencode-ai/sdk is not installed in aws-runtime-bridge",
172
+ error: 'SDK package @opencode-ai/sdk is not installed in aws-runtime-bridge',
184
173
  },
185
174
  claude: {
186
- tool: "claude",
175
+ tool: 'claude',
187
176
  installed: true,
188
- executable: "@anthropic-ai/claude-agent-sdk",
189
- version: "0.2.87",
177
+ executable: '@anthropic-ai/claude-agent-sdk',
178
+ version: '0.2.87',
190
179
  installing: false,
191
180
  error: null,
192
181
  },
@@ -195,251 +184,202 @@ describe("instance route validation", () => {
195
184
  installed: false,
196
185
  executable: null,
197
186
  version: null,
198
- error: "SDK package @opencode-ai/sdk is not installed in aws-runtime-bridge",
187
+ error: 'SDK package @opencode-ai/sdk is not installed in aws-runtime-bridge',
199
188
  },
200
189
  claude: {
201
190
  installed: true,
202
- executable: "@anthropic-ai/claude-agent-sdk",
203
- version: "0.2.87",
191
+ executable: '@anthropic-ai/claude-agent-sdk',
192
+ version: '0.2.87',
204
193
  error: null,
205
194
  },
206
195
  });
207
196
  });
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"], {
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'], {
211
200
  claude: {
212
- tool: "claude",
201
+ tool: 'claude',
213
202
  installed: false,
214
203
  executable: null,
215
204
  version: null,
216
205
  installing: false,
217
- error: "command not installed",
206
+ error: 'command not installed',
218
207
  },
219
- })).toBe("");
208
+ })).toBe('');
220
209
  });
221
- it("allows connection check when no connection key is configured", async () => {
222
- const { getConfiguredConnectionKeys } = await import("../services/auto-register.js");
210
+ it('allows connection check when no connection key is configured', async () => {
211
+ const { getConfiguredConnectionKeys } = await import('../services/auto-register.js');
223
212
  vi.mocked(getConfiguredConnectionKeys).mockReturnValue([]);
224
- const { buildConnectionCheckResponse } = await import("./instance.js");
225
- expect(buildConnectionCheckResponse("")).toEqual({
213
+ const { buildConnectionCheckResponse } = await import('./instance.js');
214
+ expect(buildConnectionCheckResponse('')).toEqual({
226
215
  status: 200,
227
216
  body: {
228
217
  ok: true,
229
- runtimeBridge: "healthy",
218
+ runtimeBridge: 'healthy',
230
219
  connectionKeyMatched: true,
231
220
  connectionKeyRequired: false,
232
221
  },
233
222
  });
234
223
  });
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");
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');
241
230
  expect(buildConnectionCheckResponse(digest).status).toBe(200);
242
- expect(buildConnectionCheckResponse("00000000000000000000000000000000")).toEqual({
231
+ expect(buildConnectionCheckResponse('00000000000000000000000000000000')).toEqual({
243
232
  status: 401,
244
233
  body: {
245
234
  ok: false,
246
- error: "connection_key_mismatch",
235
+ error: 'connection_key_mismatch',
247
236
  connectionKeyMatched: false,
248
237
  connectionKeyRequired: true,
249
238
  },
250
239
  });
251
240
  });
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");
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');
255
244
  expect(response).toEqual({
256
245
  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"),
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'),
262
251
  });
263
- expect(response.hint).toContain("http://<server-host>:7380");
252
+ expect(response.hint).toContain('http://<server-host>:7380');
264
253
  });
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("运行时访问令牌仍有效");
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('运行时访问令牌仍有效');
272
261
  });
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");
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');
278
267
  vi.mocked(requestRuntimeAccessTokenRefreshForServer).mockResolvedValue({
279
268
  success: true,
280
- runtimeAccessToken: "fresh-runtime-token-123456",
269
+ runtimeAccessToken: 'fresh-runtime-token-123456',
281
270
  updated: true,
282
271
  });
283
272
  vi.mocked(axios.get)
284
- .mockRejectedValueOnce({
285
- response: { status: 401 },
286
- message: "Request failed with status code 401",
287
- })
273
+ .mockRejectedValueOnce({ response: { status: 401 }, message: 'Request failed with status code 401' })
288
274
  .mockResolvedValueOnce({ status: 200 });
289
- const handler = instanceRouter.stack.find((layer) => layer.route?.path === "/ping")?.route?.stack[1]?.handle;
290
- expect(handler).toBeTypeOf("function");
275
+ const handler = instanceRouter.stack.find((layer) => layer.route?.path === '/ping')?.route?.stack[1]?.handle;
276
+ expect(handler).toBeTypeOf('function');
291
277
  const json = vi.fn();
292
278
  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" },
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' },
297
283
  });
298
- expect(axios.get).toHaveBeenNthCalledWith(2, "http://localhost:8080/api/runtime/ping", {
299
- headers: { "X-Runtime-Token": "fresh-runtime-token-123456" },
284
+ expect(axios.get).toHaveBeenNthCalledWith(2, 'http://localhost:8080/api/runtime/ping', {
285
+ headers: { 'X-Runtime-Token': 'fresh-runtime-token-123456' },
300
286
  });
301
287
  expect(json).toHaveBeenCalledWith({
302
288
  ok: true,
303
- runtimeBridge: "healthy",
289
+ runtimeBridge: 'healthy',
304
290
  schedulerReachable: true,
305
291
  tokenRefreshed: true,
306
- schedulerBaseUrl: "http://localhost:8080",
292
+ schedulerBaseUrl: 'http://localhost:8080',
307
293
  });
308
294
  expect(status).not.toHaveBeenCalled();
309
295
  });
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();
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');
318
302
  vi.mocked(axios.get).mockReset();
319
303
  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");
304
+ const handler = instanceRouter.stack.find((layer) => layer.route?.path === '/ping')?.route?.stack[1]?.handle;
305
+ expect(handler).toBeTypeOf('function');
322
306
  const json = vi.fn();
323
307
  const status = vi.fn(() => ({ json }));
324
- const header = vi.fn((name) => name === "X-Scheduler-Base-Url" ? "http://server.local:7380/api" : "");
308
+ const header = vi.fn((name) => (name === 'X-Scheduler-Base-Url' ? 'http://server.local:7380/api' : ''));
325
309
  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" },
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' },
330
313
  });
331
314
  expect(json).toHaveBeenCalledWith({
332
315
  ok: true,
333
- runtimeBridge: "healthy",
316
+ runtimeBridge: 'healthy',
334
317
  schedulerReachable: true,
335
318
  tokenRefreshed: false,
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",
319
+ schedulerBaseUrl: 'http://server.local:7380',
370
320
  });
371
321
  expect(status).not.toHaveBeenCalled();
372
322
  });
373
- it("explains ambiguous auto-register scheduler targets without picking the first one", async () => {
374
- const { buildAmbiguousSchedulerBaseUrlResponse } = await import("./instance.js");
323
+ it('explains ambiguous auto-register scheduler targets without picking the first one', async () => {
324
+ const { buildAmbiguousSchedulerBaseUrlResponse } = await import('./instance.js');
375
325
  const response = buildAmbiguousSchedulerBaseUrlResponse([
376
- "http://scheduler-a.local:7380",
377
- "http://scheduler-b.local:7380",
326
+ 'http://scheduler-a.local:7380',
327
+ 'http://scheduler-b.local:7380',
378
328
  ]);
379
329
  expect(response).toEqual({
380
330
  ok: false,
381
- error: "ambiguous_scheduler_base_url",
382
- failureStage: "scheduler_ping",
383
- runtimeBridge: "healthy",
384
- schedulerBaseUrl: "",
331
+ error: 'ambiguous_scheduler_base_url',
332
+ failureStage: 'scheduler_ping',
333
+ runtimeBridge: 'healthy',
334
+ schedulerBaseUrl: '',
385
335
  autoRegisterTargetUrls: [
386
- "http://scheduler-a.local:7380",
387
- "http://scheduler-b.local:7380",
336
+ 'http://scheduler-a.local:7380',
337
+ 'http://scheduler-b.local:7380',
388
338
  ],
389
- hint: expect.stringContaining("AWS_RUNTIME_SCHEDULER_BASE_URL"),
339
+ hint: expect.stringContaining('AWS_RUNTIME_SCHEDULER_BASE_URL'),
390
340
  });
391
341
  });
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");
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');
397
347
  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());
348
+ await handler?.({ body: { requestedByUserId: 'user-1' }, get: () => 'bridge.local:18081', protocol: 'http' }, { json, status }, vi.fn());
403
349
  expect(status).not.toHaveBeenCalled();
404
350
  expect(json).toHaveBeenCalledWith(expect.objectContaining({
405
351
  ok: true,
406
- message: "Bridge 更新已在后台开始,完成后会通过消息中心通知结果。",
352
+ message: 'Bridge 更新已在后台开始,完成后会通过消息中心通知结果。',
407
353
  }));
408
354
  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 });
355
+ expect(spawnMock).toHaveBeenCalledWith(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['install', '-g', 'aws-runtime-bridge@latest'], { shell: false, windowsHide: true });
410
356
  });
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");
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');
415
361
  const { json, status } = createMockResponse();
416
362
  await handler?.({}, { json, status }, vi.fn());
417
363
  expect(status).toHaveBeenCalledWith(400);
418
- expect(json).toHaveBeenCalledWith({
419
- error: "requestedByUserId is required",
420
- });
364
+ expect(json).toHaveBeenCalledWith({ error: 'requestedByUserId is required' });
421
365
  });
422
- it("responds before triggering graceful bridge restart", async () => {
366
+ it('responds before triggering graceful bridge restart', async () => {
423
367
  vi.useFakeTimers();
424
- const { instanceRouter, setGracefulShutdownFn } = await import("./instance.js");
368
+ const { instanceRouter, setGracefulShutdownFn } = await import('./instance.js');
425
369
  const gracefulShutdown = vi.fn().mockResolvedValue(undefined);
426
370
  setGracefulShutdownFn(gracefulShutdown);
427
- const handler = instanceRouter.stack.find((layer) => layer.route?.path === "/restart-bridge")?.route?.stack[1]?.handle;
428
- expect(handler).toBeTypeOf("function");
371
+ const handler = instanceRouter.stack.find((layer) => layer.route?.path === '/restart-bridge')?.route?.stack[1]?.handle;
372
+ expect(handler).toBeTypeOf('function');
429
373
  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());
374
+ await handler?.({ body: { preserveSessions: true, requestedByUserId: 'user-1' }, get: () => 'bridge.local:18081', protocol: 'http' }, { json, status }, vi.fn());
435
375
  expect(status).not.toHaveBeenCalled();
436
376
  expect(json).toHaveBeenCalledWith(expect.objectContaining({
437
377
  ok: true,
438
378
  preserveSessions: true,
439
- message: "Bridge 正在优雅退出;下次启动后会通过消息中心通知结果。若未由守护进程管理,请手动重新启动。",
379
+ message: 'Bridge 正在优雅退出;下次启动后会通过消息中心通知结果。若未由守护进程管理,请手动重新启动。',
440
380
  }));
441
381
  expect(gracefulShutdown).not.toHaveBeenCalled();
442
382
  await vi.advanceTimersByTimeAsync(250);
443
- expect(gracefulShutdown).toHaveBeenCalledWith("PANEL_RESTART", true);
383
+ expect(gracefulShutdown).toHaveBeenCalledWith('PANEL_RESTART', true);
444
384
  });
445
385
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aws-runtime-bridge",
3
- "version": "1.8.5",
3
+ "version": "1.8.7",
4
4
  "description": "AgentsWorkStudio runtime bridge service for machine-level agent runtime integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",