@mainahq/core 0.5.0 → 0.6.0
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/package.json +1 -1
- package/src/cloud/__tests__/client.test.ts +243 -0
- package/src/cloud/auth.ts +18 -6
- package/src/cloud/client.ts +60 -0
- package/src/cloud/types.ts +53 -1
- package/src/index.ts +17 -12
- package/src/verify/__tests__/types.test.ts +129 -0
- package/src/verify/types.ts +17 -0
package/package.json
CHANGED
|
@@ -250,4 +250,247 @@ describe("createCloudClient", () => {
|
|
|
250
250
|
expect(result.error).toContain("Network unreachable");
|
|
251
251
|
}
|
|
252
252
|
});
|
|
253
|
+
|
|
254
|
+
// ── submitVerify ─────────────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
test("submitVerify sends diff, repo, and base_branch", async () => {
|
|
257
|
+
mockFetch.mockImplementation(() =>
|
|
258
|
+
Promise.resolve(jsonResponse({ data: { jobId: "job-abc-123" } })),
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const client = setupClient();
|
|
262
|
+
const result = await client.submitVerify({
|
|
263
|
+
diff: "--- a/file.ts\n+++ b/file.ts\n@@ -1 +1 @@\n-old\n+new",
|
|
264
|
+
repo: "acme/app",
|
|
265
|
+
baseBranch: "main",
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
expect(result.ok).toBe(true);
|
|
269
|
+
if (result.ok) {
|
|
270
|
+
expect(result.value.jobId).toBe("job-abc-123");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const call = mockFetch.mock.calls[0] as unknown[];
|
|
274
|
+
const url = call[0] as string;
|
|
275
|
+
const requestInit = call[1] as RequestInit;
|
|
276
|
+
expect(url).toBe("https://api.test.maina.dev/verify");
|
|
277
|
+
expect(requestInit.method).toBe("POST");
|
|
278
|
+
const body = JSON.parse(requestInit.body as string);
|
|
279
|
+
expect(body.diff).toContain("+new");
|
|
280
|
+
expect(body.repo).toBe("acme/app");
|
|
281
|
+
expect(body.base_branch).toBe("main");
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("submitVerify omits base_branch when not provided", async () => {
|
|
285
|
+
mockFetch.mockImplementation(() =>
|
|
286
|
+
Promise.resolve(jsonResponse({ data: { jobId: "job-def-456" } })),
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
const client = setupClient();
|
|
290
|
+
await client.submitVerify({
|
|
291
|
+
diff: "some diff",
|
|
292
|
+
repo: "acme/app",
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const call = mockFetch.mock.calls[0] as unknown[];
|
|
296
|
+
const requestInit = call[1] as RequestInit;
|
|
297
|
+
const body = JSON.parse(requestInit.body as string);
|
|
298
|
+
expect(body.base_branch).toBeUndefined();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("submitVerify returns error on 401", async () => {
|
|
302
|
+
mockFetch.mockImplementation(() =>
|
|
303
|
+
Promise.resolve(jsonResponse({ error: "Unauthorized" }, 401)),
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
const client = setupClient();
|
|
307
|
+
const result = await client.submitVerify({
|
|
308
|
+
diff: "diff",
|
|
309
|
+
repo: "acme/app",
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
expect(result.ok).toBe(false);
|
|
313
|
+
if (!result.ok) {
|
|
314
|
+
expect(result.error).toBe("Unauthorized");
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// ── getVerifyStatus ──────────────────────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
test("getVerifyStatus returns queued status", async () => {
|
|
321
|
+
mockFetch.mockImplementation(() =>
|
|
322
|
+
Promise.resolve(
|
|
323
|
+
jsonResponse({
|
|
324
|
+
data: { status: "queued", currentStep: "Waiting in queue" },
|
|
325
|
+
}),
|
|
326
|
+
),
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const client = setupClient();
|
|
330
|
+
const result = await client.getVerifyStatus("job-abc-123");
|
|
331
|
+
|
|
332
|
+
expect(result.ok).toBe(true);
|
|
333
|
+
if (result.ok) {
|
|
334
|
+
expect(result.value.status).toBe("queued");
|
|
335
|
+
expect(result.value.currentStep).toBe("Waiting in queue");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const call = mockFetch.mock.calls[0] as unknown[];
|
|
339
|
+
const url = call[0] as string;
|
|
340
|
+
expect(url).toBe("https://api.test.maina.dev/verify/job-abc-123/status");
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("getVerifyStatus returns running status", async () => {
|
|
344
|
+
mockFetch.mockImplementation(() =>
|
|
345
|
+
Promise.resolve(
|
|
346
|
+
jsonResponse({
|
|
347
|
+
data: {
|
|
348
|
+
status: "running",
|
|
349
|
+
currentStep: "Running Biome lint",
|
|
350
|
+
},
|
|
351
|
+
}),
|
|
352
|
+
),
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
const client = setupClient();
|
|
356
|
+
const result = await client.getVerifyStatus("job-abc-123");
|
|
357
|
+
|
|
358
|
+
expect(result.ok).toBe(true);
|
|
359
|
+
if (result.ok) {
|
|
360
|
+
expect(result.value.status).toBe("running");
|
|
361
|
+
expect(result.value.currentStep).toBe("Running Biome lint");
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("getVerifyStatus returns error on 404", async () => {
|
|
366
|
+
mockFetch.mockImplementation(() =>
|
|
367
|
+
Promise.resolve(jsonResponse({ error: "Job not found" }, 404)),
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
const client = setupClient();
|
|
371
|
+
const result = await client.getVerifyStatus("nonexistent");
|
|
372
|
+
|
|
373
|
+
expect(result.ok).toBe(false);
|
|
374
|
+
if (!result.ok) {
|
|
375
|
+
expect(result.error).toBe("Job not found");
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test("getVerifyStatus retries on 500", async () => {
|
|
380
|
+
let attempts = 0;
|
|
381
|
+
mockFetch.mockImplementation(() => {
|
|
382
|
+
attempts++;
|
|
383
|
+
if (attempts === 1) {
|
|
384
|
+
return Promise.resolve(jsonResponse({ error: "Internal error" }, 500));
|
|
385
|
+
}
|
|
386
|
+
return Promise.resolve(
|
|
387
|
+
jsonResponse({
|
|
388
|
+
data: { status: "done", currentStep: "Complete" },
|
|
389
|
+
}),
|
|
390
|
+
);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const client = setupClient({ maxRetries: 2 });
|
|
394
|
+
const result = await client.getVerifyStatus("job-abc-123");
|
|
395
|
+
|
|
396
|
+
expect(result.ok).toBe(true);
|
|
397
|
+
expect(attempts).toBe(2);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// ── getVerifyResult ──────────────────────────────────────────────────────
|
|
401
|
+
|
|
402
|
+
test("getVerifyResult returns passing result", async () => {
|
|
403
|
+
const verifyResult = {
|
|
404
|
+
id: "job-abc-123",
|
|
405
|
+
status: "done",
|
|
406
|
+
passed: true,
|
|
407
|
+
findings: [],
|
|
408
|
+
findingsErrors: 0,
|
|
409
|
+
findingsWarnings: 0,
|
|
410
|
+
proofKey: "proof-xyz-789",
|
|
411
|
+
durationMs: 4523,
|
|
412
|
+
};
|
|
413
|
+
mockFetch.mockImplementation(() =>
|
|
414
|
+
Promise.resolve(jsonResponse({ data: verifyResult })),
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
const client = setupClient();
|
|
418
|
+
const result = await client.getVerifyResult("job-abc-123");
|
|
419
|
+
|
|
420
|
+
expect(result.ok).toBe(true);
|
|
421
|
+
if (result.ok) {
|
|
422
|
+
expect(result.value.id).toBe("job-abc-123");
|
|
423
|
+
expect(result.value.passed).toBe(true);
|
|
424
|
+
expect(result.value.findings).toHaveLength(0);
|
|
425
|
+
expect(result.value.proofKey).toBe("proof-xyz-789");
|
|
426
|
+
expect(result.value.durationMs).toBe(4523);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const call = mockFetch.mock.calls[0] as unknown[];
|
|
430
|
+
const url = call[0] as string;
|
|
431
|
+
expect(url).toBe("https://api.test.maina.dev/verify/job-abc-123");
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test("getVerifyResult returns failing result with findings", async () => {
|
|
435
|
+
const verifyResult = {
|
|
436
|
+
id: "job-fail-456",
|
|
437
|
+
status: "failed",
|
|
438
|
+
passed: false,
|
|
439
|
+
findings: [
|
|
440
|
+
{
|
|
441
|
+
tool: "biome",
|
|
442
|
+
file: "src/index.ts",
|
|
443
|
+
line: 42,
|
|
444
|
+
message: "Unexpected console.log",
|
|
445
|
+
severity: "error",
|
|
446
|
+
ruleId: "no-console",
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
tool: "semgrep",
|
|
450
|
+
file: "src/utils.ts",
|
|
451
|
+
line: 10,
|
|
452
|
+
message: "Potential SQL injection",
|
|
453
|
+
severity: "warning",
|
|
454
|
+
},
|
|
455
|
+
],
|
|
456
|
+
findingsErrors: 1,
|
|
457
|
+
findingsWarnings: 1,
|
|
458
|
+
proofKey: null,
|
|
459
|
+
durationMs: 3200,
|
|
460
|
+
};
|
|
461
|
+
mockFetch.mockImplementation(() =>
|
|
462
|
+
Promise.resolve(jsonResponse({ data: verifyResult })),
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
const client = setupClient();
|
|
466
|
+
const result = await client.getVerifyResult("job-fail-456");
|
|
467
|
+
|
|
468
|
+
expect(result.ok).toBe(true);
|
|
469
|
+
if (result.ok) {
|
|
470
|
+
expect(result.value.passed).toBe(false);
|
|
471
|
+
expect(result.value.findings).toHaveLength(2);
|
|
472
|
+
expect(result.value.findings[0]?.tool).toBe("biome");
|
|
473
|
+
expect(result.value.findings[0]?.severity).toBe("error");
|
|
474
|
+
expect(result.value.findings[0]?.ruleId).toBe("no-console");
|
|
475
|
+
expect(result.value.findings[1]?.tool).toBe("semgrep");
|
|
476
|
+
expect(result.value.findings[1]?.ruleId).toBeUndefined();
|
|
477
|
+
expect(result.value.findingsErrors).toBe(1);
|
|
478
|
+
expect(result.value.findingsWarnings).toBe(1);
|
|
479
|
+
expect(result.value.proofKey).toBeNull();
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test("getVerifyResult returns error on 404", async () => {
|
|
484
|
+
mockFetch.mockImplementation(() =>
|
|
485
|
+
Promise.resolve(jsonResponse({ error: "Job not found" }, 404)),
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
const client = setupClient();
|
|
489
|
+
const result = await client.getVerifyResult("nonexistent");
|
|
490
|
+
|
|
491
|
+
expect(result.ok).toBe(false);
|
|
492
|
+
if (!result.ok) {
|
|
493
|
+
expect(result.error).toBe("Job not found");
|
|
494
|
+
}
|
|
495
|
+
});
|
|
253
496
|
});
|
package/src/cloud/auth.ts
CHANGED
|
@@ -144,7 +144,7 @@ export async function startDeviceFlow(
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
const body = (await response.json()) as {
|
|
147
|
-
data?:
|
|
147
|
+
data?: Record<string, unknown>;
|
|
148
148
|
error?: string;
|
|
149
149
|
};
|
|
150
150
|
if (body.error) {
|
|
@@ -153,7 +153,14 @@ export async function startDeviceFlow(
|
|
|
153
153
|
if (!body.data) {
|
|
154
154
|
return err("Invalid response: missing data");
|
|
155
155
|
}
|
|
156
|
-
|
|
156
|
+
const d = body.data;
|
|
157
|
+
return ok({
|
|
158
|
+
userCode: (d.userCode ?? d.user_code) as string,
|
|
159
|
+
deviceCode: (d.deviceCode ?? d.device_code) as string,
|
|
160
|
+
verificationUri: (d.verificationUri ?? d.verification_uri) as string,
|
|
161
|
+
interval: (d.interval ?? 5) as number,
|
|
162
|
+
expiresIn: (d.expiresIn ?? d.expires_in ?? 900) as number,
|
|
163
|
+
});
|
|
157
164
|
} catch (e) {
|
|
158
165
|
return err(
|
|
159
166
|
`Device flow request failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -188,8 +195,8 @@ export async function pollForToken(
|
|
|
188
195
|
Accept: "application/json",
|
|
189
196
|
},
|
|
190
197
|
body: JSON.stringify({
|
|
191
|
-
deviceCode,
|
|
192
|
-
|
|
198
|
+
device_code: deviceCode,
|
|
199
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
193
200
|
}),
|
|
194
201
|
});
|
|
195
202
|
|
|
@@ -204,7 +211,7 @@ export async function pollForToken(
|
|
|
204
211
|
}
|
|
205
212
|
|
|
206
213
|
const body = (await response.json()) as {
|
|
207
|
-
data?:
|
|
214
|
+
data?: Record<string, unknown>;
|
|
208
215
|
error?: string;
|
|
209
216
|
};
|
|
210
217
|
if (body.error) {
|
|
@@ -217,7 +224,12 @@ export async function pollForToken(
|
|
|
217
224
|
if (!body.data) {
|
|
218
225
|
return err("Invalid token response: missing data");
|
|
219
226
|
}
|
|
220
|
-
|
|
227
|
+
const d = body.data;
|
|
228
|
+
return ok({
|
|
229
|
+
accessToken: (d.accessToken ?? d.access_token) as string,
|
|
230
|
+
refreshToken: (d.refreshToken ?? d.refresh_token) as string | undefined,
|
|
231
|
+
expiresIn: (d.expiresIn ?? d.expires_in ?? 0) as number,
|
|
232
|
+
});
|
|
221
233
|
} catch (e) {
|
|
222
234
|
// Network errors during polling are transient — keep trying
|
|
223
235
|
if (Date.now() >= deadline) {
|
package/src/cloud/client.ts
CHANGED
|
@@ -11,8 +11,11 @@ import type {
|
|
|
11
11
|
CloudConfig,
|
|
12
12
|
CloudFeedbackPayload,
|
|
13
13
|
PromptRecord,
|
|
14
|
+
SubmitVerifyPayload,
|
|
14
15
|
TeamInfo,
|
|
15
16
|
TeamMember,
|
|
17
|
+
VerifyResultResponse,
|
|
18
|
+
VerifyStatusResponse,
|
|
16
19
|
} from "./types";
|
|
17
20
|
|
|
18
21
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
@@ -70,6 +73,17 @@ export interface CloudClient {
|
|
|
70
73
|
postFeedback(
|
|
71
74
|
payload: CloudFeedbackPayload,
|
|
72
75
|
): Promise<Result<{ recorded: boolean }, string>>;
|
|
76
|
+
|
|
77
|
+
/** Submit a diff for cloud verification. */
|
|
78
|
+
submitVerify(
|
|
79
|
+
payload: SubmitVerifyPayload,
|
|
80
|
+
): Promise<Result<{ jobId: string }, string>>;
|
|
81
|
+
|
|
82
|
+
/** Poll the status of a verification job. */
|
|
83
|
+
getVerifyStatus(jobId: string): Promise<Result<VerifyStatusResponse, string>>;
|
|
84
|
+
|
|
85
|
+
/** Retrieve the full result of a completed verification job. */
|
|
86
|
+
getVerifyResult(jobId: string): Promise<Result<VerifyResultResponse, string>>;
|
|
73
87
|
}
|
|
74
88
|
|
|
75
89
|
/**
|
|
@@ -186,5 +200,51 @@ export function createCloudClient(config: CloudConfig): CloudClient {
|
|
|
186
200
|
|
|
187
201
|
postFeedback: (payload) =>
|
|
188
202
|
request<{ recorded: boolean }>("POST", "/feedback", payload),
|
|
203
|
+
|
|
204
|
+
submitVerify: async (payload) => {
|
|
205
|
+
// biome-ignore lint/suspicious/noExplicitAny: snake_case API mapping
|
|
206
|
+
const result = await request<any>("POST", "/verify", {
|
|
207
|
+
diff: payload.diff,
|
|
208
|
+
repo: payload.repo,
|
|
209
|
+
base_branch: payload.baseBranch,
|
|
210
|
+
});
|
|
211
|
+
if (!result.ok) return result;
|
|
212
|
+
const d = result.value;
|
|
213
|
+
return ok({ jobId: d.jobId ?? d.job_id });
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
getVerifyStatus: async (jobId) => {
|
|
217
|
+
// biome-ignore lint/suspicious/noExplicitAny: snake_case API mapping
|
|
218
|
+
const result = await request<any>("GET", `/verify/${jobId}/status`);
|
|
219
|
+
if (!result.ok) return result;
|
|
220
|
+
const d = result.value;
|
|
221
|
+
return ok({
|
|
222
|
+
status: d.status,
|
|
223
|
+
currentStep: d.currentStep ?? d.current_step ?? d.step ?? "",
|
|
224
|
+
});
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
getVerifyResult: async (jobId) => {
|
|
228
|
+
// biome-ignore lint/suspicious/noExplicitAny: snake_case API mapping
|
|
229
|
+
const result = await request<any>("GET", `/verify/${jobId}`);
|
|
230
|
+
if (!result.ok) return result;
|
|
231
|
+
const d = result.value;
|
|
232
|
+
const items = d.findings?.items ?? d.findings ?? [];
|
|
233
|
+
return ok({
|
|
234
|
+
id: d.id,
|
|
235
|
+
status: d.status,
|
|
236
|
+
passed: d.passed,
|
|
237
|
+
findings: items,
|
|
238
|
+
findingsErrors:
|
|
239
|
+
d.findingsErrors ?? d.findings_errors ?? d.findings?.errors ?? 0,
|
|
240
|
+
findingsWarnings:
|
|
241
|
+
d.findingsWarnings ??
|
|
242
|
+
d.findings_warnings ??
|
|
243
|
+
d.findings?.warnings ??
|
|
244
|
+
0,
|
|
245
|
+
proofKey: d.proofKey ?? d.proof_key ?? d.proof_url ?? null,
|
|
246
|
+
durationMs: d.durationMs ?? d.duration_ms ?? 0,
|
|
247
|
+
});
|
|
248
|
+
},
|
|
189
249
|
};
|
|
190
250
|
}
|
package/src/cloud/types.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// ── Configuration ───────────────────────────────────────────────────────────
|
|
9
9
|
|
|
10
10
|
export interface CloudConfig {
|
|
11
|
-
/** Base URL of the maina cloud API (e.g. "https://api.
|
|
11
|
+
/** Base URL of the maina cloud API (e.g. "https://api.mainahq.com"). */
|
|
12
12
|
baseUrl: string;
|
|
13
13
|
/** Bearer token for authenticated requests. */
|
|
14
14
|
token?: string;
|
|
@@ -90,6 +90,58 @@ export interface ApiResponse<T> {
|
|
|
90
90
|
meta?: Record<string, unknown>;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
// ── Verify ─────────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
export interface SubmitVerifyPayload {
|
|
96
|
+
/** Diff content to verify. */
|
|
97
|
+
diff: string;
|
|
98
|
+
/** Repository identifier (e.g. "owner/repo"). */
|
|
99
|
+
repo: string;
|
|
100
|
+
/** Base branch to compare against. */
|
|
101
|
+
baseBranch?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface VerifyStatusResponse {
|
|
105
|
+
/** Current job status. */
|
|
106
|
+
status: "queued" | "running" | "done" | "failed";
|
|
107
|
+
/** Human-readable description of the current step. */
|
|
108
|
+
currentStep: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface VerifyFinding {
|
|
112
|
+
/** Tool that produced the finding (e.g. "biome", "semgrep"). */
|
|
113
|
+
tool: string;
|
|
114
|
+
/** File path relative to repository root. */
|
|
115
|
+
file: string;
|
|
116
|
+
/** Line number in the file. */
|
|
117
|
+
line: number;
|
|
118
|
+
/** Finding description. */
|
|
119
|
+
message: string;
|
|
120
|
+
/** Severity level. */
|
|
121
|
+
severity: "error" | "warning" | "info";
|
|
122
|
+
/** Optional rule identifier. */
|
|
123
|
+
ruleId?: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface VerifyResultResponse {
|
|
127
|
+
/** Job identifier. */
|
|
128
|
+
id: string;
|
|
129
|
+
/** Final job status. */
|
|
130
|
+
status: "done" | "failed";
|
|
131
|
+
/** Whether all checks passed. */
|
|
132
|
+
passed: boolean;
|
|
133
|
+
/** Array of individual findings from verification tools. */
|
|
134
|
+
findings: VerifyFinding[];
|
|
135
|
+
/** Count of error-level findings. */
|
|
136
|
+
findingsErrors: number;
|
|
137
|
+
/** Count of warning-level findings. */
|
|
138
|
+
findingsWarnings: number;
|
|
139
|
+
/** Proof key for passing verification (null when failed). */
|
|
140
|
+
proofKey: string | null;
|
|
141
|
+
/** Total verification duration in milliseconds. */
|
|
142
|
+
durationMs: number;
|
|
143
|
+
}
|
|
144
|
+
|
|
93
145
|
// ── Feedback ────────────────────────────────────────────────────────────────
|
|
94
146
|
|
|
95
147
|
export interface CloudFeedbackPayload {
|
package/src/index.ts
CHANGED
|
@@ -66,9 +66,13 @@ export type {
|
|
|
66
66
|
CloudFeedbackPayload,
|
|
67
67
|
DeviceCodeResponse,
|
|
68
68
|
PromptRecord,
|
|
69
|
+
SubmitVerifyPayload,
|
|
69
70
|
TeamInfo,
|
|
70
71
|
TeamMember,
|
|
71
72
|
TokenResponse,
|
|
73
|
+
VerifyFinding,
|
|
74
|
+
VerifyResultResponse,
|
|
75
|
+
VerifyStatusResponse,
|
|
72
76
|
} from "./cloud/types";
|
|
73
77
|
// Config
|
|
74
78
|
export { getApiKey, isHostMode, shouldDelegateToHost } from "./config/index";
|
|
@@ -306,16 +310,12 @@ export {
|
|
|
306
310
|
runCoverage,
|
|
307
311
|
} from "./verify/coverage";
|
|
308
312
|
export {
|
|
309
|
-
type DetectedTool,
|
|
310
313
|
detectTool,
|
|
311
314
|
detectTools,
|
|
312
315
|
isToolAvailable,
|
|
313
316
|
TOOL_REGISTRY,
|
|
314
|
-
type ToolName,
|
|
315
317
|
} from "./verify/detect";
|
|
316
318
|
export {
|
|
317
|
-
type DiffFilterResult,
|
|
318
|
-
type Finding,
|
|
319
319
|
filterByDiff,
|
|
320
320
|
filterByDiffWithMap,
|
|
321
321
|
parseChangedLines,
|
|
@@ -343,12 +343,7 @@ export {
|
|
|
343
343
|
runMutation,
|
|
344
344
|
} from "./verify/mutation";
|
|
345
345
|
// Verify — Pipeline
|
|
346
|
-
export {
|
|
347
|
-
type PipelineOptions,
|
|
348
|
-
type PipelineResult,
|
|
349
|
-
runPipeline,
|
|
350
|
-
type ToolReport,
|
|
351
|
-
} from "./verify/pipeline";
|
|
346
|
+
export { runPipeline } from "./verify/pipeline";
|
|
352
347
|
// Verify — Proof
|
|
353
348
|
export {
|
|
354
349
|
formatVerificationProof,
|
|
@@ -376,12 +371,22 @@ export {
|
|
|
376
371
|
} from "./verify/sonar";
|
|
377
372
|
export {
|
|
378
373
|
parseBiomeOutput,
|
|
379
|
-
type SyntaxDiagnostic,
|
|
380
|
-
type SyntaxGuardResult,
|
|
381
374
|
syntaxGuard,
|
|
382
375
|
} from "./verify/syntax-guard";
|
|
383
376
|
// Verify — Typecheck + Consistency (built-in checks)
|
|
384
377
|
export { runTypecheck, type TypecheckResult } from "./verify/typecheck";
|
|
378
|
+
// Verify — Public Type Surface (consolidated for external consumers like maina-cloud)
|
|
379
|
+
export type {
|
|
380
|
+
DetectedTool,
|
|
381
|
+
DiffFilterResult,
|
|
382
|
+
Finding,
|
|
383
|
+
PipelineOptions,
|
|
384
|
+
PipelineResult,
|
|
385
|
+
SyntaxDiagnostic,
|
|
386
|
+
SyntaxGuardResult,
|
|
387
|
+
ToolName,
|
|
388
|
+
ToolReport,
|
|
389
|
+
} from "./verify/types";
|
|
385
390
|
// Verify — Visual
|
|
386
391
|
export {
|
|
387
392
|
captureScreenshot,
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type {
|
|
3
|
+
DetectedTool,
|
|
4
|
+
Finding,
|
|
5
|
+
PipelineOptions,
|
|
6
|
+
PipelineResult,
|
|
7
|
+
ToolReport,
|
|
8
|
+
} from "../types";
|
|
9
|
+
|
|
10
|
+
// ─── Verify Type Exports ────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
describe("verify/types re-exports", () => {
|
|
13
|
+
test("Finding type has expected shape", () => {
|
|
14
|
+
const finding: Finding = {
|
|
15
|
+
tool: "semgrep",
|
|
16
|
+
file: "src/index.ts",
|
|
17
|
+
line: 42,
|
|
18
|
+
message: "unused variable",
|
|
19
|
+
severity: "warning",
|
|
20
|
+
};
|
|
21
|
+
expect(finding.tool).toBe("semgrep");
|
|
22
|
+
expect(finding.file).toBe("src/index.ts");
|
|
23
|
+
expect(finding.line).toBe(42);
|
|
24
|
+
expect(finding.message).toBe("unused variable");
|
|
25
|
+
expect(finding.severity).toBe("warning");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("Finding type supports optional fields", () => {
|
|
29
|
+
const finding: Finding = {
|
|
30
|
+
tool: "eslint",
|
|
31
|
+
file: "app.ts",
|
|
32
|
+
line: 10,
|
|
33
|
+
column: 5,
|
|
34
|
+
message: "no-unused-vars",
|
|
35
|
+
severity: "error",
|
|
36
|
+
ruleId: "no-unused-vars",
|
|
37
|
+
};
|
|
38
|
+
expect(finding.column).toBe(5);
|
|
39
|
+
expect(finding.ruleId).toBe("no-unused-vars");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("ToolReport type has expected shape", () => {
|
|
43
|
+
const report: ToolReport = {
|
|
44
|
+
tool: "trivy",
|
|
45
|
+
findings: [],
|
|
46
|
+
skipped: false,
|
|
47
|
+
duration: 123,
|
|
48
|
+
};
|
|
49
|
+
expect(report.tool).toBe("trivy");
|
|
50
|
+
expect(report.findings).toEqual([]);
|
|
51
|
+
expect(report.skipped).toBe(false);
|
|
52
|
+
expect(report.duration).toBe(123);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("PipelineResult type has expected shape", () => {
|
|
56
|
+
const result: PipelineResult = {
|
|
57
|
+
passed: true,
|
|
58
|
+
syntaxPassed: true,
|
|
59
|
+
tools: [],
|
|
60
|
+
findings: [],
|
|
61
|
+
hiddenCount: 0,
|
|
62
|
+
detectedTools: [],
|
|
63
|
+
duration: 500,
|
|
64
|
+
cacheHits: 2,
|
|
65
|
+
cacheMisses: 1,
|
|
66
|
+
};
|
|
67
|
+
expect(result.passed).toBe(true);
|
|
68
|
+
expect(result.syntaxPassed).toBe(true);
|
|
69
|
+
expect(result.tools).toEqual([]);
|
|
70
|
+
expect(result.findings).toEqual([]);
|
|
71
|
+
expect(result.hiddenCount).toBe(0);
|
|
72
|
+
expect(result.detectedTools).toEqual([]);
|
|
73
|
+
expect(result.duration).toBe(500);
|
|
74
|
+
expect(result.cacheHits).toBe(2);
|
|
75
|
+
expect(result.cacheMisses).toBe(1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("PipelineOptions type has expected shape", () => {
|
|
79
|
+
const opts: PipelineOptions = {
|
|
80
|
+
files: ["src/app.ts"],
|
|
81
|
+
baseBranch: "main",
|
|
82
|
+
diffOnly: true,
|
|
83
|
+
deep: false,
|
|
84
|
+
cwd: "/tmp",
|
|
85
|
+
mainaDir: ".maina",
|
|
86
|
+
languages: ["typescript"],
|
|
87
|
+
};
|
|
88
|
+
expect(opts.files).toEqual(["src/app.ts"]);
|
|
89
|
+
expect(opts.baseBranch).toBe("main");
|
|
90
|
+
expect(opts.diffOnly).toBe(true);
|
|
91
|
+
expect(opts.deep).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("DetectedTool type has expected shape", () => {
|
|
95
|
+
const tool: DetectedTool = {
|
|
96
|
+
name: "biome",
|
|
97
|
+
command: "biome",
|
|
98
|
+
version: "1.5.0",
|
|
99
|
+
available: true,
|
|
100
|
+
};
|
|
101
|
+
expect(tool.name).toBe("biome");
|
|
102
|
+
expect(tool.command).toBe("biome");
|
|
103
|
+
expect(tool.version).toBe("1.5.0");
|
|
104
|
+
expect(tool.available).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("DetectedTool supports null version", () => {
|
|
108
|
+
const tool: DetectedTool = {
|
|
109
|
+
name: "semgrep",
|
|
110
|
+
command: "semgrep",
|
|
111
|
+
version: null,
|
|
112
|
+
available: false,
|
|
113
|
+
};
|
|
114
|
+
expect(tool.version).toBeNull();
|
|
115
|
+
expect(tool.available).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ─── Verify types re-exported from @mainahq/core index ──────────────────────
|
|
120
|
+
|
|
121
|
+
describe("verify types from core index", () => {
|
|
122
|
+
test("types are importable from core index", async () => {
|
|
123
|
+
const coreIndex = await import("../../index");
|
|
124
|
+
// The module should export these — we verify by checking the module loaded
|
|
125
|
+
// Type-only exports don't appear at runtime, but the module must resolve
|
|
126
|
+
expect(coreIndex).toBeDefined();
|
|
127
|
+
expect(typeof coreIndex.VERSION).toBe("string");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verify Engine — Public Type Exports.
|
|
3
|
+
*
|
|
4
|
+
* Consolidated re-exports of all verify types for the public API surface.
|
|
5
|
+
* Consumers (e.g. maina-cloud) import from here or from @mainahq/core.
|
|
6
|
+
* Types are NOT duplicated — each re-exports from its source module.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// DetectedTool, ToolName from detect
|
|
10
|
+
export type { DetectedTool, ToolName } from "./detect";
|
|
11
|
+
// Finding + DiffFilterResult from diff-filter
|
|
12
|
+
export type { DiffFilterResult, Finding } from "./diff-filter";
|
|
13
|
+
// PipelineResult, PipelineOptions, ToolReport from pipeline
|
|
14
|
+
export type { PipelineOptions, PipelineResult, ToolReport } from "./pipeline";
|
|
15
|
+
|
|
16
|
+
// SyntaxDiagnostic, SyntaxGuardResult from syntax-guard (used in PipelineResult)
|
|
17
|
+
export type { SyntaxDiagnostic, SyntaxGuardResult } from "./syntax-guard";
|