assertledger 1.1.1 → 1.2.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 +14 -4
- package/README.md +14 -4
- package/conformance/schema-extensions.json +25 -0
- package/dist/build-info.json +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +68 -20
- 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/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 +11 -5
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +454 -49
- 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/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 +7 -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 +6 -5
- package/docs/repository-init.md +38 -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
|
@@ -6,20 +6,24 @@ 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, parseVersionedRepositoryInitConfig, parseVersionedRepositoryInitLock, parseVerificationRequest, 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
13
|
import { NODE_TEST_ADAPTER_PROFILE } from "./adapters/node-test-profile.js";
|
|
14
|
+
import { BUN_TEST_ADAPTER_PROFILE } from "./adapters/bun-test-profile.js";
|
|
14
15
|
import { runNodeTestRuntimePreflight, } from "./adapters/node-test-runtime.js";
|
|
15
16
|
import { CONTAINER_LIMITATIONS, CONTAINER_REPORTER_FILE, CONTAINER_RESULT_FILE, CONTAINER_WORKSPACE, DEFAULT_CONTAINER_RUNTIME_COMMAND, prepareContainerBackend, runContainerExecution, } from "./container.js";
|
|
16
17
|
import { normalizeRuntimeFacts, RUNTIME_FACTS_VERSION } from "./adapters/runtime-facts.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,
|
|
@@ -280,6 +284,32 @@ function registerPortablePath(spellings, relative) {
|
|
|
280
284
|
throw new Error("PORTABLE_PATH_COLLISION");
|
|
281
285
|
spellings.set(key, relative);
|
|
282
286
|
}
|
|
287
|
+
/** The declared list of a valid configuration; anything unreadable falls back to the defaults,
|
|
288
|
+
* which only widens the inventory and so keeps every link check. */
|
|
289
|
+
async function configuredRepositoryExcludes(root) {
|
|
290
|
+
try {
|
|
291
|
+
const configPath = path.join(root, INIT_CONFIG_FILE);
|
|
292
|
+
if (!(await lstat(configPath)).isFile())
|
|
293
|
+
return [];
|
|
294
|
+
const value = JSON.parse(await readFile(configPath, "utf8"));
|
|
295
|
+
return parseVersionedRepositoryInitConfig(value).repository.exclude;
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
return [];
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async function configuredRepositoryFramework(root) {
|
|
302
|
+
try {
|
|
303
|
+
const configPath = path.join(root, INIT_CONFIG_FILE);
|
|
304
|
+
if (!(await lstat(configPath)).isFile())
|
|
305
|
+
return undefined;
|
|
306
|
+
const value = JSON.parse(await readFile(configPath, "utf8"));
|
|
307
|
+
return parseVersionedRepositoryInitConfig(value).framework;
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
return undefined;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
283
313
|
async function walkFiles(root, excludes) {
|
|
284
314
|
const files = [];
|
|
285
315
|
const spellings = new Map();
|
|
@@ -419,6 +449,9 @@ function initEvidenceKind(file) {
|
|
|
419
449
|
function initNodeTestSource(file) {
|
|
420
450
|
return /(?:^|\/)[^/]+\.test\.[cm]?[jt]s$/u.test(file);
|
|
421
451
|
}
|
|
452
|
+
function initBunTestSource(file) {
|
|
453
|
+
return /(?:^|\/)[^/]+\.(?:test|spec)\.[cm]?[jt]sx?$/u.test(file);
|
|
454
|
+
}
|
|
422
455
|
function initSafePathPattern(value) {
|
|
423
456
|
if (value.startsWith("-") || value.includes("\\"))
|
|
424
457
|
return false;
|
|
@@ -462,7 +495,10 @@ function initPathPatternMatcher(pattern) {
|
|
|
462
495
|
}
|
|
463
496
|
function initCommandPathMatchers(command, testSources) {
|
|
464
497
|
const conventionalDirectories = new Set(["test", "tests", "spec", "specs", "__tests__"]);
|
|
465
|
-
|
|
498
|
+
const operands = command.executable === "bun" && command.arguments[0] === "test"
|
|
499
|
+
? command.arguments.slice(1)
|
|
500
|
+
: command.arguments;
|
|
501
|
+
return operands
|
|
466
502
|
.filter((argument) => initSafePathPattern(argument) &&
|
|
467
503
|
(/[*?]/u.test(argument) ||
|
|
468
504
|
argument.includes("/") ||
|
|
@@ -571,8 +607,9 @@ function initEmptyDetections(reasonCodes) {
|
|
|
571
607
|
};
|
|
572
608
|
}
|
|
573
609
|
function initTerminalResult(status, detections, reasonCodes) {
|
|
574
|
-
|
|
575
|
-
|
|
610
|
+
const bunBuiltIn = detections.adapterRecommendation === "bun-test";
|
|
611
|
+
const result = {
|
|
612
|
+
schemaVersion: bunBuiltIn ? "2.0.0" : "1.0.0",
|
|
576
613
|
status,
|
|
577
614
|
reasonCodes: [...new Set(reasonCodes)].sort(),
|
|
578
615
|
detections: { ...detections, reasonCodes: [...new Set(detections.reasonCodes)].sort() },
|
|
@@ -580,7 +617,8 @@ function initTerminalResult(status, detections, reasonCodes) {
|
|
|
580
617
|
files: [],
|
|
581
618
|
requiredOperatorInputs: ["worlds", "candidates"],
|
|
582
619
|
nextCommands: [{ executable: "assertledger", arguments: ["audit", ".", "--json"] }],
|
|
583
|
-
}
|
|
620
|
+
};
|
|
621
|
+
return bunBuiltIn ? parseRepositoryInitResultV2(result) : parseRepositoryInitResult(result);
|
|
584
622
|
}
|
|
585
623
|
export async function initializeRepository(requestedRoot, options = {}) {
|
|
586
624
|
let root;
|
|
@@ -594,13 +632,32 @@ export async function initializeRepository(requestedRoot, options = {}) {
|
|
|
594
632
|
"REPOSITORY_ROOT_INVALID",
|
|
595
633
|
]);
|
|
596
634
|
}
|
|
635
|
+
const declaredExcludes = options.exclude ?? (await configuredRepositoryExcludes(root));
|
|
636
|
+
const repositoryExclude = [...new Set([...DEFAULT_EXCLUDES, ...declaredExcludes])].sort();
|
|
637
|
+
if (repositoryExclude.length > 1_000 ||
|
|
638
|
+
options.exclude?.some((entry) => !isRepositoryExcludeName(entry))) {
|
|
639
|
+
return initTerminalResult("CONFLICT", initEmptyDetections(["INVALID_REPOSITORY_EXCLUDE"]), [
|
|
640
|
+
"INVALID_REPOSITORY_EXCLUDE",
|
|
641
|
+
]);
|
|
642
|
+
}
|
|
643
|
+
const inventoryExcludes = effectiveExcludes([...INIT_MANAGED_FILES, ...repositoryExclude]);
|
|
597
644
|
const candidateRoots = ["tests/candidates"];
|
|
598
645
|
const outsideCandidateRoots = (file) => !candidateRoots.some((candidateRoot) => {
|
|
599
646
|
const rootKey = portablePathKey(candidateRoot);
|
|
600
647
|
const fileKey = portablePathKey(file);
|
|
601
648
|
return fileKey === rootKey || fileKey.startsWith(`${rootKey}/`);
|
|
602
649
|
});
|
|
603
|
-
|
|
650
|
+
let inventory;
|
|
651
|
+
try {
|
|
652
|
+
inventory = await walkFiles(root, inventoryExcludes);
|
|
653
|
+
}
|
|
654
|
+
catch (error) {
|
|
655
|
+
if (!(error instanceof Error) || error.message !== "UNSUPPORTED_REPOSITORY_SYMLINK")
|
|
656
|
+
throw error;
|
|
657
|
+
return initTerminalResult("CONFLICT", initEmptyDetections(["UNSUPPORTED_REPOSITORY_SYMLINK"]), [
|
|
658
|
+
"UNSUPPORTED_REPOSITORY_SYMLINK",
|
|
659
|
+
]);
|
|
660
|
+
}
|
|
604
661
|
const inScopeFiles = inventory.files.filter(outsideCandidateRoots);
|
|
605
662
|
const contents = new Map();
|
|
606
663
|
const evidenceBytes = new Map();
|
|
@@ -713,7 +770,7 @@ export async function initializeRepository(requestedRoot, options = {}) {
|
|
|
713
770
|
(contents.has("requirements.txt") &&
|
|
714
771
|
/(^|\n)pytest(?:[=<>!~]|$)/u.test(contents.get("requirements.txt") ?? "")))
|
|
715
772
|
frameworkFacts.add("pytest");
|
|
716
|
-
const frameworkOverride = options.framework;
|
|
773
|
+
const frameworkOverride = options.framework ?? (await configuredRepositoryFramework(root));
|
|
717
774
|
if (frameworkOverride !== undefined && !isInitFramework(frameworkOverride)) {
|
|
718
775
|
const detections = initEmptyDetections(["FRAMEWORK_OVERRIDE_INVALID"]);
|
|
719
776
|
detections.packageManager = packageManager;
|
|
@@ -825,8 +882,24 @@ export async function initializeRepository(requestedRoot, options = {}) {
|
|
|
825
882
|
adapterRecommendation = "node-test";
|
|
826
883
|
}
|
|
827
884
|
}
|
|
885
|
+
else if (framework === "bun:test") {
|
|
886
|
+
const testSources = inScopeFiles.filter((file) => initBunTestSource(file) &&
|
|
887
|
+
initJavaScriptModuleSpecifiers(contents.get(file) ?? "").has("bun:test"));
|
|
888
|
+
const pathMatchers = initCommandPathMatchers(testCommand, testSources);
|
|
889
|
+
const baseTestFiles = pathMatchers.length === 0
|
|
890
|
+
? testSources
|
|
891
|
+
: testSources.filter((file) => pathMatchers.some((matcher) => matcher(file)));
|
|
892
|
+
if (baseTestFiles.length > 0) {
|
|
893
|
+
adapter = { kind: "bun-test", executable: "bun", baseTestFiles };
|
|
894
|
+
adapterRecommendation = "bun-test";
|
|
895
|
+
}
|
|
896
|
+
}
|
|
828
897
|
const reasonCodes = adapter === undefined
|
|
829
|
-
? [
|
|
898
|
+
? [
|
|
899
|
+
framework === "node:test" || framework === "bun:test"
|
|
900
|
+
? "BASE_TEST_FILES_UNAVAILABLE"
|
|
901
|
+
: "OFFICIAL_ADAPTER_UNAVAILABLE",
|
|
902
|
+
]
|
|
830
903
|
: [];
|
|
831
904
|
const detections = {
|
|
832
905
|
packageManager,
|
|
@@ -839,17 +912,23 @@ export async function initializeRepository(requestedRoot, options = {}) {
|
|
|
839
912
|
if (adapter === undefined)
|
|
840
913
|
return initTerminalResult("BLOCKED", detections, reasonCodes);
|
|
841
914
|
await options.afterEvidenceSnapshot?.();
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
|
|
915
|
+
const bunBuiltIn = adapter.kind === "bun-test";
|
|
916
|
+
const configValue = {
|
|
917
|
+
schemaVersion: bunBuiltIn ? "2.0.0" : "1.0.0",
|
|
918
|
+
repository: { root: ".", exclude: repositoryExclude },
|
|
845
919
|
packageManager,
|
|
846
920
|
framework,
|
|
847
921
|
testCommand,
|
|
848
922
|
adapter,
|
|
849
923
|
candidateRoots: [...candidateRoots],
|
|
850
|
-
}
|
|
924
|
+
};
|
|
925
|
+
const config = bunBuiltIn
|
|
926
|
+
? parseRepositoryInitConfigV2(configValue)
|
|
927
|
+
: parseRepositoryInitConfig(configValue);
|
|
851
928
|
const configContent = initJsonBytes(config);
|
|
852
|
-
const configDigest =
|
|
929
|
+
const configDigest = config.schemaVersion === "2.0.0"
|
|
930
|
+
? repositoryInitConfigV2Digest(config)
|
|
931
|
+
: repositoryInitConfigDigest(config);
|
|
853
932
|
const evidenceFiles = inScopeFiles.filter((file) => initEvidenceKind(file) !== undefined);
|
|
854
933
|
if (adapterEvidencePath !== undefined && !evidenceFiles.includes(adapterEvidencePath))
|
|
855
934
|
evidenceFiles.push(adapterEvidencePath);
|
|
@@ -867,17 +946,33 @@ export async function initializeRepository(requestedRoot, options = {}) {
|
|
|
867
946
|
: (initEvidenceKind(file) ?? "ADAPTER_CONFIG"),
|
|
868
947
|
});
|
|
869
948
|
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
949
|
+
let lock;
|
|
950
|
+
if (bunBuiltIn) {
|
|
951
|
+
const lockBase = {
|
|
952
|
+
schemaVersion: "2.0.0",
|
|
953
|
+
configDigest,
|
|
954
|
+
detector: { name: "assertledger-init", version: "2.0.0" },
|
|
955
|
+
evidence,
|
|
956
|
+
detections,
|
|
957
|
+
};
|
|
958
|
+
lock = parseRepositoryInitLockV2({
|
|
959
|
+
...lockBase,
|
|
960
|
+
lockDigest: repositoryInitLockV2Digest(lockBase),
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
else {
|
|
964
|
+
const lockBase = {
|
|
965
|
+
schemaVersion: "1.0.0",
|
|
966
|
+
configDigest,
|
|
967
|
+
detector: { name: "assertledger-init", version: "1.0.0" },
|
|
968
|
+
evidence,
|
|
969
|
+
detections: RepositoryInitDetectionsSchema.parse(detections),
|
|
970
|
+
};
|
|
971
|
+
lock = parseRepositoryInitLock({
|
|
972
|
+
...lockBase,
|
|
973
|
+
lockDigest: repositoryInitLockDigest(lockBase),
|
|
974
|
+
});
|
|
975
|
+
}
|
|
881
976
|
const lockContent = initJsonBytes(lock);
|
|
882
977
|
const plannedFiles = [
|
|
883
978
|
{ path: INIT_CONFIG_FILE, digest: rawSha256(configContent), content: configContent },
|
|
@@ -885,7 +980,7 @@ export async function initializeRepository(requestedRoot, options = {}) {
|
|
|
885
980
|
];
|
|
886
981
|
let finalInScopeFiles;
|
|
887
982
|
try {
|
|
888
|
-
const finalInventory = await walkFiles(root,
|
|
983
|
+
const finalInventory = await walkFiles(root, inventoryExcludes);
|
|
889
984
|
finalInScopeFiles = finalInventory.files.filter(outsideCandidateRoots);
|
|
890
985
|
}
|
|
891
986
|
catch {
|
|
@@ -940,7 +1035,7 @@ export async function initializeRepository(requestedRoot, options = {}) {
|
|
|
940
1035
|
let structural;
|
|
941
1036
|
try {
|
|
942
1037
|
const value = JSON.parse(existingLock);
|
|
943
|
-
structural =
|
|
1038
|
+
structural = parseVersionedRepositoryInitLock(value);
|
|
944
1039
|
}
|
|
945
1040
|
catch {
|
|
946
1041
|
return initTerminalResult("CONFLICT", { ...detections, reasonCodes: ["LOCK_INVALID"] }, [
|
|
@@ -952,8 +1047,8 @@ export async function initializeRepository(requestedRoot, options = {}) {
|
|
|
952
1047
|
}
|
|
953
1048
|
}
|
|
954
1049
|
if (existingConfig === configContent && existingLock === lockContent) {
|
|
955
|
-
|
|
956
|
-
schemaVersion: "1.0.0",
|
|
1050
|
+
const result = {
|
|
1051
|
+
schemaVersion: bunBuiltIn ? "2.0.0" : "1.0.0",
|
|
957
1052
|
status: "UNCHANGED",
|
|
958
1053
|
reasonCodes: [],
|
|
959
1054
|
detections,
|
|
@@ -961,7 +1056,8 @@ export async function initializeRepository(requestedRoot, options = {}) {
|
|
|
961
1056
|
files: plannedFiles,
|
|
962
1057
|
requiredOperatorInputs: ["worlds", "candidates"],
|
|
963
1058
|
nextCommands: [{ executable: "assertledger", arguments: ["audit", ".", "--json"] }],
|
|
964
|
-
}
|
|
1059
|
+
};
|
|
1060
|
+
return bunBuiltIn ? parseRepositoryInitResultV2(result) : parseRepositoryInitResult(result);
|
|
965
1061
|
}
|
|
966
1062
|
const actions = [];
|
|
967
1063
|
if (existingConfig === undefined)
|
|
@@ -972,8 +1068,8 @@ export async function initializeRepository(requestedRoot, options = {}) {
|
|
|
972
1068
|
actions.push({ kind: "REGENERATE", path: INIT_LOCK_FILE });
|
|
973
1069
|
actions.sort((left, right) => left.path.localeCompare(right.path));
|
|
974
1070
|
const status = options.dryRun ? "WOULD_CREATE" : "CREATED";
|
|
975
|
-
const
|
|
976
|
-
schemaVersion: "1.0.0",
|
|
1071
|
+
const resultValue = {
|
|
1072
|
+
schemaVersion: bunBuiltIn ? "2.0.0" : "1.0.0",
|
|
977
1073
|
status,
|
|
978
1074
|
reasonCodes: [],
|
|
979
1075
|
detections,
|
|
@@ -981,7 +1077,10 @@ export async function initializeRepository(requestedRoot, options = {}) {
|
|
|
981
1077
|
files: plannedFiles,
|
|
982
1078
|
requiredOperatorInputs: ["worlds", "candidates"],
|
|
983
1079
|
nextCommands: [{ executable: "assertledger", arguments: ["audit", ".", "--json"] }],
|
|
984
|
-
}
|
|
1080
|
+
};
|
|
1081
|
+
const result = bunBuiltIn
|
|
1082
|
+
? parseRepositoryInitResultV2(resultValue)
|
|
1083
|
+
: parseRepositoryInitResult(resultValue);
|
|
985
1084
|
if (!options.dryRun) {
|
|
986
1085
|
for (const action of actions) {
|
|
987
1086
|
const file = plannedFiles.find((candidate) => candidate.path === action.path);
|
|
@@ -1029,9 +1128,52 @@ async function probeRuntimeTemporaryWorkspace() {
|
|
|
1029
1128
|
}
|
|
1030
1129
|
export async function doctorRepositoryRuntime(requestedRoot, options) {
|
|
1031
1130
|
const repositoryRoot = path.resolve(requestedRoot);
|
|
1131
|
+
const configuration = options.allowUnsafeExecution
|
|
1132
|
+
? await initializeRepository(repositoryRoot, { dryRun: true })
|
|
1133
|
+
: undefined;
|
|
1134
|
+
if (configuration?.schemaVersion === "2.0.0" &&
|
|
1135
|
+
configuration.detections.adapterRecommendation === "bun-test") {
|
|
1136
|
+
const configFile = configuration.files.find((file) => file.path === INIT_CONFIG_FILE);
|
|
1137
|
+
if (configFile === undefined)
|
|
1138
|
+
throw new Error("BUN_TEST_CONFIG_MISSING");
|
|
1139
|
+
const config = parseRepositoryInitConfigV2(JSON.parse(configFile.content));
|
|
1140
|
+
const environmentAllowlist = ["PATH", "SystemRoot", "WINDIR", "TEMP", "TMP"];
|
|
1141
|
+
let resolvedExecutable = config.adapter.executable;
|
|
1142
|
+
return runBunRuntimeDoctorChecks(repositoryRoot, configuration, {
|
|
1143
|
+
probeExecutable: async () => {
|
|
1144
|
+
const identity = await probeBunTestExecutable(config.adapter.executable, repositoryRoot, environmentAllowlist, 5_000, CONTROLLED_REPORT_MAXIMUM_BYTES);
|
|
1145
|
+
resolvedExecutable = identity.resolvedExecutable;
|
|
1146
|
+
return { bunVersion: identity.bunVersion };
|
|
1147
|
+
},
|
|
1148
|
+
probeDependencies: async () => {
|
|
1149
|
+
const result = await runProcess({
|
|
1150
|
+
executable: resolvedExecutable,
|
|
1151
|
+
args: ["-e", 'import("bun:test").then(() => process.stdout.write("READY"))'],
|
|
1152
|
+
cwd: repositoryRoot,
|
|
1153
|
+
environment: environmentFromAllowlist(environmentAllowlist),
|
|
1154
|
+
timeoutMs: 5_000,
|
|
1155
|
+
maximumOutputBytes: CONTROLLED_REPORT_MAXIMUM_BYTES,
|
|
1156
|
+
});
|
|
1157
|
+
if (result.outcome !== "PASS" ||
|
|
1158
|
+
result.stdout.text !== "READY" ||
|
|
1159
|
+
result.stderr.totalBytes !== 0) {
|
|
1160
|
+
throw new Error("BUN_TEST_DEPENDENCY_UNAVAILABLE");
|
|
1161
|
+
}
|
|
1162
|
+
},
|
|
1163
|
+
probeTemporaryWorkspace: probeRuntimeTemporaryWorkspace,
|
|
1164
|
+
runSyntheticPreflight: async () => {
|
|
1165
|
+
await runBunTestRuntimePreflight(resolvedExecutable, environmentAllowlist, 5_000, CONTROLLED_REPORT_MAXIMUM_BYTES);
|
|
1166
|
+
},
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1032
1169
|
let resolvedExecutable = process.execPath;
|
|
1033
1170
|
return runRuntimeDoctorChecks(repositoryRoot, options.allowUnsafeExecution, {
|
|
1034
|
-
inspectConfiguration: () =>
|
|
1171
|
+
inspectConfiguration: async () => {
|
|
1172
|
+
const inspected = configuration ?? (await initializeRepository(repositoryRoot, { dryRun: true }));
|
|
1173
|
+
if (inspected.schemaVersion !== "1.0.0")
|
|
1174
|
+
throw new Error("RUNTIME_CONFIGURATION_UNAVAILABLE");
|
|
1175
|
+
return inspected;
|
|
1176
|
+
},
|
|
1035
1177
|
probeExecutable: async () => {
|
|
1036
1178
|
const identity = await probeNodeTestExecutable(process.execPath, repositoryRoot, [], 5_000, CONTROLLED_REPORT_MAXIMUM_BYTES);
|
|
1037
1179
|
resolvedExecutable = identity.resolvedExecutable;
|
|
@@ -1368,9 +1510,10 @@ async function resolveRepositoryRoot(rootInput) {
|
|
|
1368
1510
|
throw new TypeError("INVALID_REPOSITORY_ROOT");
|
|
1369
1511
|
}
|
|
1370
1512
|
}
|
|
1371
|
-
export async function analyzeRepository(rootInput) {
|
|
1513
|
+
export async function analyzeRepository(rootInput, options = {}) {
|
|
1372
1514
|
const root = await resolveRepositoryRoot(rootInput);
|
|
1373
|
-
const
|
|
1515
|
+
const declaredExcludes = options.configuredExcludes === true ? await configuredRepositoryExcludes(root) : [];
|
|
1516
|
+
const { files } = await walkFiles(root, effectiveExcludes(declaredExcludes));
|
|
1374
1517
|
const digest = createHash("sha256");
|
|
1375
1518
|
const languageCounts = new Map();
|
|
1376
1519
|
const frameworks = new Set();
|
|
@@ -1777,7 +1920,9 @@ function parseRequest(value) {
|
|
|
1777
1920
|
if (candidateRoots.length === 0)
|
|
1778
1921
|
throw new TypeError("INVALID_CANDIDATE_ROOTS");
|
|
1779
1922
|
assertPortableStringCollection(candidateRoots, "DUPLICATE_CANDIDATE_ROOT");
|
|
1780
|
-
if (adapter.kind !== "node-test" &&
|
|
1923
|
+
if (adapter.kind !== "node-test" &&
|
|
1924
|
+
adapter.kind !== "bun-test" &&
|
|
1925
|
+
adapter.kind !== "testforge-command") {
|
|
1781
1926
|
throw new TypeError("UNSUPPORTED_ADAPTER");
|
|
1782
1927
|
}
|
|
1783
1928
|
// The versioned contract already restricts container isolation to v2 requests.
|
|
@@ -1896,13 +2041,15 @@ function parseRequest(value) {
|
|
|
1896
2041
|
if (minimumTargetWeightPermille > 1_000) {
|
|
1897
2042
|
throw new TypeError("INVALID_MINIMUM_TARGET_WEIGHT_PERMILLE");
|
|
1898
2043
|
}
|
|
1899
|
-
const normalizedAdapter = adapter.kind === "node-test"
|
|
2044
|
+
const normalizedAdapter = adapter.kind === "node-test" || adapter.kind === "bun-test"
|
|
1900
2045
|
? (() => {
|
|
1901
|
-
const extraArguments =
|
|
2046
|
+
const extraArguments = adapter.kind === "node-test"
|
|
2047
|
+
? parseNodeTestExtraArguments(adapter.extraArguments)
|
|
2048
|
+
: undefined;
|
|
1902
2049
|
const baseTestFiles = requireStringArray(adapter.baseTestFiles, "base_test_files").map((file) => assertSafeRelativePath(file, ["."]));
|
|
1903
2050
|
assertPortableStringCollection(baseTestFiles, "DUPLICATE_BASE_TEST_FILE");
|
|
1904
2051
|
return {
|
|
1905
|
-
kind:
|
|
2052
|
+
kind: adapter.kind,
|
|
1906
2053
|
executable: requireString(adapter.executable, "adapter_executable"),
|
|
1907
2054
|
baseTestFiles,
|
|
1908
2055
|
...(extraArguments === undefined ? {} : { extraArguments }),
|
|
@@ -2099,6 +2246,19 @@ async function readStructuredCommandReport(resultFile, processResult, maximumByt
|
|
|
2099
2246
|
}
|
|
2100
2247
|
return parseStructuredCommandReport(await readBoundedJsonFile(resultFile, maximumBytes), processResult);
|
|
2101
2248
|
}
|
|
2249
|
+
function readBunDriverReport(processResult) {
|
|
2250
|
+
if (processResult.outcome === "TIMEOUT" ||
|
|
2251
|
+
processResult.outcome === "INFRA_ERROR" ||
|
|
2252
|
+
processResult.stdout.truncated ||
|
|
2253
|
+
processResult.stdout.totalBytes > CONTROLLED_REPORT_MAXIMUM_BYTES)
|
|
2254
|
+
return undefined;
|
|
2255
|
+
try {
|
|
2256
|
+
return parseStructuredCommandReport(JSON.parse(processResult.stdout.text), processResult);
|
|
2257
|
+
}
|
|
2258
|
+
catch {
|
|
2259
|
+
return undefined;
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2102
2262
|
async function readNodeTestReport(resultFile, processResult, maximumBytes) {
|
|
2103
2263
|
if (processResult.outcome === "TIMEOUT" || processResult.outcome === "INFRA_ERROR") {
|
|
2104
2264
|
return undefined;
|
|
@@ -2179,6 +2339,71 @@ async function probeNodeTestExecutable(requestedExecutable, repositoryRoot, envi
|
|
|
2179
2339
|
nodeVersion: secondProbe.nodeVersion,
|
|
2180
2340
|
};
|
|
2181
2341
|
}
|
|
2342
|
+
async function runBunExecutableProbe(executable, repositoryRoot, environmentAllowlist, timeoutMs, maximumOutputBytes) {
|
|
2343
|
+
const result = await runProcess({
|
|
2344
|
+
executable,
|
|
2345
|
+
args: [
|
|
2346
|
+
"-e",
|
|
2347
|
+
"console.log(JSON.stringify({execPath:process.execPath,bunVersion:Bun.version,bunRevision:Bun.revision}))",
|
|
2348
|
+
],
|
|
2349
|
+
cwd: repositoryRoot,
|
|
2350
|
+
environment: environmentFromAllowlist(environmentAllowlist),
|
|
2351
|
+
timeoutMs: Math.min(timeoutMs, 5_000),
|
|
2352
|
+
maximumOutputBytes: Math.min(maximumOutputBytes, CONTROLLED_REPORT_MAXIMUM_BYTES),
|
|
2353
|
+
});
|
|
2354
|
+
if (result.outcome !== "PASS" || result.stdout.truncated || result.stderr.totalBytes !== 0) {
|
|
2355
|
+
throw new Error("BUN_TEST_EXECUTABLE_PROBE_FAILED");
|
|
2356
|
+
}
|
|
2357
|
+
let report;
|
|
2358
|
+
try {
|
|
2359
|
+
report = JSON.parse(result.stdout.text.trim());
|
|
2360
|
+
}
|
|
2361
|
+
catch {
|
|
2362
|
+
throw new Error("BUN_TEST_EXECUTABLE_PROBE_FAILED");
|
|
2363
|
+
}
|
|
2364
|
+
if (!isRecord(report) ||
|
|
2365
|
+
Object.keys(report).sort().join("\0") !== "bunRevision\0bunVersion\0execPath" ||
|
|
2366
|
+
typeof report.execPath !== "string" ||
|
|
2367
|
+
report.execPath.length === 0 ||
|
|
2368
|
+
typeof report.bunVersion !== "string" ||
|
|
2369
|
+
typeof report.bunRevision !== "string") {
|
|
2370
|
+
throw new Error("BUN_TEST_EXECUTABLE_PROBE_FAILED");
|
|
2371
|
+
}
|
|
2372
|
+
if (report.bunVersion !== BUN_TEST_ADAPTER_PROFILE.bunVersion ||
|
|
2373
|
+
report.bunRevision !== BUN_TEST_ADAPTER_PROFILE.bunRevision) {
|
|
2374
|
+
throw new Error("BUN_TEST_VERSION_UNSUPPORTED");
|
|
2375
|
+
}
|
|
2376
|
+
return {
|
|
2377
|
+
execPath: report.execPath,
|
|
2378
|
+
bunVersion: report.bunVersion,
|
|
2379
|
+
bunRevision: report.bunRevision,
|
|
2380
|
+
};
|
|
2381
|
+
}
|
|
2382
|
+
async function probeBunTestExecutable(requestedExecutable, repositoryRoot, environmentAllowlist, timeoutMs, maximumOutputBytes) {
|
|
2383
|
+
const first = await runBunExecutableProbe(requestedExecutable, repositoryRoot, environmentAllowlist, timeoutMs, maximumOutputBytes);
|
|
2384
|
+
let resolvedExecutable;
|
|
2385
|
+
try {
|
|
2386
|
+
resolvedExecutable = await realpath(first.execPath);
|
|
2387
|
+
if (!(await stat(resolvedExecutable)).isFile())
|
|
2388
|
+
throw new Error("not a file");
|
|
2389
|
+
}
|
|
2390
|
+
catch {
|
|
2391
|
+
throw new Error("BUN_TEST_EXECUTABLE_PROBE_FAILED");
|
|
2392
|
+
}
|
|
2393
|
+
const second = await runBunExecutableProbe(resolvedExecutable, repositoryRoot, environmentAllowlist, timeoutMs, maximumOutputBytes);
|
|
2394
|
+
if ((await realpath(second.execPath)) !== resolvedExecutable ||
|
|
2395
|
+
second.bunVersion !== first.bunVersion ||
|
|
2396
|
+
second.bunRevision !== first.bunRevision) {
|
|
2397
|
+
throw new Error("BUN_TEST_EXECUTABLE_PROBE_FAILED");
|
|
2398
|
+
}
|
|
2399
|
+
return {
|
|
2400
|
+
requestedExecutable,
|
|
2401
|
+
resolvedExecutable,
|
|
2402
|
+
executableDigest: await sha256File(resolvedExecutable),
|
|
2403
|
+
bunVersion: second.bunVersion,
|
|
2404
|
+
bunRevision: second.bunRevision,
|
|
2405
|
+
};
|
|
2406
|
+
}
|
|
2182
2407
|
// Evaluated by `node -p` inside a fresh container before any candidate code runs there.
|
|
2183
2408
|
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
2409
|
function parseJsonBytes(bytes) {
|
|
@@ -2286,6 +2511,8 @@ function containerPreflightExecutor(container, executable) {
|
|
|
2286
2511
|
}
|
|
2287
2512
|
async function executeContainerAdapter(request, container, workspace, candidateFiles) {
|
|
2288
2513
|
const adapter = request.adapter;
|
|
2514
|
+
if (adapter.kind === "bun-test")
|
|
2515
|
+
throw new Error("BUN_TEST_CONTAINER_UNSUPPORTED");
|
|
2289
2516
|
const executed = await runContainerExecution(container, {
|
|
2290
2517
|
workspace,
|
|
2291
2518
|
...(adapter.kind === "node-test" ? { reporterSource: NODE_TEST_REPORTER_SOURCE } : {}),
|
|
@@ -2370,6 +2597,9 @@ async function executeTrustedLocalAdapter(request, workspace, temporaryRoot, can
|
|
|
2370
2597
|
environment.TESTFORGE_RESULT_FILE = resultFile;
|
|
2371
2598
|
environment.TESTFORGE_CANDIDATE_FILES = JSON.stringify(candidateFiles);
|
|
2372
2599
|
}
|
|
2600
|
+
else if (request.adapter.kind === "bun-test") {
|
|
2601
|
+
environment.TESTFORGE_CANDIDATE_FILES = JSON.stringify(candidateFiles);
|
|
2602
|
+
}
|
|
2373
2603
|
else {
|
|
2374
2604
|
if (nodeTestReporterPath === undefined)
|
|
2375
2605
|
throw new Error("NODE_TEST_REPORTER_MISSING");
|
|
@@ -2385,20 +2615,30 @@ async function executeTrustedLocalAdapter(request, workspace, temporaryRoot, can
|
|
|
2385
2615
|
...request.adapter.baseTestFiles,
|
|
2386
2616
|
...candidateFiles,
|
|
2387
2617
|
]
|
|
2388
|
-
: request.adapter.
|
|
2618
|
+
: request.adapter.kind === "bun-test"
|
|
2619
|
+
? [
|
|
2620
|
+
BUN_TEST_DRIVER_PATH,
|
|
2621
|
+
request.adapter.executable,
|
|
2622
|
+
BUN_TEST_HELPER_PATH,
|
|
2623
|
+
...request.adapter.baseTestFiles,
|
|
2624
|
+
]
|
|
2625
|
+
: request.adapter.arguments;
|
|
2389
2626
|
const result = await runProcess({
|
|
2390
|
-
executable: request.adapter.executable,
|
|
2627
|
+
executable: request.adapter.kind === "bun-test" ? process.execPath : request.adapter.executable,
|
|
2391
2628
|
args,
|
|
2392
2629
|
cwd: workspace,
|
|
2393
2630
|
environment,
|
|
2394
2631
|
timeoutMs: request.budgets.timeoutMsPerExecution,
|
|
2395
2632
|
maximumOutputBytes: request.budgets.maximumOutputBytes,
|
|
2396
2633
|
});
|
|
2634
|
+
const structuredReport = request.adapter.kind === "testforge-command"
|
|
2635
|
+
? await readStructuredCommandReport(resultFile, result, CONTROLLED_REPORT_MAXIMUM_BYTES)
|
|
2636
|
+
: request.adapter.kind === "bun-test"
|
|
2637
|
+
? readBunDriverReport(result)
|
|
2638
|
+
: undefined;
|
|
2397
2639
|
return {
|
|
2398
2640
|
result,
|
|
2399
|
-
structuredReport
|
|
2400
|
-
? await readStructuredCommandReport(resultFile, result, CONTROLLED_REPORT_MAXIMUM_BYTES)
|
|
2401
|
-
: undefined,
|
|
2641
|
+
structuredReport,
|
|
2402
2642
|
nodeTestReport: request.adapter.kind === "node-test"
|
|
2403
2643
|
? await readNodeTestReport(resultFile, result, CONTROLLED_REPORT_MAXIMUM_BYTES)
|
|
2404
2644
|
: undefined,
|
|
@@ -2444,6 +2684,119 @@ async function executeObservation(request, repositoryRoot, world, candidate, att
|
|
|
2444
2684
|
await removeTemporaryDirectory(temporaryRoot);
|
|
2445
2685
|
}
|
|
2446
2686
|
}
|
|
2687
|
+
async function runBunTestRuntimePreflight(executable, environmentAllowlist, timeoutMs, maximumOutputBytes) {
|
|
2688
|
+
const probeSources = [
|
|
2689
|
+
{
|
|
2690
|
+
name: "assertion",
|
|
2691
|
+
source: 'import { test } from "bun:test"; import { assertSame } from "assertledger/bun"; test("assertion", () => assertSame(1, 2));\n',
|
|
2692
|
+
outcome: "ASSERTION_FAILURE",
|
|
2693
|
+
attributed: true,
|
|
2694
|
+
},
|
|
2695
|
+
{
|
|
2696
|
+
name: "generic-throw",
|
|
2697
|
+
source: 'import { test } from "bun:test"; test("generic", () => { throw new Error("generic"); });\n',
|
|
2698
|
+
outcome: "PROCESS_CRASH",
|
|
2699
|
+
attributed: false,
|
|
2700
|
+
},
|
|
2701
|
+
{
|
|
2702
|
+
name: "caught-assertion",
|
|
2703
|
+
source: 'import { test } from "bun:test"; import { assertSame } from "assertledger/bun"; test("caught", () => { try { assertSame(1, 2); } catch {} throw new Error("generic"); });\n',
|
|
2704
|
+
outcome: "PROCESS_CRASH",
|
|
2705
|
+
attributed: false,
|
|
2706
|
+
},
|
|
2707
|
+
{
|
|
2708
|
+
name: "replayed-assertion",
|
|
2709
|
+
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',
|
|
2710
|
+
outcome: "PROCESS_CRASH",
|
|
2711
|
+
attributed: false,
|
|
2712
|
+
},
|
|
2713
|
+
{
|
|
2714
|
+
name: "replayed-row",
|
|
2715
|
+
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',
|
|
2716
|
+
outcome: "PROCESS_CRASH",
|
|
2717
|
+
attributed: false,
|
|
2718
|
+
},
|
|
2719
|
+
{
|
|
2720
|
+
name: "async-cross-test",
|
|
2721
|
+
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',
|
|
2722
|
+
outcome: "PROCESS_CRASH",
|
|
2723
|
+
attributed: false,
|
|
2724
|
+
},
|
|
2725
|
+
{
|
|
2726
|
+
name: "multi-assertion-rows",
|
|
2727
|
+
source: 'import { test } from "bun:test"; import { assertSame } from "assertledger/bun"; test.each([1, 2])("row %i", (row) => assertSame(row, 0));\n',
|
|
2728
|
+
outcome: "ASSERTION_FAILURE",
|
|
2729
|
+
attributed: true,
|
|
2730
|
+
},
|
|
2731
|
+
{
|
|
2732
|
+
name: "operand-throw",
|
|
2733
|
+
source: 'import { test } from "bun:test"; import { assertSame } from "assertledger/bun"; test("operand", () => assertSame((() => { throw new Error("operand"); })(), 1));\n',
|
|
2734
|
+
outcome: "PROCESS_CRASH",
|
|
2735
|
+
attributed: false,
|
|
2736
|
+
},
|
|
2737
|
+
{
|
|
2738
|
+
name: "native-expect",
|
|
2739
|
+
source: 'import { test, expect } from "bun:test"; test("native", () => expect(1).toBe(2));\n',
|
|
2740
|
+
outcome: "PROCESS_CRASH",
|
|
2741
|
+
attributed: false,
|
|
2742
|
+
},
|
|
2743
|
+
{
|
|
2744
|
+
name: "hook-failure",
|
|
2745
|
+
source: 'import { afterEach, test } from "bun:test"; import { assertSame } from "assertledger/bun"; afterEach(() => { throw new Error("hook"); }); test("candidate", () => assertSame(1, 2));\n',
|
|
2746
|
+
outcome: "INFRA_ERROR",
|
|
2747
|
+
attributed: false,
|
|
2748
|
+
},
|
|
2749
|
+
];
|
|
2750
|
+
try {
|
|
2751
|
+
const probes = [];
|
|
2752
|
+
for (const probe of probeSources) {
|
|
2753
|
+
const evidence = await withEmptyWorkspace(async (workspace) => {
|
|
2754
|
+
await writeFile(path.join(workspace, "package.json"), '{"type":"module"}\n');
|
|
2755
|
+
await writeFile(path.join(workspace, "base.test.ts"), 'import { test } from "bun:test"; test("control", () => {});\n');
|
|
2756
|
+
const candidatePath = path.join(workspace, "candidate.test.ts");
|
|
2757
|
+
await writeFile(candidatePath, probe.source);
|
|
2758
|
+
const result = await runProcess({
|
|
2759
|
+
executable: process.execPath,
|
|
2760
|
+
args: [BUN_TEST_DRIVER_PATH, executable, BUN_TEST_HELPER_PATH, "base.test.ts"],
|
|
2761
|
+
cwd: workspace,
|
|
2762
|
+
environment: {
|
|
2763
|
+
...environmentFromAllowlist(environmentAllowlist),
|
|
2764
|
+
TESTFORGE_CANDIDATE_FILES: JSON.stringify(["candidate.test.ts"]),
|
|
2765
|
+
},
|
|
2766
|
+
timeoutMs: Math.min(timeoutMs, 5_000),
|
|
2767
|
+
maximumOutputBytes: Math.min(maximumOutputBytes, CONTROLLED_REPORT_MAXIMUM_BYTES),
|
|
2768
|
+
});
|
|
2769
|
+
const report = readBunDriverReport(result);
|
|
2770
|
+
const expectedCandidateCount = probe.name === "hook-failure"
|
|
2771
|
+
? 0
|
|
2772
|
+
: probe.name === "replayed-row" ||
|
|
2773
|
+
probe.name === "async-cross-test" ||
|
|
2774
|
+
probe.name === "multi-assertion-rows"
|
|
2775
|
+
? 2
|
|
2776
|
+
: 1;
|
|
2777
|
+
if (report?.outcome !== probe.outcome ||
|
|
2778
|
+
report.attributed !== probe.attributed ||
|
|
2779
|
+
report.testsDiscovered !==
|
|
2780
|
+
(expectedCandidateCount === 0 ? 0 : expectedCandidateCount + 1) ||
|
|
2781
|
+
report.candidateTestsDiscovered !== expectedCandidateCount) {
|
|
2782
|
+
throw new Error("BUN_TEST_PROFILE_PREFLIGHT_FAILED");
|
|
2783
|
+
}
|
|
2784
|
+
return {
|
|
2785
|
+
name: probe.name,
|
|
2786
|
+
outcome: report.outcome,
|
|
2787
|
+
attributed: report.attributed,
|
|
2788
|
+
testsDiscovered: report.testsDiscovered,
|
|
2789
|
+
candidateTestsDiscovered: report.candidateTestsDiscovered,
|
|
2790
|
+
};
|
|
2791
|
+
});
|
|
2792
|
+
probes.push(evidence);
|
|
2793
|
+
}
|
|
2794
|
+
return { protocolVersion: "1.0.0", probes };
|
|
2795
|
+
}
|
|
2796
|
+
catch (error) {
|
|
2797
|
+
throw new Error("BUN_TEST_PROFILE_PREFLIGHT_FAILED", { cause: error });
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2447
2800
|
export async function verifyCampaign(value, options = {}) {
|
|
2448
2801
|
const request = parseRequest(value);
|
|
2449
2802
|
const container = request.isolation.kind === "container"
|
|
@@ -2466,6 +2819,15 @@ export async function verifyCampaign(value, options = {}) {
|
|
|
2466
2819
|
if (request.adapter.kind === "node-test" && nodeIdentity !== undefined) {
|
|
2467
2820
|
request.adapter.executable = nodeIdentity.resolvedExecutable;
|
|
2468
2821
|
}
|
|
2822
|
+
const bunIdentity = request.adapter.kind === "bun-test"
|
|
2823
|
+
? await probeBunTestExecutable(request.adapter.executable, repositoryRoot, trustedLocalEnvironmentAllowlist(request), request.budgets.timeoutMsPerExecution, request.budgets.maximumOutputBytes)
|
|
2824
|
+
: undefined;
|
|
2825
|
+
if (request.adapter.kind === "bun-test" && bunIdentity !== undefined) {
|
|
2826
|
+
request.adapter.executable = bunIdentity.resolvedExecutable;
|
|
2827
|
+
}
|
|
2828
|
+
const bunDriverDigest = request.adapter.kind === "bun-test" ? await sha256File(BUN_TEST_DRIVER_PATH) : undefined;
|
|
2829
|
+
const bunHelperDigest = request.adapter.kind === "bun-test" ? await sha256File(BUN_TEST_HELPER_PATH) : undefined;
|
|
2830
|
+
const bunPreloadDigest = request.adapter.kind === "bun-test" ? await sha256File(BUN_TEST_PRELOAD_PATH) : undefined;
|
|
2469
2831
|
const nodeRuntimePreflight = request.adapter.kind === "node-test" && nodeIdentity !== undefined
|
|
2470
2832
|
? await runNodeTestRuntimePreflight({
|
|
2471
2833
|
executable: nodeIdentity.resolvedExecutable,
|
|
@@ -2481,6 +2843,9 @@ export async function verifyCampaign(value, options = {}) {
|
|
|
2481
2843
|
}),
|
|
2482
2844
|
})
|
|
2483
2845
|
: undefined;
|
|
2846
|
+
const bunRuntimePreflight = request.adapter.kind === "bun-test" && bunIdentity !== undefined
|
|
2847
|
+
? await runBunTestRuntimePreflight(bunIdentity.resolvedExecutable, trustedLocalEnvironmentAllowlist(request), request.budgets.timeoutMsPerExecution, request.budgets.maximumOutputBytes)
|
|
2848
|
+
: undefined;
|
|
2484
2849
|
const campaignRoot = await realpath(await mkdtemp(path.join(os.tmpdir(), "testforge-campaign-")));
|
|
2485
2850
|
const repositorySnapshot = path.join(campaignRoot, "repository");
|
|
2486
2851
|
try {
|
|
@@ -2495,7 +2860,9 @@ export async function verifyCampaign(value, options = {}) {
|
|
|
2495
2860
|
const inputPaths = [
|
|
2496
2861
|
...request.worlds.flatMap((world) => world.files.map((file) => file.path)),
|
|
2497
2862
|
...request.candidates.flatMap((candidate) => candidate.files.map((file) => file.path)),
|
|
2498
|
-
...(request.adapter.kind === "node-test"
|
|
2863
|
+
...(request.adapter.kind === "node-test" || request.adapter.kind === "bun-test"
|
|
2864
|
+
? request.adapter.baseTestFiles
|
|
2865
|
+
: []),
|
|
2499
2866
|
...request.candidateRoots,
|
|
2500
2867
|
];
|
|
2501
2868
|
for (const inputPath of inputPaths) {
|
|
@@ -2537,7 +2904,9 @@ export async function verifyCampaign(value, options = {}) {
|
|
|
2537
2904
|
name: request.adapter.kind,
|
|
2538
2905
|
version: request.adapter.kind === "testforge-command"
|
|
2539
2906
|
? request.adapter.protocolVersion
|
|
2540
|
-
:
|
|
2907
|
+
: request.adapter.kind === "bun-test"
|
|
2908
|
+
? (bunIdentity?.bunVersion ?? "unprobed")
|
|
2909
|
+
: (nodeIdentity?.nodeVersion ?? "unprobed"),
|
|
2541
2910
|
configuration: request.adapter.kind === "node-test" && nodeIdentity !== undefined
|
|
2542
2911
|
? {
|
|
2543
2912
|
kind: "node-test",
|
|
@@ -2560,7 +2929,31 @@ export async function verifyCampaign(value, options = {}) {
|
|
|
2560
2929
|
...request.adapter.baseTestFiles,
|
|
2561
2930
|
],
|
|
2562
2931
|
}
|
|
2563
|
-
: request.adapter
|
|
2932
|
+
: request.adapter.kind === "bun-test" && bunIdentity !== undefined
|
|
2933
|
+
? {
|
|
2934
|
+
kind: "bun-test",
|
|
2935
|
+
profile: BUN_TEST_ADAPTER_PROFILE,
|
|
2936
|
+
requestedExecutable: bunIdentity.requestedExecutable,
|
|
2937
|
+
resolvedExecutable: bunIdentity.resolvedExecutable,
|
|
2938
|
+
executableDigest: bunIdentity.executableDigest,
|
|
2939
|
+
bunVersion: bunIdentity.bunVersion,
|
|
2940
|
+
bunRevision: bunIdentity.bunRevision,
|
|
2941
|
+
driverDigest: bunDriverDigest,
|
|
2942
|
+
helperDigest: bunHelperDigest,
|
|
2943
|
+
preloadDigest: bunPreloadDigest,
|
|
2944
|
+
runtimePreflight: bunRuntimePreflight,
|
|
2945
|
+
arguments: [
|
|
2946
|
+
"test",
|
|
2947
|
+
"--max-concurrency=1",
|
|
2948
|
+
"--retry=0",
|
|
2949
|
+
"--preload",
|
|
2950
|
+
"./node_modules/assertledger/preload.mjs",
|
|
2951
|
+
"--reporter=junit",
|
|
2952
|
+
"--reporter-outfile=<generated-junit-file>",
|
|
2953
|
+
...request.adapter.baseTestFiles,
|
|
2954
|
+
],
|
|
2955
|
+
}
|
|
2956
|
+
: request.adapter,
|
|
2564
2957
|
},
|
|
2565
2958
|
execution: {
|
|
2566
2959
|
isolation: container === undefined ? "UNSANDBOXED" : "CONTAINER",
|
|
@@ -2612,9 +3005,21 @@ export async function verifyCampaign(value, options = {}) {
|
|
|
2612
3005
|
? [
|
|
2613
3006
|
"UNSANDBOXED trusted-local execution cannot safely contain hostile candidate code.",
|
|
2614
3007
|
"Process-tree termination is best effort and depends on host operating-system facilities.",
|
|
3008
|
+
...(request.adapter.kind === "bun-test"
|
|
3009
|
+
? [
|
|
3010
|
+
"Only failures from the AssertLedger-owned assertSame helper are admitted as Bun assertions; native bun:test expect failures are not assertion evidence.",
|
|
3011
|
+
"Bun callback instrumentation determines assertion ownership; JUnit counts only check completeness and never classify assertions.",
|
|
3012
|
+
]
|
|
3013
|
+
: []),
|
|
2615
3014
|
]
|
|
2616
3015
|
: [...CONTAINER_LIMITATIONS],
|
|
2617
3016
|
};
|
|
3017
|
+
if (request.adapter.kind === "bun-test" &&
|
|
3018
|
+
((await sha256File(BUN_TEST_DRIVER_PATH)) !== bunDriverDigest ||
|
|
3019
|
+
(await sha256File(BUN_TEST_HELPER_PATH)) !== bunHelperDigest ||
|
|
3020
|
+
(await sha256File(BUN_TEST_PRELOAD_PATH)) !== bunPreloadDigest)) {
|
|
3021
|
+
throw new Error("BUN_TEST_ASSET_CHANGED_DURING_CAMPAIGN");
|
|
3022
|
+
}
|
|
2618
3023
|
return sealManifestArtifact(finalManifest);
|
|
2619
3024
|
}
|
|
2620
3025
|
finally {
|