@sudobility/testomniac_runner 0.0.129 → 0.0.131
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/.env.example +39 -29
- package/bun.lock +4 -4
- package/package.json +3 -3
- package/src/adapters/PuppeteerAdapter.ts +3 -2
- package/src/auth/signic-registrar.ts +1 -1
- package/src/browser/chromium.ts +60 -1
- package/src/config/index.test.ts +2 -2
- package/src/config/index.ts +6 -8
- package/src/index.ts +11 -0
- package/src/orchestrator.ts +14 -10
- package/src/runner-manager.ts +49 -11
package/.env.example
CHANGED
|
@@ -1,67 +1,77 @@
|
|
|
1
1
|
# =============================================================================
|
|
2
|
-
# Testomniac
|
|
2
|
+
# Testomniac Runner Environment Variables
|
|
3
3
|
# =============================================================================
|
|
4
4
|
# Copy this file to .env and fill in your values.
|
|
5
5
|
|
|
6
6
|
# -----------------------------------------------------------------------------
|
|
7
7
|
# Server
|
|
8
8
|
# -----------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
# Port for the health/status HTTP endpoint (Hono server)
|
|
9
11
|
PORT=8030
|
|
10
|
-
|
|
12
|
+
|
|
13
|
+
# Pino log level: trace | debug | info | warn | error | fatal
|
|
11
14
|
LOG_LEVEL=info
|
|
12
15
|
|
|
13
16
|
# -----------------------------------------------------------------------------
|
|
14
|
-
# API
|
|
17
|
+
# API
|
|
15
18
|
# -----------------------------------------------------------------------------
|
|
16
|
-
# URL of the testomniac_api instance
|
|
17
|
-
TESTOMNIAC_API_URL=http://localhost:8027
|
|
18
19
|
|
|
19
|
-
#
|
|
20
|
+
# URL of the testomniac_api instance this runner polls for pending runs
|
|
21
|
+
TESTOMNIAC_API_URL=https://api.testomniac.com
|
|
22
|
+
|
|
23
|
+
# Shared secret for X-Scanner-Key auth. Must match SCANNER_API_KEY in the API's .env.
|
|
20
24
|
# Generate with: openssl rand -hex 32
|
|
21
25
|
SCANNER_API_KEY=
|
|
22
26
|
|
|
23
27
|
# -----------------------------------------------------------------------------
|
|
24
|
-
#
|
|
28
|
+
# Polling & Concurrency
|
|
25
29
|
# -----------------------------------------------------------------------------
|
|
26
|
-
|
|
30
|
+
|
|
31
|
+
# How often (ms) the runner polls the API for pending runs
|
|
27
32
|
SCAN_POLL_INTERVAL_MS=10000
|
|
28
33
|
|
|
34
|
+
# Maximum number of test runs executing in parallel
|
|
35
|
+
MAX_CONCURRENT_RUNNERS=5
|
|
36
|
+
|
|
29
37
|
# -----------------------------------------------------------------------------
|
|
30
|
-
#
|
|
38
|
+
# Browser
|
|
31
39
|
# -----------------------------------------------------------------------------
|
|
32
|
-
|
|
40
|
+
|
|
41
|
+
# Path to Chrome/Chromium binary. Auto-detected from Puppeteer cache if not set.
|
|
42
|
+
# Only set this to override auto-detection (e.g. Docker: /usr/bin/chromium).
|
|
43
|
+
# CHROMIUM_PATH=/usr/bin/chromium
|
|
44
|
+
|
|
45
|
+
# Directory for persistent browser profile (cookies, cache, etc.)
|
|
46
|
+
USER_DATA_DIR=./testomniac-browser-profile
|
|
47
|
+
|
|
48
|
+
# Directory for screenshots and other scan artifacts (JPEG, quality 72)
|
|
49
|
+
ARTIFACT_DIR=./testomniac-artifacts
|
|
33
50
|
|
|
34
51
|
# -----------------------------------------------------------------------------
|
|
35
|
-
# Email Reports (Optional
|
|
52
|
+
# Email Reports (Optional — only needed if sending scan report emails)
|
|
36
53
|
# -----------------------------------------------------------------------------
|
|
37
|
-
|
|
54
|
+
|
|
55
|
+
# Postmark API token for sending emails. Leave empty to disable email reports.
|
|
38
56
|
POSTMARK_SERVER_TOKEN=
|
|
39
57
|
|
|
40
|
-
# Sender email address
|
|
58
|
+
# Sender email address shown on report emails
|
|
41
59
|
POSTMARK_FROM_EMAIL=
|
|
42
60
|
|
|
43
|
-
# Secret
|
|
44
|
-
# If
|
|
61
|
+
# Secret for signing deep-link JWTs embedded in report emails.
|
|
62
|
+
# If empty, deep links use an empty key (insecure — fine for local dev).
|
|
45
63
|
# Generate with: openssl rand -hex 32
|
|
46
64
|
DEEP_LINK_SECRET=
|
|
47
65
|
|
|
48
|
-
# Base URL of the frontend app, used
|
|
66
|
+
# Base URL of the frontend app, used to build deep-link URLs in emails
|
|
49
67
|
APP_BASE_URL=http://localhost:3000
|
|
50
68
|
|
|
51
69
|
# -----------------------------------------------------------------------------
|
|
52
|
-
# Signic Email Verification (Optional
|
|
70
|
+
# Signic Email Verification (Optional — for email-based auth scanning)
|
|
53
71
|
# -----------------------------------------------------------------------------
|
|
54
|
-
SIGNIC_INDEXER_URL=https://api.signic.email/idx
|
|
55
|
-
SIGNIC_WILDDUCK_URL=https://api.signic.email/api
|
|
56
72
|
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
# -----------------------------------------------------------------------------
|
|
60
|
-
# Path to Chromium binary
|
|
61
|
-
CHROMIUM_PATH=/usr/bin/chromium
|
|
62
|
-
|
|
63
|
-
# Directory for browser profile persistence
|
|
64
|
-
USER_DATA_DIR=./browser-profile
|
|
73
|
+
# Signic indexer URL for polling verification/confirmation emails
|
|
74
|
+
SIGNIC_INDEXER_URL=https://api.signic.email/idx
|
|
65
75
|
|
|
66
|
-
#
|
|
67
|
-
|
|
76
|
+
# Signic email API URL for disposable email account management
|
|
77
|
+
SIGNIC_EMAIL_API_URL=https://api.signic.email/api
|
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.130",
|
|
12
|
+
"@sudobility/testomniac_types": "^0.0.68",
|
|
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.130", "", { "peerDependencies": { "@sudobility/testomniac_types": "^0.0.68", "openai": ">=6.0.0", "react": ">=18.0.0" }, "optionalPeers": ["openai", "react"] }, "sha512-C6I/8Zo/ZqBrOMXeiWqaAKEJIEdRI6xWvtRQoyYEtxuh51WN8CtbVyhPn5AEzDQJQTWa+RJllo9u0SxW0L/o1g=="],
|
|
176
176
|
|
|
177
|
-
"@sudobility/testomniac_types": ["@sudobility/testomniac_types@0.0.
|
|
177
|
+
"@sudobility/testomniac_types": ["@sudobility/testomniac_types@0.0.68", "", { "peerDependencies": { "@sudobility/types": "^1.9.62" } }, "sha512-io3qbAuOLuCrjC4txkD+B21n/RGrfz2MH+W0XciAOkaxcfXagdHRUdWie/520HeItC9F91jP9SA67YAcx5aZ0A=="],
|
|
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.131",
|
|
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.130",
|
|
28
|
+
"@sudobility/testomniac_types": "^0.0.68",
|
|
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
|
}
|
|
@@ -51,7 +51,7 @@ export async function autoRegister(
|
|
|
51
51
|
|
|
52
52
|
const client = new SignicClient({
|
|
53
53
|
indexerUrl: config.signicIndexerUrl,
|
|
54
|
-
wildduckUrl: config.
|
|
54
|
+
wildduckUrl: config.signicEmailApiUrl,
|
|
55
55
|
signMessage: async (message: string) => signEthMessage(message, privateKey),
|
|
56
56
|
});
|
|
57
57
|
|
package/src/browser/chromium.ts
CHANGED
|
@@ -1,7 +1,66 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
1
4
|
import puppeteer, { type Browser, type Page } from "puppeteer-core";
|
|
2
5
|
import type { Config } from "../config/index";
|
|
3
6
|
import type { Screen } from "@sudobility/testomniac_types";
|
|
4
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Finds the latest Chrome for Testing binary in the Puppeteer cache.
|
|
10
|
+
* Falls back to common system paths, then to the config value.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveChromiumPath(configPath: string): string {
|
|
13
|
+
// Explicit env var / config always wins
|
|
14
|
+
if (process.env.CHROMIUM_PATH) return process.env.CHROMIUM_PATH;
|
|
15
|
+
|
|
16
|
+
// Scan Puppeteer cache for the latest Chrome for Testing
|
|
17
|
+
const cacheDir = join(homedir(), ".cache", "puppeteer", "chrome");
|
|
18
|
+
if (existsSync(cacheDir)) {
|
|
19
|
+
const platform =
|
|
20
|
+
process.platform === "darwin"
|
|
21
|
+
? process.arch === "arm64"
|
|
22
|
+
? "mac_arm"
|
|
23
|
+
: "mac-x64"
|
|
24
|
+
: "linux";
|
|
25
|
+
const versions = readdirSync(cacheDir)
|
|
26
|
+
.filter(d => d.startsWith(platform + "-"))
|
|
27
|
+
.sort()
|
|
28
|
+
.reverse();
|
|
29
|
+
|
|
30
|
+
for (const version of versions) {
|
|
31
|
+
const candidates =
|
|
32
|
+
process.platform === "darwin"
|
|
33
|
+
? [
|
|
34
|
+
join(
|
|
35
|
+
cacheDir,
|
|
36
|
+
version,
|
|
37
|
+
"chrome-mac-arm64",
|
|
38
|
+
"Google Chrome for Testing.app",
|
|
39
|
+
"Contents",
|
|
40
|
+
"MacOS",
|
|
41
|
+
"Google Chrome for Testing"
|
|
42
|
+
),
|
|
43
|
+
join(
|
|
44
|
+
cacheDir,
|
|
45
|
+
version,
|
|
46
|
+
"chrome-mac-x64",
|
|
47
|
+
"Google Chrome for Testing.app",
|
|
48
|
+
"Contents",
|
|
49
|
+
"MacOS",
|
|
50
|
+
"Google Chrome for Testing"
|
|
51
|
+
),
|
|
52
|
+
]
|
|
53
|
+
: [join(cacheDir, version, "chrome-linux64", "chrome")];
|
|
54
|
+
|
|
55
|
+
for (const candidate of candidates) {
|
|
56
|
+
if (existsSync(candidate)) return candidate;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return configPath;
|
|
62
|
+
}
|
|
63
|
+
|
|
5
64
|
export class ChromiumManager {
|
|
6
65
|
private browser: Browser | null = null;
|
|
7
66
|
|
|
@@ -9,7 +68,7 @@ export class ChromiumManager {
|
|
|
9
68
|
|
|
10
69
|
async launch(): Promise<Browser> {
|
|
11
70
|
this.browser = await puppeteer.launch({
|
|
12
|
-
executablePath: this.config.chromiumPath,
|
|
71
|
+
executablePath: resolveChromiumPath(this.config.chromiumPath),
|
|
13
72
|
userDataDir: this.config.userDataDir,
|
|
14
73
|
headless: true,
|
|
15
74
|
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
package/src/config/index.test.ts
CHANGED
|
@@ -17,7 +17,7 @@ describe("config", () => {
|
|
|
17
17
|
|
|
18
18
|
it("uses defaults for optional values", () => {
|
|
19
19
|
const config = loadConfig();
|
|
20
|
-
expect(config.artifactDir).toBe("./artifacts");
|
|
21
|
-
expect(config.userDataDir).toBe("./browser-profile");
|
|
20
|
+
expect(config.artifactDir).toBe("./testomniac-artifacts");
|
|
21
|
+
expect(config.userDataDir).toBe("./testomniac-browser-profile");
|
|
22
22
|
});
|
|
23
23
|
});
|
package/src/config/index.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
export interface Config {
|
|
2
2
|
apiUrl: string;
|
|
3
3
|
scannerApiKey: string;
|
|
4
|
-
openaiApiKey: string;
|
|
5
4
|
postmarkServerToken: string;
|
|
6
5
|
postmarkFromEmail: string;
|
|
7
6
|
deepLinkSecret: string;
|
|
8
7
|
appBaseUrl: string;
|
|
9
8
|
signicIndexerUrl: string;
|
|
10
|
-
|
|
9
|
+
signicEmailApiUrl: string;
|
|
11
10
|
chromiumPath: string;
|
|
12
11
|
userDataDir: string;
|
|
13
12
|
artifactDir: string;
|
|
@@ -16,20 +15,19 @@ export interface Config {
|
|
|
16
15
|
|
|
17
16
|
export function loadConfig(): Config {
|
|
18
17
|
return {
|
|
19
|
-
apiUrl: process.env.TESTOMNIAC_API_URL || "
|
|
18
|
+
apiUrl: process.env.TESTOMNIAC_API_URL || "https://api.testomniac.com",
|
|
20
19
|
scannerApiKey: process.env.SCANNER_API_KEY || "",
|
|
21
|
-
openaiApiKey: process.env.OPENAI_API_KEY || "",
|
|
22
20
|
postmarkServerToken: process.env.POSTMARK_SERVER_TOKEN || "",
|
|
23
21
|
postmarkFromEmail: process.env.POSTMARK_FROM_EMAIL || "",
|
|
24
22
|
deepLinkSecret: process.env.DEEP_LINK_SECRET || "",
|
|
25
23
|
appBaseUrl: process.env.APP_BASE_URL || "http://localhost:3000",
|
|
26
24
|
signicIndexerUrl:
|
|
27
25
|
process.env.SIGNIC_INDEXER_URL || "https://api.signic.email/idx",
|
|
28
|
-
|
|
29
|
-
process.env.
|
|
26
|
+
signicEmailApiUrl:
|
|
27
|
+
process.env.SIGNIC_EMAIL_API_URL || "https://api.signic.email/api",
|
|
30
28
|
chromiumPath: process.env.CHROMIUM_PATH || "/usr/bin/chromium",
|
|
31
|
-
userDataDir: process.env.USER_DATA_DIR || "./browser-profile",
|
|
32
|
-
artifactDir: process.env.ARTIFACT_DIR || "./artifacts",
|
|
29
|
+
userDataDir: process.env.USER_DATA_DIR || "./testomniac-browser-profile",
|
|
30
|
+
artifactDir: process.env.ARTIFACT_DIR || "./testomniac-artifacts",
|
|
33
31
|
maxConcurrentRunners: Number(process.env.MAX_CONCURRENT_RUNNERS ?? 5),
|
|
34
32
|
};
|
|
35
33
|
}
|
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;
|
|
@@ -115,6 +113,7 @@ export async function runFullScan(options: RunOptions): Promise<void> {
|
|
|
115
113
|
entityCredentialId: options.entityCredentialId,
|
|
116
114
|
credentials: options.credentials,
|
|
117
115
|
quickScan: options.quickScan,
|
|
116
|
+
scanMode: options.scanMode,
|
|
118
117
|
},
|
|
119
118
|
api,
|
|
120
119
|
expertises,
|
|
@@ -138,7 +137,11 @@ export async function runFullScan(options: RunOptions): Promise<void> {
|
|
|
138
137
|
});
|
|
139
138
|
}
|
|
140
139
|
|
|
141
|
-
|
|
140
|
+
try {
|
|
141
|
+
await page.close();
|
|
142
|
+
} catch (err) {
|
|
143
|
+
logger.debug({ err }, "page already closed during scan cleanup");
|
|
144
|
+
}
|
|
142
145
|
|
|
143
146
|
logger.info(
|
|
144
147
|
{ scanId, sizeClass, totalDurationMs: elapsed(runStart) },
|
|
@@ -159,10 +162,7 @@ export async function runSequenceScan(
|
|
|
159
162
|
options: SequenceRunOptions
|
|
160
163
|
): Promise<void> {
|
|
161
164
|
const config = loadConfig();
|
|
162
|
-
const api = getApiClient(
|
|
163
|
-
config.apiUrl + "/api/v1/scanner",
|
|
164
|
-
config.scannerApiKey
|
|
165
|
-
);
|
|
165
|
+
const api = getApiClient(config.apiUrl, config.scannerApiKey);
|
|
166
166
|
|
|
167
167
|
const runStart = Date.now();
|
|
168
168
|
const sizeClass = (options.sizeClass as SizeClass) || SizeClass.Desktop;
|
|
@@ -205,7 +205,11 @@ export async function runSequenceScan(
|
|
|
205
205
|
eventHandler
|
|
206
206
|
);
|
|
207
207
|
|
|
208
|
-
|
|
208
|
+
try {
|
|
209
|
+
await page.close();
|
|
210
|
+
} catch (err) {
|
|
211
|
+
logger.debug({ err }, "page already closed during sequence cleanup");
|
|
212
|
+
}
|
|
209
213
|
|
|
210
214
|
logger.info(
|
|
211
215
|
{
|
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
|
+
}
|