@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 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.128",
12
- "@sudobility/testomniac_types": "^0.0.67",
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.128", "", { "peerDependencies": { "@sudobility/testomniac_types": "^0.0.67", "openai": ">=6.0.0", "react": ">=18.0.0" }, "optionalPeers": ["openai", "react"] }, "sha512-YWFjcMmZROSHSNUZLt9czUJeWoYmIj69JCXUP9CD9sw5/UTzFFLwIdkqtAE2lG4RDo/Yb/5heZPmffZ1EfPPqw=="],
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.67", "", { "peerDependencies": { "@sudobility/types": "^1.9.62" } }, "sha512-kNBDk7AsFx1rqUWLq4nx0nf1alV9G7U5xbxj8qDvZKa+cCnUSiZFRtDB88ENWQzeIPVJAU8MaVo5yUKVL46eYg=="],
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.130",
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.128",
28
- "@sudobility/testomniac_types": "^0.0.67",
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: (options?.type as "jpeg" | "png") || "jpeg",
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,
@@ -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
- await page.close();
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
- await page.close();
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
  {
@@ -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
- const runner = await api.getRunner(pendingRun.runnerId);
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
- await api.completeTestRun(pendingRun.id, { status: "failed" });
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
- const claimed = await api.claimTestRun(
73
- pendingRun.id,
74
- runnerInstanceId,
75
- runnerInstanceName
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
+ }