@attest-ai/core 1.0.0-rc.2 → 1.0.0-rc.3
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/dist/cjs/index.cjs +1001 -120
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +313 -5
- package/dist/cjs/proto/index.cjs +7 -0
- package/dist/cjs/proto/index.cjs.map +1 -1
- package/dist/cjs/proto/index.d.cts +18 -2
- package/dist/esm/cli.d.ts +6 -1
- package/dist/esm/cli.js +310 -20
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/index.d.ts +313 -5
- package/dist/esm/index.js +977 -120
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/proto/index.d.ts +18 -2
- package/dist/esm/proto/index.js +5 -0
- package/dist/esm/proto/index.js.map +1 -1
- package/package.json +3 -3
package/dist/esm/cli.js
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
5
|
-
import * as
|
|
5
|
+
import * as fs4 from "fs";
|
|
6
6
|
import * as os2 from "os";
|
|
7
|
-
import
|
|
7
|
+
import * as path4 from "path";
|
|
8
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8
9
|
|
|
9
10
|
// src/version.ts
|
|
10
|
-
var VERSION = "1.0.0-rc.
|
|
11
|
-
var ENGINE_VERSION = "1.0.0-rc.
|
|
11
|
+
var VERSION = "1.0.0-rc.3";
|
|
12
|
+
var ENGINE_VERSION = "1.0.0-rc.3";
|
|
12
13
|
|
|
13
14
|
// src/engine-manager.ts
|
|
14
15
|
import { spawn, spawnSync } from "child_process";
|
|
@@ -76,7 +77,7 @@ function httpsGet(url, redirectCount = 0) {
|
|
|
76
77
|
new Error(`Too many redirects (>${MAX_REDIRECTS}) following ${url}`)
|
|
77
78
|
);
|
|
78
79
|
}
|
|
79
|
-
return new Promise((
|
|
80
|
+
return new Promise((resolve2, reject) => {
|
|
80
81
|
const client = url.startsWith("https:") ? https : http;
|
|
81
82
|
client.get(url, (res) => {
|
|
82
83
|
const status = res.statusCode ?? 0;
|
|
@@ -87,7 +88,7 @@ function httpsGet(url, redirectCount = 0) {
|
|
|
87
88
|
return;
|
|
88
89
|
}
|
|
89
90
|
res.resume();
|
|
90
|
-
|
|
91
|
+
resolve2(httpsGet(location, redirectCount + 1));
|
|
91
92
|
return;
|
|
92
93
|
}
|
|
93
94
|
if (status < 200 || status >= 300) {
|
|
@@ -97,7 +98,7 @@ function httpsGet(url, redirectCount = 0) {
|
|
|
97
98
|
}
|
|
98
99
|
const chunks = [];
|
|
99
100
|
res.on("data", (chunk) => chunks.push(chunk));
|
|
100
|
-
res.on("end", () =>
|
|
101
|
+
res.on("end", () => resolve2(Buffer.concat(chunks)));
|
|
101
102
|
res.on("error", reject);
|
|
102
103
|
}).on("error", reject);
|
|
103
104
|
});
|
|
@@ -228,8 +229,8 @@ Install options:
|
|
|
228
229
|
}
|
|
229
230
|
return [await downloadEngine(), "download"];
|
|
230
231
|
}
|
|
231
|
-
async function resolveEngine(explicitPath,
|
|
232
|
-
const [enginePath, source] = await locateEngine(explicitPath,
|
|
232
|
+
async function resolveEngine(explicitPath, options2 = {}) {
|
|
233
|
+
const [enginePath, source] = await locateEngine(explicitPath, options2.allowDownload ?? true);
|
|
233
234
|
const found = engineVersion(enginePath);
|
|
234
235
|
if (found !== ENGINE_VERSION) {
|
|
235
236
|
throw new EngineVersionMismatchError(enginePath, source, found, ENGINE_VERSION);
|
|
@@ -288,8 +289,8 @@ not_evaluated: warn
|
|
|
288
289
|
`;
|
|
289
290
|
var GITHUB_WORKFLOW_TEMPLATE = `# Written by \`attest init --ci github\`.
|
|
290
291
|
# The test step records its outcome and continues so the report is always uploaded,
|
|
291
|
-
# the policy evaluated and the pull request
|
|
292
|
-
# when the tests failed. A policy warning (exit 2) does not fail the job.
|
|
292
|
+
# the JUnit file written, the policy evaluated and the pull request summarised; the last
|
|
293
|
+
# step fails the job when the tests failed. A policy warning (exit 2) does not fail the job.
|
|
293
294
|
name: attest
|
|
294
295
|
|
|
295
296
|
on:
|
|
@@ -299,11 +300,13 @@ on:
|
|
|
299
300
|
|
|
300
301
|
permissions:
|
|
301
302
|
contents: read
|
|
302
|
-
pull-requests: write
|
|
303
303
|
|
|
304
304
|
jobs:
|
|
305
305
|
attest:
|
|
306
306
|
runs-on: ubuntu-latest
|
|
307
|
+
permissions:
|
|
308
|
+
contents: read
|
|
309
|
+
pull-requests: write
|
|
307
310
|
steps:
|
|
308
311
|
- uses: actions/checkout@v6
|
|
309
312
|
with:
|
|
@@ -317,12 +320,17 @@ jobs:
|
|
|
317
320
|
id: tests
|
|
318
321
|
continue-on-error: true
|
|
319
322
|
run: npx vitest run ${REPORT_ARGS}
|
|
323
|
+
- name: Write the JUnit report
|
|
324
|
+
if: always()
|
|
325
|
+
run: npx attest report junit --report attest-report.json --out attest-junit.xml
|
|
320
326
|
- name: Upload the Attest report
|
|
321
327
|
if: always()
|
|
322
328
|
uses: actions/upload-artifact@v7
|
|
323
329
|
with:
|
|
324
330
|
name: attest-report
|
|
325
|
-
path:
|
|
331
|
+
path: |
|
|
332
|
+
attest-report.json
|
|
333
|
+
attest-junit.xml
|
|
326
334
|
if-no-files-found: error
|
|
327
335
|
- name: Evaluate the merge policy
|
|
328
336
|
if: always()
|
|
@@ -336,8 +344,9 @@ jobs:
|
|
|
336
344
|
fi
|
|
337
345
|
exit "$code"
|
|
338
346
|
- name: Annotate the pull request
|
|
339
|
-
#
|
|
340
|
-
#
|
|
347
|
+
# Posts one summary comment (updated on every run) and annotates failures at their
|
|
348
|
+
# file and line. Fork pull requests get a read-only token that cannot comment; the
|
|
349
|
+
# last step still fails the job when tests failed.
|
|
341
350
|
if: >-
|
|
342
351
|
always() && github.event_name == 'pull_request' &&
|
|
343
352
|
github.event.pull_request.head.repo.full_name == github.repository
|
|
@@ -361,10 +370,10 @@ function write(file, content) {
|
|
|
361
370
|
process.stdout.write(`Created: ${file}
|
|
362
371
|
`);
|
|
363
372
|
}
|
|
364
|
-
function scaffoldProject(targetDir,
|
|
373
|
+
function scaffoldProject(targetDir, options2 = {}) {
|
|
365
374
|
write(path2.join(targetDir, "tests", "my-agent.test.ts"), SAMPLE_TEST_TEMPLATE);
|
|
366
375
|
write(path2.join(targetDir, "attest.policy.yaml"), POLICY_TEMPLATE);
|
|
367
|
-
if (
|
|
376
|
+
if (options2.ci === "github") {
|
|
368
377
|
write(path2.join(targetDir, ".github", "workflows", "attest.yml"), GITHUB_WORKFLOW_TEMPLATE);
|
|
369
378
|
}
|
|
370
379
|
process.stdout.write(
|
|
@@ -374,9 +383,232 @@ Next: run \`npx vitest run\`. Then change the line marked ${DEMO_MARKER} in test
|
|
|
374
383
|
);
|
|
375
384
|
}
|
|
376
385
|
|
|
386
|
+
// src/capture.ts
|
|
387
|
+
import { spawn as spawn2 } from "child_process";
|
|
388
|
+
import * as fs3 from "fs";
|
|
389
|
+
import * as path3 from "path";
|
|
390
|
+
import { fileURLToPath } from "url";
|
|
391
|
+
var FIXTURE_VERSION = 1;
|
|
392
|
+
var DEFAULT_FIXTURE_DIR = path3.join("tests", "attest", "fixtures");
|
|
393
|
+
var CaptureError = class extends Error {
|
|
394
|
+
/** The fixture written, when the engine wrote one. */
|
|
395
|
+
fixture;
|
|
396
|
+
constructor(message, fixture = null) {
|
|
397
|
+
super(message);
|
|
398
|
+
this.name = "CaptureError";
|
|
399
|
+
this.fixture = fixture;
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
function toPath(p) {
|
|
403
|
+
return p instanceof URL ? fileURLToPath(p) : p;
|
|
404
|
+
}
|
|
405
|
+
function loadFixture(file) {
|
|
406
|
+
const p = toPath(file);
|
|
407
|
+
const doc = JSON.parse(fs3.readFileSync(p, "utf8"));
|
|
408
|
+
if (!isObject(doc) || doc["fixture_version"] !== FIXTURE_VERSION) {
|
|
409
|
+
throw new CaptureError(`${p}: not a fixture_version ${FIXTURE_VERSION} capture fixture`);
|
|
410
|
+
}
|
|
411
|
+
validate(p, doc);
|
|
412
|
+
return doc;
|
|
413
|
+
}
|
|
414
|
+
var NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
415
|
+
function isObject(v) {
|
|
416
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
417
|
+
}
|
|
418
|
+
function kindOf(v) {
|
|
419
|
+
if (Array.isArray(v)) return "array";
|
|
420
|
+
if (isObject(v)) return "object";
|
|
421
|
+
if (typeof v === "string" || typeof v === "boolean") return typeof v;
|
|
422
|
+
return "other";
|
|
423
|
+
}
|
|
424
|
+
function validate(p, doc) {
|
|
425
|
+
const need = (where, obj, fields) => {
|
|
426
|
+
if (!isObject(obj)) throw new CaptureError(`${p}: ${where} is not an object`);
|
|
427
|
+
for (const [key, kind] of Object.entries(fields)) {
|
|
428
|
+
if (kindOf(obj[key]) !== kind) throw new CaptureError(`${p}: ${where}.${key} is missing or not a ${kind}`);
|
|
429
|
+
}
|
|
430
|
+
return obj;
|
|
431
|
+
};
|
|
432
|
+
need("fixture", doc, { name: "string", source: "object", trace: "object", exchanges: "array", contracts: "array", verification: "object" });
|
|
433
|
+
if (!("world" in doc)) throw new CaptureError(`${p}: fixture.world is missing`);
|
|
434
|
+
if (!NAME_PATTERN.test(doc["name"])) {
|
|
435
|
+
throw new CaptureError(`${p}: fixture name ${JSON.stringify(doc["name"])} is not a valid fixture name`);
|
|
436
|
+
}
|
|
437
|
+
const v = need("fixture.verification", doc["verification"], {
|
|
438
|
+
verified: "boolean",
|
|
439
|
+
expect_fail: "boolean",
|
|
440
|
+
results: "array",
|
|
441
|
+
expected_failures: "array"
|
|
442
|
+
});
|
|
443
|
+
doc["contracts"].forEach(
|
|
444
|
+
(c, i) => need(`fixture.contracts[${i}]`, c, { id: "string", kind: "string", description: "string", type: "string", spec: "object" })
|
|
445
|
+
);
|
|
446
|
+
v["results"].forEach(
|
|
447
|
+
(r, i) => need(`fixture.verification.results[${i}]`, r, { contract: "string", status: "string", explanation: "string" })
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
function objectKey(k) {
|
|
451
|
+
if (k === "__proto__") return `[${JSON.stringify(k)}]`;
|
|
452
|
+
return /^[A-Za-z_$][\w$]*$/.test(k) ? k : JSON.stringify(k);
|
|
453
|
+
}
|
|
454
|
+
function comment(text) {
|
|
455
|
+
return text.replace(/[\p{Cc}\p{Zl}\p{Zp}]/gu, " ");
|
|
456
|
+
}
|
|
457
|
+
function ts(value) {
|
|
458
|
+
if (value === null || typeof value === "boolean" || typeof value === "number") {
|
|
459
|
+
return JSON.stringify(value);
|
|
460
|
+
}
|
|
461
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
462
|
+
if (Array.isArray(value)) return `[${value.map(ts).join(", ")}]`;
|
|
463
|
+
if (typeof value === "object") {
|
|
464
|
+
const entries = Object.entries(value).map(
|
|
465
|
+
([k, v]) => `${objectKey(k)}: ${ts(v)}`
|
|
466
|
+
);
|
|
467
|
+
return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }`;
|
|
468
|
+
}
|
|
469
|
+
throw new Error(`value ${String(value)} has no literal`);
|
|
470
|
+
}
|
|
471
|
+
function str(spec, key) {
|
|
472
|
+
const v = spec[key];
|
|
473
|
+
if (typeof v !== "string") throw new Error(`missing ${key}`);
|
|
474
|
+
return v;
|
|
475
|
+
}
|
|
476
|
+
function options(entries) {
|
|
477
|
+
const set = entries.filter(([, v]) => v !== void 0 && v !== null && v !== false);
|
|
478
|
+
return set.length === 0 ? "" : `{ ${set.map(([k, v]) => `${k}: ${ts(v)}`).join(", ")} }`;
|
|
479
|
+
}
|
|
480
|
+
function call(method, args, opts) {
|
|
481
|
+
return `.${method}(${[...args, ...opts ? [opts] : []].join(", ")})`;
|
|
482
|
+
}
|
|
483
|
+
function dslCall(contract) {
|
|
484
|
+
const s = contract.spec;
|
|
485
|
+
const soft = s["soft"] === true ? true : void 0;
|
|
486
|
+
try {
|
|
487
|
+
if (contract.type === "trace") {
|
|
488
|
+
const check = s["check"];
|
|
489
|
+
if (check === "trajectory" && s["mode"] !== "partial_order") {
|
|
490
|
+
const mode = s["mode"] === "strict" || s["mode"] === void 0 ? void 0 : s["mode"];
|
|
491
|
+
if (!Array.isArray(s["tools"])) throw new Error("missing tools");
|
|
492
|
+
return call("trajectory", [ts(s["tools"])], options([["mode", mode], ["soft", soft]]));
|
|
493
|
+
}
|
|
494
|
+
if (check === "tool_order") {
|
|
495
|
+
return call("toolCalledBefore", [ts(str(s, "before")), ts(str(s, "after"))], options([["soft", soft]]));
|
|
496
|
+
}
|
|
497
|
+
if (check === "tool_cardinality") {
|
|
498
|
+
return call(
|
|
499
|
+
"toolCardinality",
|
|
500
|
+
[ts(str(s, "tool"))],
|
|
501
|
+
options([
|
|
502
|
+
["per", s["per"]],
|
|
503
|
+
["atLeast", s["min"]],
|
|
504
|
+
["atMost", s["max"]],
|
|
505
|
+
["soft", soft]
|
|
506
|
+
])
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
if (contract.type === "schema" && s["target"] === "output.structured") {
|
|
511
|
+
if (typeof s["schema"] !== "object" || s["schema"] === null) throw new Error("missing schema");
|
|
512
|
+
return call("outputMatchesSchema", [ts(s["schema"])], "");
|
|
513
|
+
}
|
|
514
|
+
if (contract.type === "constraint" && s["field"] === "metadata.cost_usd" && s["operator"] === "lte") {
|
|
515
|
+
if (typeof s["value"] !== "number") throw new Error("missing value");
|
|
516
|
+
return call("costUnder", [ts(s["value"])], options([["soft", soft]]));
|
|
517
|
+
}
|
|
518
|
+
} catch (err) {
|
|
519
|
+
throw new Error(`contract ${contract.id}: malformed spec (${err.message})`);
|
|
520
|
+
}
|
|
521
|
+
throw new Error(`contract ${contract.id}: no TypeScript DSL call for ${contract.type} spec`);
|
|
522
|
+
}
|
|
523
|
+
function testPathFor(fixturePath) {
|
|
524
|
+
const name = path3.basename(fixturePath).replace(/\.attest\.json$/, "");
|
|
525
|
+
return path3.join(path3.dirname(path3.dirname(fixturePath)), `${name}.test.ts`);
|
|
526
|
+
}
|
|
527
|
+
function renderVitest(fixture, fixturePath, testPath) {
|
|
528
|
+
const calls = [];
|
|
529
|
+
const skipped = [];
|
|
530
|
+
for (const c of fixture.contracts) {
|
|
531
|
+
try {
|
|
532
|
+
calls.push([c, dslCall(c)]);
|
|
533
|
+
} catch (err) {
|
|
534
|
+
skipped.push(err.message);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
const rel = path3.relative(path3.dirname(testPath), fixturePath).split(path3.sep).join("/");
|
|
538
|
+
const status = new Map(fixture.verification.results.map((r) => [r.contract, r.status]));
|
|
539
|
+
const expectFail = fixture.verification.expect_fail;
|
|
540
|
+
const lines = [
|
|
541
|
+
`// Contracts drafted by \`attest capture\` from fixture ${comment(ts(fixture.name))}.`,
|
|
542
|
+
"//",
|
|
543
|
+
"// Review and edit before committing: they check the captured trace without running",
|
|
544
|
+
"// the agent. Re-capturing does not overwrite this file.",
|
|
545
|
+
`import { attestExpect, loadFixture, type ExpectChain } from "@attest-ai/core";`,
|
|
546
|
+
`import { ${expectFail ? "inspect" : "evaluate"} } from "@attest-ai/vitest";`,
|
|
547
|
+
`import { describe, ${expectFail ? "expect, " : ""}it } from "vitest";`,
|
|
548
|
+
"",
|
|
549
|
+
`const FIXTURE = loadFixture(new URL(${ts(rel.startsWith(".") ? rel : `./${rel}`)}, import.meta.url));`
|
|
550
|
+
];
|
|
551
|
+
if (skipped.length > 0) {
|
|
552
|
+
lines.push("", "// Drafted contracts without a TypeScript DSL call (left out):");
|
|
553
|
+
for (const s of skipped) lines.push(`// - ${comment(s)}`);
|
|
554
|
+
}
|
|
555
|
+
lines.push("", "function contracts(): ExpectChain {", " return attestExpect(FIXTURE.trace)");
|
|
556
|
+
for (const [c, dsl] of calls) {
|
|
557
|
+
lines.push(` // ${comment(`${c.id}: ${c.description}`)}`, ` ${dsl}`);
|
|
558
|
+
}
|
|
559
|
+
lines[lines.length - 1] += ";";
|
|
560
|
+
lines.push("}");
|
|
561
|
+
if (expectFail) {
|
|
562
|
+
lines.push("", "// Status of each contract on the captured incident, in contracts() order.");
|
|
563
|
+
lines.push("const EXPECTED: Array<[string, string]> = [");
|
|
564
|
+
for (const [c] of calls) lines.push(` [${ts(c.id)}, ${ts(status.get(c.id) ?? "pass")}],`);
|
|
565
|
+
lines.push(
|
|
566
|
+
"];",
|
|
567
|
+
"",
|
|
568
|
+
`describe(${ts(fixture.name)}, () => {`,
|
|
569
|
+
` it("reproduces the incident", async () => {`,
|
|
570
|
+
" const result = await inspect(contracts());",
|
|
571
|
+
"",
|
|
572
|
+
" expect(result.assertionResults.map((row) => row.status)).toEqual(EXPECTED.map(([, s]) => s));",
|
|
573
|
+
" });",
|
|
574
|
+
"});"
|
|
575
|
+
);
|
|
576
|
+
} else {
|
|
577
|
+
lines.push(
|
|
578
|
+
"",
|
|
579
|
+
`describe(${ts(fixture.name)}, () => {`,
|
|
580
|
+
` it("keeps its drafted contracts", async () => {`,
|
|
581
|
+
" await evaluate(contracts());",
|
|
582
|
+
" });",
|
|
583
|
+
"});"
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
return `${lines.join("\n")}
|
|
587
|
+
`;
|
|
588
|
+
}
|
|
589
|
+
function emitVitest(fixturePath) {
|
|
590
|
+
const testPath = testPathFor(fixturePath);
|
|
591
|
+
const existing = fs3.lstatSync(testPath, { throwIfNoEntry: false });
|
|
592
|
+
if (existing?.isSymbolicLink()) {
|
|
593
|
+
throw new CaptureError(`${testPath} is a symbolic link; refusing to draft a test through it`);
|
|
594
|
+
}
|
|
595
|
+
if (existing !== void 0) return null;
|
|
596
|
+
const source = renderVitest(loadFixture(fixturePath), fixturePath, testPath);
|
|
597
|
+
fs3.mkdirSync(path3.dirname(testPath), { recursive: true });
|
|
598
|
+
try {
|
|
599
|
+
fs3.writeFileSync(testPath, source, { encoding: "utf8", flag: "wx" });
|
|
600
|
+
} catch (err) {
|
|
601
|
+
if (err.code === "EEXIST") {
|
|
602
|
+
throw new CaptureError(`${testPath} appeared while drafting; left unchanged`);
|
|
603
|
+
}
|
|
604
|
+
throw err;
|
|
605
|
+
}
|
|
606
|
+
return testPath;
|
|
607
|
+
}
|
|
608
|
+
|
|
377
609
|
// src/cli.ts
|
|
378
610
|
var HELP_FLAGS = /* @__PURE__ */ new Set(["-h", "--help"]);
|
|
379
|
-
var SDK_USAGE = "usage: attest <command> [args]\n\nSDK commands:\n init [--ci github] scaffold a sample test and merge policy here\n (--ci github also writes .github/workflows/attest.yml)\n\noptions:\n --version print the SDK version and the engine version it requires\n --help print this help\n";
|
|
611
|
+
var SDK_USAGE = "usage: attest <command> [args]\n\nSDK commands:\n init [--ci github] scaffold a sample test and merge policy here\n (--ci github also writes .github/workflows/attest.yml)\n\n`attest capture` (engine) also drafts tests/attest/<name>.test.ts from a verified\nfixture, unless that file exists.\n\noptions:\n --version print the SDK version and the engine version it requires\n --help print this help\n";
|
|
380
612
|
var INIT_USAGE = "usage: attest init [--ci github] (scaffolds into the current directory)";
|
|
381
613
|
function cmdInit(args) {
|
|
382
614
|
if (args.some((a) => HELP_FLAGS.has(a))) {
|
|
@@ -468,10 +700,67 @@ async function forward(args) {
|
|
|
468
700
|
);
|
|
469
701
|
return 1;
|
|
470
702
|
}
|
|
703
|
+
if (command === "capture" && captureWritesFixture(args.slice(1))) {
|
|
704
|
+
return captureAndEmit(engine, args.slice(1));
|
|
705
|
+
}
|
|
471
706
|
const proc = spawnSync2(engine, args, { stdio: "inherit" });
|
|
472
707
|
if (proc.error) throw proc.error;
|
|
473
708
|
return exitStatus(proc.status, proc.signal);
|
|
474
709
|
}
|
|
710
|
+
var CAPTURE_NO_TEST = /* @__PURE__ */ new Set(["check-redaction", "serve", "h", "help"]);
|
|
711
|
+
function flagName(arg) {
|
|
712
|
+
if (!arg.startsWith("-") || arg === "-" || arg === "--") return null;
|
|
713
|
+
return arg.replace(/^-+/, "").split("=", 1)[0] ?? null;
|
|
714
|
+
}
|
|
715
|
+
function captureWritesFixture(args) {
|
|
716
|
+
for (const a of args) {
|
|
717
|
+
if (a === "--") break;
|
|
718
|
+
const name = flagName(a);
|
|
719
|
+
if (name !== null && CAPTURE_NO_TEST.has(name)) return false;
|
|
720
|
+
}
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
723
|
+
function userResultFile(args) {
|
|
724
|
+
for (const a of args) {
|
|
725
|
+
if (a === "--") break;
|
|
726
|
+
if (flagName(a) === "result-file") return true;
|
|
727
|
+
}
|
|
728
|
+
return false;
|
|
729
|
+
}
|
|
730
|
+
function captureAndEmit(engine, args) {
|
|
731
|
+
if (userResultFile(args)) {
|
|
732
|
+
process.stderr.write(
|
|
733
|
+
"attest: capture --result-file is set by the attest CLI; run attest-engine capture directly to choose it\n"
|
|
734
|
+
);
|
|
735
|
+
return 2;
|
|
736
|
+
}
|
|
737
|
+
const tmp = fs4.mkdtempSync(path4.join(os2.tmpdir(), "attest-capture-"));
|
|
738
|
+
const resultFile = path4.join(tmp, "result.json");
|
|
739
|
+
let result;
|
|
740
|
+
let code;
|
|
741
|
+
try {
|
|
742
|
+
const proc = spawnSync2(engine, ["capture", "--result-file", resultFile, ...args], { stdio: "inherit" });
|
|
743
|
+
if (proc.error) throw proc.error;
|
|
744
|
+
code = exitStatus(proc.status, proc.signal);
|
|
745
|
+
if (!fs4.existsSync(resultFile)) return code;
|
|
746
|
+
result = JSON.parse(fs4.readFileSync(resultFile, "utf8"));
|
|
747
|
+
} finally {
|
|
748
|
+
fs4.rmSync(tmp, { recursive: true, force: true });
|
|
749
|
+
}
|
|
750
|
+
const fixture = result.fixture;
|
|
751
|
+
if (!result.verified) {
|
|
752
|
+
process.stderr.write(` test: none drafted for unverified ${fixture}
|
|
753
|
+
`);
|
|
754
|
+
return code;
|
|
755
|
+
}
|
|
756
|
+
const written = emitVitest(fixture);
|
|
757
|
+
process.stderr.write(
|
|
758
|
+
written === null ? ` test: ${testPathFor(fixture)} exists; left unchanged
|
|
759
|
+
` : ` test: drafted ${written}
|
|
760
|
+
`
|
|
761
|
+
);
|
|
762
|
+
return code;
|
|
763
|
+
}
|
|
475
764
|
async function main(argv = process.argv.slice(2)) {
|
|
476
765
|
const [command] = argv;
|
|
477
766
|
if (command === void 0 || HELP_FLAGS.has(command) || command === "help" && argv.length === 1) {
|
|
@@ -495,8 +784,8 @@ ${SDK_USAGE}`);
|
|
|
495
784
|
}
|
|
496
785
|
function isDirectExecution() {
|
|
497
786
|
const script = process.argv[1];
|
|
498
|
-
if (script === void 0 || !
|
|
499
|
-
return
|
|
787
|
+
if (script === void 0 || !fs4.existsSync(script)) return false;
|
|
788
|
+
return fs4.realpathSync(script) === fs4.realpathSync(fileURLToPath2(import.meta.url));
|
|
500
789
|
}
|
|
501
790
|
if (isDirectExecution()) {
|
|
502
791
|
main().then(
|
|
@@ -509,6 +798,7 @@ if (isDirectExecution()) {
|
|
|
509
798
|
);
|
|
510
799
|
}
|
|
511
800
|
export {
|
|
801
|
+
captureWritesFixture,
|
|
512
802
|
engineCommands,
|
|
513
803
|
exitStatus,
|
|
514
804
|
main
|