assertledger 1.1.1 → 1.3.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 +34 -4
- package/README.md +33 -4
- package/conformance/schema-extensions.json +25 -0
- package/dist/build-info.json +1 -1
- package/dist/cli.d.ts +8 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +292 -25
- package/dist/cli.js.map +1 -1
- package/dist/contracts/index.d.ts +510 -4
- package/dist/contracts/index.d.ts.map +1 -1
- package/dist/contracts/index.js +230 -16
- package/dist/contracts/index.js.map +1 -1
- package/dist/contracts/runtime-doctor.d.ts +123 -0
- package/dist/contracts/runtime-doctor.d.ts.map +1 -1
- package/dist/contracts/runtime-doctor.js +64 -0
- package/dist/contracts/runtime-doctor.js.map +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +5 -2
- package/dist/core/index.js.map +1 -1
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +26 -2
- package/dist/diagnostics.js.map +1 -1
- package/dist/engine/adapters/bun-test-profile.d.ts +17 -0
- package/dist/engine/adapters/bun-test-profile.d.ts.map +1 -0
- package/dist/engine/adapters/bun-test-profile.js +17 -0
- package/dist/engine/adapters/bun-test-profile.js.map +1 -0
- package/dist/engine/connection.d.ts +25 -1
- package/dist/engine/connection.d.ts.map +1 -1
- package/dist/engine/connection.js +144 -31
- package/dist/engine/connection.js.map +1 -1
- package/dist/engine/demo.d.ts +16 -0
- package/dist/engine/demo.d.ts.map +1 -0
- package/dist/engine/demo.js +51 -0
- package/dist/engine/demo.js.map +1 -0
- package/dist/engine/git-regression.d.ts +2 -2
- package/dist/engine/git-regression.d.ts.map +1 -1
- package/dist/engine/git-regression.js.map +1 -1
- package/dist/engine/index.d.ts +26 -5
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +539 -58
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/runtime-doctor.d.ts +13 -4
- package/dist/engine/runtime-doctor.d.ts.map +1 -1
- package/dist/engine/runtime-doctor.js +80 -2
- package/dist/engine/runtime-doctor.js.map +1 -1
- package/dist/engine/setup.d.ts +39 -0
- package/dist/engine/setup.d.ts.map +1 -0
- package/dist/engine/setup.js +411 -0
- package/dist/engine/setup.js.map +1 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +36 -6
- package/dist/mcp/index.js.map +1 -1
- package/dist/sdk/index.d.ts +7 -6
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +20 -7
- package/dist/sdk/index.js.map +1 -1
- package/docs/adapter-protocol.md +61 -0
- package/docs/architecture.md +4 -1
- package/docs/developer-experience.md +39 -5
- package/docs/distribution.md +8 -3
- package/docs/migration-verification-v3.md +33 -0
- package/docs/project-intent.md +7 -5
- package/docs/reference.md +9 -5
- package/docs/repository-init.md +52 -7
- package/docs/runtime-doctor.md +12 -9
- package/integrations/bun/assertions.d.mts +4 -0
- package/integrations/bun/assertions.mjs +25 -0
- package/integrations/bun/driver.d.mts +27 -0
- package/integrations/bun/driver.mjs +383 -0
- package/integrations/bun/preload.mjs +165 -0
- package/integrations/skill/SKILL.md +4 -1
- package/package.json +7 -3
- package/schemas/evidence-manifest.v3.json +897 -0
- package/schemas/repository-init-config.v2.json +210 -0
- package/schemas/repository-init-lock.v2.json +162 -0
- package/schemas/repository-init-result.v2.json +212 -0
- package/schemas/verification-request.v3.json +484 -0
package/dist/engine/index.js
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
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, 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, 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
|
-
import { pathToFileURL } from "node:url";
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
10
10
|
import { createScanner, SyntaxKind } from "typescript/unstable/ast";
|
|
11
|
-
import { AdapterSchema, parseAgenticBenchmarkAcquisitionRequest, parseAgenticBenchmarkAcquisitionResult, parseEvidenceManifest, parseRepositoryAudit, parseRepositoryInitConfig, parseRepositoryInitLock, parseRepositoryInitResult, parseVerificationRequest, parseVersionedVerificationRequest, repositoryInitConfigDigest, repositoryInitLockDigest, } 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, } from "../contracts/index.js";
|
|
12
12
|
import { assertSafeRelativePath, canonicalize, createAgenticBenchmark, decideEvidence, portablePathKey, replayAgenticBenchmark, replayEvidenceManifest, sealManifestArtifact, sha256Canonical, } from "../core/index.js";
|
|
13
|
+
import { BUN_TEST_ADAPTER_PROFILE } from "./adapters/bun-test-profile.js";
|
|
13
14
|
import { NODE_TEST_ADAPTER_PROFILE } from "./adapters/node-test-profile.js";
|
|
14
15
|
import { runNodeTestRuntimePreflight, } from "./adapters/node-test-runtime.js";
|
|
15
|
-
import { CONTAINER_LIMITATIONS, CONTAINER_REPORTER_FILE, CONTAINER_RESULT_FILE, CONTAINER_WORKSPACE, DEFAULT_CONTAINER_RUNTIME_COMMAND, prepareContainerBackend, runContainerExecution, } from "./container.js";
|
|
16
16
|
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";
|
|
17
18
|
import { NODE_TEST_REPORTER_SOURCE } from "./node-test-reporter.js";
|
|
18
|
-
import { runRuntimeDoctorChecks } from "./runtime-doctor.js";
|
|
19
|
+
import { runBunRuntimeDoctorChecks, runRuntimeDoctorChecks, } from "./runtime-doctor.js";
|
|
19
20
|
const DEFAULT_EXCLUDES = new Set([".git", ".testforge", "node_modules"]);
|
|
20
21
|
const SHA256_PREFIX = "sha256:";
|
|
21
22
|
const PROCESS_TERMINATION_GRACE_MS = 250;
|
|
22
23
|
const CONTROLLED_REPORT_MAXIMUM_BYTES = 64 * 1024;
|
|
24
|
+
const BUN_TEST_DRIVER_PATH = fileURLToPath(new URL("../../integrations/bun/driver.mjs", import.meta.url));
|
|
25
|
+
const BUN_TEST_HELPER_PATH = fileURLToPath(new URL("../../integrations/bun/assertions.mjs", import.meta.url));
|
|
26
|
+
const BUN_TEST_PRELOAD_PATH = fileURLToPath(new URL("../../integrations/bun/preload.mjs", import.meta.url));
|
|
23
27
|
const TEMPORARY_CLEANUP_OPTIONS = {
|
|
24
28
|
recursive: true,
|
|
25
29
|
force: true,
|
|
@@ -31,6 +35,20 @@ const INIT_LOCK_FILE = "assertledger.lock.json";
|
|
|
31
35
|
const INIT_MANAGED_FILES = new Set([INIT_CONFIG_FILE, INIT_LOCK_FILE]);
|
|
32
36
|
const INIT_PACKAGE_MANAGERS = ["pnpm", "npm", "yarn", "bun", "uv", "poetry", "pip"];
|
|
33
37
|
const INIT_FRAMEWORKS = ["node:test", "vitest", "jest", "bun:test", "pytest"];
|
|
38
|
+
export class RepositoryInitWriteError extends Error {
|
|
39
|
+
targetPath;
|
|
40
|
+
temporaryPath;
|
|
41
|
+
temporaryCleanup;
|
|
42
|
+
installedPaths;
|
|
43
|
+
constructor(cause, targetPath, temporaryPath, temporaryCleanup, installedPaths) {
|
|
44
|
+
super("INIT_WRITE_FAILED", { cause });
|
|
45
|
+
this.name = "RepositoryInitWriteError";
|
|
46
|
+
this.targetPath = targetPath;
|
|
47
|
+
this.temporaryPath = temporaryPath;
|
|
48
|
+
this.temporaryCleanup = temporaryCleanup;
|
|
49
|
+
this.installedPaths = installedPaths;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
34
52
|
async function removeTemporaryDirectory(directory) {
|
|
35
53
|
await rm(directory, TEMPORARY_CLEANUP_OPTIONS);
|
|
36
54
|
}
|
|
@@ -280,6 +298,32 @@ function registerPortablePath(spellings, relative) {
|
|
|
280
298
|
throw new Error("PORTABLE_PATH_COLLISION");
|
|
281
299
|
spellings.set(key, relative);
|
|
282
300
|
}
|
|
301
|
+
/** The declared list of a valid configuration; anything unreadable falls back to the defaults,
|
|
302
|
+
* which only widens the inventory and so keeps every link check. */
|
|
303
|
+
async function configuredRepositoryExcludes(root) {
|
|
304
|
+
try {
|
|
305
|
+
const configPath = path.join(root, INIT_CONFIG_FILE);
|
|
306
|
+
if (!(await lstat(configPath)).isFile())
|
|
307
|
+
return [];
|
|
308
|
+
const value = JSON.parse(await readFile(configPath, "utf8"));
|
|
309
|
+
return parseVersionedRepositoryInitConfig(value).repository.exclude;
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return [];
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
async function configuredRepositoryFramework(root) {
|
|
316
|
+
try {
|
|
317
|
+
const configPath = path.join(root, INIT_CONFIG_FILE);
|
|
318
|
+
if (!(await lstat(configPath)).isFile())
|
|
319
|
+
return undefined;
|
|
320
|
+
const value = JSON.parse(await readFile(configPath, "utf8"));
|
|
321
|
+
return parseVersionedRepositoryInitConfig(value).framework;
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
return undefined;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
283
327
|
async function walkFiles(root, excludes) {
|
|
284
328
|
const files = [];
|
|
285
329
|
const spellings = new Map();
|
|
@@ -419,6 +463,9 @@ function initEvidenceKind(file) {
|
|
|
419
463
|
function initNodeTestSource(file) {
|
|
420
464
|
return /(?:^|\/)[^/]+\.test\.[cm]?[jt]s$/u.test(file);
|
|
421
465
|
}
|
|
466
|
+
function initBunTestSource(file) {
|
|
467
|
+
return /(?:^|\/)[^/]+\.(?:test|spec)\.[cm]?[jt]sx?$/u.test(file);
|
|
468
|
+
}
|
|
422
469
|
function initSafePathPattern(value) {
|
|
423
470
|
if (value.startsWith("-") || value.includes("\\"))
|
|
424
471
|
return false;
|
|
@@ -462,7 +509,10 @@ function initPathPatternMatcher(pattern) {
|
|
|
462
509
|
}
|
|
463
510
|
function initCommandPathMatchers(command, testSources) {
|
|
464
511
|
const conventionalDirectories = new Set(["test", "tests", "spec", "specs", "__tests__"]);
|
|
465
|
-
|
|
512
|
+
const operands = command.executable === "bun" && command.arguments[0] === "test"
|
|
513
|
+
? command.arguments.slice(1)
|
|
514
|
+
: command.arguments;
|
|
515
|
+
return operands
|
|
466
516
|
.filter((argument) => initSafePathPattern(argument) &&
|
|
467
517
|
(/[*?]/u.test(argument) ||
|
|
468
518
|
argument.includes("/") ||
|
|
@@ -548,16 +598,75 @@ function initJavaScriptModuleSpecifiers(source) {
|
|
|
548
598
|
specifiers.delete("");
|
|
549
599
|
return specifiers;
|
|
550
600
|
}
|
|
551
|
-
async function atomicInitWrite(root, relative, content) {
|
|
601
|
+
async function atomicInitWrite(root, relative, content, action, installedPaths, dependencies = {}) {
|
|
552
602
|
const target = path.join(root, relative);
|
|
553
603
|
const temporary = path.join(root, `.${relative}.${process.pid}.${Date.now()}.tmp`);
|
|
604
|
+
const openTemporary = dependencies.openTemporary ?? ((temporaryPath) => open(temporaryPath, "wx"));
|
|
605
|
+
const writeTemporary = dependencies.writeTemporary ??
|
|
606
|
+
((_temporaryPath, temporaryContent, handle) => handle.writeFile(temporaryContent));
|
|
607
|
+
const renameTemporary = dependencies.renameTemporary ?? rename;
|
|
608
|
+
const linkTemporary = dependencies.linkTemporary ?? link;
|
|
609
|
+
const removeTemporary = dependencies.removeTemporary ?? ((temporaryPath) => rm(temporaryPath, { force: true }));
|
|
610
|
+
let temporaryOwned = false;
|
|
611
|
+
let temporaryHandle;
|
|
612
|
+
let temporaryHandleClosed = true;
|
|
613
|
+
let targetInstalled = false;
|
|
554
614
|
try {
|
|
555
|
-
await
|
|
556
|
-
|
|
615
|
+
temporaryHandle = await openTemporary(temporary);
|
|
616
|
+
temporaryOwned = true;
|
|
617
|
+
temporaryHandleClosed = false;
|
|
618
|
+
try {
|
|
619
|
+
await writeTemporary(temporary, content, temporaryHandle);
|
|
620
|
+
}
|
|
621
|
+
finally {
|
|
622
|
+
await temporaryHandle.close();
|
|
623
|
+
temporaryHandleClosed = true;
|
|
624
|
+
}
|
|
625
|
+
if (action === "CREATE") {
|
|
626
|
+
await linkTemporary(temporary, target);
|
|
627
|
+
targetInstalled = true;
|
|
628
|
+
await removeTemporary(temporary);
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
await renameTemporary(temporary, target);
|
|
632
|
+
targetInstalled = true;
|
|
633
|
+
}
|
|
557
634
|
}
|
|
558
635
|
catch (error) {
|
|
559
|
-
|
|
560
|
-
|
|
636
|
+
let temporaryCleanup = "NOT_OWNED";
|
|
637
|
+
if (!temporaryOwned) {
|
|
638
|
+
try {
|
|
639
|
+
await lstat(temporary);
|
|
640
|
+
temporaryCleanup = "UNRESOLVED";
|
|
641
|
+
}
|
|
642
|
+
catch (inspectionError) {
|
|
643
|
+
if (inspectionError.code !== "ENOENT") {
|
|
644
|
+
temporaryCleanup = "UNRESOLVED";
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (temporaryOwned && !temporaryHandleClosed && temporaryHandle !== undefined) {
|
|
649
|
+
try {
|
|
650
|
+
await temporaryHandle.close();
|
|
651
|
+
temporaryHandleClosed = true;
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
temporaryCleanup = "UNRESOLVED";
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
if (temporaryOwned) {
|
|
658
|
+
temporaryCleanup = "REMOVED";
|
|
659
|
+
try {
|
|
660
|
+
await removeTemporary(temporary);
|
|
661
|
+
}
|
|
662
|
+
catch {
|
|
663
|
+
temporaryCleanup = "UNRESOLVED";
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
throw new RepositoryInitWriteError(error, target, temporary, temporaryCleanup, [
|
|
667
|
+
...installedPaths,
|
|
668
|
+
...(targetInstalled ? [target] : []),
|
|
669
|
+
]);
|
|
561
670
|
}
|
|
562
671
|
}
|
|
563
672
|
function initEmptyDetections(reasonCodes) {
|
|
@@ -571,8 +680,9 @@ function initEmptyDetections(reasonCodes) {
|
|
|
571
680
|
};
|
|
572
681
|
}
|
|
573
682
|
function initTerminalResult(status, detections, reasonCodes) {
|
|
574
|
-
|
|
575
|
-
|
|
683
|
+
const bunBuiltIn = detections.adapterRecommendation === "bun-test";
|
|
684
|
+
const result = {
|
|
685
|
+
schemaVersion: bunBuiltIn ? "2.0.0" : "1.0.0",
|
|
576
686
|
status,
|
|
577
687
|
reasonCodes: [...new Set(reasonCodes)].sort(),
|
|
578
688
|
detections: { ...detections, reasonCodes: [...new Set(detections.reasonCodes)].sort() },
|
|
@@ -580,9 +690,10 @@ function initTerminalResult(status, detections, reasonCodes) {
|
|
|
580
690
|
files: [],
|
|
581
691
|
requiredOperatorInputs: ["worlds", "candidates"],
|
|
582
692
|
nextCommands: [{ executable: "assertledger", arguments: ["audit", ".", "--json"] }],
|
|
583
|
-
}
|
|
693
|
+
};
|
|
694
|
+
return bunBuiltIn ? parseRepositoryInitResultV2(result) : parseRepositoryInitResult(result);
|
|
584
695
|
}
|
|
585
|
-
export async function initializeRepository(requestedRoot, options = {}) {
|
|
696
|
+
export async function initializeRepository(requestedRoot, options = {}, writeDependencies = {}) {
|
|
586
697
|
let root;
|
|
587
698
|
try {
|
|
588
699
|
root = await realpath(requestedRoot);
|
|
@@ -594,13 +705,32 @@ export async function initializeRepository(requestedRoot, options = {}) {
|
|
|
594
705
|
"REPOSITORY_ROOT_INVALID",
|
|
595
706
|
]);
|
|
596
707
|
}
|
|
708
|
+
const declaredExcludes = options.exclude ?? (await configuredRepositoryExcludes(root));
|
|
709
|
+
const repositoryExclude = [...new Set([...DEFAULT_EXCLUDES, ...declaredExcludes])].sort();
|
|
710
|
+
if (repositoryExclude.length > 1_000 ||
|
|
711
|
+
options.exclude?.some((entry) => !isRepositoryExcludeName(entry))) {
|
|
712
|
+
return initTerminalResult("CONFLICT", initEmptyDetections(["INVALID_REPOSITORY_EXCLUDE"]), [
|
|
713
|
+
"INVALID_REPOSITORY_EXCLUDE",
|
|
714
|
+
]);
|
|
715
|
+
}
|
|
716
|
+
const inventoryExcludes = effectiveExcludes([...INIT_MANAGED_FILES, ...repositoryExclude]);
|
|
597
717
|
const candidateRoots = ["tests/candidates"];
|
|
598
718
|
const outsideCandidateRoots = (file) => !candidateRoots.some((candidateRoot) => {
|
|
599
719
|
const rootKey = portablePathKey(candidateRoot);
|
|
600
720
|
const fileKey = portablePathKey(file);
|
|
601
721
|
return fileKey === rootKey || fileKey.startsWith(`${rootKey}/`);
|
|
602
722
|
});
|
|
603
|
-
|
|
723
|
+
let inventory;
|
|
724
|
+
try {
|
|
725
|
+
inventory = await walkFiles(root, inventoryExcludes);
|
|
726
|
+
}
|
|
727
|
+
catch (error) {
|
|
728
|
+
if (!(error instanceof Error) || error.message !== "UNSUPPORTED_REPOSITORY_SYMLINK")
|
|
729
|
+
throw error;
|
|
730
|
+
return initTerminalResult("CONFLICT", initEmptyDetections(["UNSUPPORTED_REPOSITORY_SYMLINK"]), [
|
|
731
|
+
"UNSUPPORTED_REPOSITORY_SYMLINK",
|
|
732
|
+
]);
|
|
733
|
+
}
|
|
604
734
|
const inScopeFiles = inventory.files.filter(outsideCandidateRoots);
|
|
605
735
|
const contents = new Map();
|
|
606
736
|
const evidenceBytes = new Map();
|
|
@@ -713,7 +843,7 @@ export async function initializeRepository(requestedRoot, options = {}) {
|
|
|
713
843
|
(contents.has("requirements.txt") &&
|
|
714
844
|
/(^|\n)pytest(?:[=<>!~]|$)/u.test(contents.get("requirements.txt") ?? "")))
|
|
715
845
|
frameworkFacts.add("pytest");
|
|
716
|
-
const frameworkOverride = options.framework;
|
|
846
|
+
const frameworkOverride = options.framework ?? (await configuredRepositoryFramework(root));
|
|
717
847
|
if (frameworkOverride !== undefined && !isInitFramework(frameworkOverride)) {
|
|
718
848
|
const detections = initEmptyDetections(["FRAMEWORK_OVERRIDE_INVALID"]);
|
|
719
849
|
detections.packageManager = packageManager;
|
|
@@ -825,8 +955,24 @@ export async function initializeRepository(requestedRoot, options = {}) {
|
|
|
825
955
|
adapterRecommendation = "node-test";
|
|
826
956
|
}
|
|
827
957
|
}
|
|
958
|
+
else if (framework === "bun:test") {
|
|
959
|
+
const testSources = inScopeFiles.filter((file) => initBunTestSource(file) &&
|
|
960
|
+
initJavaScriptModuleSpecifiers(contents.get(file) ?? "").has("bun:test"));
|
|
961
|
+
const pathMatchers = initCommandPathMatchers(testCommand, testSources);
|
|
962
|
+
const baseTestFiles = pathMatchers.length === 0
|
|
963
|
+
? testSources
|
|
964
|
+
: testSources.filter((file) => pathMatchers.some((matcher) => matcher(file)));
|
|
965
|
+
if (baseTestFiles.length > 0) {
|
|
966
|
+
adapter = { kind: "bun-test", executable: "bun", baseTestFiles };
|
|
967
|
+
adapterRecommendation = "bun-test";
|
|
968
|
+
}
|
|
969
|
+
}
|
|
828
970
|
const reasonCodes = adapter === undefined
|
|
829
|
-
? [
|
|
971
|
+
? [
|
|
972
|
+
framework === "node:test" || framework === "bun:test"
|
|
973
|
+
? "BASE_TEST_FILES_UNAVAILABLE"
|
|
974
|
+
: "OFFICIAL_ADAPTER_UNAVAILABLE",
|
|
975
|
+
]
|
|
830
976
|
: [];
|
|
831
977
|
const detections = {
|
|
832
978
|
packageManager,
|
|
@@ -839,17 +985,23 @@ export async function initializeRepository(requestedRoot, options = {}) {
|
|
|
839
985
|
if (adapter === undefined)
|
|
840
986
|
return initTerminalResult("BLOCKED", detections, reasonCodes);
|
|
841
987
|
await options.afterEvidenceSnapshot?.();
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
|
|
988
|
+
const bunBuiltIn = adapter.kind === "bun-test";
|
|
989
|
+
const configValue = {
|
|
990
|
+
schemaVersion: bunBuiltIn ? "2.0.0" : "1.0.0",
|
|
991
|
+
repository: { root: ".", exclude: repositoryExclude },
|
|
845
992
|
packageManager,
|
|
846
993
|
framework,
|
|
847
994
|
testCommand,
|
|
848
995
|
adapter,
|
|
849
996
|
candidateRoots: [...candidateRoots],
|
|
850
|
-
}
|
|
997
|
+
};
|
|
998
|
+
const config = bunBuiltIn
|
|
999
|
+
? parseRepositoryInitConfigV2(configValue)
|
|
1000
|
+
: parseRepositoryInitConfig(configValue);
|
|
851
1001
|
const configContent = initJsonBytes(config);
|
|
852
|
-
const configDigest =
|
|
1002
|
+
const configDigest = config.schemaVersion === "2.0.0"
|
|
1003
|
+
? repositoryInitConfigV2Digest(config)
|
|
1004
|
+
: repositoryInitConfigDigest(config);
|
|
853
1005
|
const evidenceFiles = inScopeFiles.filter((file) => initEvidenceKind(file) !== undefined);
|
|
854
1006
|
if (adapterEvidencePath !== undefined && !evidenceFiles.includes(adapterEvidencePath))
|
|
855
1007
|
evidenceFiles.push(adapterEvidencePath);
|
|
@@ -867,17 +1019,33 @@ export async function initializeRepository(requestedRoot, options = {}) {
|
|
|
867
1019
|
: (initEvidenceKind(file) ?? "ADAPTER_CONFIG"),
|
|
868
1020
|
});
|
|
869
1021
|
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
1022
|
+
let lock;
|
|
1023
|
+
if (bunBuiltIn) {
|
|
1024
|
+
const lockBase = {
|
|
1025
|
+
schemaVersion: "2.0.0",
|
|
1026
|
+
configDigest,
|
|
1027
|
+
detector: { name: "assertledger-init", version: "2.0.0" },
|
|
1028
|
+
evidence,
|
|
1029
|
+
detections,
|
|
1030
|
+
};
|
|
1031
|
+
lock = parseRepositoryInitLockV2({
|
|
1032
|
+
...lockBase,
|
|
1033
|
+
lockDigest: repositoryInitLockV2Digest(lockBase),
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
else {
|
|
1037
|
+
const lockBase = {
|
|
1038
|
+
schemaVersion: "1.0.0",
|
|
1039
|
+
configDigest,
|
|
1040
|
+
detector: { name: "assertledger-init", version: "1.0.0" },
|
|
1041
|
+
evidence,
|
|
1042
|
+
detections: RepositoryInitDetectionsSchema.parse(detections),
|
|
1043
|
+
};
|
|
1044
|
+
lock = parseRepositoryInitLock({
|
|
1045
|
+
...lockBase,
|
|
1046
|
+
lockDigest: repositoryInitLockDigest(lockBase),
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
881
1049
|
const lockContent = initJsonBytes(lock);
|
|
882
1050
|
const plannedFiles = [
|
|
883
1051
|
{ path: INIT_CONFIG_FILE, digest: rawSha256(configContent), content: configContent },
|
|
@@ -885,7 +1053,7 @@ export async function initializeRepository(requestedRoot, options = {}) {
|
|
|
885
1053
|
];
|
|
886
1054
|
let finalInScopeFiles;
|
|
887
1055
|
try {
|
|
888
|
-
const finalInventory = await walkFiles(root,
|
|
1056
|
+
const finalInventory = await walkFiles(root, inventoryExcludes);
|
|
889
1057
|
finalInScopeFiles = finalInventory.files.filter(outsideCandidateRoots);
|
|
890
1058
|
}
|
|
891
1059
|
catch {
|
|
@@ -940,7 +1108,7 @@ export async function initializeRepository(requestedRoot, options = {}) {
|
|
|
940
1108
|
let structural;
|
|
941
1109
|
try {
|
|
942
1110
|
const value = JSON.parse(existingLock);
|
|
943
|
-
structural =
|
|
1111
|
+
structural = parseVersionedRepositoryInitLock(value);
|
|
944
1112
|
}
|
|
945
1113
|
catch {
|
|
946
1114
|
return initTerminalResult("CONFLICT", { ...detections, reasonCodes: ["LOCK_INVALID"] }, [
|
|
@@ -952,8 +1120,8 @@ export async function initializeRepository(requestedRoot, options = {}) {
|
|
|
952
1120
|
}
|
|
953
1121
|
}
|
|
954
1122
|
if (existingConfig === configContent && existingLock === lockContent) {
|
|
955
|
-
|
|
956
|
-
schemaVersion: "1.0.0",
|
|
1123
|
+
const result = {
|
|
1124
|
+
schemaVersion: bunBuiltIn ? "2.0.0" : "1.0.0",
|
|
957
1125
|
status: "UNCHANGED",
|
|
958
1126
|
reasonCodes: [],
|
|
959
1127
|
detections,
|
|
@@ -961,7 +1129,8 @@ export async function initializeRepository(requestedRoot, options = {}) {
|
|
|
961
1129
|
files: plannedFiles,
|
|
962
1130
|
requiredOperatorInputs: ["worlds", "candidates"],
|
|
963
1131
|
nextCommands: [{ executable: "assertledger", arguments: ["audit", ".", "--json"] }],
|
|
964
|
-
}
|
|
1132
|
+
};
|
|
1133
|
+
return bunBuiltIn ? parseRepositoryInitResultV2(result) : parseRepositoryInitResult(result);
|
|
965
1134
|
}
|
|
966
1135
|
const actions = [];
|
|
967
1136
|
if (existingConfig === undefined)
|
|
@@ -972,8 +1141,8 @@ export async function initializeRepository(requestedRoot, options = {}) {
|
|
|
972
1141
|
actions.push({ kind: "REGENERATE", path: INIT_LOCK_FILE });
|
|
973
1142
|
actions.sort((left, right) => left.path.localeCompare(right.path));
|
|
974
1143
|
const status = options.dryRun ? "WOULD_CREATE" : "CREATED";
|
|
975
|
-
const
|
|
976
|
-
schemaVersion: "1.0.0",
|
|
1144
|
+
const resultValue = {
|
|
1145
|
+
schemaVersion: bunBuiltIn ? "2.0.0" : "1.0.0",
|
|
977
1146
|
status,
|
|
978
1147
|
reasonCodes: [],
|
|
979
1148
|
detections,
|
|
@@ -981,13 +1150,19 @@ export async function initializeRepository(requestedRoot, options = {}) {
|
|
|
981
1150
|
files: plannedFiles,
|
|
982
1151
|
requiredOperatorInputs: ["worlds", "candidates"],
|
|
983
1152
|
nextCommands: [{ executable: "assertledger", arguments: ["audit", ".", "--json"] }],
|
|
984
|
-
}
|
|
1153
|
+
};
|
|
1154
|
+
const result = bunBuiltIn
|
|
1155
|
+
? parseRepositoryInitResultV2(resultValue)
|
|
1156
|
+
: parseRepositoryInitResult(resultValue);
|
|
985
1157
|
if (!options.dryRun) {
|
|
1158
|
+
const installedPaths = [];
|
|
986
1159
|
for (const action of actions) {
|
|
987
1160
|
const file = plannedFiles.find((candidate) => candidate.path === action.path);
|
|
988
1161
|
if (file === undefined)
|
|
989
1162
|
throw new Error("INIT_PLAN_INCONSISTENT");
|
|
990
|
-
await atomicInitWrite(root, file.path, file.content);
|
|
1163
|
+
await atomicInitWrite(root, file.path, file.content, action.kind, installedPaths, writeDependencies);
|
|
1164
|
+
if (action.kind === "CREATE")
|
|
1165
|
+
installedPaths.push(path.join(root, file.path));
|
|
991
1166
|
}
|
|
992
1167
|
}
|
|
993
1168
|
return result;
|
|
@@ -1029,9 +1204,52 @@ async function probeRuntimeTemporaryWorkspace() {
|
|
|
1029
1204
|
}
|
|
1030
1205
|
export async function doctorRepositoryRuntime(requestedRoot, options) {
|
|
1031
1206
|
const repositoryRoot = path.resolve(requestedRoot);
|
|
1207
|
+
const configuration = options.allowUnsafeExecution
|
|
1208
|
+
? await initializeRepository(repositoryRoot, { dryRun: true })
|
|
1209
|
+
: undefined;
|
|
1210
|
+
if (configuration?.schemaVersion === "2.0.0" &&
|
|
1211
|
+
configuration.detections.adapterRecommendation === "bun-test") {
|
|
1212
|
+
const configFile = configuration.files.find((file) => file.path === INIT_CONFIG_FILE);
|
|
1213
|
+
if (configFile === undefined)
|
|
1214
|
+
throw new Error("BUN_TEST_CONFIG_MISSING");
|
|
1215
|
+
const config = parseRepositoryInitConfigV2(JSON.parse(configFile.content));
|
|
1216
|
+
const environmentAllowlist = ["PATH", "SystemRoot", "WINDIR", "TEMP", "TMP"];
|
|
1217
|
+
let resolvedExecutable = config.adapter.executable;
|
|
1218
|
+
return runBunRuntimeDoctorChecks(repositoryRoot, configuration, {
|
|
1219
|
+
probeExecutable: async () => {
|
|
1220
|
+
const identity = await probeBunTestExecutable(config.adapter.executable, repositoryRoot, environmentAllowlist, 5_000, CONTROLLED_REPORT_MAXIMUM_BYTES);
|
|
1221
|
+
resolvedExecutable = identity.resolvedExecutable;
|
|
1222
|
+
return { bunVersion: identity.bunVersion };
|
|
1223
|
+
},
|
|
1224
|
+
probeDependencies: async () => {
|
|
1225
|
+
const result = await runProcess({
|
|
1226
|
+
executable: resolvedExecutable,
|
|
1227
|
+
args: ["-e", 'import("bun:test").then(() => process.stdout.write("READY"))'],
|
|
1228
|
+
cwd: repositoryRoot,
|
|
1229
|
+
environment: environmentFromAllowlist(environmentAllowlist),
|
|
1230
|
+
timeoutMs: 5_000,
|
|
1231
|
+
maximumOutputBytes: CONTROLLED_REPORT_MAXIMUM_BYTES,
|
|
1232
|
+
});
|
|
1233
|
+
if (result.outcome !== "PASS" ||
|
|
1234
|
+
result.stdout.text !== "READY" ||
|
|
1235
|
+
result.stderr.totalBytes !== 0) {
|
|
1236
|
+
throw new Error("BUN_TEST_DEPENDENCY_UNAVAILABLE");
|
|
1237
|
+
}
|
|
1238
|
+
},
|
|
1239
|
+
probeTemporaryWorkspace: probeRuntimeTemporaryWorkspace,
|
|
1240
|
+
runSyntheticPreflight: async () => {
|
|
1241
|
+
await runBunTestRuntimePreflight(resolvedExecutable, environmentAllowlist, 5_000, CONTROLLED_REPORT_MAXIMUM_BYTES);
|
|
1242
|
+
},
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1032
1245
|
let resolvedExecutable = process.execPath;
|
|
1033
1246
|
return runRuntimeDoctorChecks(repositoryRoot, options.allowUnsafeExecution, {
|
|
1034
|
-
inspectConfiguration: () =>
|
|
1247
|
+
inspectConfiguration: async () => {
|
|
1248
|
+
const inspected = configuration ?? (await initializeRepository(repositoryRoot, { dryRun: true }));
|
|
1249
|
+
if (inspected.schemaVersion !== "1.0.0")
|
|
1250
|
+
throw new Error("RUNTIME_CONFIGURATION_UNAVAILABLE");
|
|
1251
|
+
return inspected;
|
|
1252
|
+
},
|
|
1035
1253
|
probeExecutable: async () => {
|
|
1036
1254
|
const identity = await probeNodeTestExecutable(process.execPath, repositoryRoot, [], 5_000, CONTROLLED_REPORT_MAXIMUM_BYTES);
|
|
1037
1255
|
resolvedExecutable = identity.resolvedExecutable;
|
|
@@ -1368,9 +1586,10 @@ async function resolveRepositoryRoot(rootInput) {
|
|
|
1368
1586
|
throw new TypeError("INVALID_REPOSITORY_ROOT");
|
|
1369
1587
|
}
|
|
1370
1588
|
}
|
|
1371
|
-
export async function analyzeRepository(rootInput) {
|
|
1589
|
+
export async function analyzeRepository(rootInput, options = {}) {
|
|
1372
1590
|
const root = await resolveRepositoryRoot(rootInput);
|
|
1373
|
-
const
|
|
1591
|
+
const declaredExcludes = options.configuredExcludes === true ? await configuredRepositoryExcludes(root) : [];
|
|
1592
|
+
const { files } = await walkFiles(root, effectiveExcludes(declaredExcludes));
|
|
1374
1593
|
const digest = createHash("sha256");
|
|
1375
1594
|
const languageCounts = new Map();
|
|
1376
1595
|
const frameworks = new Set();
|
|
@@ -1777,7 +1996,9 @@ function parseRequest(value) {
|
|
|
1777
1996
|
if (candidateRoots.length === 0)
|
|
1778
1997
|
throw new TypeError("INVALID_CANDIDATE_ROOTS");
|
|
1779
1998
|
assertPortableStringCollection(candidateRoots, "DUPLICATE_CANDIDATE_ROOT");
|
|
1780
|
-
if (adapter.kind !== "node-test" &&
|
|
1999
|
+
if (adapter.kind !== "node-test" &&
|
|
2000
|
+
adapter.kind !== "bun-test" &&
|
|
2001
|
+
adapter.kind !== "testforge-command") {
|
|
1781
2002
|
throw new TypeError("UNSUPPORTED_ADAPTER");
|
|
1782
2003
|
}
|
|
1783
2004
|
// The versioned contract already restricts container isolation to v2 requests.
|
|
@@ -1896,13 +2117,15 @@ function parseRequest(value) {
|
|
|
1896
2117
|
if (minimumTargetWeightPermille > 1_000) {
|
|
1897
2118
|
throw new TypeError("INVALID_MINIMUM_TARGET_WEIGHT_PERMILLE");
|
|
1898
2119
|
}
|
|
1899
|
-
const normalizedAdapter = adapter.kind === "node-test"
|
|
2120
|
+
const normalizedAdapter = adapter.kind === "node-test" || adapter.kind === "bun-test"
|
|
1900
2121
|
? (() => {
|
|
1901
|
-
const extraArguments =
|
|
2122
|
+
const extraArguments = adapter.kind === "node-test"
|
|
2123
|
+
? parseNodeTestExtraArguments(adapter.extraArguments)
|
|
2124
|
+
: undefined;
|
|
1902
2125
|
const baseTestFiles = requireStringArray(adapter.baseTestFiles, "base_test_files").map((file) => assertSafeRelativePath(file, ["."]));
|
|
1903
2126
|
assertPortableStringCollection(baseTestFiles, "DUPLICATE_BASE_TEST_FILE");
|
|
1904
2127
|
return {
|
|
1905
|
-
kind:
|
|
2128
|
+
kind: adapter.kind,
|
|
1906
2129
|
executable: requireString(adapter.executable, "adapter_executable"),
|
|
1907
2130
|
baseTestFiles,
|
|
1908
2131
|
...(extraArguments === undefined ? {} : { extraArguments }),
|
|
@@ -2099,6 +2322,19 @@ async function readStructuredCommandReport(resultFile, processResult, maximumByt
|
|
|
2099
2322
|
}
|
|
2100
2323
|
return parseStructuredCommandReport(await readBoundedJsonFile(resultFile, maximumBytes), processResult);
|
|
2101
2324
|
}
|
|
2325
|
+
function readBunDriverReport(processResult) {
|
|
2326
|
+
if (processResult.outcome === "TIMEOUT" ||
|
|
2327
|
+
processResult.outcome === "INFRA_ERROR" ||
|
|
2328
|
+
processResult.stdout.truncated ||
|
|
2329
|
+
processResult.stdout.totalBytes > CONTROLLED_REPORT_MAXIMUM_BYTES)
|
|
2330
|
+
return undefined;
|
|
2331
|
+
try {
|
|
2332
|
+
return parseStructuredCommandReport(JSON.parse(processResult.stdout.text), processResult);
|
|
2333
|
+
}
|
|
2334
|
+
catch {
|
|
2335
|
+
return undefined;
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2102
2338
|
async function readNodeTestReport(resultFile, processResult, maximumBytes) {
|
|
2103
2339
|
if (processResult.outcome === "TIMEOUT" || processResult.outcome === "INFRA_ERROR") {
|
|
2104
2340
|
return undefined;
|
|
@@ -2179,6 +2415,71 @@ async function probeNodeTestExecutable(requestedExecutable, repositoryRoot, envi
|
|
|
2179
2415
|
nodeVersion: secondProbe.nodeVersion,
|
|
2180
2416
|
};
|
|
2181
2417
|
}
|
|
2418
|
+
async function runBunExecutableProbe(executable, repositoryRoot, environmentAllowlist, timeoutMs, maximumOutputBytes) {
|
|
2419
|
+
const result = await runProcess({
|
|
2420
|
+
executable,
|
|
2421
|
+
args: [
|
|
2422
|
+
"-e",
|
|
2423
|
+
"console.log(JSON.stringify({execPath:process.execPath,bunVersion:Bun.version,bunRevision:Bun.revision}))",
|
|
2424
|
+
],
|
|
2425
|
+
cwd: repositoryRoot,
|
|
2426
|
+
environment: environmentFromAllowlist(environmentAllowlist),
|
|
2427
|
+
timeoutMs: Math.min(timeoutMs, 5_000),
|
|
2428
|
+
maximumOutputBytes: Math.min(maximumOutputBytes, CONTROLLED_REPORT_MAXIMUM_BYTES),
|
|
2429
|
+
});
|
|
2430
|
+
if (result.outcome !== "PASS" || result.stdout.truncated || result.stderr.totalBytes !== 0) {
|
|
2431
|
+
throw new Error("BUN_TEST_EXECUTABLE_PROBE_FAILED");
|
|
2432
|
+
}
|
|
2433
|
+
let report;
|
|
2434
|
+
try {
|
|
2435
|
+
report = JSON.parse(result.stdout.text.trim());
|
|
2436
|
+
}
|
|
2437
|
+
catch {
|
|
2438
|
+
throw new Error("BUN_TEST_EXECUTABLE_PROBE_FAILED");
|
|
2439
|
+
}
|
|
2440
|
+
if (!isRecord(report) ||
|
|
2441
|
+
Object.keys(report).sort().join("\0") !== "bunRevision\0bunVersion\0execPath" ||
|
|
2442
|
+
typeof report.execPath !== "string" ||
|
|
2443
|
+
report.execPath.length === 0 ||
|
|
2444
|
+
typeof report.bunVersion !== "string" ||
|
|
2445
|
+
typeof report.bunRevision !== "string") {
|
|
2446
|
+
throw new Error("BUN_TEST_EXECUTABLE_PROBE_FAILED");
|
|
2447
|
+
}
|
|
2448
|
+
if (report.bunVersion !== BUN_TEST_ADAPTER_PROFILE.bunVersion ||
|
|
2449
|
+
report.bunRevision !== BUN_TEST_ADAPTER_PROFILE.bunRevision) {
|
|
2450
|
+
throw new Error("BUN_TEST_VERSION_UNSUPPORTED");
|
|
2451
|
+
}
|
|
2452
|
+
return {
|
|
2453
|
+
execPath: report.execPath,
|
|
2454
|
+
bunVersion: report.bunVersion,
|
|
2455
|
+
bunRevision: report.bunRevision,
|
|
2456
|
+
};
|
|
2457
|
+
}
|
|
2458
|
+
async function probeBunTestExecutable(requestedExecutable, repositoryRoot, environmentAllowlist, timeoutMs, maximumOutputBytes) {
|
|
2459
|
+
const first = await runBunExecutableProbe(requestedExecutable, repositoryRoot, environmentAllowlist, timeoutMs, maximumOutputBytes);
|
|
2460
|
+
let resolvedExecutable;
|
|
2461
|
+
try {
|
|
2462
|
+
resolvedExecutable = await realpath(first.execPath);
|
|
2463
|
+
if (!(await stat(resolvedExecutable)).isFile())
|
|
2464
|
+
throw new Error("not a file");
|
|
2465
|
+
}
|
|
2466
|
+
catch {
|
|
2467
|
+
throw new Error("BUN_TEST_EXECUTABLE_PROBE_FAILED");
|
|
2468
|
+
}
|
|
2469
|
+
const second = await runBunExecutableProbe(resolvedExecutable, repositoryRoot, environmentAllowlist, timeoutMs, maximumOutputBytes);
|
|
2470
|
+
if ((await realpath(second.execPath)) !== resolvedExecutable ||
|
|
2471
|
+
second.bunVersion !== first.bunVersion ||
|
|
2472
|
+
second.bunRevision !== first.bunRevision) {
|
|
2473
|
+
throw new Error("BUN_TEST_EXECUTABLE_PROBE_FAILED");
|
|
2474
|
+
}
|
|
2475
|
+
return {
|
|
2476
|
+
requestedExecutable,
|
|
2477
|
+
resolvedExecutable,
|
|
2478
|
+
executableDigest: await sha256File(resolvedExecutable),
|
|
2479
|
+
bunVersion: second.bunVersion,
|
|
2480
|
+
bunRevision: second.bunRevision,
|
|
2481
|
+
};
|
|
2482
|
+
}
|
|
2182
2483
|
// Evaluated by `node -p` inside a fresh container before any candidate code runs there.
|
|
2183
2484
|
const CONTAINER_NODE_PROBE_SOURCE = '(() => { const fs = require("node:fs"); const crypto = require("node:crypto"); const execPath = fs.realpathSync(process.execPath); return JSON.stringify({ digest: "sha256:" + crypto.createHash("sha256").update(fs.readFileSync(execPath)).digest("hex"), execPath, node: process.versions.node }); })()';
|
|
2184
2485
|
function parseJsonBytes(bytes) {
|
|
@@ -2286,6 +2587,8 @@ function containerPreflightExecutor(container, executable) {
|
|
|
2286
2587
|
}
|
|
2287
2588
|
async function executeContainerAdapter(request, container, workspace, candidateFiles) {
|
|
2288
2589
|
const adapter = request.adapter;
|
|
2590
|
+
if (adapter.kind === "bun-test")
|
|
2591
|
+
throw new Error("BUN_TEST_CONTAINER_UNSUPPORTED");
|
|
2289
2592
|
const executed = await runContainerExecution(container, {
|
|
2290
2593
|
workspace,
|
|
2291
2594
|
...(adapter.kind === "node-test" ? { reporterSource: NODE_TEST_REPORTER_SOURCE } : {}),
|
|
@@ -2370,6 +2673,9 @@ async function executeTrustedLocalAdapter(request, workspace, temporaryRoot, can
|
|
|
2370
2673
|
environment.TESTFORGE_RESULT_FILE = resultFile;
|
|
2371
2674
|
environment.TESTFORGE_CANDIDATE_FILES = JSON.stringify(candidateFiles);
|
|
2372
2675
|
}
|
|
2676
|
+
else if (request.adapter.kind === "bun-test") {
|
|
2677
|
+
environment.TESTFORGE_CANDIDATE_FILES = JSON.stringify(candidateFiles);
|
|
2678
|
+
}
|
|
2373
2679
|
else {
|
|
2374
2680
|
if (nodeTestReporterPath === undefined)
|
|
2375
2681
|
throw new Error("NODE_TEST_REPORTER_MISSING");
|
|
@@ -2385,20 +2691,30 @@ async function executeTrustedLocalAdapter(request, workspace, temporaryRoot, can
|
|
|
2385
2691
|
...request.adapter.baseTestFiles,
|
|
2386
2692
|
...candidateFiles,
|
|
2387
2693
|
]
|
|
2388
|
-
: request.adapter.
|
|
2694
|
+
: request.adapter.kind === "bun-test"
|
|
2695
|
+
? [
|
|
2696
|
+
BUN_TEST_DRIVER_PATH,
|
|
2697
|
+
request.adapter.executable,
|
|
2698
|
+
BUN_TEST_HELPER_PATH,
|
|
2699
|
+
...request.adapter.baseTestFiles,
|
|
2700
|
+
]
|
|
2701
|
+
: request.adapter.arguments;
|
|
2389
2702
|
const result = await runProcess({
|
|
2390
|
-
executable: request.adapter.executable,
|
|
2703
|
+
executable: request.adapter.kind === "bun-test" ? process.execPath : request.adapter.executable,
|
|
2391
2704
|
args,
|
|
2392
2705
|
cwd: workspace,
|
|
2393
2706
|
environment,
|
|
2394
2707
|
timeoutMs: request.budgets.timeoutMsPerExecution,
|
|
2395
2708
|
maximumOutputBytes: request.budgets.maximumOutputBytes,
|
|
2396
2709
|
});
|
|
2710
|
+
const structuredReport = request.adapter.kind === "testforge-command"
|
|
2711
|
+
? await readStructuredCommandReport(resultFile, result, CONTROLLED_REPORT_MAXIMUM_BYTES)
|
|
2712
|
+
: request.adapter.kind === "bun-test"
|
|
2713
|
+
? readBunDriverReport(result)
|
|
2714
|
+
: undefined;
|
|
2397
2715
|
return {
|
|
2398
2716
|
result,
|
|
2399
|
-
structuredReport
|
|
2400
|
-
? await readStructuredCommandReport(resultFile, result, CONTROLLED_REPORT_MAXIMUM_BYTES)
|
|
2401
|
-
: undefined,
|
|
2717
|
+
structuredReport,
|
|
2402
2718
|
nodeTestReport: request.adapter.kind === "node-test"
|
|
2403
2719
|
? await readNodeTestReport(resultFile, result, CONTROLLED_REPORT_MAXIMUM_BYTES)
|
|
2404
2720
|
: undefined,
|
|
@@ -2444,6 +2760,119 @@ async function executeObservation(request, repositoryRoot, world, candidate, att
|
|
|
2444
2760
|
await removeTemporaryDirectory(temporaryRoot);
|
|
2445
2761
|
}
|
|
2446
2762
|
}
|
|
2763
|
+
async function runBunTestRuntimePreflight(executable, environmentAllowlist, timeoutMs, maximumOutputBytes) {
|
|
2764
|
+
const probeSources = [
|
|
2765
|
+
{
|
|
2766
|
+
name: "assertion",
|
|
2767
|
+
source: 'import { test } from "bun:test"; import { assertSame } from "assertledger/bun"; test("assertion", () => assertSame(1, 2));\n',
|
|
2768
|
+
outcome: "ASSERTION_FAILURE",
|
|
2769
|
+
attributed: true,
|
|
2770
|
+
},
|
|
2771
|
+
{
|
|
2772
|
+
name: "generic-throw",
|
|
2773
|
+
source: 'import { test } from "bun:test"; test("generic", () => { throw new Error("generic"); });\n',
|
|
2774
|
+
outcome: "PROCESS_CRASH",
|
|
2775
|
+
attributed: false,
|
|
2776
|
+
},
|
|
2777
|
+
{
|
|
2778
|
+
name: "caught-assertion",
|
|
2779
|
+
source: 'import { test } from "bun:test"; import { assertSame } from "assertledger/bun"; test("caught", () => { try { assertSame(1, 2); } catch {} throw new Error("generic"); });\n',
|
|
2780
|
+
outcome: "PROCESS_CRASH",
|
|
2781
|
+
attributed: false,
|
|
2782
|
+
},
|
|
2783
|
+
{
|
|
2784
|
+
name: "replayed-assertion",
|
|
2785
|
+
source: 'import { test } from "bun:test"; import { assertSame } from "assertledger/bun"; let saved; try { assertSame(1, 2); } catch (error) { saved = error; } test("replayed", () => { throw saved; });\n',
|
|
2786
|
+
outcome: "PROCESS_CRASH",
|
|
2787
|
+
attributed: false,
|
|
2788
|
+
},
|
|
2789
|
+
{
|
|
2790
|
+
name: "replayed-row",
|
|
2791
|
+
source: 'import { test } from "bun:test"; import { assertSame } from "assertledger/bun"; let saved; test.each([0, 1])("row %i", (row) => { if (row === 0) { try { assertSame(1, 2); } catch (error) { saved = error; } return; } throw saved; });\n',
|
|
2792
|
+
outcome: "PROCESS_CRASH",
|
|
2793
|
+
attributed: false,
|
|
2794
|
+
},
|
|
2795
|
+
{
|
|
2796
|
+
name: "async-cross-test",
|
|
2797
|
+
source: 'import { test } from "bun:test"; import { assertSame } from "assertledger/bun"; let signalReady; const ready = new Promise((resolve) => { signalReady = resolve; }); let rejectReceiver; test("origin", () => { ready.then(() => { try { assertSame(1, 2); } catch (error) { rejectReceiver(error); } }); }); test("receiver", () => new Promise((_, reject) => { rejectReceiver = reject; signalReady(); }));\n',
|
|
2798
|
+
outcome: "PROCESS_CRASH",
|
|
2799
|
+
attributed: false,
|
|
2800
|
+
},
|
|
2801
|
+
{
|
|
2802
|
+
name: "multi-assertion-rows",
|
|
2803
|
+
source: 'import { test } from "bun:test"; import { assertSame } from "assertledger/bun"; test.each([1, 2])("row %i", (row) => assertSame(row, 0));\n',
|
|
2804
|
+
outcome: "ASSERTION_FAILURE",
|
|
2805
|
+
attributed: true,
|
|
2806
|
+
},
|
|
2807
|
+
{
|
|
2808
|
+
name: "operand-throw",
|
|
2809
|
+
source: 'import { test } from "bun:test"; import { assertSame } from "assertledger/bun"; test("operand", () => assertSame((() => { throw new Error("operand"); })(), 1));\n',
|
|
2810
|
+
outcome: "PROCESS_CRASH",
|
|
2811
|
+
attributed: false,
|
|
2812
|
+
},
|
|
2813
|
+
{
|
|
2814
|
+
name: "native-expect",
|
|
2815
|
+
source: 'import { test, expect } from "bun:test"; test("native", () => expect(1).toBe(2));\n',
|
|
2816
|
+
outcome: "PROCESS_CRASH",
|
|
2817
|
+
attributed: false,
|
|
2818
|
+
},
|
|
2819
|
+
{
|
|
2820
|
+
name: "hook-failure",
|
|
2821
|
+
source: 'import { afterEach, test } from "bun:test"; import { assertSame } from "assertledger/bun"; afterEach(() => { throw new Error("hook"); }); test("candidate", () => assertSame(1, 2));\n',
|
|
2822
|
+
outcome: "INFRA_ERROR",
|
|
2823
|
+
attributed: false,
|
|
2824
|
+
},
|
|
2825
|
+
];
|
|
2826
|
+
try {
|
|
2827
|
+
const probes = [];
|
|
2828
|
+
for (const probe of probeSources) {
|
|
2829
|
+
const evidence = await withEmptyWorkspace(async (workspace) => {
|
|
2830
|
+
await writeFile(path.join(workspace, "package.json"), '{"type":"module"}\n');
|
|
2831
|
+
await writeFile(path.join(workspace, "base.test.ts"), 'import { test } from "bun:test"; test("control", () => {});\n');
|
|
2832
|
+
const candidatePath = path.join(workspace, "candidate.test.ts");
|
|
2833
|
+
await writeFile(candidatePath, probe.source);
|
|
2834
|
+
const result = await runProcess({
|
|
2835
|
+
executable: process.execPath,
|
|
2836
|
+
args: [BUN_TEST_DRIVER_PATH, executable, BUN_TEST_HELPER_PATH, "base.test.ts"],
|
|
2837
|
+
cwd: workspace,
|
|
2838
|
+
environment: {
|
|
2839
|
+
...environmentFromAllowlist(environmentAllowlist),
|
|
2840
|
+
TESTFORGE_CANDIDATE_FILES: JSON.stringify(["candidate.test.ts"]),
|
|
2841
|
+
},
|
|
2842
|
+
timeoutMs: Math.min(timeoutMs, 5_000),
|
|
2843
|
+
maximumOutputBytes: Math.min(maximumOutputBytes, CONTROLLED_REPORT_MAXIMUM_BYTES),
|
|
2844
|
+
});
|
|
2845
|
+
const report = readBunDriverReport(result);
|
|
2846
|
+
const expectedCandidateCount = probe.name === "hook-failure"
|
|
2847
|
+
? 0
|
|
2848
|
+
: probe.name === "replayed-row" ||
|
|
2849
|
+
probe.name === "async-cross-test" ||
|
|
2850
|
+
probe.name === "multi-assertion-rows"
|
|
2851
|
+
? 2
|
|
2852
|
+
: 1;
|
|
2853
|
+
if (report?.outcome !== probe.outcome ||
|
|
2854
|
+
report.attributed !== probe.attributed ||
|
|
2855
|
+
report.testsDiscovered !==
|
|
2856
|
+
(expectedCandidateCount === 0 ? 0 : expectedCandidateCount + 1) ||
|
|
2857
|
+
report.candidateTestsDiscovered !== expectedCandidateCount) {
|
|
2858
|
+
throw new Error("BUN_TEST_PROFILE_PREFLIGHT_FAILED");
|
|
2859
|
+
}
|
|
2860
|
+
return {
|
|
2861
|
+
name: probe.name,
|
|
2862
|
+
outcome: report.outcome,
|
|
2863
|
+
attributed: report.attributed,
|
|
2864
|
+
testsDiscovered: report.testsDiscovered,
|
|
2865
|
+
candidateTestsDiscovered: report.candidateTestsDiscovered,
|
|
2866
|
+
};
|
|
2867
|
+
});
|
|
2868
|
+
probes.push(evidence);
|
|
2869
|
+
}
|
|
2870
|
+
return { protocolVersion: "1.0.0", probes };
|
|
2871
|
+
}
|
|
2872
|
+
catch (error) {
|
|
2873
|
+
throw new Error("BUN_TEST_PROFILE_PREFLIGHT_FAILED", { cause: error });
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2447
2876
|
export async function verifyCampaign(value, options = {}) {
|
|
2448
2877
|
const request = parseRequest(value);
|
|
2449
2878
|
const container = request.isolation.kind === "container"
|
|
@@ -2466,6 +2895,15 @@ export async function verifyCampaign(value, options = {}) {
|
|
|
2466
2895
|
if (request.adapter.kind === "node-test" && nodeIdentity !== undefined) {
|
|
2467
2896
|
request.adapter.executable = nodeIdentity.resolvedExecutable;
|
|
2468
2897
|
}
|
|
2898
|
+
const bunIdentity = request.adapter.kind === "bun-test"
|
|
2899
|
+
? await probeBunTestExecutable(request.adapter.executable, repositoryRoot, trustedLocalEnvironmentAllowlist(request), request.budgets.timeoutMsPerExecution, request.budgets.maximumOutputBytes)
|
|
2900
|
+
: undefined;
|
|
2901
|
+
if (request.adapter.kind === "bun-test" && bunIdentity !== undefined) {
|
|
2902
|
+
request.adapter.executable = bunIdentity.resolvedExecutable;
|
|
2903
|
+
}
|
|
2904
|
+
const bunDriverDigest = request.adapter.kind === "bun-test" ? await sha256File(BUN_TEST_DRIVER_PATH) : undefined;
|
|
2905
|
+
const bunHelperDigest = request.adapter.kind === "bun-test" ? await sha256File(BUN_TEST_HELPER_PATH) : undefined;
|
|
2906
|
+
const bunPreloadDigest = request.adapter.kind === "bun-test" ? await sha256File(BUN_TEST_PRELOAD_PATH) : undefined;
|
|
2469
2907
|
const nodeRuntimePreflight = request.adapter.kind === "node-test" && nodeIdentity !== undefined
|
|
2470
2908
|
? await runNodeTestRuntimePreflight({
|
|
2471
2909
|
executable: nodeIdentity.resolvedExecutable,
|
|
@@ -2481,6 +2919,9 @@ export async function verifyCampaign(value, options = {}) {
|
|
|
2481
2919
|
}),
|
|
2482
2920
|
})
|
|
2483
2921
|
: undefined;
|
|
2922
|
+
const bunRuntimePreflight = request.adapter.kind === "bun-test" && bunIdentity !== undefined
|
|
2923
|
+
? await runBunTestRuntimePreflight(bunIdentity.resolvedExecutable, trustedLocalEnvironmentAllowlist(request), request.budgets.timeoutMsPerExecution, request.budgets.maximumOutputBytes)
|
|
2924
|
+
: undefined;
|
|
2484
2925
|
const campaignRoot = await realpath(await mkdtemp(path.join(os.tmpdir(), "testforge-campaign-")));
|
|
2485
2926
|
const repositorySnapshot = path.join(campaignRoot, "repository");
|
|
2486
2927
|
try {
|
|
@@ -2495,7 +2936,9 @@ export async function verifyCampaign(value, options = {}) {
|
|
|
2495
2936
|
const inputPaths = [
|
|
2496
2937
|
...request.worlds.flatMap((world) => world.files.map((file) => file.path)),
|
|
2497
2938
|
...request.candidates.flatMap((candidate) => candidate.files.map((file) => file.path)),
|
|
2498
|
-
...(request.adapter.kind === "node-test"
|
|
2939
|
+
...(request.adapter.kind === "node-test" || request.adapter.kind === "bun-test"
|
|
2940
|
+
? request.adapter.baseTestFiles
|
|
2941
|
+
: []),
|
|
2499
2942
|
...request.candidateRoots,
|
|
2500
2943
|
];
|
|
2501
2944
|
for (const inputPath of inputPaths) {
|
|
@@ -2537,7 +2980,9 @@ export async function verifyCampaign(value, options = {}) {
|
|
|
2537
2980
|
name: request.adapter.kind,
|
|
2538
2981
|
version: request.adapter.kind === "testforge-command"
|
|
2539
2982
|
? request.adapter.protocolVersion
|
|
2540
|
-
:
|
|
2983
|
+
: request.adapter.kind === "bun-test"
|
|
2984
|
+
? (bunIdentity?.bunVersion ?? "unprobed")
|
|
2985
|
+
: (nodeIdentity?.nodeVersion ?? "unprobed"),
|
|
2541
2986
|
configuration: request.adapter.kind === "node-test" && nodeIdentity !== undefined
|
|
2542
2987
|
? {
|
|
2543
2988
|
kind: "node-test",
|
|
@@ -2560,7 +3005,31 @@ export async function verifyCampaign(value, options = {}) {
|
|
|
2560
3005
|
...request.adapter.baseTestFiles,
|
|
2561
3006
|
],
|
|
2562
3007
|
}
|
|
2563
|
-
: request.adapter
|
|
3008
|
+
: request.adapter.kind === "bun-test" && bunIdentity !== undefined
|
|
3009
|
+
? {
|
|
3010
|
+
kind: "bun-test",
|
|
3011
|
+
profile: BUN_TEST_ADAPTER_PROFILE,
|
|
3012
|
+
requestedExecutable: bunIdentity.requestedExecutable,
|
|
3013
|
+
resolvedExecutable: bunIdentity.resolvedExecutable,
|
|
3014
|
+
executableDigest: bunIdentity.executableDigest,
|
|
3015
|
+
bunVersion: bunIdentity.bunVersion,
|
|
3016
|
+
bunRevision: bunIdentity.bunRevision,
|
|
3017
|
+
driverDigest: bunDriverDigest,
|
|
3018
|
+
helperDigest: bunHelperDigest,
|
|
3019
|
+
preloadDigest: bunPreloadDigest,
|
|
3020
|
+
runtimePreflight: bunRuntimePreflight,
|
|
3021
|
+
arguments: [
|
|
3022
|
+
"test",
|
|
3023
|
+
"--max-concurrency=1",
|
|
3024
|
+
"--retry=0",
|
|
3025
|
+
"--preload",
|
|
3026
|
+
"./node_modules/assertledger/preload.mjs",
|
|
3027
|
+
"--reporter=junit",
|
|
3028
|
+
"--reporter-outfile=<generated-junit-file>",
|
|
3029
|
+
...request.adapter.baseTestFiles,
|
|
3030
|
+
],
|
|
3031
|
+
}
|
|
3032
|
+
: request.adapter,
|
|
2564
3033
|
},
|
|
2565
3034
|
execution: {
|
|
2566
3035
|
isolation: container === undefined ? "UNSANDBOXED" : "CONTAINER",
|
|
@@ -2612,9 +3081,21 @@ export async function verifyCampaign(value, options = {}) {
|
|
|
2612
3081
|
? [
|
|
2613
3082
|
"UNSANDBOXED trusted-local execution cannot safely contain hostile candidate code.",
|
|
2614
3083
|
"Process-tree termination is best effort and depends on host operating-system facilities.",
|
|
3084
|
+
...(request.adapter.kind === "bun-test"
|
|
3085
|
+
? [
|
|
3086
|
+
"Only failures from the AssertLedger-owned assertSame helper are admitted as Bun assertions; native bun:test expect failures are not assertion evidence.",
|
|
3087
|
+
"Bun callback instrumentation determines assertion ownership; JUnit counts only check completeness and never classify assertions.",
|
|
3088
|
+
]
|
|
3089
|
+
: []),
|
|
2615
3090
|
]
|
|
2616
3091
|
: [...CONTAINER_LIMITATIONS],
|
|
2617
3092
|
};
|
|
3093
|
+
if (request.adapter.kind === "bun-test" &&
|
|
3094
|
+
((await sha256File(BUN_TEST_DRIVER_PATH)) !== bunDriverDigest ||
|
|
3095
|
+
(await sha256File(BUN_TEST_HELPER_PATH)) !== bunHelperDigest ||
|
|
3096
|
+
(await sha256File(BUN_TEST_PRELOAD_PATH)) !== bunPreloadDigest)) {
|
|
3097
|
+
throw new Error("BUN_TEST_ASSET_CHANGED_DURING_CAMPAIGN");
|
|
3098
|
+
}
|
|
2618
3099
|
return sealManifestArtifact(finalManifest);
|
|
2619
3100
|
}
|
|
2620
3101
|
finally {
|