@skyramp/mcp 0.5.0-rc.2 → 0.5.0-rc.4
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/build/index.js +15 -1
- package/build/services/FleetExecutionService.d.ts +87 -16
- package/build/services/FleetExecutionService.js +487 -48
- package/build/services/FleetRunRegistry.d.ts +49 -0
- package/build/services/FleetRunRegistry.js +178 -0
- package/build/services/fleetLimitConfirmation.d.ts +19 -0
- package/build/services/fleetLimitConfirmation.js +126 -0
- package/build/tool-phases.js +5 -0
- package/build/tools/executeFleetTool.d.ts +6 -4
- package/build/tools/executeFleetTool.js +67 -23
- package/build/tools/fleetLifecycleTools.d.ts +20 -0
- package/build/tools/fleetLifecycleTools.js +124 -0
- package/build/tools/generate-tests/generateUILoadRestTool.d.ts +19 -1
- package/build/tools/generate-tests/generateUILoadRestTool.js +316 -50
- package/package.json +2 -2
- package/plugin/skills/setup-testbot-target-sut/SKILL.md +6 -4
- package/plugin/skills/setup-testbot-target-sut/reference/deployed-environment.md +56 -0
package/build/index.js
CHANGED
|
@@ -7,6 +7,8 @@ import { registerTraceTool } from "./tools/trace/startTraceCollectionTool.js";
|
|
|
7
7
|
import { registerTraceStopTool } from "./tools/trace/stopTraceCollectionTool.js";
|
|
8
8
|
import { registerExecuteSkyrampTestTool } from "./tools/executeSkyrampTestTool.js";
|
|
9
9
|
import { registerExecuteFleetTool } from "./tools/executeFleetTool.js";
|
|
10
|
+
import { registerFleetLifecycleTools } from "./tools/fleetLifecycleTools.js";
|
|
11
|
+
import { fleetRuns } from "./services/FleetRunRegistry.js";
|
|
10
12
|
import { AUTH_PLACEHOLDER_TOKEN } from "./types/TestTypes.js";
|
|
11
13
|
import { AUTH_CONFLICT_ERROR_MSG } from "./prompts/test-recommendation/recommendationSections.js";
|
|
12
14
|
import { logger } from "./utils/logger.js";
|
|
@@ -242,6 +244,7 @@ async function createServer({ era }) {
|
|
|
242
244
|
registerLogoutTool,
|
|
243
245
|
registerExecuteSkyrampTestTool,
|
|
244
246
|
registerExecuteFleetTool,
|
|
247
|
+
registerFleetLifecycleTools,
|
|
245
248
|
registerTraceTool,
|
|
246
249
|
registerTraceStopTool,
|
|
247
250
|
];
|
|
@@ -328,11 +331,18 @@ async function createServer({ era }) {
|
|
|
328
331
|
runInitTrigger();
|
|
329
332
|
return server;
|
|
330
333
|
}
|
|
334
|
+
// A crashing process must not stay alive anywhere near the full graceful
|
|
335
|
+
// shutdown's 30s grace — it still gives workers a moment to cancel, but caps
|
|
336
|
+
// it low enough that a crash can't hang the process.
|
|
337
|
+
const CRASH_CLEANUP_WAIT_MS = 3_000;
|
|
338
|
+
/** Interrupt running fleets (bounded wait) so an exit does not leave workers loading. */
|
|
339
|
+
const cancelFleets = () => fleetRuns.cancelAll(CRASH_CLEANUP_WAIT_MS).catch(() => { });
|
|
331
340
|
// Global error handlers for crash telemetry
|
|
332
341
|
process.on("uncaughtException", async (error) => {
|
|
333
342
|
// Don't send analytics for EPIPE errors
|
|
334
343
|
const isEpipeError = "code" in error && error.code === "EPIPE";
|
|
335
344
|
if (isEpipeError) {
|
|
345
|
+
await cancelFleets();
|
|
336
346
|
process.exit(0);
|
|
337
347
|
}
|
|
338
348
|
logger.critical("Uncaught exception - MCP server crashing", {
|
|
@@ -345,6 +355,7 @@ process.on("uncaughtException", async (error) => {
|
|
|
345
355
|
catch (telemetryError) {
|
|
346
356
|
logger.error("Failed to send crash telemetry", { telemetryError });
|
|
347
357
|
}
|
|
358
|
+
await cancelFleets();
|
|
348
359
|
process.exit(1);
|
|
349
360
|
});
|
|
350
361
|
// Handle unhandled promise rejections — without this handler Node.js 15+
|
|
@@ -354,6 +365,7 @@ process.on("unhandledRejection", async (reason) => {
|
|
|
354
365
|
const error = reason instanceof Error ? reason : new Error(String(reason));
|
|
355
366
|
// EPIPE means the client already disconnected — exit cleanly
|
|
356
367
|
if ("code" in error && error.code === "EPIPE") {
|
|
368
|
+
await cancelFleets();
|
|
357
369
|
process.exit(0);
|
|
358
370
|
}
|
|
359
371
|
// gRPC/Docker connectivity errors — log and survive.
|
|
@@ -400,7 +412,9 @@ async function main() {
|
|
|
400
412
|
const cleanupPlaywright = playwrightService
|
|
401
413
|
? playwrightService.shutdown().catch(() => { })
|
|
402
414
|
: Promise.resolve();
|
|
403
|
-
|
|
415
|
+
// A running fleet is interrupted so it can cancel its workers, not left loading them.
|
|
416
|
+
const cleanupFleets = fleetRuns.cancelAll().catch(() => { });
|
|
417
|
+
Promise.all([cleanupPlaywright, cleanupFleets]).finally(() => {
|
|
404
418
|
handle.close().finally(() => {
|
|
405
419
|
process.exit(0);
|
|
406
420
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Docker from "dockerode";
|
|
2
|
+
import { Writable } from "stream";
|
|
2
3
|
/**
|
|
3
4
|
* Where the mounted pieces live inside the executor, per container platform.
|
|
4
5
|
*
|
|
@@ -11,8 +12,12 @@ export type ContainerPlatform = "linux" | "windows";
|
|
|
11
12
|
interface Layout {
|
|
12
13
|
work: string;
|
|
13
14
|
certs: string;
|
|
14
|
-
/** The directory a branch-built library jar is mounted from. */
|
|
15
|
+
/** The directory a branch-built library jar or package is mounted from. */
|
|
15
16
|
overrideDir: string;
|
|
17
|
+
/** The native library the image's SDKs load. */
|
|
18
|
+
nativeLib: string;
|
|
19
|
+
/** Where a branch-built npm SDK is installed, ahead of the image's on NODE_PATH. */
|
|
20
|
+
npmOverride: string;
|
|
16
21
|
/** The directory a credential pool outside the work directory is mounted from. */
|
|
17
22
|
poolDir: string;
|
|
18
23
|
imageClasses: string;
|
|
@@ -49,11 +54,20 @@ export interface FleetExecutionOptions {
|
|
|
49
54
|
executorImage?: string;
|
|
50
55
|
platform?: ContainerPlatform;
|
|
51
56
|
entryClass?: string;
|
|
52
|
-
|
|
57
|
+
/** Required only when the fleet deploys on a remote engine, which is mTLS. */
|
|
58
|
+
certsDir?: string;
|
|
59
|
+
/** Opt-in: the mounted daemon socket gives the fleet root-level access to this host. */
|
|
60
|
+
localDocker?: boolean;
|
|
53
61
|
credentials?: string;
|
|
54
62
|
libraryJar?: string;
|
|
63
|
+
/** A branch-built pip (.whl, .tar.gz) or npm (.tgz) SDK for a Python, TS or JS fleet. */
|
|
64
|
+
libraryPackage?: string;
|
|
55
65
|
timeoutSec?: number;
|
|
56
66
|
env?: Record<string, string>;
|
|
67
|
+
/** Internal live-output sink, fed whole lines; never supplied through a tool's JSON schema. */
|
|
68
|
+
onOutput?: (chunk: string) => void;
|
|
69
|
+
/** Internal: aborting it interrupts the run, then kills it after CANCEL_GRACE_MS. */
|
|
70
|
+
signal?: AbortSignal;
|
|
57
71
|
}
|
|
58
72
|
export interface FleetExecutionResult {
|
|
59
73
|
passed: boolean;
|
|
@@ -62,17 +76,47 @@ export interface FleetExecutionResult {
|
|
|
62
76
|
entryClass: string;
|
|
63
77
|
exitCode: number | null;
|
|
64
78
|
report: string;
|
|
79
|
+
/** Only the last MAX_BUFFERED_OUTPUT characters; the complete run is at outputLog. */
|
|
65
80
|
output: string;
|
|
66
81
|
result?: unknown;
|
|
67
82
|
timedOut?: boolean;
|
|
83
|
+
cancelled?: boolean;
|
|
84
|
+
/** The run's complete output on the host, when it could be written. */
|
|
85
|
+
outputLog?: string;
|
|
68
86
|
}
|
|
69
|
-
/**
|
|
70
|
-
export declare const
|
|
87
|
+
/** How long an interrupted fleet gets to cancel its workers before it is killed. */
|
|
88
|
+
export declare const CANCEL_GRACE_MS = 20000;
|
|
71
89
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
|
|
90
|
+
* A sink for one of the container's streams that passes on whole lines only. Docker
|
|
91
|
+
* interleaves stdout and stderr frames, so a line is only whole within its own stream:
|
|
92
|
+
* one shared sink could split a SKYRAMP_LIMIT line around a stderr frame.
|
|
93
|
+
*/
|
|
94
|
+
export declare function lineSink(emit: (line: string) => void): {
|
|
95
|
+
sink: Writable;
|
|
96
|
+
flush: () => void;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* A simple Java class name, or a Python, TypeScript or JavaScript fleet script's file name.
|
|
100
|
+
*
|
|
101
|
+
* No `$`: the name is interpolated unquoted into the `sh`/PowerShell command built below, and
|
|
102
|
+
* `$` there is a variable sigil, not a literal character.
|
|
103
|
+
*/
|
|
104
|
+
export declare const FLEET_ENTRY_NAME: RegExp;
|
|
105
|
+
export type FleetLanguage = "java" | "python" | "typescript" | "javascript";
|
|
106
|
+
/** The language a fleet entry is written in: a script by its extension, else a Java class. */
|
|
107
|
+
export declare function fleetLanguage(entry: string): FleetLanguage;
|
|
108
|
+
/**
|
|
109
|
+
* The SDK a branch-built package replaces: a pip sdist or wheel for Python, an npm tarball
|
|
110
|
+
* for TypeScript and JavaScript. Java takes a jar through libraryJar instead.
|
|
111
|
+
*/
|
|
112
|
+
export declare function packageLanguages(pkg: string): FleetLanguage[];
|
|
113
|
+
/** The file an entry lives in, relative to the fleet directory. */
|
|
114
|
+
export declare function fleetFileName(entry: string): string;
|
|
115
|
+
/**
|
|
116
|
+
* The generated fleet manager: the JUnit class (Java) or the script (Python, TypeScript)
|
|
117
|
+
* the run starts. The journey and the load file travel with it because they sit beside it
|
|
118
|
+
* -- the journey is an input the fleet ships to the worker, and the load file only supplies
|
|
119
|
+
* the scenario.
|
|
76
120
|
*/
|
|
77
121
|
export declare function findEntryClass(directory: string, explicit?: string): string;
|
|
78
122
|
/**
|
|
@@ -84,12 +128,25 @@ export declare function findEntryClass(directory: string, explicit?: string): st
|
|
|
84
128
|
*/
|
|
85
129
|
export declare function assertNoPinnedImage(fleetFile: string): void;
|
|
86
130
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
131
|
+
* Which Docker daemons the fleet deploys its workers through, read off its hosts.
|
|
132
|
+
*
|
|
133
|
+
* `remote`: a host names a tcp or http(s) engine. That connection is mTLS, so the run needs
|
|
134
|
+
* the client certificates.
|
|
135
|
+
*
|
|
136
|
+
* `local`: a host deploys with no remote engine, which is the daemon this tool runs the
|
|
137
|
+
* executor on. The fleet reaches it through the daemon's socket and needs no certificates:
|
|
138
|
+
* the way to try a fleet on one's own machine.
|
|
139
|
+
*
|
|
140
|
+
* A host that carries an address drives a worker that already runs and deploys nothing, so
|
|
141
|
+
* it counts as neither. Throws when no host, a non-literal dockerHost, or a host built from a
|
|
142
|
+
* spread or a bare variable leaves the engine unknown -- the last of those would otherwise
|
|
143
|
+
* read as local with no dockerHost found, mounting the daemon's socket for a host that may
|
|
144
|
+
* actually be remote.
|
|
91
145
|
*/
|
|
92
|
-
export declare function
|
|
146
|
+
export declare function fleetDaemons(fleetFile: string): {
|
|
147
|
+
remote: boolean;
|
|
148
|
+
local: boolean;
|
|
149
|
+
};
|
|
93
150
|
/**
|
|
94
151
|
* The image the run happens in.
|
|
95
152
|
*
|
|
@@ -105,10 +162,24 @@ export interface ContainerCommand {
|
|
|
105
162
|
/** The shell script argv ends with -- named, because its index differs per platform. */
|
|
106
163
|
script: string;
|
|
107
164
|
}
|
|
108
|
-
/**
|
|
109
|
-
|
|
110
|
-
|
|
165
|
+
/**
|
|
166
|
+
* Java: javac, then the JUnit console runner, both against the image's classpath. Python,
|
|
167
|
+
* TypeScript and JavaScript: the fleet script, run from the work directory so its imports
|
|
168
|
+
* of the load file resolve beside it -- after installing a branch-built SDK package over
|
|
169
|
+
* the image's own, when one is mounted.
|
|
170
|
+
*/
|
|
171
|
+
export declare function buildCommand(entryClass: string, overrideJar: string | undefined, platform?: ContainerPlatform, overridePackage?: string): ContainerCommand;
|
|
172
|
+
/**
|
|
173
|
+
* The lines the fleet printed, which is what a reader wants out of the JUnit noise: the
|
|
174
|
+
* one-line digest, or the report blocks (name in column one, continuation lines indented).
|
|
175
|
+
*/
|
|
111
176
|
export declare function extractReport(output: string): string;
|
|
177
|
+
/**
|
|
178
|
+
* The report the library rendered itself, carried in the structured result as `report`.
|
|
179
|
+
* Preferred over the stdout filter: it is the complete text, not whatever survived the
|
|
180
|
+
* JUnit runner's console, and it is the same text every SDK prints.
|
|
181
|
+
*/
|
|
182
|
+
export declare function embeddedReport(result: unknown): string | undefined;
|
|
112
183
|
/**
|
|
113
184
|
* Where a path inside the container lands on the host.
|
|
114
185
|
*
|