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.
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -28
- package/dist/config.test.js +1 -26
- package/dist/routes/instance.d.ts.map +1 -1
- package/dist/routes/instance.js +14 -31
- package/dist/routes/instance.test.js +168 -228
- package/package.json +1 -1
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,WAAW;AACX,eAAO,MAAM,IAAI,EAAE,MAElB,CAAC;AAEF,kBAAkB;AAClB,eAAO,MAAM,0BAA0B,0BAA0B,CAAC;AAElE,MAAM,MAAM,sBAAsB,GAC9B,SAAS,GACT,KAAK,GACL,6BAA6B,GAC7B,SAAS,GACT,iCAAiC,CAAC;AAEtC,MAAM,WAAW,0BAA0B;IACzC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,sBAAsB,CAAC;IAC/B,mBAAmB,EAAE,MAAM,EAAE,CAAC;CAC/B;AA+
|
|
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
|
-
|
|
102
|
+
const autoRegisterTargetUrls = readAutoRegisterTargetUrls();
|
|
103
|
+
if (autoRegisterTargetUrls.length === 1) {
|
|
128
104
|
return {
|
|
129
|
-
url:
|
|
105
|
+
url: autoRegisterTargetUrls[0] || DEFAULT_SCHEDULER_BASE_URL,
|
|
130
106
|
source: "single-auto-register-target",
|
|
131
107
|
ambiguousTargetUrls: [],
|
|
132
108
|
};
|
package/dist/config.test.js
CHANGED
|
@@ -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":"
|
|
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"}
|
package/dist/routes/instance.js
CHANGED
|
@@ -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
|
|
8
|
-
import { discoverCcSwitchConfiguredItems, loadCcSwitchSdk
|
|
9
|
-
import { initInstance, syncLegacyStateFromSdk
|
|
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
|
-
|
|
311
|
-
let tokenRefreshed = false;
|
|
303
|
+
const runtimeAccessToken = getRuntimeAccessToken(undefined, schedulerBaseUrl);
|
|
312
304
|
if (!runtimeAccessToken) {
|
|
313
|
-
|
|
314
|
-
|
|
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:
|
|
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
|
|
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
|
|
5
|
-
import { EventEmitter } from
|
|
4
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
5
|
+
import { EventEmitter } from 'node:events';
|
|
6
6
|
const spawnMock = vi.hoisted(() => vi.fn());
|
|
7
|
-
vi.mock(
|
|
7
|
+
vi.mock('axios', () => ({
|
|
8
8
|
default: {
|
|
9
9
|
get: vi.fn(),
|
|
10
10
|
post: vi.fn(),
|
|
11
11
|
},
|
|
12
12
|
}));
|
|
13
|
-
vi.mock(
|
|
13
|
+
vi.mock('../services/auto-register.js', () => ({
|
|
14
14
|
getConfiguredConnectionKeys: vi.fn(() => []),
|
|
15
15
|
requestRuntimeAccessTokenRefreshForServer: vi.fn(),
|
|
16
16
|
}));
|
|
17
|
-
vi.mock(
|
|
18
|
-
getRuntimeAccessToken: vi.fn(() =>
|
|
19
|
-
loadRuntimeBinding: vi.fn(() => ({
|
|
20
|
-
|
|
21
|
-
userId: "user-1",
|
|
22
|
-
schedulerBaseUrl: "http://server.local:7380",
|
|
23
|
-
})),
|
|
24
|
-
normalizeSchedulerBaseUrl: vi.fn((value) => value ? String(value).replace(/\/+$/, "") : undefined),
|
|
17
|
+
vi.mock('../services/runtime-binding.js', () => ({
|
|
18
|
+
getRuntimeAccessToken: vi.fn(() => 'stale-runtime-token-123456'),
|
|
19
|
+
loadRuntimeBinding: vi.fn(() => ({ status: 'paired', userId: 'user-1', schedulerBaseUrl: 'http://server.local:7380' })),
|
|
20
|
+
normalizeSchedulerBaseUrl: vi.fn((value) => (value ? String(value).replace(/\/+$/, '') : undefined)),
|
|
25
21
|
}));
|
|
26
|
-
vi.mock(
|
|
22
|
+
vi.mock('../services/lifecycle-state.js', () => ({
|
|
27
23
|
createBridgeLifecycleOperation: vi.fn((input) => ({
|
|
28
|
-
operationId:
|
|
29
|
-
requestedAt:
|
|
30
|
-
updatedAt:
|
|
24
|
+
operationId: 'operation-1',
|
|
25
|
+
requestedAt: '2026-01-01T00:00:00.000Z',
|
|
26
|
+
updatedAt: '2026-01-01T00:00:00.000Z',
|
|
31
27
|
...input,
|
|
32
28
|
})),
|
|
33
29
|
deliverPendingBridgeLifecycleNotifications: vi.fn(),
|
|
34
30
|
upsertBridgeLifecycleOperation: vi.fn(),
|
|
35
31
|
}));
|
|
36
|
-
vi.mock(
|
|
32
|
+
vi.mock('../config.js', async (importOriginal) => ({
|
|
37
33
|
...(await importOriginal()),
|
|
38
34
|
getRuntimeHomeDir: vi.fn(() => process.cwd()),
|
|
39
35
|
}));
|
|
40
|
-
vi.mock(
|
|
36
|
+
vi.mock('node:child_process', async (importOriginal) => ({
|
|
41
37
|
...(await importOriginal()),
|
|
42
38
|
spawn: spawnMock,
|
|
43
39
|
}));
|
|
@@ -45,19 +41,19 @@ afterEach(() => {
|
|
|
45
41
|
vi.useRealTimers();
|
|
46
42
|
vi.clearAllMocks();
|
|
47
43
|
});
|
|
48
|
-
function mockSpawnResult(exitCode, stdoutText =
|
|
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(
|
|
51
|
+
child.stdout.emit('data', Buffer.from(stdoutText));
|
|
56
52
|
}
|
|
57
53
|
if (stderrText) {
|
|
58
|
-
child.stderr.emit(
|
|
54
|
+
child.stderr.emit('data', Buffer.from(stderrText));
|
|
59
55
|
}
|
|
60
|
-
child.emit(
|
|
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(
|
|
71
|
-
it(
|
|
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:
|
|
70
|
+
runtimeBridge: 'healthy',
|
|
75
71
|
schedulerReachable: schedulerStatus >= 200 && schedulerStatus < 300,
|
|
76
72
|
schedulerBaseUrl,
|
|
77
73
|
});
|
|
78
|
-
const healthyResponse = buildPingResponse(200,
|
|
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,
|
|
77
|
+
const unhealthyResponse = buildPingResponse(502, 'http://localhost:8080');
|
|
82
78
|
expect(unhealthyResponse.schedulerReachable).toBe(false);
|
|
83
79
|
});
|
|
84
|
-
it(
|
|
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:
|
|
87
|
+
expect(validateInitRequest({ agentId: 'agent-1', workspacePath: '/path' }).valid).toBe(true);
|
|
95
88
|
});
|
|
96
|
-
it(
|
|
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: [
|
|
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([
|
|
102
|
+
expect(options.ccSwitchEnabledTools).toEqual(['claude', 'opencode']);
|
|
110
103
|
});
|
|
111
|
-
it(
|
|
104
|
+
it('requires agentId for state query', () => {
|
|
112
105
|
const validateStateRequest = (body) => {
|
|
113
106
|
if (!body.agentId)
|
|
114
|
-
return { valid: false, error:
|
|
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:
|
|
111
|
+
expect(validateStateRequest({ agentId: 'agent-1' }).valid).toBe(true);
|
|
119
112
|
});
|
|
120
|
-
it(
|
|
121
|
-
const { buildToolStatusDetectionTargets } = await import(
|
|
122
|
-
expect(buildToolStatusDetectionTargets([
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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(
|
|
130
|
-
const { SUPPORTED_INSTALLABLE_TOOLS } = await import(
|
|
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([
|
|
143
|
-
expect(SUPPORTED_INSTALLABLE_TOOLS).toContain(
|
|
131
|
+
expect(normalizeInstallableTools(['Codex'])).toEqual(['codex']);
|
|
132
|
+
expect(SUPPORTED_INSTALLABLE_TOOLS).toContain('codex');
|
|
144
133
|
});
|
|
145
|
-
it(
|
|
146
|
-
const { SUPPORTED_UNINSTALLABLE_TOOLS } = await import(
|
|
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([
|
|
159
|
-
|
|
143
|
+
expect(normalizeUninstallableTools(['Claude', 'OpenCode', 'Codex'])).toEqual([
|
|
144
|
+
'claude',
|
|
145
|
+
'opencode',
|
|
146
|
+
'codex',
|
|
147
|
+
]);
|
|
148
|
+
expect(normalizeUninstallableTools(['unknown-tool'])).toEqual([]);
|
|
160
149
|
});
|
|
161
|
-
it(
|
|
162
|
-
const { buildUninstallFailureMessage } = await import(
|
|
163
|
-
expect(buildUninstallFailureMessage([
|
|
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:
|
|
154
|
+
tool: 'opencode',
|
|
166
155
|
installed: true,
|
|
167
|
-
executable:
|
|
168
|
-
version:
|
|
156
|
+
executable: '/home/user/.opencode/bin/opencode',
|
|
157
|
+
version: '1.14.39',
|
|
169
158
|
installing: false,
|
|
170
|
-
error:
|
|
159
|
+
error: 'uninstall completed but command is still available',
|
|
171
160
|
},
|
|
172
|
-
})).toContain(
|
|
161
|
+
})).toContain('opencode: uninstall completed but command is still available');
|
|
173
162
|
});
|
|
174
|
-
it(
|
|
175
|
-
const { summarizeToolStatus } = await import(
|
|
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:
|
|
167
|
+
tool: 'opencode',
|
|
179
168
|
installed: false,
|
|
180
169
|
executable: null,
|
|
181
170
|
version: null,
|
|
182
171
|
installing: false,
|
|
183
|
-
error:
|
|
172
|
+
error: 'SDK package @opencode-ai/sdk is not installed in aws-runtime-bridge',
|
|
184
173
|
},
|
|
185
174
|
claude: {
|
|
186
|
-
tool:
|
|
175
|
+
tool: 'claude',
|
|
187
176
|
installed: true,
|
|
188
|
-
executable:
|
|
189
|
-
version:
|
|
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:
|
|
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:
|
|
203
|
-
version:
|
|
191
|
+
executable: '@anthropic-ai/claude-agent-sdk',
|
|
192
|
+
version: '0.2.87',
|
|
204
193
|
error: null,
|
|
205
194
|
},
|
|
206
195
|
});
|
|
207
196
|
});
|
|
208
|
-
it(
|
|
209
|
-
const { buildUninstallFailureMessage } = await import(
|
|
210
|
-
expect(buildUninstallFailureMessage([
|
|
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:
|
|
201
|
+
tool: 'claude',
|
|
213
202
|
installed: false,
|
|
214
203
|
executable: null,
|
|
215
204
|
version: null,
|
|
216
205
|
installing: false,
|
|
217
|
-
error:
|
|
206
|
+
error: 'command not installed',
|
|
218
207
|
},
|
|
219
|
-
})).toBe(
|
|
208
|
+
})).toBe('');
|
|
220
209
|
});
|
|
221
|
-
it(
|
|
222
|
-
const { getConfiguredConnectionKeys } = await import(
|
|
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(
|
|
225
|
-
expect(buildConnectionCheckResponse(
|
|
213
|
+
const { buildConnectionCheckResponse } = await import('./instance.js');
|
|
214
|
+
expect(buildConnectionCheckResponse('')).toEqual({
|
|
226
215
|
status: 200,
|
|
227
216
|
body: {
|
|
228
217
|
ok: true,
|
|
229
|
-
runtimeBridge:
|
|
218
|
+
runtimeBridge: 'healthy',
|
|
230
219
|
connectionKeyMatched: true,
|
|
231
220
|
connectionKeyRequired: false,
|
|
232
221
|
},
|
|
233
222
|
});
|
|
234
223
|
});
|
|
235
|
-
it(
|
|
236
|
-
const { createHash } = await import(
|
|
237
|
-
const { getConfiguredConnectionKeys } = await import(
|
|
238
|
-
vi.mocked(getConfiguredConnectionKeys).mockReturnValue([
|
|
239
|
-
const { buildConnectionCheckResponse } = await import(
|
|
240
|
-
const digest = createHash(
|
|
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(
|
|
231
|
+
expect(buildConnectionCheckResponse('00000000000000000000000000000000')).toEqual({
|
|
243
232
|
status: 401,
|
|
244
233
|
body: {
|
|
245
234
|
ok: false,
|
|
246
|
-
error:
|
|
235
|
+
error: 'connection_key_mismatch',
|
|
247
236
|
connectionKeyMatched: false,
|
|
248
237
|
connectionKeyRequired: true,
|
|
249
238
|
},
|
|
250
239
|
});
|
|
251
240
|
});
|
|
252
|
-
it(
|
|
253
|
-
const { buildSchedulerPingFailureResponse } = await import(
|
|
254
|
-
const response = buildSchedulerPingFailureResponse(new Error(
|
|
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:
|
|
258
|
-
failureStage:
|
|
259
|
-
runtimeBridge:
|
|
260
|
-
schedulerBaseUrl:
|
|
261
|
-
hint: expect.stringContaining(
|
|
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(
|
|
252
|
+
expect(response.hint).toContain('http://<server-host>:7380');
|
|
264
253
|
});
|
|
265
|
-
it(
|
|
266
|
-
const { buildSchedulerPingFailureResponse } = await import(
|
|
267
|
-
const response = buildSchedulerPingFailureResponse(new Error(
|
|
268
|
-
expect(response.schedulerBaseUrl).toBe(
|
|
269
|
-
expect(response.failureStage).toBe(
|
|
270
|
-
expect(response.runtimeBridge).toBe(
|
|
271
|
-
expect(response.hint).toContain(
|
|
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(
|
|
274
|
-
process.env.AWS_RUNTIME_SCHEDULER_BASE_URL =
|
|
275
|
-
const { default: axios } = await import(
|
|
276
|
-
const { requestRuntimeAccessTokenRefreshForServer } = await import(
|
|
277
|
-
const { instanceRouter } = await import(
|
|
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:
|
|
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 ===
|
|
290
|
-
expect(handler).toBeTypeOf(
|
|
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(() =>
|
|
294
|
-
expect(requestRuntimeAccessTokenRefreshForServer).toHaveBeenCalledWith(
|
|
295
|
-
expect(axios.get).toHaveBeenNthCalledWith(1,
|
|
296
|
-
headers: {
|
|
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,
|
|
299
|
-
headers: {
|
|
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:
|
|
289
|
+
runtimeBridge: 'healthy',
|
|
304
290
|
schedulerReachable: true,
|
|
305
291
|
tokenRefreshed: true,
|
|
306
|
-
schedulerBaseUrl:
|
|
292
|
+
schedulerBaseUrl: 'http://localhost:8080',
|
|
307
293
|
});
|
|
308
294
|
expect(status).not.toHaveBeenCalled();
|
|
309
295
|
});
|
|
310
|
-
it(
|
|
311
|
-
process.env.AWS_RUNTIME_SCHEDULER_BASE_URL =
|
|
312
|
-
const { default: axios } = await import(
|
|
313
|
-
const {
|
|
314
|
-
const {
|
|
315
|
-
|
|
316
|
-
vi.mocked(getRuntimeAccessToken).mockReturnValue("stale-runtime-token-123456");
|
|
317
|
-
vi.mocked(requestRuntimeAccessTokenRefreshForServer).mockReset();
|
|
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 ===
|
|
321
|
-
expect(handler).toBeTypeOf(
|
|
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 ===
|
|
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,
|
|
327
|
-
expect(
|
|
328
|
-
|
|
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:
|
|
316
|
+
runtimeBridge: 'healthy',
|
|
334
317
|
schedulerReachable: true,
|
|
335
318
|
tokenRefreshed: false,
|
|
336
|
-
schedulerBaseUrl:
|
|
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(
|
|
374
|
-
const { buildAmbiguousSchedulerBaseUrlResponse } = await import(
|
|
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
|
-
|
|
377
|
-
|
|
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:
|
|
382
|
-
failureStage:
|
|
383
|
-
runtimeBridge:
|
|
384
|
-
schedulerBaseUrl:
|
|
331
|
+
error: 'ambiguous_scheduler_base_url',
|
|
332
|
+
failureStage: 'scheduler_ping',
|
|
333
|
+
runtimeBridge: 'healthy',
|
|
334
|
+
schedulerBaseUrl: '',
|
|
385
335
|
autoRegisterTargetUrls: [
|
|
386
|
-
|
|
387
|
-
|
|
336
|
+
'http://scheduler-a.local:7380',
|
|
337
|
+
'http://scheduler-b.local:7380',
|
|
388
338
|
],
|
|
389
|
-
hint: expect.stringContaining(
|
|
339
|
+
hint: expect.stringContaining('AWS_RUNTIME_SCHEDULER_BASE_URL'),
|
|
390
340
|
});
|
|
391
341
|
});
|
|
392
|
-
it(
|
|
393
|
-
const { instanceRouter } = await import(
|
|
394
|
-
mockSpawnResult(0,
|
|
395
|
-
const handler = instanceRouter.stack.find((layer) => layer.route?.path ===
|
|
396
|
-
expect(handler).toBeTypeOf(
|
|
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:
|
|
352
|
+
message: 'Bridge 更新已在后台开始,完成后会通过消息中心通知结果。',
|
|
407
353
|
}));
|
|
408
354
|
await new Promise((resolve) => setImmediate(resolve));
|
|
409
|
-
expect(spawnMock).toHaveBeenCalledWith(process.platform ===
|
|
355
|
+
expect(spawnMock).toHaveBeenCalledWith(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['install', '-g', 'aws-runtime-bridge@latest'], { shell: false, windowsHide: true });
|
|
410
356
|
});
|
|
411
|
-
it(
|
|
412
|
-
const { instanceRouter } = await import(
|
|
413
|
-
const handler = instanceRouter.stack.find((layer) => layer.route?.path ===
|
|
414
|
-
expect(handler).toBeTypeOf(
|
|
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(
|
|
366
|
+
it('responds before triggering graceful bridge restart', async () => {
|
|
423
367
|
vi.useFakeTimers();
|
|
424
|
-
const { instanceRouter, setGracefulShutdownFn } = await import(
|
|
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 ===
|
|
428
|
-
expect(handler).toBeTypeOf(
|
|
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:
|
|
379
|
+
message: 'Bridge 正在优雅退出;下次启动后会通过消息中心通知结果。若未由守护进程管理,请手动重新启动。',
|
|
440
380
|
}));
|
|
441
381
|
expect(gracefulShutdown).not.toHaveBeenCalled();
|
|
442
382
|
await vi.advanceTimersByTimeAsync(250);
|
|
443
|
-
expect(gracefulShutdown).toHaveBeenCalledWith(
|
|
383
|
+
expect(gracefulShutdown).toHaveBeenCalledWith('PANEL_RESTART', true);
|
|
444
384
|
});
|
|
445
385
|
});
|