assertledger 1.3.1 → 1.5.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/README.fr.md +6 -3
- package/README.md +6 -3
- package/conformance/schema-extensions.json +15 -0
- package/dist/build-info.json +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +218 -8
- package/dist/cli.js.map +1 -1
- package/dist/contracts/index.d.ts +467 -4
- package/dist/contracts/index.d.ts.map +1 -1
- package/dist/contracts/index.js +256 -3
- package/dist/contracts/index.js.map +1 -1
- package/dist/contracts/qualification.d.ts +528 -0
- package/dist/contracts/qualification.d.ts.map +1 -0
- package/dist/contracts/qualification.js +357 -0
- package/dist/contracts/qualification.js.map +1 -0
- package/dist/core/index.d.ts +9 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +46 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/qualification.d.ts +17 -0
- package/dist/core/qualification.d.ts.map +1 -0
- package/dist/core/qualification.js +571 -0
- package/dist/core/qualification.js.map +1 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +81 -4
- package/dist/diagnostics.js.map +1 -1
- package/dist/engine/adapters/bun-native.d.ts +35 -0
- package/dist/engine/adapters/bun-native.d.ts.map +1 -0
- package/dist/engine/adapters/bun-native.js +179 -0
- package/dist/engine/adapters/bun-native.js.map +1 -0
- package/dist/engine/adapters/bun-test-designated-profile.d.ts +23 -0
- package/dist/engine/adapters/bun-test-designated-profile.d.ts.map +1 -0
- package/dist/engine/adapters/bun-test-designated-profile.js +24 -0
- package/dist/engine/adapters/bun-test-designated-profile.js.map +1 -0
- package/dist/engine/container.d.ts +7 -0
- package/dist/engine/container.d.ts.map +1 -1
- package/dist/engine/container.js +22 -7
- package/dist/engine/container.js.map +1 -1
- package/dist/engine/git-regression.d.ts.map +1 -1
- package/dist/engine/git-regression.js +9 -2
- package/dist/engine/git-regression.js.map +1 -1
- package/dist/engine/index.d.ts +14 -0
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +828 -97
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/qualification.d.ts +154 -0
- package/dist/engine/qualification.d.ts.map +1 -0
- package/dist/engine/qualification.js +725 -0
- package/dist/engine/qualification.js.map +1 -0
- package/dist/engine/test-closure.d.ts +39 -0
- package/dist/engine/test-closure.d.ts.map +1 -0
- package/dist/engine/test-closure.js +513 -0
- package/dist/engine/test-closure.js.map +1 -0
- package/dist/engine/witness-import.d.ts +31 -0
- package/dist/engine/witness-import.d.ts.map +1 -0
- package/dist/engine/witness-import.js +224 -0
- package/dist/engine/witness-import.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +65 -0
- package/dist/mcp/index.js.map +1 -1
- package/dist/sdk/index.d.ts +292 -3
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +39 -1
- package/dist/sdk/index.js.map +1 -1
- package/docs/container-isolation.md +6 -0
- package/docs/migration-orchestration-qualification-v1.md +109 -0
- package/docs/migration-verification-v4.md +94 -0
- package/docs/orchestration-qualification.md +167 -0
- package/docs/reference.md +13 -0
- package/docs/witness-import.md +67 -0
- package/examples/orchestration/README.md +68 -0
- package/examples/orchestration/consumer-obligations.json +101 -0
- package/examples/orchestration/workspace/bitbucket-pipelines.yml +10 -0
- package/examples/orchestration/workspace/command-shape.test.mjs +25 -0
- package/examples/orchestration/workspace/commands.mjs +14 -0
- package/examples/orchestration/workspace/completion.mjs +39 -0
- package/examples/orchestration/workspace/faults.json +1 -0
- package/examples/orchestration/workspace/native.test.ts +4 -0
- package/examples/orchestration/workspace/package.json +6 -0
- package/examples/orchestration/workspace/packages/app/package.json +12 -0
- package/examples/orchestration/workspace/packages/app/verdict.txt +1 -0
- package/examples/orchestration/workspace/packages/leaf/package.json +9 -0
- package/examples/orchestration/workspace/packages/leaf/verdict.txt +1 -0
- package/examples/orchestration/workspace/pnpm-lock.yaml +170 -0
- package/examples/orchestration/workspace/pnpm-workspace.yaml +2 -0
- package/examples/orchestration/workspace/task.mjs +23 -0
- package/examples/orchestration/workspace/turbo.json +38 -0
- package/examples/orchestration/workspace/wrapper.mjs +45 -0
- package/integrations/bun/driver.d.mts +30 -1
- package/integrations/bun/driver.mjs +215 -6
- package/integrations/bun/preload.mjs +186 -5
- package/integrations/bun-native/README.md +86 -0
- package/integrations/bun-native/driver.mjs +396 -0
- package/integrations/bun-native/preload.mjs +375 -0
- package/integrations/skill/SKILL.md +25 -5
- package/package.json +1 -1
- package/schemas/evidence-manifest.v4.json +995 -0
- package/schemas/qualification-execution-request.v1.json +571 -0
- package/schemas/qualification-plan.v1.json +528 -0
- package/schemas/qualification-receipt.v1.json +821 -0
- package/schemas/qualification-replay-result.v1.json +37 -0
- package/schemas/verification-request.v4.json +590 -0
- package/schemas/witness-import-request.v1.json +231 -0
package/dist/engine/index.js
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { createReadStream } from "node:fs";
|
|
4
|
-
import { cp, link, lstat, mkdir, mkdtemp, open, readdir, readFile, realpath, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
4
|
+
import { cp, link, lstat, mkdir, mkdtemp, open, readdir, readFile, readlink, realpath, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { Readable } from "node:stream";
|
|
8
8
|
import { pipeline } from "node:stream/promises";
|
|
9
9
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
10
10
|
import { createScanner, SyntaxKind } from "typescript/unstable/ast";
|
|
11
|
-
import { AdapterSchema, isRepositoryExcludeName, parseAgenticBenchmarkAcquisitionRequest, parseAgenticBenchmarkAcquisitionResult, parseEvidenceManifest, parseRepositoryAudit, parseRepositoryInitConfig, parseRepositoryInitConfigV2, parseRepositoryInitLock, parseRepositoryInitLockV2, parseRepositoryInitResult, parseRepositoryInitResultV2, parseVerificationRequest, parseVersionedRepositoryInitConfig, parseVersionedRepositoryInitLock, parseVersionedVerificationRequest, RepositoryInitDetectionsSchema, repositoryInitConfigDigest, repositoryInitConfigV2Digest, repositoryInitLockDigest, repositoryInitLockV2Digest, } from "../contracts/index.js";
|
|
11
|
+
import { AdapterSchema, isRepositoryExcludeName, parseAgenticBenchmarkAcquisitionRequest, parseAgenticBenchmarkAcquisitionResult, parseEvidenceManifest, parseRepositoryAudit, parseRepositoryInitConfig, parseRepositoryInitConfigV2, parseRepositoryInitLock, parseRepositoryInitLockV2, parseRepositoryInitResult, parseRepositoryInitResultV2, parseVerificationRequest, parseVersionedRepositoryInitConfig, parseVersionedRepositoryInitLock, parseVersionedVerificationRequest, RepositoryInitDetectionsSchema, repositoryInitConfigDigest, repositoryInitConfigV2Digest, repositoryInitLockDigest, repositoryInitLockV2Digest, VERIFICATION_SCHEMA_VERSION_V4, } from "../contracts/index.js";
|
|
12
12
|
import { assertSafeRelativePath, canonicalize, createAgenticBenchmark, decideEvidence, portablePathKey, replayAgenticBenchmark, replayEvidenceManifest, sealManifestArtifact, sha256Canonical, } from "../core/index.js";
|
|
13
|
+
import { BUN_TEST_DESIGNATED_ADAPTER_PROFILE } from "./adapters/bun-test-designated-profile.js";
|
|
13
14
|
import { BUN_TEST_ADAPTER_PROFILE } from "./adapters/bun-test-profile.js";
|
|
14
15
|
import { NODE_TEST_ADAPTER_PROFILE } from "./adapters/node-test-profile.js";
|
|
15
16
|
import { runNodeTestRuntimePreflight, } from "./adapters/node-test-runtime.js";
|
|
16
17
|
import { normalizeRuntimeFacts, RUNTIME_FACTS_VERSION } from "./adapters/runtime-facts.js";
|
|
17
|
-
import { CONTAINER_LIMITATIONS, CONTAINER_REPORTER_FILE, CONTAINER_RESULT_FILE, CONTAINER_WORKSPACE, DEFAULT_CONTAINER_RUNTIME_COMMAND, prepareContainerBackend, runContainerExecution, } from "./container.js";
|
|
18
|
+
import { CONTAINER_ASSET_DIRECTORY, CONTAINER_LIMITATIONS, CONTAINER_REPORTER_FILE, CONTAINER_RESULT_FILE, CONTAINER_WORKSPACE, DEFAULT_CONTAINER_RUNTIME_COMMAND, prepareContainerBackend, runContainerExecution, } from "./container.js";
|
|
18
19
|
import { NODE_TEST_REPORTER_SOURCE } from "./node-test-reporter.js";
|
|
20
|
+
import { computeTestClosure, } from "./test-closure.js";
|
|
19
21
|
import { runBunRuntimeDoctorChecks, runRuntimeDoctorChecks, } from "./runtime-doctor.js";
|
|
20
22
|
const DEFAULT_EXCLUDES = new Set([".git", ".testforge", "node_modules"]);
|
|
21
23
|
const SHA256_PREFIX = "sha256:";
|
|
@@ -331,7 +333,9 @@ function repositoryLinkError(relative) {
|
|
|
331
333
|
cause: relative.split(path.sep).join("/"),
|
|
332
334
|
});
|
|
333
335
|
}
|
|
334
|
-
|
|
336
|
+
/** Walks the regular files of a repository. A link refuses the walk unless `links` collects it;
|
|
337
|
+
* a collected link is never followed. */
|
|
338
|
+
async function walkFiles(root, excludes, links) {
|
|
335
339
|
const files = [];
|
|
336
340
|
const spellings = new Map();
|
|
337
341
|
let totalBytes = 0;
|
|
@@ -344,8 +348,13 @@ async function walkFiles(root, excludes) {
|
|
|
344
348
|
continue;
|
|
345
349
|
const relative = path.posix.join(relativeDirectory.split(path.sep).join("/"), entry.name);
|
|
346
350
|
const entryStats = await lstat(path.join(root, ...relative.split("/")));
|
|
347
|
-
if (entryStats.isSymbolicLink())
|
|
348
|
-
|
|
351
|
+
if (entryStats.isSymbolicLink()) {
|
|
352
|
+
if (links === undefined)
|
|
353
|
+
throw repositoryLinkError(relative);
|
|
354
|
+
registerPortablePath(spellings, relative);
|
|
355
|
+
links.push(relative);
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
349
358
|
registerPortablePath(spellings, relative);
|
|
350
359
|
if (entryStats.isDirectory())
|
|
351
360
|
await visit(relative);
|
|
@@ -357,8 +366,176 @@ async function walkFiles(root, excludes) {
|
|
|
357
366
|
}
|
|
358
367
|
await visit("");
|
|
359
368
|
files.sort(comparePortablePaths);
|
|
369
|
+
links?.sort(comparePortablePaths);
|
|
360
370
|
return { files, spellings, totalBytes };
|
|
361
371
|
}
|
|
372
|
+
/** Whether a link resolves, or would resolve when dangling, outside the repository root. */
|
|
373
|
+
async function linkEscapesRoot(root, relative) {
|
|
374
|
+
const linkPath = path.join(root, ...relative.split("/"));
|
|
375
|
+
let target;
|
|
376
|
+
try {
|
|
377
|
+
target = await realpath(linkPath);
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
try {
|
|
381
|
+
target = path.resolve(path.dirname(linkPath), await readlink(linkPath));
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const fromRoot = path.relative(root, target);
|
|
388
|
+
return fromRoot === ".." || fromRoot.startsWith(`..${path.sep}`) || path.isAbsolute(fromRoot);
|
|
389
|
+
}
|
|
390
|
+
const LINK_DISCOVERY_ENTRY_LIMIT = 100_000;
|
|
391
|
+
/**
|
|
392
|
+
* Whether a runner discovering tests from the root could reach one through this link: the link
|
|
393
|
+
* is itself named as a test, or it resolves to a directory holding one. A nested link, or a tree
|
|
394
|
+
* past the entry limit, is assumed to reach one.
|
|
395
|
+
*/
|
|
396
|
+
async function linkReachesDiscoverableTest(root, link, discoverable) {
|
|
397
|
+
if (discoverable(link))
|
|
398
|
+
return true;
|
|
399
|
+
let target;
|
|
400
|
+
try {
|
|
401
|
+
target = await realpath(path.join(root, ...link.split("/")));
|
|
402
|
+
if (!(await stat(target)).isDirectory())
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
let seen = 0;
|
|
409
|
+
const pending = [""];
|
|
410
|
+
for (let relative = pending.pop(); relative !== undefined; relative = pending.pop()) {
|
|
411
|
+
for (const entry of await readdir(path.join(target, relative), { withFileTypes: true })) {
|
|
412
|
+
seen += 1;
|
|
413
|
+
if (seen > LINK_DISCOVERY_ENTRY_LIMIT || entry.isSymbolicLink())
|
|
414
|
+
return true;
|
|
415
|
+
const child = relative === "" ? entry.name : `${relative}/${entry.name}`;
|
|
416
|
+
if (entry.isDirectory())
|
|
417
|
+
pending.push(child);
|
|
418
|
+
else if (discoverable(`${link}/${child}`))
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Refuses a link that leaves the root or lies in the closure of `roots`; every other link stays
|
|
426
|
+
* outside the evidence. With `discoverable`, the tests a runner finds by itself, a link through
|
|
427
|
+
* which one can be reached is in the closure too. `strictUnbounded` also refuses any link when the
|
|
428
|
+
* closure is unbounded, as a campaign must; a static diagnostic reports that case without refusing.
|
|
429
|
+
* Without `roots` the closure is unknown and every link refuses.
|
|
430
|
+
*/
|
|
431
|
+
async function assessRepositoryLinks(root, files, links, roots, read, strictUnbounded, discoverable) {
|
|
432
|
+
const assessment = {
|
|
433
|
+
links: [...links],
|
|
434
|
+
refused: undefined,
|
|
435
|
+
omitted: [],
|
|
436
|
+
unbounded: [],
|
|
437
|
+
};
|
|
438
|
+
if (links.length === 0)
|
|
439
|
+
return assessment;
|
|
440
|
+
for (const link of links) {
|
|
441
|
+
if (await linkEscapesRoot(root, link)) {
|
|
442
|
+
assessment.refused = { path: link, reasonCode: "REPOSITORY_LINK_ESCAPES_ROOT" };
|
|
443
|
+
return assessment;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (roots === undefined) {
|
|
447
|
+
assessment.refused = { path: links[0], reasonCode: "TEST_CLOSURE_UNSUPPORTED" };
|
|
448
|
+
return assessment;
|
|
449
|
+
}
|
|
450
|
+
if (discoverable !== undefined) {
|
|
451
|
+
for (const link of links) {
|
|
452
|
+
if (await linkReachesDiscoverableTest(root, link, discoverable)) {
|
|
453
|
+
assessment.refused = { path: link, reasonCode: "REPOSITORY_LINK_IN_TEST_CLOSURE" };
|
|
454
|
+
return assessment;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const closure = await computeTestClosure({ files, links, roots, read });
|
|
459
|
+
assessment.unbounded = closure.unbounded;
|
|
460
|
+
const linked = closure.links[0];
|
|
461
|
+
if (linked !== undefined) {
|
|
462
|
+
assessment.refused = { path: linked, reasonCode: "REPOSITORY_LINK_IN_TEST_CLOSURE" };
|
|
463
|
+
return assessment;
|
|
464
|
+
}
|
|
465
|
+
if (strictUnbounded && closure.unbounded.length > 0) {
|
|
466
|
+
assessment.refused = { path: links[0], reasonCode: "TEST_CLOSURE_UNBOUNDED" };
|
|
467
|
+
return assessment;
|
|
468
|
+
}
|
|
469
|
+
assessment.omitted = [...links];
|
|
470
|
+
return assessment;
|
|
471
|
+
}
|
|
472
|
+
/** The digest analyzeRepository computes, over an inventory already walked. */
|
|
473
|
+
async function repositoryInventoryDigest(root, files) {
|
|
474
|
+
const digest = createHash("sha256");
|
|
475
|
+
for (const file of files) {
|
|
476
|
+
const content = await readFile(path.join(root, ...file.split("/")));
|
|
477
|
+
digest.update(Buffer.from(`${file}\0${content.byteLength}\0`, "utf8"));
|
|
478
|
+
digest.update(content);
|
|
479
|
+
}
|
|
480
|
+
return `${SHA256_PREFIX}${digest.digest("hex")}`;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Judges a v4 campaign's links against the module closure of every test the adapter executes,
|
|
484
|
+
* read with each world's and candidate's overlay. A link that leaves the root or lies in that
|
|
485
|
+
* closure refuses the campaign and is named; so does any link when the closure is unbounded or
|
|
486
|
+
* unknown, as for a structured command. The others are left out of the snapshot and recorded.
|
|
487
|
+
*/
|
|
488
|
+
async function campaignOmittedLinks(request, root, files, links) {
|
|
489
|
+
const overlays = new Map();
|
|
490
|
+
for (const overlay of [
|
|
491
|
+
...request.worlds.flatMap((world) => world.files),
|
|
492
|
+
...request.candidates.flatMap((candidate) => candidate.files),
|
|
493
|
+
]) {
|
|
494
|
+
const key = portablePathKey(overlay.path);
|
|
495
|
+
const entry = overlays.get(key) ?? { path: overlay.path, contents: [] };
|
|
496
|
+
entry.contents.push(overlay.content);
|
|
497
|
+
overlays.set(key, entry);
|
|
498
|
+
}
|
|
499
|
+
const repositoryFiles = new Set(files.map(portablePathKey));
|
|
500
|
+
const roots = request.adapter.kind === "node-test" || request.adapter.kind === "bun-test"
|
|
501
|
+
? [
|
|
502
|
+
...request.adapter.baseTestFiles,
|
|
503
|
+
...request.candidates.flatMap((candidate) => candidate.files.map((file) => file.path)),
|
|
504
|
+
]
|
|
505
|
+
: request.adapter.kind === "bun-test-designated"
|
|
506
|
+
? request.candidates.flatMap((candidate) => candidate.designated === undefined ? [] : [candidate.designated.file])
|
|
507
|
+
: undefined;
|
|
508
|
+
const assessment = await assessRepositoryLinks(root, [
|
|
509
|
+
...files,
|
|
510
|
+
...[...overlays.entries()]
|
|
511
|
+
.filter(([key]) => !repositoryFiles.has(key))
|
|
512
|
+
.map(([, entry]) => entry.path),
|
|
513
|
+
], links, roots, async (file) => {
|
|
514
|
+
const key = portablePathKey(file);
|
|
515
|
+
const texts = overlays.get(key)?.contents ?? [];
|
|
516
|
+
return repositoryFiles.has(key)
|
|
517
|
+
? [await readFile(path.join(root, ...file.split("/")), "utf8"), ...texts]
|
|
518
|
+
: texts;
|
|
519
|
+
}, true);
|
|
520
|
+
const refused = assessment.refused;
|
|
521
|
+
if (refused !== undefined) {
|
|
522
|
+
throw new Error(refused.reasonCode === "TEST_CLOSURE_UNSUPPORTED"
|
|
523
|
+
? "UNSUPPORTED_REPOSITORY_SYMLINK"
|
|
524
|
+
: refused.reasonCode, { cause: refused.path });
|
|
525
|
+
}
|
|
526
|
+
return assessment.omitted;
|
|
527
|
+
}
|
|
528
|
+
/** The closure roots of a test framework's discovered sources; undefined when not analyzable. */
|
|
529
|
+
function frameworkClosureRoots(framework, files) {
|
|
530
|
+
if (framework === "bun:test") {
|
|
531
|
+
return files.filter((file) => /(?:^|\/)[^/]+[._](?:test|spec)\.[cm]?[jt]sx?$/u.test(file));
|
|
532
|
+
}
|
|
533
|
+
if (framework === "node:test" || framework === "vitest" || framework === "jest") {
|
|
534
|
+
return files.filter((file) => (initEvidenceKind(file) === "TEST_SOURCE" && /\.[cm]?[jt]sx?$/u.test(file)) ||
|
|
535
|
+
/(?:^|\/)(?:vitest|jest)\.config\.[cm]?[jt]s$/u.test(file));
|
|
536
|
+
}
|
|
537
|
+
return undefined;
|
|
538
|
+
}
|
|
362
539
|
function rawSha256(content) {
|
|
363
540
|
return `${SHA256_PREFIX}${createHash("sha256").update(content).digest("hex")}`;
|
|
364
541
|
}
|
|
@@ -727,16 +904,28 @@ export async function initializeRepository(requestedRoot, options = {}, writeDep
|
|
|
727
904
|
const fileKey = portablePathKey(file);
|
|
728
905
|
return fileKey === rootKey || fileKey.startsWith(`${rootKey}/`);
|
|
729
906
|
});
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
907
|
+
const links = [];
|
|
908
|
+
const inventory = await walkFiles(root, inventoryExcludes, links);
|
|
909
|
+
// A link is judged against the tests that would run, once the framework is known; one that
|
|
910
|
+
// leaves the root refuses the repository whatever the framework.
|
|
911
|
+
const linkConflict = (assessment, detections) => {
|
|
912
|
+
options.onLinkAssessment?.(assessment);
|
|
913
|
+
const code = assessment.refused?.reasonCode;
|
|
914
|
+
const codes = [
|
|
738
915
|
"UNSUPPORTED_REPOSITORY_SYMLINK",
|
|
739
|
-
|
|
916
|
+
...(code === undefined || code === "TEST_CLOSURE_UNSUPPORTED" ? [] : [code]),
|
|
917
|
+
];
|
|
918
|
+
return initTerminalResult("CONFLICT", { ...detections, reasonCodes: codes }, codes);
|
|
919
|
+
};
|
|
920
|
+
for (const link of links) {
|
|
921
|
+
if (await linkEscapesRoot(root, link)) {
|
|
922
|
+
return linkConflict({
|
|
923
|
+
links: [...links],
|
|
924
|
+
refused: { path: link, reasonCode: "REPOSITORY_LINK_ESCAPES_ROOT" },
|
|
925
|
+
omitted: [],
|
|
926
|
+
unbounded: [],
|
|
927
|
+
}, initEmptyDetections([]));
|
|
928
|
+
}
|
|
740
929
|
}
|
|
741
930
|
const inScopeFiles = inventory.files.filter(outsideCandidateRoots);
|
|
742
931
|
const contents = new Map();
|
|
@@ -870,6 +1059,20 @@ export async function initializeRepository(requestedRoot, options = {}, writeDep
|
|
|
870
1059
|
detections.packageManager = packageManager;
|
|
871
1060
|
return initTerminalResult("CONFLICT", detections, ["FRAMEWORK_UNDETECTED"]);
|
|
872
1061
|
}
|
|
1062
|
+
const linkReasonCodes = [];
|
|
1063
|
+
if (links.length > 0) {
|
|
1064
|
+
const assessment = await assessRepositoryLinks(root, inScopeFiles, links, frameworkClosureRoots(framework, inScopeFiles), async (file) => [await readFile(path.join(root, ...file.split("/")), "utf8")], false, (file) => frameworkClosureRoots(framework, [file])?.length === 1);
|
|
1065
|
+
if (assessment.refused !== undefined) {
|
|
1066
|
+
const detections = initEmptyDetections([]);
|
|
1067
|
+
detections.packageManager = packageManager;
|
|
1068
|
+
detections.framework = framework;
|
|
1069
|
+
return linkConflict(assessment, detections);
|
|
1070
|
+
}
|
|
1071
|
+
options.onLinkAssessment?.(assessment);
|
|
1072
|
+
linkReasonCodes.push("REPOSITORY_LINK_OUTSIDE_TEST_CLOSURE");
|
|
1073
|
+
if (assessment.unbounded.length > 0)
|
|
1074
|
+
linkReasonCodes.push("TEST_CLOSURE_UNBOUNDED");
|
|
1075
|
+
}
|
|
873
1076
|
let testCommand = options.testCommand === undefined ? undefined : safeInitCommand(options.testCommand);
|
|
874
1077
|
if (options.testCommand !== undefined && testCommand === undefined) {
|
|
875
1078
|
const detections = initEmptyDetections(["TEST_COMMAND_INVALID"]);
|
|
@@ -974,13 +1177,16 @@ export async function initializeRepository(requestedRoot, options = {}, writeDep
|
|
|
974
1177
|
adapterRecommendation = "bun-test";
|
|
975
1178
|
}
|
|
976
1179
|
}
|
|
977
|
-
const reasonCodes =
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1180
|
+
const reasonCodes = [
|
|
1181
|
+
...(adapter === undefined
|
|
1182
|
+
? [
|
|
1183
|
+
framework === "node:test" || framework === "bun:test"
|
|
1184
|
+
? "BASE_TEST_FILES_UNAVAILABLE"
|
|
1185
|
+
: "OFFICIAL_ADAPTER_UNAVAILABLE",
|
|
1186
|
+
]
|
|
1187
|
+
: []),
|
|
1188
|
+
...linkReasonCodes,
|
|
1189
|
+
].sort();
|
|
984
1190
|
const detections = {
|
|
985
1191
|
packageManager,
|
|
986
1192
|
framework,
|
|
@@ -1059,14 +1265,16 @@ export async function initializeRepository(requestedRoot, options = {}, writeDep
|
|
|
1059
1265
|
{ path: INIT_LOCK_FILE, digest: rawSha256(lockContent), content: lockContent },
|
|
1060
1266
|
];
|
|
1061
1267
|
let finalInScopeFiles;
|
|
1268
|
+
const finalLinks = [];
|
|
1062
1269
|
try {
|
|
1063
|
-
const finalInventory = await walkFiles(root, inventoryExcludes);
|
|
1270
|
+
const finalInventory = await walkFiles(root, inventoryExcludes, finalLinks);
|
|
1064
1271
|
finalInScopeFiles = finalInventory.files.filter(outsideCandidateRoots);
|
|
1065
1272
|
}
|
|
1066
1273
|
catch {
|
|
1067
1274
|
finalInScopeFiles = [];
|
|
1068
1275
|
}
|
|
1069
|
-
let repositoryChanged = JSON.stringify(finalInScopeFiles) !== JSON.stringify(inScopeFiles)
|
|
1276
|
+
let repositoryChanged = JSON.stringify(finalInScopeFiles) !== JSON.stringify(inScopeFiles) ||
|
|
1277
|
+
JSON.stringify(finalLinks) !== JSON.stringify(links);
|
|
1070
1278
|
if (!repositoryChanged) {
|
|
1071
1279
|
for (const [file, snapshot] of evidenceBytes) {
|
|
1072
1280
|
try {
|
|
@@ -1130,7 +1338,7 @@ export async function initializeRepository(requestedRoot, options = {}, writeDep
|
|
|
1130
1338
|
const result = {
|
|
1131
1339
|
schemaVersion: bunBuiltIn ? "2.0.0" : "1.0.0",
|
|
1132
1340
|
status: "UNCHANGED",
|
|
1133
|
-
reasonCodes:
|
|
1341
|
+
reasonCodes: detections.reasonCodes,
|
|
1134
1342
|
detections,
|
|
1135
1343
|
actions: [],
|
|
1136
1344
|
files: plannedFiles,
|
|
@@ -1151,7 +1359,8 @@ export async function initializeRepository(requestedRoot, options = {}, writeDep
|
|
|
1151
1359
|
const resultValue = {
|
|
1152
1360
|
schemaVersion: bunBuiltIn ? "2.0.0" : "1.0.0",
|
|
1153
1361
|
status,
|
|
1154
|
-
|
|
1362
|
+
// Only non-blocking link findings can remain here; the contract mirrors the detections.
|
|
1363
|
+
reasonCodes: detections.reasonCodes,
|
|
1155
1364
|
detections,
|
|
1156
1365
|
actions,
|
|
1157
1366
|
files: plannedFiles,
|
|
@@ -1245,7 +1454,7 @@ export async function doctorRepositoryRuntime(requestedRoot, options) {
|
|
|
1245
1454
|
},
|
|
1246
1455
|
probeTemporaryWorkspace: probeRuntimeTemporaryWorkspace,
|
|
1247
1456
|
runSyntheticPreflight: async () => {
|
|
1248
|
-
await runBunTestRuntimePreflight(resolvedExecutable, environmentAllowlist, 5_000, CONTROLLED_REPORT_MAXIMUM_BYTES);
|
|
1457
|
+
await runBunTestRuntimePreflight(localBunDriverExecutor(resolvedExecutable, environmentAllowlist, 5_000, CONTROLLED_REPORT_MAXIMUM_BYTES));
|
|
1249
1458
|
},
|
|
1250
1459
|
});
|
|
1251
1460
|
}
|
|
@@ -2005,6 +2214,7 @@ function parseRequest(value) {
|
|
|
2005
2214
|
assertPortableStringCollection(candidateRoots, "DUPLICATE_CANDIDATE_ROOT");
|
|
2006
2215
|
if (adapter.kind !== "node-test" &&
|
|
2007
2216
|
adapter.kind !== "bun-test" &&
|
|
2217
|
+
adapter.kind !== "bun-test-designated" &&
|
|
2008
2218
|
adapter.kind !== "testforge-command") {
|
|
2009
2219
|
throw new TypeError("UNSUPPORTED_ADAPTER");
|
|
2010
2220
|
}
|
|
@@ -2021,10 +2231,17 @@ function parseRequest(value) {
|
|
|
2021
2231
|
})()
|
|
2022
2232
|
: undefined;
|
|
2023
2233
|
if (containerIsolation === undefined) {
|
|
2024
|
-
if (isolation.kind !== "trusted-local")
|
|
2234
|
+
if (isolation.kind !== "trusted-local" && isolation.kind !== "windows-native") {
|
|
2025
2235
|
throw new TypeError("UNSUPPORTED_ISOLATION");
|
|
2236
|
+
}
|
|
2026
2237
|
if (isolation.acknowledgedUnsafeExecution !== true) {
|
|
2027
|
-
throw new Error("
|
|
2238
|
+
throw new Error(isolation.kind === "windows-native"
|
|
2239
|
+
? "WINDOWS_NATIVE_EXECUTION_NOT_ACKNOWLEDGED"
|
|
2240
|
+
: "UNSAFE_LOCAL_EXECUTION_NOT_ACKNOWLEDGED");
|
|
2241
|
+
}
|
|
2242
|
+
// The declared level names a Windows host; it is never claimed for any other platform.
|
|
2243
|
+
if (isolation.kind === "windows-native" && process.platform !== "win32") {
|
|
2244
|
+
throw new Error("WINDOWS_NATIVE_HOST_REQUIRED");
|
|
2028
2245
|
}
|
|
2029
2246
|
}
|
|
2030
2247
|
const parsedBudgets = {
|
|
@@ -2073,6 +2290,27 @@ function parseRequest(value) {
|
|
|
2073
2290
|
});
|
|
2074
2291
|
const candidates = request.candidates.map((value) => {
|
|
2075
2292
|
const candidate = requireRecord(value, "candidate");
|
|
2293
|
+
if (candidate.test !== undefined) {
|
|
2294
|
+
const test = requireRecord(candidate.test, "designated_test");
|
|
2295
|
+
let file;
|
|
2296
|
+
try {
|
|
2297
|
+
file = assertSafeRelativePath(requireString(test.file, "designated_test_file"), candidateRoots);
|
|
2298
|
+
}
|
|
2299
|
+
catch {
|
|
2300
|
+
throw new Error("FORBIDDEN_CANDIDATE_PATH");
|
|
2301
|
+
}
|
|
2302
|
+
return {
|
|
2303
|
+
id: requireString(candidate.id, "candidate_id"),
|
|
2304
|
+
files: [],
|
|
2305
|
+
designated: {
|
|
2306
|
+
file,
|
|
2307
|
+
path: requireStringArray(test.path, "designated_test_path"),
|
|
2308
|
+
expectedFailure: candidate.expectedFailure === null
|
|
2309
|
+
? null
|
|
2310
|
+
: requireString(candidate.expectedFailure, "expected_failure"),
|
|
2311
|
+
},
|
|
2312
|
+
};
|
|
2313
|
+
}
|
|
2076
2314
|
if (!Array.isArray(candidate.files) || candidate.files.length === 0) {
|
|
2077
2315
|
throw new TypeError("INVALID_CANDIDATE_FILES");
|
|
2078
2316
|
}
|
|
@@ -2138,21 +2376,41 @@ function parseRequest(value) {
|
|
|
2138
2376
|
...(extraArguments === undefined ? {} : { extraArguments }),
|
|
2139
2377
|
};
|
|
2140
2378
|
})()
|
|
2141
|
-
:
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2379
|
+
: adapter.kind === "bun-test-designated"
|
|
2380
|
+
? {
|
|
2381
|
+
kind: "bun-test-designated",
|
|
2382
|
+
executable: requireString(adapter.executable, "adapter_executable"),
|
|
2383
|
+
testTimeoutMs: adapter.testTimeoutMs === null || adapter.testTimeoutMs === undefined
|
|
2384
|
+
? null
|
|
2385
|
+
: requirePositiveInteger(adapter.testTimeoutMs, "adapter_test_timeout_ms"),
|
|
2386
|
+
}
|
|
2387
|
+
: {
|
|
2388
|
+
kind: "testforge-command",
|
|
2389
|
+
executable: requireString(adapter.executable, "adapter_executable"),
|
|
2390
|
+
arguments: requireStringArray(adapter.arguments, "adapter_arguments"),
|
|
2391
|
+
protocolVersion: "1.0.0",
|
|
2392
|
+
};
|
|
2393
|
+
// A designated test runs with the same bytes in every world: no world may overlay it.
|
|
2394
|
+
const designatedFiles = new Set(candidates.flatMap((candidate) => candidate.designated === undefined ? [] : [portablePathKey(candidate.designated.file)]));
|
|
2395
|
+
if (worlds.some((world) => world.files.some((file) => designatedFiles.has(portablePathKey(file.path))))) {
|
|
2396
|
+
throw new Error("DESIGNATED_TEST_OVERLAID");
|
|
2397
|
+
}
|
|
2398
|
+
const includeDependencies = repository.includeDependencies === true;
|
|
2399
|
+
const exclude = requireStringArray(repository.exclude, "repository_exclude");
|
|
2400
|
+
if (includeDependencies && exclude.some((entry) => portablePathKey(entry) === "node_modules")) {
|
|
2401
|
+
throw new Error("REPOSITORY_DEPENDENCIES_EXCLUDED");
|
|
2402
|
+
}
|
|
2147
2403
|
return {
|
|
2148
2404
|
schemaVersion: requireString(request.schemaVersion, "schema_version"),
|
|
2149
2405
|
repository: {
|
|
2150
2406
|
root: requireString(repository.root, "repository_root"),
|
|
2151
|
-
exclude
|
|
2407
|
+
exclude,
|
|
2408
|
+
includeDependencies,
|
|
2409
|
+
git: repository.git === "synthesized" ? "synthesized" : "excluded",
|
|
2152
2410
|
},
|
|
2153
2411
|
adapter: normalizedAdapter,
|
|
2154
2412
|
isolation: containerIsolation ?? {
|
|
2155
|
-
kind: "trusted-local",
|
|
2413
|
+
kind: isolation.kind === "windows-native" ? "windows-native" : "trusted-local",
|
|
2156
2414
|
acknowledgedUnsafeExecution: true,
|
|
2157
2415
|
environmentAllowlist,
|
|
2158
2416
|
},
|
|
@@ -2592,10 +2850,113 @@ function containerPreflightExecutor(container, executable) {
|
|
|
2592
2850
|
};
|
|
2593
2851
|
});
|
|
2594
2852
|
}
|
|
2595
|
-
|
|
2853
|
+
const CONTAINER_BUN_PROBE_SOURCE = 'const fs = require("node:fs"); const crypto = require("node:crypto"); const execPath = fs.realpathSync(process.execPath); console.log(JSON.stringify({ digest: "sha256:" + crypto.createHash("sha256").update(fs.readFileSync(execPath)).digest("hex"), execPath, bunVersion: Bun.version, bunRevision: Bun.revision }));';
|
|
2854
|
+
async function runContainerBunProbe(container, executable, workspace, timeoutMs, maximumOutputBytes) {
|
|
2855
|
+
const { process: result } = await runContainerExecution(container, {
|
|
2856
|
+
workspace,
|
|
2857
|
+
executable,
|
|
2858
|
+
args: ["-e", CONTAINER_BUN_PROBE_SOURCE],
|
|
2859
|
+
environment: {},
|
|
2860
|
+
timeoutMs: Math.min(timeoutMs, 30_000),
|
|
2861
|
+
maximumOutputBytes: Math.min(maximumOutputBytes, CONTROLLED_REPORT_MAXIMUM_BYTES),
|
|
2862
|
+
}, runBoundedProcess, os.tmpdir());
|
|
2863
|
+
if (result.outcome !== "PASS" || result.stdout.truncated || result.stderr.totalBytes > 0) {
|
|
2864
|
+
throw new Error("BUN_TEST_EXECUTABLE_PROBE_FAILED");
|
|
2865
|
+
}
|
|
2866
|
+
let report;
|
|
2867
|
+
try {
|
|
2868
|
+
report = JSON.parse(result.stdout.text.trim());
|
|
2869
|
+
}
|
|
2870
|
+
catch {
|
|
2871
|
+
throw new Error("BUN_TEST_EXECUTABLE_PROBE_FAILED");
|
|
2872
|
+
}
|
|
2873
|
+
if (!isRecord(report) ||
|
|
2874
|
+
Object.keys(report).sort().join("\0") !== "bunRevision\0bunVersion\0digest\0execPath" ||
|
|
2875
|
+
typeof report.execPath !== "string" ||
|
|
2876
|
+
!report.execPath.startsWith("/") ||
|
|
2877
|
+
typeof report.digest !== "string" ||
|
|
2878
|
+
!/^sha256:[a-f0-9]{64}$/u.test(report.digest) ||
|
|
2879
|
+
typeof report.bunVersion !== "string" ||
|
|
2880
|
+
typeof report.bunRevision !== "string") {
|
|
2881
|
+
throw new Error("BUN_TEST_EXECUTABLE_PROBE_FAILED");
|
|
2882
|
+
}
|
|
2883
|
+
if (report.bunVersion !== BUN_TEST_ADAPTER_PROFILE.bunVersion ||
|
|
2884
|
+
report.bunRevision !== BUN_TEST_ADAPTER_PROFILE.bunRevision) {
|
|
2885
|
+
throw new Error("BUN_TEST_VERSION_UNSUPPORTED");
|
|
2886
|
+
}
|
|
2887
|
+
return {
|
|
2888
|
+
execPath: report.execPath,
|
|
2889
|
+
bunVersion: report.bunVersion,
|
|
2890
|
+
bunRevision: report.bunRevision,
|
|
2891
|
+
executableDigest: report.digest,
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2894
|
+
/** Resolves the image's Bun twice, as the local probe does, and binds its digest. */
|
|
2895
|
+
async function probeContainerBunExecutable(container, requestedExecutable, timeoutMs, maximumOutputBytes) {
|
|
2896
|
+
return withEmptyWorkspace(async (workspace) => {
|
|
2897
|
+
const first = await runContainerBunProbe(container, requestedExecutable, workspace, timeoutMs, maximumOutputBytes);
|
|
2898
|
+
const second = await runContainerBunProbe(container, first.execPath, workspace, timeoutMs, maximumOutputBytes);
|
|
2899
|
+
if (second.execPath !== first.execPath ||
|
|
2900
|
+
second.bunVersion !== first.bunVersion ||
|
|
2901
|
+
second.bunRevision !== first.bunRevision ||
|
|
2902
|
+
second.executableDigest !== first.executableDigest) {
|
|
2903
|
+
throw new Error("BUN_TEST_EXECUTABLE_PROBE_FAILED");
|
|
2904
|
+
}
|
|
2905
|
+
return {
|
|
2906
|
+
requestedExecutable,
|
|
2907
|
+
resolvedExecutable: first.execPath,
|
|
2908
|
+
executableDigest: first.executableDigest,
|
|
2909
|
+
bunVersion: first.bunVersion,
|
|
2910
|
+
bunRevision: first.bunRevision,
|
|
2911
|
+
};
|
|
2912
|
+
});
|
|
2913
|
+
}
|
|
2914
|
+
/** The driver, its preload and the assertion helper, as one container asset set. */
|
|
2915
|
+
async function bunContainerAssets() {
|
|
2916
|
+
return [
|
|
2917
|
+
{ name: "driver.mjs", content: await readFile(BUN_TEST_DRIVER_PATH, "utf8") },
|
|
2918
|
+
{ name: "preload.mjs", content: await readFile(BUN_TEST_PRELOAD_PATH, "utf8") },
|
|
2919
|
+
{ name: "assertions.mjs", content: await readFile(BUN_TEST_HELPER_PATH, "utf8") },
|
|
2920
|
+
];
|
|
2921
|
+
}
|
|
2922
|
+
/** Runs the Bun driver under the image's own Bun; the report comes back as result.json only. */
|
|
2923
|
+
async function runContainerBunDriver(container, workspace, executable, files, environment, timeoutMs, maximumOutputBytes) {
|
|
2924
|
+
const executed = await runContainerExecution(container, {
|
|
2925
|
+
workspace,
|
|
2926
|
+
assets: await bunContainerAssets(),
|
|
2927
|
+
executable,
|
|
2928
|
+
args: [
|
|
2929
|
+
`${CONTAINER_ASSET_DIRECTORY}/driver.mjs`,
|
|
2930
|
+
executable,
|
|
2931
|
+
`${CONTAINER_ASSET_DIRECTORY}/assertions.mjs`,
|
|
2932
|
+
...files,
|
|
2933
|
+
],
|
|
2934
|
+
environment: { ...environment, ASSERTLEDGER_BUN_RESULT_FILE: CONTAINER_RESULT_FILE },
|
|
2935
|
+
timeoutMs,
|
|
2936
|
+
maximumOutputBytes,
|
|
2937
|
+
resultMaximumBytes: CONTROLLED_REPORT_MAXIMUM_BYTES,
|
|
2938
|
+
}, runBoundedProcess, os.tmpdir());
|
|
2939
|
+
const result = executed.process;
|
|
2940
|
+
const document = result.outcome === "TIMEOUT" || result.outcome === "INFRA_ERROR"
|
|
2941
|
+
? undefined
|
|
2942
|
+
: parseJsonBytes(executed.result);
|
|
2943
|
+
return { result, report: parseStructuredCommandReport(document, result) };
|
|
2944
|
+
}
|
|
2945
|
+
async function executeContainerBunAdapter(request, container, workspace, candidate, candidateFiles) {
|
|
2596
2946
|
const adapter = request.adapter;
|
|
2597
|
-
if (adapter.kind
|
|
2598
|
-
throw new Error("
|
|
2947
|
+
if (adapter.kind !== "bun-test" && adapter.kind !== "bun-test-designated") {
|
|
2948
|
+
throw new Error("UNSUPPORTED_ADAPTER");
|
|
2949
|
+
}
|
|
2950
|
+
const { result, report } = await runContainerBunDriver(container, workspace, adapter.executable, adapter.kind === "bun-test" ? adapter.baseTestFiles : designatedFiles(request, candidate), adapter.kind === "bun-test"
|
|
2951
|
+
? { TESTFORGE_CANDIDATE_FILES: JSON.stringify(candidateFiles) }
|
|
2952
|
+
: { ASSERTLEDGER_BUN_DESIGNATED: JSON.stringify(designatedSelection(request, candidate)) }, request.budgets.timeoutMsPerExecution, request.budgets.maximumOutputBytes);
|
|
2953
|
+
return { result, structuredReport: report, nodeTestReport: undefined };
|
|
2954
|
+
}
|
|
2955
|
+
async function executeContainerAdapter(request, container, workspace, candidate, candidateFiles) {
|
|
2956
|
+
const adapter = request.adapter;
|
|
2957
|
+
if (adapter.kind === "bun-test" || adapter.kind === "bun-test-designated") {
|
|
2958
|
+
return executeContainerBunAdapter(request, container, workspace, candidate, candidateFiles);
|
|
2959
|
+
}
|
|
2599
2960
|
const executed = await runContainerExecution(container, {
|
|
2600
2961
|
workspace,
|
|
2601
2962
|
...(adapter.kind === "node-test" ? { reporterSource: NODE_TEST_REPORTER_SOURCE } : {}),
|
|
@@ -2636,7 +2997,114 @@ async function executeContainerAdapter(request, container, workspace, candidateF
|
|
|
2636
2997
|
};
|
|
2637
2998
|
}
|
|
2638
2999
|
function trustedLocalEnvironmentAllowlist(request) {
|
|
2639
|
-
return request.isolation.kind === "
|
|
3000
|
+
return request.isolation.kind === "container" ? [] : request.isolation.environmentAllowlist;
|
|
3001
|
+
}
|
|
3002
|
+
/** The snapshot exclusions: the defaults and the request's names, keeping dependencies on request. */
|
|
3003
|
+
function campaignExcludes(request) {
|
|
3004
|
+
const excludes = effectiveExcludes(request.repository.exclude);
|
|
3005
|
+
if (request.repository.includeDependencies)
|
|
3006
|
+
excludes.delete(portablePathKey("node_modules"));
|
|
3007
|
+
return excludes;
|
|
3008
|
+
}
|
|
3009
|
+
/** A workspace copy keeps the snapshot's synthesized Git metadata; the source's own never enters. */
|
|
3010
|
+
function workspaceExcludes(request) {
|
|
3011
|
+
const excludes = campaignExcludes(request);
|
|
3012
|
+
if (request.repository.git === "synthesized")
|
|
3013
|
+
excludes.delete(portablePathKey(".git"));
|
|
3014
|
+
return excludes;
|
|
3015
|
+
}
|
|
3016
|
+
/**
|
|
3017
|
+
* Commits the snapshot once into a fresh repository with a fixed identity and date, no template,
|
|
3018
|
+
* hooks or global configuration, honoring the snapshot's own .gitignore. Returns the tree identity.
|
|
3019
|
+
*/
|
|
3020
|
+
async function synthesizeGitWorktree(snapshot) {
|
|
3021
|
+
// An empty file outside the snapshot: Git for Windows cannot open os.devNull as a config file.
|
|
3022
|
+
const globalConfig = path.join(path.dirname(snapshot), "git-global-config");
|
|
3023
|
+
await writeFile(globalConfig, "", { flag: "wx" });
|
|
3024
|
+
const environment = {
|
|
3025
|
+
...environmentFromAllowlist(["PATH", "SystemRoot", "WINDIR", "TEMP", "TMP"]),
|
|
3026
|
+
GIT_CONFIG_NOSYSTEM: "1",
|
|
3027
|
+
GIT_ATTR_NOSYSTEM: "1",
|
|
3028
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
3029
|
+
GIT_CONFIG_GLOBAL: globalConfig,
|
|
3030
|
+
GIT_AUTHOR_NAME: "AssertLedger",
|
|
3031
|
+
GIT_AUTHOR_EMAIL: "snapshot@assertledger.invalid",
|
|
3032
|
+
GIT_AUTHOR_DATE: "1970-01-01T00:00:00Z",
|
|
3033
|
+
GIT_COMMITTER_NAME: "AssertLedger",
|
|
3034
|
+
GIT_COMMITTER_EMAIL: "snapshot@assertledger.invalid",
|
|
3035
|
+
GIT_COMMITTER_DATE: "1970-01-01T00:00:00Z",
|
|
3036
|
+
};
|
|
3037
|
+
const git = async (args) => {
|
|
3038
|
+
const result = await runProcess({
|
|
3039
|
+
executable: "git",
|
|
3040
|
+
args: [
|
|
3041
|
+
"-c",
|
|
3042
|
+
"core.autocrlf=false",
|
|
3043
|
+
"-c",
|
|
3044
|
+
"core.safecrlf=false",
|
|
3045
|
+
"-c",
|
|
3046
|
+
"core.fileMode=false",
|
|
3047
|
+
"-c",
|
|
3048
|
+
"core.hooksPath=",
|
|
3049
|
+
"-c",
|
|
3050
|
+
"commit.gpgsign=false",
|
|
3051
|
+
...args,
|
|
3052
|
+
],
|
|
3053
|
+
cwd: snapshot,
|
|
3054
|
+
environment,
|
|
3055
|
+
timeoutMs: 600_000,
|
|
3056
|
+
maximumOutputBytes: CONTROLLED_REPORT_MAXIMUM_BYTES,
|
|
3057
|
+
});
|
|
3058
|
+
if (result.outcome !== "PASS" || result.stdout.truncated) {
|
|
3059
|
+
throw new Error("REPOSITORY_GIT_SYNTHESIS_FAILED", {
|
|
3060
|
+
cause: result.stderr.text.slice(0, 500),
|
|
3061
|
+
});
|
|
3062
|
+
}
|
|
3063
|
+
return result.stdout.text.trim();
|
|
3064
|
+
};
|
|
3065
|
+
const gitVersion = await git(["--version"]);
|
|
3066
|
+
await git(["init", "--quiet", "--template="]);
|
|
3067
|
+
await git(["add", "--all"]);
|
|
3068
|
+
await git([
|
|
3069
|
+
"commit",
|
|
3070
|
+
"--quiet",
|
|
3071
|
+
"--no-verify",
|
|
3072
|
+
"--allow-empty",
|
|
3073
|
+
"--message",
|
|
3074
|
+
"AssertLedger snapshot",
|
|
3075
|
+
]);
|
|
3076
|
+
const treeId = await git(["rev-parse", "HEAD^{tree}"]);
|
|
3077
|
+
if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(treeId)) {
|
|
3078
|
+
throw new Error("REPOSITORY_GIT_SYNTHESIS_FAILED");
|
|
3079
|
+
}
|
|
3080
|
+
return { mode: "synthesized", treeId, gitVersion };
|
|
3081
|
+
}
|
|
3082
|
+
/** The selection a designated campaign gives the Bun driver: load every test, or run one. */
|
|
3083
|
+
function designatedSelection(request, candidate) {
|
|
3084
|
+
if (candidate !== null) {
|
|
3085
|
+
const designated = candidate.designated;
|
|
3086
|
+
if (designated === undefined)
|
|
3087
|
+
throw new Error("DESIGNATED_CANDIDATE_REQUIRED");
|
|
3088
|
+
return {
|
|
3089
|
+
mode: "run",
|
|
3090
|
+
test: { file: designated.file, path: designated.path },
|
|
3091
|
+
expectedFailure: designated.expectedFailure,
|
|
3092
|
+
testTimeoutMs: request.adapter.kind === "bun-test-designated" ? request.adapter.testTimeoutMs : null,
|
|
3093
|
+
};
|
|
3094
|
+
}
|
|
3095
|
+
return {
|
|
3096
|
+
mode: "load",
|
|
3097
|
+
tests: request.candidates.map((entry) => {
|
|
3098
|
+
if (entry.designated === undefined)
|
|
3099
|
+
throw new Error("DESIGNATED_CANDIDATE_REQUIRED");
|
|
3100
|
+
return { file: entry.designated.file, path: entry.designated.path };
|
|
3101
|
+
}),
|
|
3102
|
+
};
|
|
3103
|
+
}
|
|
3104
|
+
/** The test files a designated run loads: every designated file for a control, one for a run. */
|
|
3105
|
+
function designatedFiles(request, candidate) {
|
|
3106
|
+
const files = (candidate === null ? request.candidates : [candidate]).map((entry) => entry.designated?.file ?? "");
|
|
3107
|
+
return [...new Map(files.map((file) => [portablePathKey(file), file])).values()].sort(comparePortablePaths);
|
|
2640
3108
|
}
|
|
2641
3109
|
function classifyNodeTest(result, report, hasCandidate) {
|
|
2642
3110
|
if (result.outcome === "TIMEOUT")
|
|
@@ -2654,7 +3122,7 @@ function classifyNodeTest(result, report, hasCandidate) {
|
|
|
2654
3122
|
candidateFailuresAllAssertions: report?.candidateFailuresAllAssertions ?? false,
|
|
2655
3123
|
});
|
|
2656
3124
|
}
|
|
2657
|
-
async function copyRepository(source, destination, excludes) {
|
|
3125
|
+
async function copyRepository(source, destination, excludes, omittedLinks = new Set()) {
|
|
2658
3126
|
await cp(source, destination, {
|
|
2659
3127
|
recursive: true,
|
|
2660
3128
|
force: false,
|
|
@@ -2667,13 +3135,16 @@ async function copyRepository(source, destination, excludes) {
|
|
|
2667
3135
|
return false;
|
|
2668
3136
|
}
|
|
2669
3137
|
if ((await lstat(sourcePath)).isSymbolicLink()) {
|
|
3138
|
+
// Only a link already judged outside every executed test's closure is left out.
|
|
3139
|
+
if (omittedLinks.has(portablePathKey(relative.split(path.sep).join("/"))))
|
|
3140
|
+
return false;
|
|
2670
3141
|
throw new Error("UNSUPPORTED_REPOSITORY_SYMLINK");
|
|
2671
3142
|
}
|
|
2672
3143
|
return true;
|
|
2673
3144
|
},
|
|
2674
3145
|
});
|
|
2675
3146
|
}
|
|
2676
|
-
async function executeTrustedLocalAdapter(request, workspace, temporaryRoot, candidateFiles, nodeTestReporterPath) {
|
|
3147
|
+
async function executeTrustedLocalAdapter(request, workspace, temporaryRoot, candidate, candidateFiles, nodeTestReporterPath) {
|
|
2677
3148
|
const resultFile = path.join(temporaryRoot, "structured-command-result.json");
|
|
2678
3149
|
const environment = environmentFromAllowlist(trustedLocalEnvironmentAllowlist(request));
|
|
2679
3150
|
if (request.adapter.kind === "testforge-command") {
|
|
@@ -2683,6 +3154,9 @@ async function executeTrustedLocalAdapter(request, workspace, temporaryRoot, can
|
|
|
2683
3154
|
else if (request.adapter.kind === "bun-test") {
|
|
2684
3155
|
environment.TESTFORGE_CANDIDATE_FILES = JSON.stringify(candidateFiles);
|
|
2685
3156
|
}
|
|
3157
|
+
else if (request.adapter.kind === "bun-test-designated") {
|
|
3158
|
+
environment.ASSERTLEDGER_BUN_DESIGNATED = JSON.stringify(designatedSelection(request, candidate));
|
|
3159
|
+
}
|
|
2686
3160
|
else {
|
|
2687
3161
|
if (nodeTestReporterPath === undefined)
|
|
2688
3162
|
throw new Error("NODE_TEST_REPORTER_MISSING");
|
|
@@ -2705,9 +3179,18 @@ async function executeTrustedLocalAdapter(request, workspace, temporaryRoot, can
|
|
|
2705
3179
|
BUN_TEST_HELPER_PATH,
|
|
2706
3180
|
...request.adapter.baseTestFiles,
|
|
2707
3181
|
]
|
|
2708
|
-
: request.adapter.
|
|
3182
|
+
: request.adapter.kind === "bun-test-designated"
|
|
3183
|
+
? [
|
|
3184
|
+
BUN_TEST_DRIVER_PATH,
|
|
3185
|
+
request.adapter.executable,
|
|
3186
|
+
BUN_TEST_HELPER_PATH,
|
|
3187
|
+
...designatedFiles(request, candidate),
|
|
3188
|
+
]
|
|
3189
|
+
: request.adapter.arguments;
|
|
2709
3190
|
const result = await runProcess({
|
|
2710
|
-
executable: request.adapter.kind === "bun-test"
|
|
3191
|
+
executable: request.adapter.kind === "bun-test" || request.adapter.kind === "bun-test-designated"
|
|
3192
|
+
? process.execPath
|
|
3193
|
+
: request.adapter.executable,
|
|
2711
3194
|
args,
|
|
2712
3195
|
cwd: workspace,
|
|
2713
3196
|
environment,
|
|
@@ -2716,7 +3199,7 @@ async function executeTrustedLocalAdapter(request, workspace, temporaryRoot, can
|
|
|
2716
3199
|
});
|
|
2717
3200
|
const structuredReport = request.adapter.kind === "testforge-command"
|
|
2718
3201
|
? await readStructuredCommandReport(resultFile, result, CONTROLLED_REPORT_MAXIMUM_BYTES)
|
|
2719
|
-
: request.adapter.kind === "bun-test"
|
|
3202
|
+
: request.adapter.kind === "bun-test" || request.adapter.kind === "bun-test-designated"
|
|
2720
3203
|
? readBunDriverReport(result)
|
|
2721
3204
|
: undefined;
|
|
2722
3205
|
return {
|
|
@@ -2731,14 +3214,14 @@ async function executeObservation(request, repositoryRoot, world, candidate, att
|
|
|
2731
3214
|
const temporaryRoot = await realpath(await mkdtemp(path.join(os.tmpdir(), "testforge-run-")));
|
|
2732
3215
|
const workspace = path.join(temporaryRoot, "repository");
|
|
2733
3216
|
try {
|
|
2734
|
-
await copyRepository(repositoryRoot, workspace,
|
|
3217
|
+
await copyRepository(repositoryRoot, workspace, workspaceExcludes(request));
|
|
2735
3218
|
await applyFiles(workspace, world.files);
|
|
2736
3219
|
if (candidate)
|
|
2737
3220
|
await applyFiles(workspace, candidate.files);
|
|
2738
3221
|
const candidateFiles = candidate ? candidate.files.map((file) => file.path) : [];
|
|
2739
3222
|
const { result, structuredReport, nodeTestReport } = container === undefined
|
|
2740
|
-
? await executeTrustedLocalAdapter(request, workspace, temporaryRoot, candidateFiles, nodeTestReporterPath)
|
|
2741
|
-
: await executeContainerAdapter(request, container, workspace, candidateFiles);
|
|
3223
|
+
? await executeTrustedLocalAdapter(request, workspace, temporaryRoot, candidate, candidateFiles, nodeTestReporterPath)
|
|
3224
|
+
: await executeContainerAdapter(request, container, workspace, candidate, candidateFiles);
|
|
2742
3225
|
const nodeClassification = request.adapter.kind === "node-test"
|
|
2743
3226
|
? classifyNodeTest(result, nodeTestReport, candidate !== null)
|
|
2744
3227
|
: undefined;
|
|
@@ -2767,7 +3250,119 @@ async function executeObservation(request, repositoryRoot, world, candidate, att
|
|
|
2767
3250
|
await removeTemporaryDirectory(temporaryRoot);
|
|
2768
3251
|
}
|
|
2769
3252
|
}
|
|
2770
|
-
|
|
3253
|
+
function localBunDriverExecutor(executable, environmentAllowlist, timeoutMs, maximumOutputBytes) {
|
|
3254
|
+
return async (workspace, files, environment) => readBunDriverReport(await runProcess({
|
|
3255
|
+
executable: process.execPath,
|
|
3256
|
+
args: [BUN_TEST_DRIVER_PATH, executable, BUN_TEST_HELPER_PATH, ...files],
|
|
3257
|
+
cwd: workspace,
|
|
3258
|
+
environment: { ...environmentFromAllowlist(environmentAllowlist), ...environment },
|
|
3259
|
+
timeoutMs: Math.min(timeoutMs, 5_000),
|
|
3260
|
+
maximumOutputBytes: Math.min(maximumOutputBytes, CONTROLLED_REPORT_MAXIMUM_BYTES),
|
|
3261
|
+
}));
|
|
3262
|
+
}
|
|
3263
|
+
function containerBunDriverExecutor(container, executable, timeoutMs, maximumOutputBytes) {
|
|
3264
|
+
return async (workspace, files, environment) => (await runContainerBunDriver(container, workspace, executable, files, environment, Math.min(timeoutMs, 30_000), Math.min(maximumOutputBytes, CONTROLLED_REPORT_MAXIMUM_BYTES))).report;
|
|
3265
|
+
}
|
|
3266
|
+
const DESIGNATED_PROBES = (() => {
|
|
3267
|
+
const run = (name, body, outcome, expectedFailure) => {
|
|
3268
|
+
const counted = outcome !== "NO_TEST_DISCOVERED" && outcome !== "INFRA_ERROR";
|
|
3269
|
+
return {
|
|
3270
|
+
name,
|
|
3271
|
+
body,
|
|
3272
|
+
mode: "run",
|
|
3273
|
+
...(expectedFailure === undefined ? {} : { expectedFailure }),
|
|
3274
|
+
outcome,
|
|
3275
|
+
attributed: outcome === "PASS" || outcome === "ASSERTION_FAILURE",
|
|
3276
|
+
testsDiscovered: counted ? 1 : 0,
|
|
3277
|
+
candidateTestsDiscovered: counted ? 1 : 0,
|
|
3278
|
+
};
|
|
3279
|
+
};
|
|
3280
|
+
const failing = 'test("probe", () => expect(1).toBe(2));';
|
|
3281
|
+
return [
|
|
3282
|
+
run("expect-assertion", failing, "ASSERTION_FAILURE"),
|
|
3283
|
+
run("expected-line", failing, "ASSERTION_FAILURE", "error: expect(received).toBe(expected)"),
|
|
3284
|
+
run("other-line", failing, "PROCESS_CRASH", "expect(received).toEqual(expected)"),
|
|
3285
|
+
run("assert-same", 'test("probe", () => assertSame(1, 2));', "ASSERTION_FAILURE"),
|
|
3286
|
+
run("async-matcher", 'test("probe", async () => { await expect(Promise.resolve(1)).resolves.toBe(2); });', "ASSERTION_FAILURE"),
|
|
3287
|
+
run("forged-message", 'test("probe", () => { throw new Error("expect(received).toBe(expected)"); });', "PROCESS_CRASH"),
|
|
3288
|
+
run("caught-expect", 'test("probe", () => { try { expect(1).toBe(2); } catch {} throw new Error("generic"); });', "PROCESS_CRASH"),
|
|
3289
|
+
run("replayed-expect", 'let saved; try { expect(1).toBe(2); } catch (error) { saved = error; } test("probe", () => { throw saved; });', "PROCESS_CRASH"),
|
|
3290
|
+
run("matcher-usage", 'test("probe", () => expect(1).toContain());', "PROCESS_CRASH"),
|
|
3291
|
+
run("custom-matcher", 'expect.extend({ toFail() { throw new Error("expect(received).toFail()"); } }); test("probe", () => expect(1).toFail());', "PROCESS_CRASH"),
|
|
3292
|
+
run("passing", 'test("probe", () => expect(1).toBe(1));', "PASS"),
|
|
3293
|
+
run("absent", 'test("other", () => expect(1).toBe(2));', "NO_TEST_DISCOVERED"),
|
|
3294
|
+
run("hook-failure", 'afterEach(() => { throw new Error("hook"); }); test("probe", () => expect(1).toBe(2));', "INFRA_ERROR"),
|
|
3295
|
+
{
|
|
3296
|
+
// The callback fails its matcher only after Bun's per-test timeout: never an assertion.
|
|
3297
|
+
...run("late-after-timeout", 'test("probe", () => { const end = Date.now() + 300; while (Date.now() < end); expect(1).toBe(2); });', "INFRA_ERROR"),
|
|
3298
|
+
testTimeoutMs: 50,
|
|
3299
|
+
},
|
|
3300
|
+
{
|
|
3301
|
+
name: "load",
|
|
3302
|
+
body: 'test("probe", () => expect(1).toBe(2)); test("other", () => {});',
|
|
3303
|
+
mode: "load",
|
|
3304
|
+
outcome: "PASS",
|
|
3305
|
+
attributed: false,
|
|
3306
|
+
testsDiscovered: 2,
|
|
3307
|
+
candidateTestsDiscovered: 0,
|
|
3308
|
+
},
|
|
3309
|
+
{
|
|
3310
|
+
name: "load-failure",
|
|
3311
|
+
body: 'import "./missing-module"; test("probe", () => {});',
|
|
3312
|
+
mode: "load",
|
|
3313
|
+
outcome: "INFRA_ERROR",
|
|
3314
|
+
attributed: false,
|
|
3315
|
+
testsDiscovered: 0,
|
|
3316
|
+
candidateTestsDiscovered: 0,
|
|
3317
|
+
},
|
|
3318
|
+
];
|
|
3319
|
+
})();
|
|
3320
|
+
/**
|
|
3321
|
+
* Qualifies the designated mode before a campaign: only a built-in matcher's or assertSame's
|
|
3322
|
+
* failure inside the designated test, with the declared first line, is an assertion; forged,
|
|
3323
|
+
* caught, replayed, misused and custom failures, hook and load failures and an absent test are not.
|
|
3324
|
+
*/
|
|
3325
|
+
async function runBunDesignatedPreflight(executor) {
|
|
3326
|
+
const header = 'import { afterEach, expect, test } from "bun:test"; import { assertSame } from "assertledger/bun";';
|
|
3327
|
+
try {
|
|
3328
|
+
const recorded = [];
|
|
3329
|
+
for (const probe of DESIGNATED_PROBES) {
|
|
3330
|
+
const report = await withEmptyWorkspace(async (workspace) => {
|
|
3331
|
+
await writeFile(path.join(workspace, "package.json"), '{"type":"module"}\n');
|
|
3332
|
+
await writeFile(path.join(workspace, "probe.test.ts"), `${header} ${probe.body}\n`);
|
|
3333
|
+
const test = { file: "probe.test.ts", path: ["probe"] };
|
|
3334
|
+
return executor(workspace, ["probe.test.ts"], {
|
|
3335
|
+
ASSERTLEDGER_BUN_DESIGNATED: JSON.stringify(probe.mode === "run"
|
|
3336
|
+
? {
|
|
3337
|
+
mode: "run",
|
|
3338
|
+
test,
|
|
3339
|
+
expectedFailure: probe.expectedFailure ?? null,
|
|
3340
|
+
testTimeoutMs: probe.testTimeoutMs ?? null,
|
|
3341
|
+
}
|
|
3342
|
+
: { mode: "load", tests: [test] }),
|
|
3343
|
+
});
|
|
3344
|
+
});
|
|
3345
|
+
if (report?.outcome !== probe.outcome ||
|
|
3346
|
+
report.attributed !== probe.attributed ||
|
|
3347
|
+
report.testsDiscovered !== probe.testsDiscovered ||
|
|
3348
|
+
report.candidateTestsDiscovered !== probe.candidateTestsDiscovered) {
|
|
3349
|
+
throw new Error("BUN_TEST_PROFILE_PREFLIGHT_FAILED", { cause: probe.name });
|
|
3350
|
+
}
|
|
3351
|
+
recorded.push({
|
|
3352
|
+
name: probe.name,
|
|
3353
|
+
outcome: report.outcome,
|
|
3354
|
+
attributed: report.attributed,
|
|
3355
|
+
testsDiscovered: report.testsDiscovered,
|
|
3356
|
+
candidateTestsDiscovered: report.candidateTestsDiscovered,
|
|
3357
|
+
});
|
|
3358
|
+
}
|
|
3359
|
+
return { protocolVersion: "1.0.0", probes: recorded };
|
|
3360
|
+
}
|
|
3361
|
+
catch (error) {
|
|
3362
|
+
throw new Error("BUN_TEST_PROFILE_PREFLIGHT_FAILED", { cause: error });
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
async function runBunTestRuntimePreflight(executor) {
|
|
2771
3366
|
const probeSources = [
|
|
2772
3367
|
{
|
|
2773
3368
|
name: "assertion",
|
|
@@ -2838,18 +3433,9 @@ async function runBunTestRuntimePreflight(executable, environmentAllowlist, time
|
|
|
2838
3433
|
await writeFile(path.join(workspace, "base.test.ts"), 'import { test } from "bun:test"; test("control", () => {});\n');
|
|
2839
3434
|
const candidatePath = path.join(workspace, "candidate.test.ts");
|
|
2840
3435
|
await writeFile(candidatePath, probe.source);
|
|
2841
|
-
const
|
|
2842
|
-
|
|
2843
|
-
args: [BUN_TEST_DRIVER_PATH, executable, BUN_TEST_HELPER_PATH, "base.test.ts"],
|
|
2844
|
-
cwd: workspace,
|
|
2845
|
-
environment: {
|
|
2846
|
-
...environmentFromAllowlist(environmentAllowlist),
|
|
2847
|
-
TESTFORGE_CANDIDATE_FILES: JSON.stringify(["candidate.test.ts"]),
|
|
2848
|
-
},
|
|
2849
|
-
timeoutMs: Math.min(timeoutMs, 5_000),
|
|
2850
|
-
maximumOutputBytes: Math.min(maximumOutputBytes, CONTROLLED_REPORT_MAXIMUM_BYTES),
|
|
3436
|
+
const report = await executor(workspace, ["base.test.ts"], {
|
|
3437
|
+
TESTFORGE_CANDIDATE_FILES: JSON.stringify(["candidate.test.ts"]),
|
|
2851
3438
|
});
|
|
2852
|
-
const report = readBunDriverReport(result);
|
|
2853
3439
|
const expectedCandidateCount = probe.name === "hook-failure"
|
|
2854
3440
|
? 0
|
|
2855
3441
|
: probe.name === "replayed-row" ||
|
|
@@ -2886,14 +3472,20 @@ export async function verifyCampaign(value, options = {}) {
|
|
|
2886
3472
|
? await prepareContainerBackend(options.containerRuntime?.command ?? DEFAULT_CONTAINER_RUNTIME_COMMAND, request.isolation, runBoundedProcess, os.tmpdir())
|
|
2887
3473
|
: undefined;
|
|
2888
3474
|
const repositoryRoot = await resolveRepositoryRoot(request.repository.root);
|
|
2889
|
-
const
|
|
2890
|
-
const
|
|
3475
|
+
const version4 = request.schemaVersion === VERIFICATION_SCHEMA_VERSION_V4;
|
|
3476
|
+
const excludes = campaignExcludes(request);
|
|
3477
|
+
// Before v4 any link refuses the walk; v4 collects them and judges each one below.
|
|
3478
|
+
const sourceLinks = version4 ? [] : undefined;
|
|
3479
|
+
const sourceInventory = await walkFiles(repositoryRoot, excludes, sourceLinks);
|
|
2891
3480
|
if (sourceInventory.files.length > request.budgets.maximumRepositoryFiles) {
|
|
2892
3481
|
throw new Error("REPOSITORY_FILE_BUDGET_EXCEEDED");
|
|
2893
3482
|
}
|
|
2894
3483
|
if (sourceInventory.totalBytes > request.budgets.maximumRepositoryBytes) {
|
|
2895
3484
|
throw new Error("REPOSITORY_BYTES_BUDGET_EXCEEDED");
|
|
2896
3485
|
}
|
|
3486
|
+
const omittedLinks = sourceLinks === undefined || sourceLinks.length === 0
|
|
3487
|
+
? []
|
|
3488
|
+
: await campaignOmittedLinks(request, repositoryRoot, sourceInventory.files, sourceLinks);
|
|
2897
3489
|
const nodeIdentity = request.adapter.kind !== "node-test"
|
|
2898
3490
|
? undefined
|
|
2899
3491
|
: container === undefined
|
|
@@ -2902,15 +3494,19 @@ export async function verifyCampaign(value, options = {}) {
|
|
|
2902
3494
|
if (request.adapter.kind === "node-test" && nodeIdentity !== undefined) {
|
|
2903
3495
|
request.adapter.executable = nodeIdentity.resolvedExecutable;
|
|
2904
3496
|
}
|
|
2905
|
-
const
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
3497
|
+
const bunAdapter = request.adapter.kind === "bun-test" || request.adapter.kind === "bun-test-designated";
|
|
3498
|
+
const bunIdentity = request.adapter.kind !== "bun-test" && request.adapter.kind !== "bun-test-designated"
|
|
3499
|
+
? undefined
|
|
3500
|
+
: container === undefined
|
|
3501
|
+
? await probeBunTestExecutable(request.adapter.executable, repositoryRoot, trustedLocalEnvironmentAllowlist(request), request.budgets.timeoutMsPerExecution, request.budgets.maximumOutputBytes)
|
|
3502
|
+
: await probeContainerBunExecutable(container, request.adapter.executable, request.budgets.timeoutMsPerExecution, request.budgets.maximumOutputBytes);
|
|
3503
|
+
if ((request.adapter.kind === "bun-test" || request.adapter.kind === "bun-test-designated") &&
|
|
3504
|
+
bunIdentity !== undefined) {
|
|
2909
3505
|
request.adapter.executable = bunIdentity.resolvedExecutable;
|
|
2910
3506
|
}
|
|
2911
|
-
const bunDriverDigest =
|
|
2912
|
-
const bunHelperDigest =
|
|
2913
|
-
const bunPreloadDigest =
|
|
3507
|
+
const bunDriverDigest = bunAdapter ? await sha256File(BUN_TEST_DRIVER_PATH) : undefined;
|
|
3508
|
+
const bunHelperDigest = bunAdapter ? await sha256File(BUN_TEST_HELPER_PATH) : undefined;
|
|
3509
|
+
const bunPreloadDigest = bunAdapter ? await sha256File(BUN_TEST_PRELOAD_PATH) : undefined;
|
|
2914
3510
|
const nodeRuntimePreflight = request.adapter.kind === "node-test" && nodeIdentity !== undefined
|
|
2915
3511
|
? await runNodeTestRuntimePreflight({
|
|
2916
3512
|
executable: nodeIdentity.resolvedExecutable,
|
|
@@ -2926,13 +3522,20 @@ export async function verifyCampaign(value, options = {}) {
|
|
|
2926
3522
|
}),
|
|
2927
3523
|
})
|
|
2928
3524
|
: undefined;
|
|
2929
|
-
const
|
|
2930
|
-
?
|
|
2931
|
-
: undefined
|
|
3525
|
+
const bunExecutor = bunIdentity === undefined
|
|
3526
|
+
? undefined
|
|
3527
|
+
: container === undefined
|
|
3528
|
+
? localBunDriverExecutor(bunIdentity.resolvedExecutable, trustedLocalEnvironmentAllowlist(request), request.budgets.timeoutMsPerExecution, request.budgets.maximumOutputBytes)
|
|
3529
|
+
: containerBunDriverExecutor(container, bunIdentity.resolvedExecutable, request.budgets.timeoutMsPerExecution, request.budgets.maximumOutputBytes);
|
|
3530
|
+
const bunRuntimePreflight = bunExecutor === undefined
|
|
3531
|
+
? undefined
|
|
3532
|
+
: request.adapter.kind === "bun-test-designated"
|
|
3533
|
+
? await runBunDesignatedPreflight(bunExecutor)
|
|
3534
|
+
: await runBunTestRuntimePreflight(bunExecutor);
|
|
2932
3535
|
const campaignRoot = await realpath(await mkdtemp(path.join(os.tmpdir(), "testforge-campaign-")));
|
|
2933
3536
|
const repositorySnapshot = path.join(campaignRoot, "repository");
|
|
2934
3537
|
try {
|
|
2935
|
-
await copyRepository(repositoryRoot, repositorySnapshot, excludes);
|
|
3538
|
+
await copyRepository(repositoryRoot, repositorySnapshot, excludes, new Set(omittedLinks.map(portablePathKey)));
|
|
2936
3539
|
const snapshotInventory = await walkFiles(repositorySnapshot, excludes);
|
|
2937
3540
|
if (snapshotInventory.files.length > request.budgets.maximumRepositoryFiles) {
|
|
2938
3541
|
throw new Error("REPOSITORY_FILE_BUDGET_EXCEEDED");
|
|
@@ -2940,9 +3543,18 @@ export async function verifyCampaign(value, options = {}) {
|
|
|
2940
3543
|
if (snapshotInventory.totalBytes > request.budgets.maximumRepositoryBytes) {
|
|
2941
3544
|
throw new Error("REPOSITORY_BYTES_BUDGET_EXCEEDED");
|
|
2942
3545
|
}
|
|
3546
|
+
const gitRecord = request.repository.git === "synthesized"
|
|
3547
|
+
? await synthesizeGitWorktree(repositorySnapshot)
|
|
3548
|
+
: null;
|
|
3549
|
+
const designatedTestFiles = request.candidates.flatMap((candidate) => candidate.designated === undefined ? [] : [candidate.designated.file]);
|
|
3550
|
+
const snapshotFiles = new Set(snapshotInventory.files.map(portablePathKey));
|
|
3551
|
+
if (designatedTestFiles.some((file) => !snapshotFiles.has(portablePathKey(file)))) {
|
|
3552
|
+
throw new Error("DESIGNATED_TEST_FILE_MISSING");
|
|
3553
|
+
}
|
|
2943
3554
|
const inputPaths = [
|
|
2944
3555
|
...request.worlds.flatMap((world) => world.files.map((file) => file.path)),
|
|
2945
3556
|
...request.candidates.flatMap((candidate) => candidate.files.map((file) => file.path)),
|
|
3557
|
+
...designatedTestFiles,
|
|
2946
3558
|
...(request.adapter.kind === "node-test" || request.adapter.kind === "bun-test"
|
|
2947
3559
|
? request.adapter.baseTestFiles
|
|
2948
3560
|
: []),
|
|
@@ -2964,7 +3576,11 @@ export async function verifyCampaign(value, options = {}) {
|
|
|
2964
3576
|
if (nodeTestReporterPath !== undefined) {
|
|
2965
3577
|
await writeFile(nodeTestReporterPath, NODE_TEST_REPORTER_SOURCE, { flag: "wx" });
|
|
2966
3578
|
}
|
|
2967
|
-
|
|
3579
|
+
// A v4 digest covers exactly the snapshot, dependencies included when the request says so.
|
|
3580
|
+
const repositoryDigest = version4
|
|
3581
|
+
? await repositoryInventoryDigest(repositorySnapshot, snapshotInventory.files)
|
|
3582
|
+
: (await analyzeRepository(repositorySnapshot))
|
|
3583
|
+
.repositoryDigest;
|
|
2968
3584
|
const observations = [];
|
|
2969
3585
|
for (const world of request.worlds) {
|
|
2970
3586
|
for (let attempt = 1; attempt <= request.policy.requiredAttempts; attempt += 1) {
|
|
@@ -2978,16 +3594,37 @@ export async function verifyCampaign(value, options = {}) {
|
|
|
2978
3594
|
}
|
|
2979
3595
|
}
|
|
2980
3596
|
}
|
|
3597
|
+
// A designated candidate is identified by its selection and the bytes of its test file.
|
|
3598
|
+
const designatedIdentities = new Map();
|
|
3599
|
+
for (const candidate of request.candidates) {
|
|
3600
|
+
if (candidate.designated === undefined)
|
|
3601
|
+
continue;
|
|
3602
|
+
const bytes = await readFile(path.join(repositorySnapshot, ...candidate.designated.file.split("/")));
|
|
3603
|
+
designatedIdentities.set(candidate.id, {
|
|
3604
|
+
digest: sha256Canonical({
|
|
3605
|
+
test: { file: candidate.designated.file, path: candidate.designated.path },
|
|
3606
|
+
expectedFailure: candidate.designated.expectedFailure,
|
|
3607
|
+
testFileDigest: rawSha256(bytes),
|
|
3608
|
+
}),
|
|
3609
|
+
sizeBytes: bytes.byteLength,
|
|
3610
|
+
});
|
|
3611
|
+
}
|
|
3612
|
+
const windowsNative = request.isolation.kind === "windows-native";
|
|
3613
|
+
const isolationLevel = container !== undefined
|
|
3614
|
+
? "CONTAINER"
|
|
3615
|
+
: windowsNative
|
|
3616
|
+
? "WINDOWS_NATIVE_UNSANDBOXED"
|
|
3617
|
+
: "UNSANDBOXED";
|
|
2981
3618
|
const evidence = {
|
|
2982
3619
|
schemaVersion: request.schemaVersion,
|
|
2983
|
-
repositoryDigest
|
|
3620
|
+
repositoryDigest,
|
|
2984
3621
|
evidenceContext: {
|
|
2985
3622
|
engine: { name: "testforge", version: "0.1.0" },
|
|
2986
3623
|
adapter: {
|
|
2987
3624
|
name: request.adapter.kind,
|
|
2988
3625
|
version: request.adapter.kind === "testforge-command"
|
|
2989
3626
|
? request.adapter.protocolVersion
|
|
2990
|
-
:
|
|
3627
|
+
: bunAdapter
|
|
2991
3628
|
? (bunIdentity?.bunVersion ?? "unprobed")
|
|
2992
3629
|
: (nodeIdentity?.nodeVersion ?? "unprobed"),
|
|
2993
3630
|
configuration: request.adapter.kind === "node-test" && nodeIdentity !== undefined
|
|
@@ -3036,10 +3673,45 @@ export async function verifyCampaign(value, options = {}) {
|
|
|
3036
3673
|
...request.adapter.baseTestFiles,
|
|
3037
3674
|
],
|
|
3038
3675
|
}
|
|
3039
|
-
: request.adapter
|
|
3676
|
+
: request.adapter.kind === "bun-test-designated" && bunIdentity !== undefined
|
|
3677
|
+
? {
|
|
3678
|
+
kind: "bun-test-designated",
|
|
3679
|
+
profile: BUN_TEST_DESIGNATED_ADAPTER_PROFILE,
|
|
3680
|
+
requestedExecutable: bunIdentity.requestedExecutable,
|
|
3681
|
+
resolvedExecutable: bunIdentity.resolvedExecutable,
|
|
3682
|
+
executableDigest: bunIdentity.executableDigest,
|
|
3683
|
+
bunVersion: bunIdentity.bunVersion,
|
|
3684
|
+
bunRevision: bunIdentity.bunRevision,
|
|
3685
|
+
driverDigest: bunDriverDigest,
|
|
3686
|
+
helperDigest: bunHelperDigest,
|
|
3687
|
+
preloadDigest: bunPreloadDigest,
|
|
3688
|
+
runtimePreflight: bunRuntimePreflight,
|
|
3689
|
+
arguments: [
|
|
3690
|
+
"test",
|
|
3691
|
+
"--max-concurrency=1",
|
|
3692
|
+
"--retry=0",
|
|
3693
|
+
"--preload",
|
|
3694
|
+
"./node_modules/assertledger/preload.mjs",
|
|
3695
|
+
"--reporter=junit",
|
|
3696
|
+
"--reporter-outfile=<generated-junit-file>",
|
|
3697
|
+
"--test-name-pattern=<anchored-designated-test-path>",
|
|
3698
|
+
...(request.adapter.testTimeoutMs === null
|
|
3699
|
+
? []
|
|
3700
|
+
: [`--timeout=${request.adapter.testTimeoutMs}`]),
|
|
3701
|
+
"<designated-test-file>",
|
|
3702
|
+
],
|
|
3703
|
+
testTimeoutMs: request.adapter.testTimeoutMs,
|
|
3704
|
+
designatedTests: request.candidates.map((candidate) => ({
|
|
3705
|
+
candidateId: candidate.id,
|
|
3706
|
+
file: candidate.designated?.file ?? null,
|
|
3707
|
+
path: candidate.designated?.path ?? [],
|
|
3708
|
+
expectedFailure: candidate.designated?.expectedFailure ?? null,
|
|
3709
|
+
})),
|
|
3710
|
+
}
|
|
3711
|
+
: request.adapter,
|
|
3040
3712
|
},
|
|
3041
3713
|
execution: {
|
|
3042
|
-
isolation:
|
|
3714
|
+
isolation: isolationLevel,
|
|
3043
3715
|
environmentAllowlist: container === undefined
|
|
3044
3716
|
? trustedLocalEnvironmentAllowlist(request)
|
|
3045
3717
|
: container.isolation.environment.map((variable) => variable.name).sort(),
|
|
@@ -3048,11 +3720,26 @@ export async function verifyCampaign(value, options = {}) {
|
|
|
3048
3720
|
...(request.schemaVersion === "1.0.0"
|
|
3049
3721
|
? {}
|
|
3050
3722
|
: {
|
|
3051
|
-
backend: container
|
|
3052
|
-
?
|
|
3053
|
-
:
|
|
3723
|
+
backend: container !== undefined
|
|
3724
|
+
? container.record
|
|
3725
|
+
: windowsNative
|
|
3726
|
+
? {
|
|
3727
|
+
kind: "windows-native",
|
|
3728
|
+
level: "WINDOWS_NATIVE_UNSANDBOXED",
|
|
3729
|
+
platform: "win32",
|
|
3730
|
+
}
|
|
3731
|
+
: { kind: "trusted-local", level: "UNSANDBOXED" },
|
|
3054
3732
|
}),
|
|
3055
3733
|
},
|
|
3734
|
+
...(version4
|
|
3735
|
+
? {
|
|
3736
|
+
repository: {
|
|
3737
|
+
includeDependencies: request.repository.includeDependencies,
|
|
3738
|
+
omittedLinks: [...omittedLinks].sort(),
|
|
3739
|
+
git: gitRecord,
|
|
3740
|
+
},
|
|
3741
|
+
}
|
|
3742
|
+
: {}),
|
|
3056
3743
|
worlds: request.worlds.map((world) => ({
|
|
3057
3744
|
id: world.id,
|
|
3058
3745
|
provenance: world.provenance ?? "unspecified",
|
|
@@ -3064,29 +3751,71 @@ export async function verifyCampaign(value, options = {}) {
|
|
|
3064
3751
|
},
|
|
3065
3752
|
policy: request.policy,
|
|
3066
3753
|
worlds: request.worlds.map(({ files: _files, provenance: _provenance, ...world }) => world),
|
|
3067
|
-
candidates: request.candidates.map((candidate) =>
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3754
|
+
candidates: request.candidates.map((candidate) => {
|
|
3755
|
+
const designated = designatedIdentities.get(candidate.id);
|
|
3756
|
+
return {
|
|
3757
|
+
id: candidate.id,
|
|
3758
|
+
digest: designated?.digest ?? sha256Canonical(candidate.files),
|
|
3759
|
+
sizeBytes: designated?.sizeBytes ??
|
|
3760
|
+
candidate.files.reduce((total, file) => total + Buffer.byteLength(file.content), 0),
|
|
3761
|
+
};
|
|
3762
|
+
}),
|
|
3072
3763
|
observations,
|
|
3073
3764
|
};
|
|
3074
3765
|
const manifest = decideEvidence(evidence);
|
|
3766
|
+
const designatedLimitations = request.adapter.kind === "bun-test-designated"
|
|
3767
|
+
? [
|
|
3768
|
+
"Only a failure thrown inside the designated test by a bun:test built-in matcher, in Bun's matcher message form, or by assertSame is admitted as an assertion; with a declared expected line, its first message line must equal it. Custom matchers, usage errors and every other error are not assertion evidence.",
|
|
3769
|
+
"Controls load every designated test file without running a test: they show that each world evaluates the file and its imports and still registers the designated test, not that other tests pass.",
|
|
3770
|
+
"Bun callback instrumentation determines assertion ownership; JUnit counts only check completeness and never classify assertions.",
|
|
3771
|
+
]
|
|
3772
|
+
: [];
|
|
3773
|
+
const snapshotLimitations = version4
|
|
3774
|
+
? [
|
|
3775
|
+
...(request.repository.includeDependencies
|
|
3776
|
+
? [
|
|
3777
|
+
"Dependency directories (node_modules) are part of the snapshot and its digest as found; AssertLedger did not install or verify them.",
|
|
3778
|
+
]
|
|
3779
|
+
: []),
|
|
3780
|
+
...(gitRecord !== null
|
|
3781
|
+
? [
|
|
3782
|
+
"Each execution ran in a synthesized Git repository: the snapshot committed once with a fixed identity and date; the source repository's own Git metadata and history were not used.",
|
|
3783
|
+
]
|
|
3784
|
+
: []),
|
|
3785
|
+
...(omittedLinks.length > 0
|
|
3786
|
+
? [
|
|
3787
|
+
`${omittedLinks.length} repository link(s) outside the module closure of every executed test were left out of the snapshot; the closure follows literal module specifiers only and does not cover files read at run time.`,
|
|
3788
|
+
]
|
|
3789
|
+
: []),
|
|
3790
|
+
]
|
|
3791
|
+
: [];
|
|
3075
3792
|
const finalManifest = {
|
|
3076
3793
|
...manifest,
|
|
3077
3794
|
adapter: request.adapter.kind === "testforge-command"
|
|
3078
3795
|
? { kind: request.adapter.kind, protocolVersion: request.adapter.protocolVersion }
|
|
3079
3796
|
: { kind: request.adapter.kind },
|
|
3080
|
-
isolation: container
|
|
3081
|
-
? {
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3797
|
+
isolation: container !== undefined
|
|
3798
|
+
? { kind: "container", level: "CONTAINER", runtimeCommand: container.runtimeCommand }
|
|
3799
|
+
: windowsNative
|
|
3800
|
+
? {
|
|
3801
|
+
kind: "windows-native",
|
|
3802
|
+
level: "WINDOWS_NATIVE_UNSANDBOXED",
|
|
3803
|
+
acknowledgedUnsafeExecution: true,
|
|
3804
|
+
}
|
|
3805
|
+
: {
|
|
3806
|
+
kind: "trusted-local",
|
|
3807
|
+
level: "UNSANDBOXED",
|
|
3808
|
+
acknowledgedUnsafeExecution: true,
|
|
3809
|
+
},
|
|
3087
3810
|
limitations: container === undefined
|
|
3088
3811
|
? [
|
|
3089
|
-
|
|
3812
|
+
...(windowsNative
|
|
3813
|
+
? [
|
|
3814
|
+
"WINDOWS_NATIVE_UNSANDBOXED execution ran directly on the Windows host at the operator's explicit request; it cannot safely contain hostile code.",
|
|
3815
|
+
]
|
|
3816
|
+
: [
|
|
3817
|
+
"UNSANDBOXED trusted-local execution cannot safely contain hostile candidate code.",
|
|
3818
|
+
]),
|
|
3090
3819
|
"Process-tree termination is best effort and depends on host operating-system facilities.",
|
|
3091
3820
|
...(request.adapter.kind === "bun-test"
|
|
3092
3821
|
? [
|
|
@@ -3094,10 +3823,12 @@ export async function verifyCampaign(value, options = {}) {
|
|
|
3094
3823
|
"Bun callback instrumentation determines assertion ownership; JUnit counts only check completeness and never classify assertions.",
|
|
3095
3824
|
]
|
|
3096
3825
|
: []),
|
|
3826
|
+
...designatedLimitations,
|
|
3827
|
+
...snapshotLimitations,
|
|
3097
3828
|
]
|
|
3098
|
-
: [...CONTAINER_LIMITATIONS],
|
|
3829
|
+
: [...CONTAINER_LIMITATIONS, ...designatedLimitations, ...snapshotLimitations],
|
|
3099
3830
|
};
|
|
3100
|
-
if (
|
|
3831
|
+
if (bunAdapter &&
|
|
3101
3832
|
((await sha256File(BUN_TEST_DRIVER_PATH)) !== bunDriverDigest ||
|
|
3102
3833
|
(await sha256File(BUN_TEST_HELPER_PATH)) !== bunHelperDigest ||
|
|
3103
3834
|
(await sha256File(BUN_TEST_PRELOAD_PATH)) !== bunPreloadDigest)) {
|