aws-runtime-bridge 1.7.41 → 1.7.42
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/routes/terminal.d.ts +5 -0
- package/dist/routes/terminal.d.ts.map +1 -1
- package/dist/routes/terminal.js +38 -12
- package/dist/routes/terminal.test.js +91 -1
- package/dist/services/instance-state.d.ts.map +1 -1
- package/dist/services/instance-state.js +33 -18
- package/dist/services/instance-state.test.js +41 -9
- package/dist/services/startup-config-wizard.d.ts.map +1 -1
- package/dist/services/startup-config-wizard.js +12 -13
- package/dist/utils/file-utils.d.ts +2 -1
- package/dist/utils/file-utils.d.ts.map +1 -1
- package/dist/utils/file-utils.js +35 -5
- package/dist/utils/file-utils.test.js +43 -2
- package/package.json +1 -1
|
@@ -60,6 +60,11 @@ export declare function evaluatePersistedSessionReuse(session: Pick<PersistedSes
|
|
|
60
60
|
*/
|
|
61
61
|
export declare function formatTerminalPrompt(currentDirectory: string): string;
|
|
62
62
|
export declare function buildRuntimeEnv(agentId: string, workspacePath: string | undefined, baseEnv: NodeJS.ProcessEnv, envOverrides?: Record<string, unknown>): Record<string, string>;
|
|
63
|
+
/**
|
|
64
|
+
* 将 SDK 工具结束事件转换为时间线返回数据状态。
|
|
65
|
+
* 主流程:格式化工具结果 -> 保留 MCP 专用展示 -> 其它工具按动作类型追加到对应时间线事件。
|
|
66
|
+
*/
|
|
67
|
+
export declare function buildToolResultTimelineActionInfo(event: ProviderEvent): RuntimeStatusActionInfo | undefined;
|
|
63
68
|
export declare function formatSdkOutputEvent(event: ProviderEvent): string | undefined;
|
|
64
69
|
export declare function resolveSdkProviderId(command?: string): string;
|
|
65
70
|
//# sourceMappingURL=terminal.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"terminal.d.ts","sourceRoot":"","sources":["../../src/routes/terminal.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAKxC,OAAO,EAEL,KAAK,oBAAoB,
|
|
1
|
+
{"version":3,"file":"terminal.d.ts","sourceRoot":"","sources":["../../src/routes/terminal.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAKxC,OAAO,EAEL,KAAK,oBAAoB,EAGzB,KAAK,aAAa,EAClB,KAAK,uBAAuB,EAI7B,MAAM,qBAAqB,CAAC;AAc7B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,eAAO,MAAM,cAAc,4CAAW,CAAC;AAIvC,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,oBAAoB,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC;AAGD,eAAO,MAAM,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAa,CAAC;AAKnE,wBAAgB,wBAAwB,CAAC,UAAU,CAAC,EAAE,uBAAuB;;;cAS5E;AAaD;;GAEG;AACH,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEpE;AAED;;;GAGG;AACH,wBAAgB,6BAA6B,CAC3C,QAAQ,GAAE,MAAM,CAAC,QAA2B,EAC5C,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,CAMR;AAED;;GAEG;AACH,wBAAgB,2BAA2B,CAAC,QAAQ,SAAkC,GAAG,WAAW,CAMnG;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,UAAQ,GAAG,MAAM,CAQnG;AAED;;GAEG;AACH,wBAAgB,kCAAkC,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAiB7F;AA2CD,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,YAAY,GAAG,aAAa,GAAG,UAAU,CAAC;CACnD;AAED;;;GAGG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,EACtC,YAAY,GAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAA0B,GACxD,6BAA6B,CAU/B;AA4BD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,gBAAgB,EAAE,MAAM,GAAG,MAAM,CAErE;AAyGD,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,OAAO,EAAE,MAAM,CAAC,UAAU,EAC1B,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACrC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA+BxB;AA8BD;;;GAGG;AACH,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,aAAa,GAAG,uBAAuB,GAAG,SAAS,CAiC3G;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,GAAG,SAAS,CAU7E;AAYD,wBAAgB,oBAAoB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAE7D"}
|
package/dist/routes/terminal.js
CHANGED
|
@@ -11,7 +11,7 @@ import path from 'node:path';
|
|
|
11
11
|
import { TextDecoder } from 'node:util';
|
|
12
12
|
import { Router } from 'express';
|
|
13
13
|
import { v4 as uuidv4 } from 'uuid';
|
|
14
|
-
import { adapterRegistry, registerSdkProviders, resolveSdkProviderIdByCommand, } from '../adapter/index.js';
|
|
14
|
+
import { adapterRegistry, getToolActionInfo, registerSdkProviders, resolveSdkProviderIdByCommand, } from '../adapter/index.js';
|
|
15
15
|
import { validateToken } from '../middleware/auth.js';
|
|
16
16
|
import { getAgentProcessManager } from '../services/agent-process-manager.js';
|
|
17
17
|
import { enqueueMcpLaunchBinding } from '../services/mcp-launch-binding-queue.js';
|
|
@@ -317,6 +317,40 @@ function isMcpProviderEvent(event) {
|
|
|
317
317
|
const toolName = String(event.data.toolName || '').toLowerCase();
|
|
318
318
|
return toolName.startsWith('mcp__') || toolName.includes('-mcp__') || toolName.includes('mcp_');
|
|
319
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* 将 SDK 工具结束事件转换为时间线返回数据状态。
|
|
322
|
+
* 主流程:格式化工具结果 -> 保留 MCP 专用展示 -> 其它工具按动作类型追加到对应时间线事件。
|
|
323
|
+
*/
|
|
324
|
+
export function buildToolResultTimelineActionInfo(event) {
|
|
325
|
+
if (event.type !== 'tool_use_end') {
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
const actionResult = formatToolResultForTimeline(event.data.toolResult ?? event.data.actionResult);
|
|
329
|
+
if (!actionResult) {
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
const inferredAction = getToolActionInfo(event.data.toolName, event.data.toolInput);
|
|
333
|
+
const actionType = event.data.actionType || inferredAction.actionType;
|
|
334
|
+
if (actionType === 'idle') {
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
if (actionType === 'mcp' && isMcpProviderEvent(event)) {
|
|
338
|
+
return {
|
|
339
|
+
actionType: 'mcp',
|
|
340
|
+
actionLabel: 'MCP返回',
|
|
341
|
+
actionDetail: event.data.actionDetail || event.data.toolName,
|
|
342
|
+
actionId: event.data.actionId || event.data.toolUseId,
|
|
343
|
+
actionResult,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
actionType,
|
|
348
|
+
actionLabel: event.data.actionLabel || inferredAction.actionLabel,
|
|
349
|
+
actionDetail: event.data.actionDetail || inferredAction.actionDetail || event.data.toolName,
|
|
350
|
+
actionId: event.data.actionId || event.data.toolUseId,
|
|
351
|
+
actionResult,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
320
354
|
export function formatSdkOutputEvent(event) {
|
|
321
355
|
if (event.type === 'text_delta' || event.type === 'thinking') {
|
|
322
356
|
return event.data.text || undefined;
|
|
@@ -351,17 +385,9 @@ function wireSdkAdapterEvents(adapter, definition) {
|
|
|
351
385
|
if (entry) {
|
|
352
386
|
console.log(`[${definition.displayName} Adapter] Event: ${event.type} for session ${event.sessionId}`);
|
|
353
387
|
forwardSdkOutputEvent(event, entry);
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
void sendStatus(entry.agentId, event.sessionId, 'tool_result', {
|
|
358
|
-
actionType: 'mcp',
|
|
359
|
-
actionLabel: 'MCP返回',
|
|
360
|
-
actionDetail: event.data.toolName,
|
|
361
|
-
actionId: event.data.toolUseId,
|
|
362
|
-
actionResult,
|
|
363
|
-
});
|
|
364
|
-
}
|
|
388
|
+
const toolResultActionInfo = buildToolResultTimelineActionInfo(event);
|
|
389
|
+
if (toolResultActionInfo) {
|
|
390
|
+
void sendStatus(entry.agentId, event.sessionId, 'tool_result', toolResultActionInfo);
|
|
365
391
|
}
|
|
366
392
|
if (event.type === 'turn_complete' && event.data.usage) {
|
|
367
393
|
sendStatus(entry.agentId, event.sessionId, 'waiting_input', undefined, resolveStatusChangeUsage({ usage: event.data.usage }));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { SDK_PROVIDER_DEFINITIONS } from '../adapter/SdkProviderSpi.js';
|
|
3
|
-
import { buildRuntimeEnv, createTerminalOutputDecoder, decodeTerminalOutputChunk, evaluatePersistedSessionReuse, formatTerminalPrompt, formatSdkOutputEvent, normalizeTerminalCommandInput, parseTerminalDirectoryChangeTarget, resolveTerminalOutputEncoding, resolveSdkProviderId, resolveStatusChangeUsage, } from './terminal.js';
|
|
3
|
+
import { buildRuntimeEnv, buildToolResultTimelineActionInfo, createTerminalOutputDecoder, decodeTerminalOutputChunk, evaluatePersistedSessionReuse, formatTerminalPrompt, formatSdkOutputEvent, normalizeTerminalCommandInput, parseTerminalDirectoryChangeTarget, resolveTerminalOutputEncoding, resolveSdkProviderId, resolveStatusChangeUsage, } from './terminal.js';
|
|
4
4
|
describe('terminal route validation', () => {
|
|
5
5
|
it('requires agentId and workspacePath for start', () => {
|
|
6
6
|
const validateStartRequest = (body) => {
|
|
@@ -188,3 +188,93 @@ describe('SDK output forwarding', () => {
|
|
|
188
188
|
expect(formatSdkOutputEvent(toolEvent)).toBeUndefined();
|
|
189
189
|
});
|
|
190
190
|
});
|
|
191
|
+
describe('SDK tool result timeline action info', () => {
|
|
192
|
+
it('keeps MCP tool results on MCP timeline events', () => {
|
|
193
|
+
const event = {
|
|
194
|
+
type: 'tool_use_end',
|
|
195
|
+
sessionId: 'session-1',
|
|
196
|
+
timestamp: new Date(0).toISOString(),
|
|
197
|
+
data: {
|
|
198
|
+
toolName: 'mcp__aws-mcp__get_profile',
|
|
199
|
+
toolUseId: 'tool-1',
|
|
200
|
+
toolResult: '{"displayName":"Agent"}',
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
expect(buildToolResultTimelineActionInfo(event)).toEqual({
|
|
204
|
+
actionType: 'mcp',
|
|
205
|
+
actionLabel: 'MCP返回',
|
|
206
|
+
actionDetail: 'mcp__aws-mcp__get_profile',
|
|
207
|
+
actionId: 'tool-1',
|
|
208
|
+
actionResult: '{"displayName":"Agent"}',
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
it('forwards bash tool results to command timeline events', () => {
|
|
212
|
+
const event = {
|
|
213
|
+
type: 'tool_use_end',
|
|
214
|
+
sessionId: 'session-1',
|
|
215
|
+
timestamp: new Date(0).toISOString(),
|
|
216
|
+
data: {
|
|
217
|
+
toolName: 'bash',
|
|
218
|
+
toolInput: { command: 'npm run build' },
|
|
219
|
+
toolUseId: 'tool-2',
|
|
220
|
+
toolResult: 'build passed',
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
expect(buildToolResultTimelineActionInfo(event)).toEqual({
|
|
224
|
+
actionType: 'bash',
|
|
225
|
+
actionLabel: '执行命令',
|
|
226
|
+
actionDetail: 'npm run build',
|
|
227
|
+
actionId: 'tool-2',
|
|
228
|
+
actionResult: 'build passed',
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
it('forwards read tool results to file-read timeline events', () => {
|
|
232
|
+
const event = {
|
|
233
|
+
type: 'tool_use_end',
|
|
234
|
+
sessionId: 'session-1',
|
|
235
|
+
timestamp: new Date(0).toISOString(),
|
|
236
|
+
data: {
|
|
237
|
+
toolName: 'read_file',
|
|
238
|
+
toolInput: { path: 'src/index.ts' },
|
|
239
|
+
actionId: 'tool-3',
|
|
240
|
+
actionResult: 'export const ok = true',
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
expect(buildToolResultTimelineActionInfo(event)).toEqual({
|
|
244
|
+
actionType: 'read_file',
|
|
245
|
+
actionLabel: '读取文件',
|
|
246
|
+
actionDetail: 'src/index.ts',
|
|
247
|
+
actionId: 'tool-3',
|
|
248
|
+
actionResult: 'export const ok = true',
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
it('keeps MCP-named filesystem results on semantic file-read timeline events', () => {
|
|
252
|
+
const event = {
|
|
253
|
+
type: 'tool_use_end',
|
|
254
|
+
sessionId: 'session-1',
|
|
255
|
+
timestamp: new Date(0).toISOString(),
|
|
256
|
+
data: {
|
|
257
|
+
toolName: 'mcp__filesystem__read_file',
|
|
258
|
+
toolInput: { path: 'src/index.ts' },
|
|
259
|
+
toolUseId: 'tool-4',
|
|
260
|
+
toolResult: 'export const ok = true',
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
expect(buildToolResultTimelineActionInfo(event)).toEqual({
|
|
264
|
+
actionType: 'read_file',
|
|
265
|
+
actionLabel: '读取文件',
|
|
266
|
+
actionDetail: 'src/index.ts',
|
|
267
|
+
actionId: 'tool-4',
|
|
268
|
+
actionResult: 'export const ok = true',
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
it('ignores tool result events without displayable result content', () => {
|
|
272
|
+
const event = {
|
|
273
|
+
type: 'tool_use_end',
|
|
274
|
+
sessionId: 'session-1',
|
|
275
|
+
timestamp: new Date(0).toISOString(),
|
|
276
|
+
data: { toolName: 'read_file', toolResult: ' ' },
|
|
277
|
+
};
|
|
278
|
+
expect(buildToolResultTimelineActionInfo(event)).toBeUndefined();
|
|
279
|
+
});
|
|
280
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instance-state.d.ts","sourceRoot":"","sources":["../../src/services/instance-state.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"instance-state.d.ts","sourceRoot":"","sources":["../../src/services/instance-state.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAqBjD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAgB/E;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,OAAO,CAAC,aAAa,CAAC,GAC5B,OAAO,CAAC,aAAa,CAAC,CAiBxB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAExD"}
|
|
@@ -3,12 +3,27 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 管理 Agent 实例的初始化状态持久化
|
|
5
5
|
*/
|
|
6
|
-
import path from 'node:path';
|
|
7
|
-
import os from 'node:os';
|
|
8
6
|
import { promises as fs } from 'node:fs';
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { getRuntimeHomeDir } from '../config.js';
|
|
10
|
+
import { atomicWriteJsonFile, ensureDir } from '../utils/file-utils.js';
|
|
11
|
+
import { ensureEnabledTools, parseBoolean } from '../utils/validation.js';
|
|
12
|
+
function getLegacyInstanceStateFile(agentId) {
|
|
13
|
+
return path.join(os.tmpdir(), 'agentswork-runtime-init', String(agentId), 'state.json');
|
|
14
|
+
}
|
|
15
|
+
function createDefaultInstanceState() {
|
|
16
|
+
return {
|
|
17
|
+
initialized: false,
|
|
18
|
+
skills: [],
|
|
19
|
+
mcpServers: [],
|
|
20
|
+
enabledTools: ['claude', 'opencode', 'codex'],
|
|
21
|
+
toolStatus: {},
|
|
22
|
+
skillEnabled: true,
|
|
23
|
+
mcpEnabled: true,
|
|
24
|
+
updatedAt: null,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
12
27
|
/**
|
|
13
28
|
* 获取实例状态文件路径
|
|
14
29
|
*
|
|
@@ -16,7 +31,7 @@ import { ensureDir } from '../utils/file-utils.js';
|
|
|
16
31
|
* @returns 状态文件路径
|
|
17
32
|
*/
|
|
18
33
|
export function getInstanceStateFile(agentId) {
|
|
19
|
-
return path.join(
|
|
34
|
+
return path.join(getRuntimeHomeDir(), '.aws-bridge', 'instances', String(agentId), 'state.json');
|
|
20
35
|
}
|
|
21
36
|
/**
|
|
22
37
|
* 加载实例状态
|
|
@@ -26,21 +41,21 @@ export function getInstanceStateFile(agentId) {
|
|
|
26
41
|
*/
|
|
27
42
|
export async function loadInstanceState(agentId) {
|
|
28
43
|
const filePath = getInstanceStateFile(agentId);
|
|
44
|
+
const legacyFilePath = getLegacyInstanceStateFile(agentId);
|
|
29
45
|
try {
|
|
30
46
|
const raw = await fs.readFile(filePath, 'utf-8');
|
|
31
47
|
return JSON.parse(raw);
|
|
32
48
|
}
|
|
33
49
|
catch {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
};
|
|
50
|
+
try {
|
|
51
|
+
const raw = await fs.readFile(legacyFilePath, 'utf-8');
|
|
52
|
+
const legacyState = JSON.parse(raw);
|
|
53
|
+
await atomicWriteJsonFile(filePath, legacyState);
|
|
54
|
+
return legacyState;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return createDefaultInstanceState();
|
|
58
|
+
}
|
|
44
59
|
}
|
|
45
60
|
}
|
|
46
61
|
/**
|
|
@@ -65,7 +80,7 @@ export async function saveInstanceState(agentId, state) {
|
|
|
65
80
|
mcpEnabled: parseBoolean(state?.mcpEnabled, true),
|
|
66
81
|
updatedAt: new Date().toISOString(),
|
|
67
82
|
};
|
|
68
|
-
await
|
|
83
|
+
await atomicWriteJsonFile(filePath, next);
|
|
69
84
|
return next;
|
|
70
85
|
}
|
|
71
86
|
/**
|
|
@@ -75,5 +90,5 @@ export async function saveInstanceState(agentId, state) {
|
|
|
75
90
|
* @returns 初始化根目录路径
|
|
76
91
|
*/
|
|
77
92
|
export function getAgentInitRoot(agentId) {
|
|
78
|
-
return path.join(
|
|
93
|
+
return path.join(getRuntimeHomeDir(), '.aws-bridge', 'instances', String(agentId));
|
|
79
94
|
}
|
|
@@ -1,33 +1,46 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
2
3
|
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
4
|
-
import { promises as fs } from 'node:fs';
|
|
5
5
|
import { getInstanceStateFile, loadInstanceState, saveInstanceState, getAgentInitRoot, } from './instance-state.js';
|
|
6
6
|
describe('instance-state service', () => {
|
|
7
7
|
const testAgentId = 'test-agent-123';
|
|
8
|
-
const
|
|
8
|
+
const originalRuntimeHomeDir = process.env.AWS_RUNTIME_HOME_DIR;
|
|
9
|
+
const testRuntimeHomeDir = path.join(os.tmpdir(), `aws-instance-state-home-${Date.now()}`);
|
|
10
|
+
function getCurrentStateFilePath() {
|
|
11
|
+
return getInstanceStateFile(testAgentId);
|
|
12
|
+
}
|
|
9
13
|
beforeEach(async () => {
|
|
14
|
+
process.env.AWS_RUNTIME_HOME_DIR = testRuntimeHomeDir;
|
|
10
15
|
// 清理测试文件
|
|
11
16
|
try {
|
|
12
|
-
await fs.rm(
|
|
17
|
+
await fs.rm(testRuntimeHomeDir, { recursive: true, force: true });
|
|
18
|
+
await fs.rm(path.join(os.tmpdir(), 'agentswork-runtime-init', testAgentId), { recursive: true, force: true });
|
|
13
19
|
}
|
|
14
20
|
catch {
|
|
15
21
|
// 目录不存在,忽略
|
|
16
22
|
}
|
|
17
23
|
});
|
|
18
24
|
afterEach(async () => {
|
|
25
|
+
if (originalRuntimeHomeDir === undefined) {
|
|
26
|
+
delete process.env.AWS_RUNTIME_HOME_DIR;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
process.env.AWS_RUNTIME_HOME_DIR = originalRuntimeHomeDir;
|
|
30
|
+
}
|
|
19
31
|
// 清理测试文件
|
|
20
32
|
try {
|
|
21
|
-
await fs.rm(
|
|
33
|
+
await fs.rm(testRuntimeHomeDir, { recursive: true, force: true });
|
|
34
|
+
await fs.rm(path.join(os.tmpdir(), 'agentswork-runtime-init', testAgentId), { recursive: true, force: true });
|
|
22
35
|
}
|
|
23
36
|
catch {
|
|
24
37
|
// 目录不存在,忽略
|
|
25
38
|
}
|
|
26
39
|
});
|
|
27
40
|
describe('getInstanceStateFile', () => {
|
|
28
|
-
it('should return correct path in
|
|
41
|
+
it('should return correct path in runtime home directory', () => {
|
|
29
42
|
const result = getInstanceStateFile(testAgentId);
|
|
30
|
-
const expected = path.join(
|
|
43
|
+
const expected = path.join(testRuntimeHomeDir, '.aws-bridge', 'instances', testAgentId, 'state.json');
|
|
31
44
|
expect(result).toBe(expected);
|
|
32
45
|
});
|
|
33
46
|
it('should handle agentId with special characters', () => {
|
|
@@ -45,7 +58,7 @@ describe('instance-state service', () => {
|
|
|
45
58
|
describe('getAgentInitRoot', () => {
|
|
46
59
|
it('should return correct init root directory', () => {
|
|
47
60
|
const result = getAgentInitRoot(testAgentId);
|
|
48
|
-
const expected = path.join(
|
|
61
|
+
const expected = path.join(testRuntimeHomeDir, '.aws-bridge', 'instances', testAgentId);
|
|
49
62
|
expect(result).toBe(expected);
|
|
50
63
|
});
|
|
51
64
|
});
|
|
@@ -92,6 +105,7 @@ describe('instance-state service', () => {
|
|
|
92
105
|
});
|
|
93
106
|
it('should handle corrupted JSON file gracefully', async () => {
|
|
94
107
|
// 创建损坏的 JSON 文件
|
|
108
|
+
const stateFilePath = getCurrentStateFilePath();
|
|
95
109
|
await fs.mkdir(path.dirname(stateFilePath), { recursive: true });
|
|
96
110
|
await fs.writeFile(stateFilePath, '{ invalid json }', 'utf-8');
|
|
97
111
|
const state = await loadInstanceState(testAgentId);
|
|
@@ -99,6 +113,24 @@ describe('instance-state service', () => {
|
|
|
99
113
|
expect(state.initialized).toBe(false);
|
|
100
114
|
expect(state.skills).toEqual([]);
|
|
101
115
|
});
|
|
116
|
+
it('should migrate readable legacy temp state into runtime home', async () => {
|
|
117
|
+
const legacyFilePath = path.join(os.tmpdir(), 'agentswork-runtime-init', testAgentId, 'state.json');
|
|
118
|
+
const legacyState = {
|
|
119
|
+
initialized: true,
|
|
120
|
+
skills: [],
|
|
121
|
+
mcpServers: [],
|
|
122
|
+
enabledTools: ['claude'],
|
|
123
|
+
toolStatus: {},
|
|
124
|
+
skillEnabled: true,
|
|
125
|
+
mcpEnabled: false,
|
|
126
|
+
updatedAt: '2026-01-01T00:00:00.000Z',
|
|
127
|
+
};
|
|
128
|
+
await fs.mkdir(path.dirname(legacyFilePath), { recursive: true });
|
|
129
|
+
await fs.writeFile(legacyFilePath, JSON.stringify(legacyState), 'utf-8');
|
|
130
|
+
const state = await loadInstanceState(testAgentId);
|
|
131
|
+
expect(state).toEqual(legacyState);
|
|
132
|
+
expect(JSON.parse(await fs.readFile(getInstanceStateFile(testAgentId), 'utf-8'))).toEqual(legacyState);
|
|
133
|
+
});
|
|
102
134
|
});
|
|
103
135
|
describe('saveInstanceState', () => {
|
|
104
136
|
it('should save complete state with all fields', async () => {
|
|
@@ -128,7 +160,7 @@ describe('instance-state service', () => {
|
|
|
128
160
|
expect(result.mcpEnabled).toBe(true);
|
|
129
161
|
expect(result.updatedAt).not.toBeNull();
|
|
130
162
|
// 验证文件已创建
|
|
131
|
-
const fileContent = await fs.readFile(
|
|
163
|
+
const fileContent = await fs.readFile(getCurrentStateFilePath(), 'utf-8');
|
|
132
164
|
const parsed = JSON.parse(fileContent);
|
|
133
165
|
expect(parsed.initialized).toBe(true);
|
|
134
166
|
});
|
|
@@ -198,7 +230,7 @@ describe('instance-state service', () => {
|
|
|
198
230
|
const result = await saveInstanceState(testAgentId, { initialized: false });
|
|
199
231
|
expect(result.initialized).toBe(false);
|
|
200
232
|
// 验证文件内容
|
|
201
|
-
const fileContent = await fs.readFile(
|
|
233
|
+
const fileContent = await fs.readFile(getCurrentStateFilePath(), 'utf-8');
|
|
202
234
|
const parsed = JSON.parse(fileContent);
|
|
203
235
|
expect(parsed.initialized).toBe(false);
|
|
204
236
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"startup-config-wizard.d.ts","sourceRoot":"","sources":["../../src/services/startup-config-wizard.ts"],"names":[],"mappings":"AAuBA,UAAU,QAAQ;IAChB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1C,KAAK,IAAI,IAAI,CAAC;CACf;AAED,UAAU,0BAA0B;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,qBAAqB,CAAC,EAAE,MAAM,MAAM,CAAC;CACtC;AAED,MAAM,MAAM,yBAAyB,GACjC,QAAQ,GACR,SAAS,GACT,YAAY,GACZ,SAAS,GACT,iBAAiB,GACjB,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"startup-config-wizard.d.ts","sourceRoot":"","sources":["../../src/services/startup-config-wizard.ts"],"names":[],"mappings":"AAuBA,UAAU,QAAQ;IAChB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1C,KAAK,IAAI,IAAI,CAAC;CACf;AAED,UAAU,0BAA0B;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,qBAAqB,CAAC,EAAE,MAAM,MAAM,CAAC;CACtC;AAED,MAAM,MAAM,yBAAyB,GACjC,QAAQ,GACR,SAAS,GACT,YAAY,GACZ,SAAS,GACT,iBAAiB,GACjB,QAAQ,CAAC;AAoKb,wBAAsB,mBAAmB,CACvC,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,yBAAyB,CAAC,CAiDpC;AAED,wBAAsB,sBAAsB,CAC1C,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,yBAAyB,CAAC,CA0BpC"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { existsSync,
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
4
|
import { stdin as input, stdout as output } from "node:process";
|
|
6
5
|
import { createInterface } from "node:readline/promises";
|
|
6
|
+
import { atomicWriteJsonFile } from "../utils/file-utils.js";
|
|
7
7
|
import { logger } from "../utils/logger.js";
|
|
8
8
|
import { getAutoRegisterConfigFilePath } from "./auto-register.js";
|
|
9
9
|
function normalizeAnswer(answer) {
|
|
@@ -15,10 +15,6 @@ function isYes(answer) {
|
|
|
15
15
|
function isNo(answer) {
|
|
16
16
|
return ["n", "no", "否", "不", "跳过", "skip", "s"].includes(normalizeAnswer(answer));
|
|
17
17
|
}
|
|
18
|
-
function writeConfigFile(configPath, config) {
|
|
19
|
-
mkdirSync(path.dirname(configPath), { recursive: true });
|
|
20
|
-
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
21
|
-
}
|
|
22
18
|
function readExistingConfig(configPath) {
|
|
23
19
|
if (!existsSync(configPath)) {
|
|
24
20
|
return {};
|
|
@@ -38,9 +34,12 @@ function readExistingConfig(configPath) {
|
|
|
38
34
|
function generateDefaultConnectionKey() {
|
|
39
35
|
return `awsb_${randomBytes(24).toString("base64url")}`;
|
|
40
36
|
}
|
|
41
|
-
function
|
|
37
|
+
async function writeConfigFile(configPath, config) {
|
|
38
|
+
await atomicWriteJsonFile(configPath, config);
|
|
39
|
+
}
|
|
40
|
+
async function writeSkippedConfig(configPath, generateConnectionKey) {
|
|
42
41
|
const connectionKey = generateConnectionKey();
|
|
43
|
-
writeConfigFile(configPath, { connectionKey });
|
|
42
|
+
await writeConfigFile(configPath, { connectionKey });
|
|
44
43
|
const message = `[runtime-bridge] 已创建配置文件: ${configPath}\n` +
|
|
45
44
|
`[runtime-bridge] 自动生成 connectionKey: ${connectionKey}\n` +
|
|
46
45
|
"[runtime-bridge] 请保存该 connectionKey;server/面板连接此 Bridge 时需要使用它。";
|
|
@@ -115,13 +114,13 @@ export async function ensureStartupConfig(options = {}) {
|
|
|
115
114
|
}
|
|
116
115
|
const env = options.env || process.env;
|
|
117
116
|
if (env.AWS_BRIDGE_SKIP_SETUP === "true") {
|
|
118
|
-
writeSkippedConfig(configPath, generateConnectionKey);
|
|
117
|
+
await writeSkippedConfig(configPath, generateConnectionKey);
|
|
119
118
|
logger.info("[runtime-bridge] AWS_BRIDGE_SKIP_SETUP=true,跳过首次配置引导");
|
|
120
119
|
return "skipped";
|
|
121
120
|
}
|
|
122
121
|
const interactive = options.interactive ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
123
122
|
if (!interactive) {
|
|
124
|
-
writeSkippedConfig(configPath, generateConnectionKey);
|
|
123
|
+
await writeSkippedConfig(configPath, generateConnectionKey);
|
|
125
124
|
logger.info(`[runtime-bridge] 未检测到配置文件: ${configPath};当前为非交互环境,跳过首次配置引导`);
|
|
126
125
|
return "non-interactive";
|
|
127
126
|
}
|
|
@@ -129,12 +128,12 @@ export async function ensureStartupConfig(options = {}) {
|
|
|
129
128
|
try {
|
|
130
129
|
const shouldConfigure = await askShouldConfigure(prompt);
|
|
131
130
|
if (!shouldConfigure) {
|
|
132
|
-
writeSkippedConfig(configPath, generateConnectionKey);
|
|
131
|
+
await writeSkippedConfig(configPath, generateConnectionKey);
|
|
133
132
|
logger.info("[runtime-bridge] 已跳过首次配置引导");
|
|
134
133
|
return "skipped";
|
|
135
134
|
}
|
|
136
135
|
const config = await collectStartupConfig(prompt);
|
|
137
|
-
writeConfigFile(configPath, config);
|
|
136
|
+
await writeConfigFile(configPath, config);
|
|
138
137
|
logger.info(`[runtime-bridge] 已写入配置文件: ${configPath}`);
|
|
139
138
|
return "created";
|
|
140
139
|
}
|
|
@@ -160,7 +159,7 @@ export async function configureStartupConfig(options = {}) {
|
|
|
160
159
|
try {
|
|
161
160
|
const existingConfig = readExistingConfig(configPath);
|
|
162
161
|
const config = await collectStartupConfig(prompt, existingConfig);
|
|
163
|
-
writeConfigFile(configPath, config);
|
|
162
|
+
await writeConfigFile(configPath, config);
|
|
164
163
|
logger.info(`[runtime-bridge] 已更新配置文件: ${configPath}`);
|
|
165
164
|
return "configured";
|
|
166
165
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { SkillPackage, DownloadResult, Skill } from '../types.js';
|
|
2
1
|
import type { AppFlags, InstalledSkill, CcSwitchSdk } from '@cc-switch/sdk';
|
|
2
|
+
import type { SkillPackage, DownloadResult, Skill } from '../types.js';
|
|
3
3
|
export declare function ensureDir(targetDir: string): Promise<string>;
|
|
4
4
|
export declare function withFileLock<T>(targetPath: string, operation: () => Promise<T>): Promise<T>;
|
|
5
|
+
export declare function stringifyStableJson(value: unknown): string;
|
|
5
6
|
export declare function atomicWriteFile(targetPath: string, content: string): Promise<void>;
|
|
6
7
|
export declare function atomicWriteJsonFile(targetPath: string, value: unknown): Promise<void>;
|
|
7
8
|
export declare function downloadSkillZip(skillPackage: SkillPackage, targetDir: string): Promise<DownloadResult | null>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-utils.d.ts","sourceRoot":"","sources":["../../src/utils/file-utils.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"file-utils.d.ts","sourceRoot":"","sources":["../../src/utils/file-utils.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAG5E,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAIvE,wBAAsB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGlE;AAED,wBAAsB,YAAY,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAqBjG;AA2BD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAE1D;AAED,wBAAsB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBxF;AAED,wBAAsB,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAE3F;AAED,wBAAsB,gBAAgB,CACpC,YAAY,EAAE,YAAY,EAC1B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CA+ChC;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB/E;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED,wBAAsB,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBhG;AAED,wBAAsB,oCAAoC,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CASlG;AAED,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,QAAQ,CAW1E;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAM1D;AAED,wBAAsB,qBAAqB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKhF;AAED,wBAAsB,wBAAwB,CAC5C,GAAG,EAAE,WAAW,EAChB,YAAY,EAAE,YAAY,EAC1B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAyBhC"}
|
package/dist/utils/file-utils.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
1
|
import { spawn } from 'node:child_process';
|
|
3
|
-
import { promises as fs } from 'node:fs';
|
|
4
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { promises as fs } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
5
|
import axios from 'axios';
|
|
6
6
|
import { schedulerBaseUrl } from '../config.js';
|
|
7
7
|
import { getRuntimeAccessToken } from '../services/runtime-binding.js';
|
|
@@ -30,16 +30,46 @@ export async function withFileLock(targetPath, operation) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
+
function isPlainObject(value) {
|
|
34
|
+
if (!value || typeof value !== 'object') {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
const prototype = Object.getPrototypeOf(value);
|
|
38
|
+
return prototype === Object.prototype || prototype === null;
|
|
39
|
+
}
|
|
40
|
+
function sortJsonValue(value) {
|
|
41
|
+
if (Array.isArray(value)) {
|
|
42
|
+
return value.map((item) => sortJsonValue(item));
|
|
43
|
+
}
|
|
44
|
+
if (isPlainObject(value)) {
|
|
45
|
+
return Object.keys(value)
|
|
46
|
+
.sort()
|
|
47
|
+
.reduce((acc, key) => {
|
|
48
|
+
acc[key] = sortJsonValue(value[key]);
|
|
49
|
+
return acc;
|
|
50
|
+
}, {});
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
export function stringifyStableJson(value) {
|
|
55
|
+
return `${JSON.stringify(sortJsonValue(value), null, 2)}\n`;
|
|
56
|
+
}
|
|
33
57
|
export async function atomicWriteFile(targetPath, content) {
|
|
34
58
|
await ensureDir(path.dirname(targetPath));
|
|
35
59
|
const targetDir = path.dirname(targetPath);
|
|
36
60
|
const targetBase = path.basename(targetPath);
|
|
37
61
|
const tmpPath = path.join(targetDir, `.${targetBase}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
|
38
|
-
|
|
39
|
-
|
|
62
|
+
try {
|
|
63
|
+
await fs.writeFile(tmpPath, content, 'utf-8');
|
|
64
|
+
await fs.rename(tmpPath, targetPath);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
await fs.rm(tmpPath, { force: true }).catch(() => undefined);
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
40
70
|
}
|
|
41
71
|
export async function atomicWriteJsonFile(targetPath, value) {
|
|
42
|
-
await atomicWriteFile(targetPath,
|
|
72
|
+
await atomicWriteFile(targetPath, stringifyStableJson(value));
|
|
43
73
|
}
|
|
44
74
|
export async function downloadSkillZip(skillPackage, targetDir) {
|
|
45
75
|
if (!skillPackage || !skillPackage.downloadUrl) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import path from 'node:path';
|
|
3
2
|
import { promises as fs } from 'node:fs';
|
|
4
3
|
import os from 'node:os';
|
|
5
|
-
import
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { atomicWriteJsonFile, ensureDir, escapePowerShellLiteral, resolveSkillSourceDirFromExtractRoot, stringifyStableJson, toSdkAppsFromEnabledTools, resolveSkillSourceDir, } from './file-utils.js';
|
|
6
6
|
// Mock axios for download tests
|
|
7
7
|
vi.mock('axios', () => ({
|
|
8
8
|
default: {
|
|
@@ -47,6 +47,47 @@ describe('file-utils', () => {
|
|
|
47
47
|
expect(result).toBe(tempDir);
|
|
48
48
|
});
|
|
49
49
|
});
|
|
50
|
+
describe('atomicWriteJsonFile', () => {
|
|
51
|
+
const tempDir = path.join(os.tmpdir(), `aws-file-utils-json-test-${Date.now()}`);
|
|
52
|
+
afterEach(async () => {
|
|
53
|
+
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
|
|
54
|
+
});
|
|
55
|
+
it('should write stable sorted json for deterministic config files', async () => {
|
|
56
|
+
const filePath = path.join(tempDir, 'config.json');
|
|
57
|
+
await atomicWriteJsonFile(filePath, {
|
|
58
|
+
z: 1,
|
|
59
|
+
nested: { b: true, a: false },
|
|
60
|
+
array: [{ d: 4, c: 3 }],
|
|
61
|
+
});
|
|
62
|
+
expect(await fs.readFile(filePath, 'utf-8')).toBe('{\n' +
|
|
63
|
+
' "array": [\n' +
|
|
64
|
+
' {\n' +
|
|
65
|
+
' "c": 3,\n' +
|
|
66
|
+
' "d": 4\n' +
|
|
67
|
+
' }\n' +
|
|
68
|
+
' ],\n' +
|
|
69
|
+
' "nested": {\n' +
|
|
70
|
+
' "a": false,\n' +
|
|
71
|
+
' "b": true\n' +
|
|
72
|
+
' },\n' +
|
|
73
|
+
' "z": 1\n' +
|
|
74
|
+
'}\n');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe('stringifyStableJson', () => {
|
|
78
|
+
it('should preserve array order while sorting object keys', () => {
|
|
79
|
+
expect(stringifyStableJson([{ b: 2, a: 1 }, { d: 4, c: 3 }])).toBe('[\n' +
|
|
80
|
+
' {\n' +
|
|
81
|
+
' "a": 1,\n' +
|
|
82
|
+
' "b": 2\n' +
|
|
83
|
+
' },\n' +
|
|
84
|
+
' {\n' +
|
|
85
|
+
' "c": 3,\n' +
|
|
86
|
+
' "d": 4\n' +
|
|
87
|
+
' }\n' +
|
|
88
|
+
']\n');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
50
91
|
describe('escapePowerShellLiteral', () => {
|
|
51
92
|
it('should escape single quotes by doubling them', () => {
|
|
52
93
|
expect(escapePowerShellLiteral("test'value")).toBe("test''value");
|