@sudobility/testomniac_runner 0.0.130 → 0.0.132
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/bun.lock +4 -4
- package/package.json +3 -3
- package/src/adapters/PuppeteerAdapter.ts +3 -2
- package/src/index.ts +11 -0
- package/src/orchestrator.ts +30 -12
- package/src/runner-manager.ts +49 -11
package/bun.lock
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"@noble/curves": "^1.0.0",
|
|
9
9
|
"@noble/hashes": "^1.0.0",
|
|
10
10
|
"@sudobility/signic_sdk": "^0.1.7",
|
|
11
|
-
"@sudobility/testomniac_runner_service": "^0.1.
|
|
12
|
-
"@sudobility/testomniac_types": "^0.0.
|
|
11
|
+
"@sudobility/testomniac_runner_service": "^0.1.131",
|
|
12
|
+
"@sudobility/testomniac_types": "^0.0.69",
|
|
13
13
|
"hono": "^4.7.0",
|
|
14
14
|
"jose": "^6.1.2",
|
|
15
15
|
"openai": "^6.7.0",
|
|
@@ -172,9 +172,9 @@
|
|
|
172
172
|
|
|
173
173
|
"@sudobility/signic_sdk": ["@sudobility/signic_sdk@0.1.7", "", {}, "sha512-5XSgHSVsmyrMQ/ui1nDywwzt9dbRCsaeJ5tX6mKw2ZXbTZ82OsMr+dqDyV9XV3pfy7IHRIZq73af5KBamx72Fw=="],
|
|
174
174
|
|
|
175
|
-
"@sudobility/testomniac_runner_service": ["@sudobility/testomniac_runner_service@0.1.
|
|
175
|
+
"@sudobility/testomniac_runner_service": ["@sudobility/testomniac_runner_service@0.1.131", "", { "peerDependencies": { "@sudobility/testomniac_types": "^0.0.69", "openai": ">=6.0.0", "react": ">=18.0.0" }, "optionalPeers": ["openai", "react"] }, "sha512-1ZJk1EjCJlFVXJXhlU3MLNPL53VwUJ6yOq4O9qvkpEn4VoCBnZlmVNOjicVXZE0UEP2u32GUGvBNEUHJGE5rFg=="],
|
|
176
176
|
|
|
177
|
-
"@sudobility/testomniac_types": ["@sudobility/testomniac_types@0.0.
|
|
177
|
+
"@sudobility/testomniac_types": ["@sudobility/testomniac_types@0.0.69", "", { "peerDependencies": { "@sudobility/types": "^1.9.62" } }, "sha512-VkWcWdy4zwddCZspzc0qhHIqyfuuzyMUh6Dvjk614y8+A8GQ8N6D9AiNmFtqrDOoMEnfo9VNU8AvdJGOjjc/1A=="],
|
|
178
178
|
|
|
179
179
|
"@sudobility/types": ["@sudobility/types@1.9.61", "", {}, "sha512-SODGpstB/iKfK3H/4BvJx/FBcc1h3gutUjGotyxN19VnOfWyzaDoEmW7eyoxOAYhZyXMXagSiii+NIEZvuxKog=="],
|
|
180
180
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sudobility/testomniac_runner",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.132",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"@noble/curves": "^1.0.0",
|
|
25
25
|
"@noble/hashes": "^1.0.0",
|
|
26
26
|
"@sudobility/signic_sdk": "^0.1.7",
|
|
27
|
-
"@sudobility/testomniac_runner_service": "^0.1.
|
|
28
|
-
"@sudobility/testomniac_types": "^0.0.
|
|
27
|
+
"@sudobility/testomniac_runner_service": "^0.1.131",
|
|
28
|
+
"@sudobility/testomniac_types": "^0.0.69",
|
|
29
29
|
"hono": "^4.7.0",
|
|
30
30
|
"jose": "^6.1.2",
|
|
31
31
|
"openai": "^6.7.0",
|
|
@@ -241,9 +241,10 @@ export class PuppeteerAdapter implements BrowserAdapter {
|
|
|
241
241
|
type?: string;
|
|
242
242
|
quality?: number;
|
|
243
243
|
}): Promise<Uint8Array> {
|
|
244
|
+
const type = (options?.type as "jpeg" | "png") || "jpeg";
|
|
244
245
|
const buffer = await this.page.screenshot({
|
|
245
|
-
type
|
|
246
|
-
quality: options?.quality || 72,
|
|
246
|
+
type,
|
|
247
|
+
...(type === "jpeg" ? { quality: options?.quality || 72 } : {}),
|
|
247
248
|
});
|
|
248
249
|
return new Uint8Array(buffer);
|
|
249
250
|
}
|
package/src/index.ts
CHANGED
|
@@ -44,6 +44,7 @@ if (import.meta.main) {
|
|
|
44
44
|
"runner-id": { type: "string" },
|
|
45
45
|
"base-url": { type: "string" },
|
|
46
46
|
"size-class": { type: "string", default: "desktop" },
|
|
47
|
+
"scan-mode": { type: "string" },
|
|
47
48
|
},
|
|
48
49
|
strict: false,
|
|
49
50
|
});
|
|
@@ -54,6 +55,7 @@ if (import.meta.main) {
|
|
|
54
55
|
const runnerId = Number(args["runner-id"]);
|
|
55
56
|
const baseUrl = String(args["base-url"] ?? "");
|
|
56
57
|
const sizeClass = String(args["size-class"] ?? "desktop");
|
|
58
|
+
const scanMode = readScanMode(args["scan-mode"]);
|
|
57
59
|
|
|
58
60
|
logger.info({ scanId, runnerId }, "one-shot mode: executing test run");
|
|
59
61
|
try {
|
|
@@ -63,6 +65,7 @@ if (import.meta.main) {
|
|
|
63
65
|
scanUrl: baseUrl,
|
|
64
66
|
baseUrl,
|
|
65
67
|
sizeClass,
|
|
68
|
+
scanMode,
|
|
66
69
|
runnerInstanceId: crypto.randomUUID(),
|
|
67
70
|
runnerInstanceName: "mcp-runner",
|
|
68
71
|
});
|
|
@@ -104,6 +107,14 @@ if (import.meta.main) {
|
|
|
104
107
|
}
|
|
105
108
|
}
|
|
106
109
|
|
|
110
|
+
function readScanMode(
|
|
111
|
+
value: unknown
|
|
112
|
+
): "full" | "partial" | "minimum" | undefined {
|
|
113
|
+
return value === "full" || value === "partial" || value === "minimum"
|
|
114
|
+
? value
|
|
115
|
+
: undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
107
118
|
export default {
|
|
108
119
|
port,
|
|
109
120
|
fetch: app.fetch,
|
package/src/orchestrator.ts
CHANGED
|
@@ -40,14 +40,12 @@ export interface RunOptions {
|
|
|
40
40
|
loginUrl?: string;
|
|
41
41
|
entityCredentialId?: number;
|
|
42
42
|
quickScan?: boolean;
|
|
43
|
+
scanMode?: "full" | "partial" | "minimum";
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
export async function runFullScan(options: RunOptions): Promise<void> {
|
|
46
47
|
const config = loadConfig();
|
|
47
|
-
const api = getApiClient(
|
|
48
|
-
config.apiUrl + "/api/v1/scanner",
|
|
49
|
-
config.scannerApiKey
|
|
50
|
-
);
|
|
48
|
+
const api = getApiClient(config.apiUrl, config.scannerApiKey);
|
|
51
49
|
const { scanUrl, baseUrl, userEmail } = options;
|
|
52
50
|
const runnerName = options.runnerName || new URL(scanUrl).hostname;
|
|
53
51
|
const sizeClass = (options.sizeClass as SizeClass) || SizeClass.Desktop;
|
|
@@ -65,7 +63,12 @@ export async function runFullScan(options: RunOptions): Promise<void> {
|
|
|
65
63
|
const page = await chromium.newPage(defaultScreen);
|
|
66
64
|
const adapter = new PuppeteerAdapter(page);
|
|
67
65
|
|
|
68
|
-
const eventHandler: ScanEventHandler
|
|
66
|
+
const eventHandler: ScanEventHandler & {
|
|
67
|
+
onStatusUpdate?: (update: {
|
|
68
|
+
message: string;
|
|
69
|
+
testRunId?: number;
|
|
70
|
+
}) => void;
|
|
71
|
+
} = {
|
|
69
72
|
onPageFound: p =>
|
|
70
73
|
logger.info(
|
|
71
74
|
{ relativePath: p.relativePath, pageId: p.pageId },
|
|
@@ -91,6 +94,8 @@ export async function runFullScan(options: RunOptions): Promise<void> {
|
|
|
91
94
|
onFindingCreated: f =>
|
|
92
95
|
logger.warn({ type: f.type, title: f.title }, "finding created"),
|
|
93
96
|
onStatsUpdated: stats => logger.debug(stats, "stats updated"),
|
|
97
|
+
onStatusUpdate: update =>
|
|
98
|
+
logger.info({ testRunId: update.testRunId }, update.message),
|
|
94
99
|
onScreenshotCaptured: () => {},
|
|
95
100
|
onScanComplete: summary => logger.info(summary, "scan complete"),
|
|
96
101
|
onError: err => logger.error(err, "scan error"),
|
|
@@ -115,6 +120,7 @@ export async function runFullScan(options: RunOptions): Promise<void> {
|
|
|
115
120
|
entityCredentialId: options.entityCredentialId,
|
|
116
121
|
credentials: options.credentials,
|
|
117
122
|
quickScan: options.quickScan,
|
|
123
|
+
scanMode: options.scanMode,
|
|
118
124
|
},
|
|
119
125
|
api,
|
|
120
126
|
expertises,
|
|
@@ -138,7 +144,11 @@ export async function runFullScan(options: RunOptions): Promise<void> {
|
|
|
138
144
|
});
|
|
139
145
|
}
|
|
140
146
|
|
|
141
|
-
|
|
147
|
+
try {
|
|
148
|
+
await page.close();
|
|
149
|
+
} catch (err) {
|
|
150
|
+
logger.debug({ err }, "page already closed during scan cleanup");
|
|
151
|
+
}
|
|
142
152
|
|
|
143
153
|
logger.info(
|
|
144
154
|
{ scanId, sizeClass, totalDurationMs: elapsed(runStart) },
|
|
@@ -159,10 +169,7 @@ export async function runSequenceScan(
|
|
|
159
169
|
options: SequenceRunOptions
|
|
160
170
|
): Promise<void> {
|
|
161
171
|
const config = loadConfig();
|
|
162
|
-
const api = getApiClient(
|
|
163
|
-
config.apiUrl + "/api/v1/scanner",
|
|
164
|
-
config.scannerApiKey
|
|
165
|
-
);
|
|
172
|
+
const api = getApiClient(config.apiUrl, config.scannerApiKey);
|
|
166
173
|
|
|
167
174
|
const runStart = Date.now();
|
|
168
175
|
const sizeClass = (options.sizeClass as SizeClass) || SizeClass.Desktop;
|
|
@@ -177,7 +184,12 @@ export async function runSequenceScan(
|
|
|
177
184
|
const adapter = new PuppeteerAdapter(page);
|
|
178
185
|
const expertises = createDefaultExpertises();
|
|
179
186
|
|
|
180
|
-
const eventHandler: ScanEventHandler
|
|
187
|
+
const eventHandler: ScanEventHandler & {
|
|
188
|
+
onStatusUpdate?: (update: {
|
|
189
|
+
message: string;
|
|
190
|
+
testRunId?: number;
|
|
191
|
+
}) => void;
|
|
192
|
+
} = {
|
|
181
193
|
onPageFound: p => logger.info(p, "page found"),
|
|
182
194
|
onPageStateCreated: s => logger.info(s, "page state created"),
|
|
183
195
|
onTestSurfaceCreated: s => logger.info(s, "surface created"),
|
|
@@ -186,6 +198,8 @@ export async function runSequenceScan(
|
|
|
186
198
|
onTestRunCompleted: r => logger.info(r, "run completed"),
|
|
187
199
|
onFindingCreated: f => logger.warn(f, "finding created"),
|
|
188
200
|
onStatsUpdated: s => logger.debug(s, "stats"),
|
|
201
|
+
onStatusUpdate: update =>
|
|
202
|
+
logger.info({ testRunId: update.testRunId }, update.message),
|
|
189
203
|
onScreenshotCaptured: () => {},
|
|
190
204
|
onScanComplete: s => logger.info(s, "complete"),
|
|
191
205
|
onError: e => logger.error(e, "error"),
|
|
@@ -205,7 +219,11 @@ export async function runSequenceScan(
|
|
|
205
219
|
eventHandler
|
|
206
220
|
);
|
|
207
221
|
|
|
208
|
-
|
|
222
|
+
try {
|
|
223
|
+
await page.close();
|
|
224
|
+
} catch (err) {
|
|
225
|
+
logger.debug({ err }, "page already closed during sequence cleanup");
|
|
226
|
+
}
|
|
209
227
|
|
|
210
228
|
logger.info(
|
|
211
229
|
{
|
package/src/runner-manager.ts
CHANGED
|
@@ -33,10 +33,7 @@ export class RunnerManager {
|
|
|
33
33
|
this.tickInFlight = true;
|
|
34
34
|
try {
|
|
35
35
|
const config = loadConfig();
|
|
36
|
-
const api = getApiClient(
|
|
37
|
-
config.apiUrl + "/api/v1/scanner",
|
|
38
|
-
config.scannerApiKey
|
|
39
|
-
);
|
|
36
|
+
const api = getApiClient(config.apiUrl, config.scannerApiKey);
|
|
40
37
|
|
|
41
38
|
while (this.activeRuns.size < this.maxConcurrentRunners) {
|
|
42
39
|
const pendingRun = await api.getPendingTestRun();
|
|
@@ -57,23 +54,50 @@ export class RunnerManager {
|
|
|
57
54
|
}
|
|
58
55
|
|
|
59
56
|
const slotNumber = this.activeRuns.size + 1;
|
|
60
|
-
|
|
57
|
+
|
|
58
|
+
let runner;
|
|
59
|
+
try {
|
|
60
|
+
runner = await api.getRunner(pendingRun.runnerId);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
logger.error(
|
|
63
|
+
{ err, runnerId: pendingRun.runnerId, runId: pendingRun.id },
|
|
64
|
+
"failed to fetch runner, will retry next tick"
|
|
65
|
+
);
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
61
68
|
if (!runner) {
|
|
62
69
|
logger.error(
|
|
63
70
|
{ runnerId: pendingRun.runnerId, runId: pendingRun.id },
|
|
64
71
|
"runner not found for pending run"
|
|
65
72
|
);
|
|
66
|
-
|
|
73
|
+
try {
|
|
74
|
+
await api.completeTestRun(pendingRun.id, { status: "failed" });
|
|
75
|
+
} catch (err) {
|
|
76
|
+
logger.error(
|
|
77
|
+
{ err, runId: pendingRun.id },
|
|
78
|
+
"failed to mark run as failed"
|
|
79
|
+
);
|
|
80
|
+
}
|
|
67
81
|
continue;
|
|
68
82
|
}
|
|
69
83
|
|
|
70
84
|
const runnerInstanceId = `${this.processInstanceId}:${slotNumber}`;
|
|
71
85
|
const runnerInstanceName = `${runner.title} [slot ${slotNumber}]`;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
86
|
+
|
|
87
|
+
let claimed;
|
|
88
|
+
try {
|
|
89
|
+
claimed = await api.claimTestRun(
|
|
90
|
+
pendingRun.id,
|
|
91
|
+
runnerInstanceId,
|
|
92
|
+
runnerInstanceName
|
|
93
|
+
);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
logger.error(
|
|
96
|
+
{ err, runId: pendingRun.id },
|
|
97
|
+
"failed to claim run, will retry next tick"
|
|
98
|
+
);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
77
101
|
if (!claimed) {
|
|
78
102
|
logger.info(
|
|
79
103
|
{ runId: pendingRun.id },
|
|
@@ -106,8 +130,11 @@ export class RunnerManager {
|
|
|
106
130
|
runnerInstanceId,
|
|
107
131
|
runnerInstanceName,
|
|
108
132
|
quickScan: pendingRun.quickScan ?? false,
|
|
133
|
+
scanMode: readScanMode(pendingRun),
|
|
109
134
|
});
|
|
110
135
|
}
|
|
136
|
+
} catch (err) {
|
|
137
|
+
logger.error({ err }, "tick failed");
|
|
111
138
|
} finally {
|
|
112
139
|
this.tickInFlight = false;
|
|
113
140
|
}
|
|
@@ -122,6 +149,7 @@ export class RunnerManager {
|
|
|
122
149
|
runnerInstanceId: string;
|
|
123
150
|
runnerInstanceName: string;
|
|
124
151
|
quickScan: boolean;
|
|
152
|
+
scanMode?: "full" | "partial" | "minimum";
|
|
125
153
|
}): Promise<void> {
|
|
126
154
|
const config = loadConfig();
|
|
127
155
|
const api = getApiClient(
|
|
@@ -140,6 +168,7 @@ export class RunnerManager {
|
|
|
140
168
|
runnerInstanceId: params.runnerInstanceId,
|
|
141
169
|
runnerInstanceName: params.runnerInstanceName,
|
|
142
170
|
quickScan: params.quickScan,
|
|
171
|
+
scanMode: params.scanMode,
|
|
143
172
|
});
|
|
144
173
|
|
|
145
174
|
logger.info({ runId: params.runId }, "run completed successfully");
|
|
@@ -161,3 +190,12 @@ export class RunnerManager {
|
|
|
161
190
|
}
|
|
162
191
|
}
|
|
163
192
|
}
|
|
193
|
+
|
|
194
|
+
function readScanMode(
|
|
195
|
+
run: unknown
|
|
196
|
+
): "full" | "partial" | "minimum" | undefined {
|
|
197
|
+
const value = (run as { scanMode?: unknown }).scanMode;
|
|
198
|
+
return value === "full" || value === "partial" || value === "minimum"
|
|
199
|
+
? value
|
|
200
|
+
: undefined;
|
|
201
|
+
}
|