assertledger 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.fr.md +5 -2
- package/README.md +5 -2
- package/conformance/schema-extensions.json +15 -0
- package/dist/build-info.json +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +213 -8
- package/dist/cli.js.map +1 -1
- package/dist/contracts/index.d.ts +467 -4
- package/dist/contracts/index.d.ts.map +1 -1
- package/dist/contracts/index.js +256 -3
- package/dist/contracts/index.js.map +1 -1
- package/dist/contracts/qualification.d.ts +528 -0
- package/dist/contracts/qualification.d.ts.map +1 -0
- package/dist/contracts/qualification.js +357 -0
- package/dist/contracts/qualification.js.map +1 -0
- package/dist/core/index.d.ts +9 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +46 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/qualification.d.ts +17 -0
- package/dist/core/qualification.d.ts.map +1 -0
- package/dist/core/qualification.js +571 -0
- package/dist/core/qualification.js.map +1 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +81 -4
- package/dist/diagnostics.js.map +1 -1
- package/dist/engine/adapters/bun-native.d.ts +35 -0
- package/dist/engine/adapters/bun-native.d.ts.map +1 -0
- package/dist/engine/adapters/bun-native.js +179 -0
- package/dist/engine/adapters/bun-native.js.map +1 -0
- package/dist/engine/adapters/bun-test-designated-profile.d.ts +23 -0
- package/dist/engine/adapters/bun-test-designated-profile.d.ts.map +1 -0
- package/dist/engine/adapters/bun-test-designated-profile.js +24 -0
- package/dist/engine/adapters/bun-test-designated-profile.js.map +1 -0
- package/dist/engine/container.d.ts +7 -0
- package/dist/engine/container.d.ts.map +1 -1
- package/dist/engine/container.js +22 -7
- package/dist/engine/container.js.map +1 -1
- package/dist/engine/git-regression.d.ts.map +1 -1
- package/dist/engine/git-regression.js +9 -2
- package/dist/engine/git-regression.js.map +1 -1
- package/dist/engine/index.d.ts +14 -0
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +828 -97
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/qualification.d.ts +154 -0
- package/dist/engine/qualification.d.ts.map +1 -0
- package/dist/engine/qualification.js +725 -0
- package/dist/engine/qualification.js.map +1 -0
- package/dist/engine/test-closure.d.ts +39 -0
- package/dist/engine/test-closure.d.ts.map +1 -0
- package/dist/engine/test-closure.js +513 -0
- package/dist/engine/test-closure.js.map +1 -0
- package/dist/engine/witness-import.d.ts +31 -0
- package/dist/engine/witness-import.d.ts.map +1 -0
- package/dist/engine/witness-import.js +224 -0
- package/dist/engine/witness-import.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +65 -0
- package/dist/mcp/index.js.map +1 -1
- package/dist/sdk/index.d.ts +292 -3
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +39 -1
- package/dist/sdk/index.js.map +1 -1
- package/docs/container-isolation.md +6 -0
- package/docs/migration-orchestration-qualification-v1.md +109 -0
- package/docs/migration-verification-v4.md +94 -0
- package/docs/orchestration-qualification.md +167 -0
- package/docs/reference.md +7 -0
- package/docs/witness-import.md +67 -0
- package/examples/orchestration/README.md +68 -0
- package/examples/orchestration/consumer-obligations.json +101 -0
- package/examples/orchestration/workspace/bitbucket-pipelines.yml +10 -0
- package/examples/orchestration/workspace/command-shape.test.mjs +25 -0
- package/examples/orchestration/workspace/commands.mjs +14 -0
- package/examples/orchestration/workspace/completion.mjs +39 -0
- package/examples/orchestration/workspace/faults.json +1 -0
- package/examples/orchestration/workspace/native.test.ts +4 -0
- package/examples/orchestration/workspace/package.json +6 -0
- package/examples/orchestration/workspace/packages/app/package.json +12 -0
- package/examples/orchestration/workspace/packages/app/verdict.txt +1 -0
- package/examples/orchestration/workspace/packages/leaf/package.json +9 -0
- package/examples/orchestration/workspace/packages/leaf/verdict.txt +1 -0
- package/examples/orchestration/workspace/pnpm-lock.yaml +170 -0
- package/examples/orchestration/workspace/pnpm-workspace.yaml +2 -0
- package/examples/orchestration/workspace/task.mjs +23 -0
- package/examples/orchestration/workspace/turbo.json +38 -0
- package/examples/orchestration/workspace/wrapper.mjs +45 -0
- package/integrations/bun/driver.d.mts +30 -1
- package/integrations/bun/driver.mjs +215 -6
- package/integrations/bun/preload.mjs +186 -5
- package/integrations/bun-native/README.md +86 -0
- package/integrations/bun-native/driver.mjs +396 -0
- package/integrations/bun-native/preload.mjs +375 -0
- package/integrations/skill/SKILL.md +25 -5
- package/package.json +1 -1
- package/schemas/evidence-manifest.v4.json +995 -0
- package/schemas/qualification-execution-request.v1.json +571 -0
- package/schemas/qualification-plan.v1.json +528 -0
- package/schemas/qualification-receipt.v1.json +821 -0
- package/schemas/qualification-replay-result.v1.json +37 -0
- package/schemas/verification-request.v4.json +590 -0
- package/schemas/witness-import-request.v1.json +231 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
initial
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
---
|
|
2
|
+
lockfileVersion: '9.0'
|
|
3
|
+
|
|
4
|
+
importers:
|
|
5
|
+
|
|
6
|
+
.:
|
|
7
|
+
configDependencies: {}
|
|
8
|
+
packageManagerDependencies:
|
|
9
|
+
pnpm:
|
|
10
|
+
specifier: 12.9.1
|
|
11
|
+
version: 12.9.1
|
|
12
|
+
|
|
13
|
+
packages:
|
|
14
|
+
|
|
15
|
+
'@pnpm/exe.android-arm64@12.9.1':
|
|
16
|
+
resolution: {integrity: sha512-Hgm1XdqMTfBC8PdGLL+NjZWv4xyi7D6XNrN9fb24XGYlyTBmUtdCefKnCLmHujShRD8BEO1zqM30SNcKBDLysw==}
|
|
17
|
+
cpu: [arm64]
|
|
18
|
+
os: [android]
|
|
19
|
+
|
|
20
|
+
'@pnpm/exe.android-x64@12.9.1':
|
|
21
|
+
resolution: {integrity: sha512-WDGCe6jZRVZP3RC/q6hOkZylkPkIQIi1/GcrR/d/YzbihhAHXCczrk+UYnV5TgbsEnAYO7Cx+2rX3nfhHvy24w==}
|
|
22
|
+
cpu: [x64]
|
|
23
|
+
os: [android]
|
|
24
|
+
|
|
25
|
+
'@pnpm/exe.darwin-arm64@12.9.1':
|
|
26
|
+
resolution: {integrity: sha512-+7iiEEBgYN9szPWUnAuN6eOVMTI6Hrki5bcuC9n9xl9tTnT53Tpxpo45Hml+W4IrAU0d+Tmd0sPnVIbX59PD6w==}
|
|
27
|
+
cpu: [arm64]
|
|
28
|
+
os: [darwin]
|
|
29
|
+
|
|
30
|
+
'@pnpm/exe.darwin-x64@12.9.1':
|
|
31
|
+
resolution: {integrity: sha512-SK7TxJiR12GbJZGffRFHItkc+Mx+99v6xMMJ9/X//8NwWV1wo17a2bwfGI8qZpl17YwsrmPEFOOqhzE1GIlrtQ==}
|
|
32
|
+
cpu: [x64]
|
|
33
|
+
os: [darwin]
|
|
34
|
+
|
|
35
|
+
'@pnpm/exe.freebsd-x64@12.9.1':
|
|
36
|
+
resolution: {integrity: sha512-4AjLk6uvTs2iO6wMit2hcxJ2M32xb7OV5SNFk+w6G8/NzcjcMB7ENEpyeXyLiP0kcnp8uaCGUct2NfrLCJ35eQ==}
|
|
37
|
+
cpu: [x64]
|
|
38
|
+
os: [freebsd]
|
|
39
|
+
|
|
40
|
+
'@pnpm/exe.linux-arm64-musl@12.9.1':
|
|
41
|
+
resolution: {integrity: sha512-dZL2ES1ckDWM9v+EtxoOklZ0WNdtayooi9iLiDUubopudX3zXPNR2mlrxBRgjbDgECOndQen2SmyxL5eZiBuKw==}
|
|
42
|
+
cpu: [arm64]
|
|
43
|
+
os: [linux]
|
|
44
|
+
libc: [musl]
|
|
45
|
+
|
|
46
|
+
'@pnpm/exe.linux-arm64@12.9.1':
|
|
47
|
+
resolution: {integrity: sha512-cx+uF6xqwEtm7B8NV+1ve0xBg9gcuzQZ5R+0WgOWvKG/h978UljS6BgWs1ipIGuGgUdeVhm3PKmBltXIcczLXA==}
|
|
48
|
+
cpu: [arm64]
|
|
49
|
+
os: [linux]
|
|
50
|
+
libc: [glibc]
|
|
51
|
+
|
|
52
|
+
'@pnpm/exe.linux-ppc64@12.9.1':
|
|
53
|
+
resolution: {integrity: sha512-MpjPsy4XR17zAtLziQZfsN+wveWa/bUJggZtwfo3+SuyRmXnSvVkFD4+v8/3RTWyYaM6mwLUTjZDJ+g3urL+ew==}
|
|
54
|
+
cpu: [ppc64]
|
|
55
|
+
os: [linux]
|
|
56
|
+
libc: [glibc]
|
|
57
|
+
|
|
58
|
+
'@pnpm/exe.linux-riscv64@12.9.1':
|
|
59
|
+
resolution: {integrity: sha512-Oo67TKqqH+Bno+L6X7Xyorwp1QG82zKIkL0vbo1x2kcPTDiyPbdM+d6lIs4+hzeB1KHT5FDoGzZj2dad7Ft+eg==}
|
|
60
|
+
cpu: [riscv64]
|
|
61
|
+
os: [linux]
|
|
62
|
+
libc: [glibc]
|
|
63
|
+
|
|
64
|
+
'@pnpm/exe.linux-s390x@12.9.1':
|
|
65
|
+
resolution: {integrity: sha512-QAD5FRCdW36HfD58BV7DoDm6z8yv+GgSqcUV1+ERwYjpVQuG6ZTpf5o0x3KDas8eTLIW7MLYVdDdlAurWeArEg==}
|
|
66
|
+
cpu: [s390x]
|
|
67
|
+
os: [linux]
|
|
68
|
+
libc: [glibc]
|
|
69
|
+
|
|
70
|
+
'@pnpm/exe.linux-x64-musl@12.9.1':
|
|
71
|
+
resolution: {integrity: sha512-mQDhBsGqcTkrLQM4q5igmRFcr6EAl4UM/FGawY+0CWGcqPpUmYUvDy9LHy80xxkHIWVJ886gvvso2XOjfav1Jw==}
|
|
72
|
+
cpu: [x64]
|
|
73
|
+
os: [linux]
|
|
74
|
+
libc: [musl]
|
|
75
|
+
|
|
76
|
+
'@pnpm/exe.linux-x64@12.9.1':
|
|
77
|
+
resolution: {integrity: sha512-+M5N74hM4fc87AAckxx/UIW+dUnWf2eko+HchJs/NZUxehLhekNyMI/GNaNXlvd+kWR+97XXBD51XRkkNiUtrA==}
|
|
78
|
+
cpu: [x64]
|
|
79
|
+
os: [linux]
|
|
80
|
+
libc: [glibc]
|
|
81
|
+
|
|
82
|
+
'@pnpm/exe.win32-arm64@12.9.1':
|
|
83
|
+
resolution: {integrity: sha512-mI28C2wxobrDi6x1OYhBRyG10hnDq/4sEIyy5RzoX6LB4kLuv6K/d25HOpMwlW20cuM4M72k9NZpOdcmUxMbEA==}
|
|
84
|
+
cpu: [arm64]
|
|
85
|
+
os: [win32]
|
|
86
|
+
|
|
87
|
+
'@pnpm/exe.win32-x64@12.9.1':
|
|
88
|
+
resolution: {integrity: sha512-SWoDMT0oD7NrDSIoPpkKD09D9fAQKNjwFZ3lezlCUhAfXxMv70zsfrNT2/P+GoCK2kefXF9OysUZkH1l5vvvfA==}
|
|
89
|
+
cpu: [x64]
|
|
90
|
+
os: [win32]
|
|
91
|
+
|
|
92
|
+
pnpm@12.9.1:
|
|
93
|
+
resolution: {integrity: sha512-BrBV//XNINwSeB3hc87TMQzglRvy7GICEaFgRdVETVyTpmMhB2TvKaZTphBFm6A2jw50+E2AxUcjz5Wq57wNbQ==}
|
|
94
|
+
engines: {node: '>=18.*'}
|
|
95
|
+
hasBin: true
|
|
96
|
+
|
|
97
|
+
snapshots:
|
|
98
|
+
|
|
99
|
+
'@pnpm/exe.android-arm64@12.9.1':
|
|
100
|
+
optional: true
|
|
101
|
+
|
|
102
|
+
'@pnpm/exe.android-x64@12.9.1':
|
|
103
|
+
optional: true
|
|
104
|
+
|
|
105
|
+
'@pnpm/exe.darwin-arm64@12.9.1':
|
|
106
|
+
optional: true
|
|
107
|
+
|
|
108
|
+
'@pnpm/exe.darwin-x64@12.9.1':
|
|
109
|
+
optional: true
|
|
110
|
+
|
|
111
|
+
'@pnpm/exe.freebsd-x64@12.9.1':
|
|
112
|
+
optional: true
|
|
113
|
+
|
|
114
|
+
'@pnpm/exe.linux-arm64-musl@12.9.1':
|
|
115
|
+
optional: true
|
|
116
|
+
|
|
117
|
+
'@pnpm/exe.linux-arm64@12.9.1':
|
|
118
|
+
optional: true
|
|
119
|
+
|
|
120
|
+
'@pnpm/exe.linux-ppc64@12.9.1':
|
|
121
|
+
optional: true
|
|
122
|
+
|
|
123
|
+
'@pnpm/exe.linux-riscv64@12.9.1':
|
|
124
|
+
optional: true
|
|
125
|
+
|
|
126
|
+
'@pnpm/exe.linux-s390x@12.9.1':
|
|
127
|
+
optional: true
|
|
128
|
+
|
|
129
|
+
'@pnpm/exe.linux-x64-musl@12.9.1':
|
|
130
|
+
optional: true
|
|
131
|
+
|
|
132
|
+
'@pnpm/exe.linux-x64@12.9.1':
|
|
133
|
+
optional: true
|
|
134
|
+
|
|
135
|
+
'@pnpm/exe.win32-arm64@12.9.1':
|
|
136
|
+
optional: true
|
|
137
|
+
|
|
138
|
+
'@pnpm/exe.win32-x64@12.9.1':
|
|
139
|
+
optional: true
|
|
140
|
+
|
|
141
|
+
pnpm@12.9.1:
|
|
142
|
+
optionalDependencies:
|
|
143
|
+
'@pnpm/exe.android-arm64': 12.9.1
|
|
144
|
+
'@pnpm/exe.android-x64': 12.9.1
|
|
145
|
+
'@pnpm/exe.darwin-arm64': 12.9.1
|
|
146
|
+
'@pnpm/exe.darwin-x64': 12.9.1
|
|
147
|
+
'@pnpm/exe.freebsd-x64': 12.9.1
|
|
148
|
+
'@pnpm/exe.linux-arm64': 12.9.1
|
|
149
|
+
'@pnpm/exe.linux-arm64-musl': 12.9.1
|
|
150
|
+
'@pnpm/exe.linux-ppc64': 12.9.1
|
|
151
|
+
'@pnpm/exe.linux-riscv64': 12.9.1
|
|
152
|
+
'@pnpm/exe.linux-s390x': 12.9.1
|
|
153
|
+
'@pnpm/exe.linux-x64': 12.9.1
|
|
154
|
+
'@pnpm/exe.linux-x64-musl': 12.9.1
|
|
155
|
+
'@pnpm/exe.win32-arm64': 12.9.1
|
|
156
|
+
'@pnpm/exe.win32-x64': 12.9.1
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
lockfileVersion: '9.0'
|
|
160
|
+
settings:
|
|
161
|
+
autoInstallPeers: true
|
|
162
|
+
excludeLinksFromLockfile: false
|
|
163
|
+
importers:
|
|
164
|
+
.: {}
|
|
165
|
+
packages/leaf: {}
|
|
166
|
+
packages/app:
|
|
167
|
+
dependencies:
|
|
168
|
+
'@public/leaf':
|
|
169
|
+
specifier: workspace:*
|
|
170
|
+
version: link:../leaf
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { complete } from "./completion.mjs";
|
|
3
|
+
const kind = process.argv[2];
|
|
4
|
+
const name = JSON.parse(readFileSync("package.json", "utf8")).name;
|
|
5
|
+
const task = `${name}#${kind}`;
|
|
6
|
+
const emit = (event) =>
|
|
7
|
+
appendFileSync(
|
|
8
|
+
process.env.ASSERTLEDGER_QUALIFICATION_TRACE,
|
|
9
|
+
`${JSON.stringify({ nonce: process.env.ASSERTLEDGER_QUALIFICATION_NONCE, task, event })}\n`,
|
|
10
|
+
);
|
|
11
|
+
emit("start");
|
|
12
|
+
if (kind === "fail" && name === "@public/leaf") {
|
|
13
|
+
emit("fail");
|
|
14
|
+
complete(7);
|
|
15
|
+
process.exit(7);
|
|
16
|
+
}
|
|
17
|
+
if (kind === "build") {
|
|
18
|
+
mkdirSync("dist", { recursive: true });
|
|
19
|
+
const verdict = readFileSync("verdict.txt", "utf8");
|
|
20
|
+
writeFileSync("dist/result.txt", verdict);
|
|
21
|
+
writeFileSync("dist/manifest.txt", "required-output\n");
|
|
22
|
+
}
|
|
23
|
+
emit("finish");
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://turbo.build/schema.json",
|
|
3
|
+
"globalEnv": ["PATH"],
|
|
4
|
+
"globalPassThroughEnv": [
|
|
5
|
+
"ASSERTLEDGER_QUALIFICATION_TRACE",
|
|
6
|
+
"ASSERTLEDGER_QUALIFICATION_NONCE",
|
|
7
|
+
"ASSERTLEDGER_QUALIFICATION_CACHE",
|
|
8
|
+
"ASSERTLEDGER_QUALIFICATION_RESULT_FILE"
|
|
9
|
+
],
|
|
10
|
+
"tasks": {
|
|
11
|
+
"build": {
|
|
12
|
+
"dependsOn": ["^build"],
|
|
13
|
+
"inputs": [
|
|
14
|
+
"$TURBO_DEFAULT$",
|
|
15
|
+
"$TURBO_ROOT$/task.mjs",
|
|
16
|
+
"$TURBO_ROOT$/completion.mjs",
|
|
17
|
+
"!dist/**",
|
|
18
|
+
"!.turbo/**",
|
|
19
|
+
"!node_modules/**",
|
|
20
|
+
"verdict.txt"
|
|
21
|
+
],
|
|
22
|
+
"outputs": ["dist/**"]
|
|
23
|
+
},
|
|
24
|
+
"check": {
|
|
25
|
+
"cache": false,
|
|
26
|
+
"inputs": [
|
|
27
|
+
"$TURBO_DEFAULT$",
|
|
28
|
+
"$TURBO_ROOT$/task.mjs",
|
|
29
|
+
"$TURBO_ROOT$/completion.mjs",
|
|
30
|
+
"!dist/**",
|
|
31
|
+
"!.turbo/**",
|
|
32
|
+
"!node_modules/**"
|
|
33
|
+
],
|
|
34
|
+
"outputs": []
|
|
35
|
+
},
|
|
36
|
+
"fail": { "dependsOn": ["^fail"], "cache": false, "outputs": [] }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { appendFileSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { complete, execute } from "./completion.mjs";
|
|
3
|
+
import { failCommandArgs, wrapperCommandArgs } from "./commands.mjs";
|
|
4
|
+
const faults = JSON.parse(readFileSync("faults.json", "utf8"));
|
|
5
|
+
const emit = (task, event) =>
|
|
6
|
+
appendFileSync(
|
|
7
|
+
process.env.ASSERTLEDGER_QUALIFICATION_TRACE,
|
|
8
|
+
`${JSON.stringify({ nonce: process.env.ASSERTLEDGER_QUALIFICATION_NONCE, task, event })}\n`,
|
|
9
|
+
);
|
|
10
|
+
const layer = process.argv[2] ?? "terminal";
|
|
11
|
+
emit(layer, "start");
|
|
12
|
+
if (layer === "seed") {
|
|
13
|
+
writeFileSync("success-receipt.json", JSON.stringify({ exitCode: 0 }));
|
|
14
|
+
emit(layer, "finish");
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
let status;
|
|
18
|
+
if (layer === "orchestrator") {
|
|
19
|
+
if (faults.omitStage || faults.staleResult) status = 0;
|
|
20
|
+
else {
|
|
21
|
+
emit("mandatory-stage", "start");
|
|
22
|
+
emit("mandatory-stage", "finish");
|
|
23
|
+
status = execute(
|
|
24
|
+
process.env.PUBLIC_TURBO_EXECUTABLE,
|
|
25
|
+
failCommandArgs({
|
|
26
|
+
emptySelection: faults.emptySelection,
|
|
27
|
+
cacheDirectory: process.env.ASSERTLEDGER_QUALIFICATION_CACHE,
|
|
28
|
+
}),
|
|
29
|
+
"leaf",
|
|
30
|
+
);
|
|
31
|
+
if (faults.swallow) status = 0;
|
|
32
|
+
if (faults.trailingMask) {
|
|
33
|
+
// A subsequent successful command replaces the failing command's status.
|
|
34
|
+
status = execute(process.execPath, ["-e", "process.exit(0)"], "trailing");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} else if (faults.staleResult) {
|
|
38
|
+
// A prior success is deliberately seeded by a separate, ordered action.
|
|
39
|
+
status = JSON.parse(readFileSync("success-receipt.json", "utf8")).exitCode;
|
|
40
|
+
} else {
|
|
41
|
+
status = execute(process.execPath, wrapperCommandArgs(import.meta.filename), "orchestrator");
|
|
42
|
+
}
|
|
43
|
+
emit(layer, status === 0 ? "finish" : "fail");
|
|
44
|
+
complete(status);
|
|
45
|
+
process.exit(status);
|
|
@@ -17,9 +17,38 @@ export type BunTestEvent =
|
|
|
17
17
|
| { kind: "end"; id: string; status: "pass" | "fail"; owned: boolean }
|
|
18
18
|
| { kind: "hook-error" };
|
|
19
19
|
|
|
20
|
+
/** The JUnit totals; `timeouts` counts the tests Bun failed with a TimeoutError. */
|
|
21
|
+
export interface BunJunitSummary {
|
|
22
|
+
tests: number;
|
|
23
|
+
failures: number;
|
|
24
|
+
skipped: number;
|
|
25
|
+
timeouts?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type BunDesignatedEvent =
|
|
29
|
+
| { kind: "found"; id: string; file: string; designated: boolean }
|
|
30
|
+
| { kind: "end"; id: string; status: "pass" | "fail"; owned: boolean }
|
|
31
|
+
| { kind: "registered"; designated: number; file: string }
|
|
32
|
+
| { kind: "hook-error" };
|
|
33
|
+
|
|
34
|
+
export function classifyBunDesignatedLoad(
|
|
35
|
+
events: readonly BunDesignatedEvent[] | undefined,
|
|
36
|
+
junit: BunJunitSummary | undefined,
|
|
37
|
+
designatedCount: number,
|
|
38
|
+
exitCode: number | null,
|
|
39
|
+
operationalError: boolean,
|
|
40
|
+
): BunInstrumentedResult;
|
|
41
|
+
|
|
42
|
+
export function classifyBunDesignatedRun(
|
|
43
|
+
events: readonly BunDesignatedEvent[] | undefined,
|
|
44
|
+
junit: BunJunitSummary | undefined,
|
|
45
|
+
exitCode: number | null,
|
|
46
|
+
operationalError: boolean,
|
|
47
|
+
): BunInstrumentedResult;
|
|
48
|
+
|
|
20
49
|
export function classifyBunInstrumentedEvidence(
|
|
21
50
|
events: readonly BunTestEvent[] | undefined,
|
|
22
|
-
junit:
|
|
51
|
+
junit: BunJunitSummary | undefined,
|
|
23
52
|
baseFiles: ReadonlySet<string>,
|
|
24
53
|
candidateFiles: ReadonlySet<string>,
|
|
25
54
|
exitCode: number | null,
|
|
@@ -83,10 +83,13 @@ function parseJunitSummary(xml) {
|
|
|
83
83
|
) {
|
|
84
84
|
return undefined;
|
|
85
85
|
}
|
|
86
|
-
|
|
86
|
+
// Bun reports a per-test timeout as a TimeoutError failure, even when the callback threw later.
|
|
87
|
+
const timeouts = [...xml.matchAll(/<failure\b[^>]*\btype="TimeoutError"/gu)].length;
|
|
88
|
+
if (timeouts > failures) return undefined;
|
|
89
|
+
return { tests, failures, skipped, timeouts };
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
function parseEvents(content, root, allowedFiles, evidenceKey) {
|
|
92
|
+
function parseEvents(content, root, allowedFiles, evidenceKey, designated = false) {
|
|
90
93
|
if (!content.endsWith("\n")) return undefined;
|
|
91
94
|
const lines = content.trimEnd().split("\n");
|
|
92
95
|
if (lines.length === 1 && lines[0] === "") return [];
|
|
@@ -120,14 +123,26 @@ function parseEvents(content, root, allowedFiles, evidenceKey) {
|
|
|
120
123
|
events.push({ kind: "hook-error" });
|
|
121
124
|
continue;
|
|
122
125
|
}
|
|
126
|
+
if (value.kind === "registered") {
|
|
127
|
+
if (!designated || Object.keys(value).sort().join(",") !== "designated,file,kind") {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
const file = normalizeAbsoluteFile(value.file, root);
|
|
131
|
+
if (file === undefined || !allowedFiles.has(file) || !Number.isSafeInteger(value.designated))
|
|
132
|
+
return undefined;
|
|
133
|
+
events.push({ kind: "registered", file, designated: value.designated });
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
123
136
|
if (typeof value.id !== "string" || !/^[0-9a-f-]{36}$/u.test(value.id)) {
|
|
124
137
|
return undefined;
|
|
125
138
|
}
|
|
126
139
|
if (value.kind === "found") {
|
|
127
|
-
|
|
140
|
+
const expectedKeys = designated ? "designated,file,id,kind" : "file,id,kind";
|
|
141
|
+
if (Object.keys(value).sort().join(",") !== expectedKeys) return undefined;
|
|
142
|
+
if (designated && typeof value.designated !== "boolean") return undefined;
|
|
128
143
|
const file = normalizeAbsoluteFile(value.file, root);
|
|
129
144
|
if (file === undefined || !allowedFiles.has(file)) return undefined;
|
|
130
|
-
events.push({ kind: "found", id: value.id, file });
|
|
145
|
+
events.push({ kind: "found", id: value.id, file, designated: value.designated === true });
|
|
131
146
|
} else if (value.kind === "end") {
|
|
132
147
|
if (value.status !== "pass" && value.status !== "fail") return undefined;
|
|
133
148
|
const expectedKeys = value.status === "pass" ? "id,kind,status" : "id,kind,owned,status";
|
|
@@ -157,6 +172,8 @@ export function classifyBunInstrumentedEvidence(
|
|
|
157
172
|
) {
|
|
158
173
|
return infrastructureFailure("INCOMPLETE_CONTROLLED_REPORT");
|
|
159
174
|
}
|
|
175
|
+
// A test Bun timed out is never assertion evidence, whatever its callback threw afterwards.
|
|
176
|
+
if (junit.timeouts > 0) return infrastructureFailure("BUN_TEST_TIMEOUT");
|
|
160
177
|
const found = new Map();
|
|
161
178
|
const ended = new Map();
|
|
162
179
|
for (const event of events) {
|
|
@@ -209,6 +226,131 @@ export function classifyBunInstrumentedEvidence(
|
|
|
209
226
|
);
|
|
210
227
|
}
|
|
211
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Loads designated files without registering a test. Success proves that each file and every module
|
|
231
|
+
* it imports evaluate in this world and that each designated test is registered exactly once.
|
|
232
|
+
*/
|
|
233
|
+
export function classifyBunDesignatedLoad(
|
|
234
|
+
events,
|
|
235
|
+
junit,
|
|
236
|
+
designatedCount,
|
|
237
|
+
exitCode,
|
|
238
|
+
operationalError,
|
|
239
|
+
) {
|
|
240
|
+
if (
|
|
241
|
+
events === undefined ||
|
|
242
|
+
exitCode !== 0 ||
|
|
243
|
+
operationalError ||
|
|
244
|
+
(junit !== undefined && junit.tests !== 0) ||
|
|
245
|
+
events.some((event) => event.kind !== "registered")
|
|
246
|
+
) {
|
|
247
|
+
return infrastructureFailure("DESIGNATED_LOAD_INCOMPLETE");
|
|
248
|
+
}
|
|
249
|
+
for (let index = 0; index < designatedCount; index += 1) {
|
|
250
|
+
const matches = events.filter((event) => event.designated === index).length;
|
|
251
|
+
if (matches === 0) return report("NO_TEST_DISCOVERED", events.length, 0, false);
|
|
252
|
+
if (matches > 1) return infrastructureFailure("DESIGNATED_TEST_AMBIGUOUS");
|
|
253
|
+
}
|
|
254
|
+
if (events.some((event) => event.designated < -1 || event.designated >= designatedCount)) {
|
|
255
|
+
return infrastructureFailure("DESIGNATED_TEST_INDEX_INVALID");
|
|
256
|
+
}
|
|
257
|
+
return report("PASS", events.length, 0, false);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Runs exactly one designated test. Only its owned assertion failure, with the expected first line
|
|
262
|
+
* when one is declared, is an assertion; no matching test is never a failure.
|
|
263
|
+
*/
|
|
264
|
+
export function classifyBunDesignatedRun(events, junit, exitCode, operationalError) {
|
|
265
|
+
if (
|
|
266
|
+
events === undefined ||
|
|
267
|
+
junit === undefined ||
|
|
268
|
+
exitCode === null ||
|
|
269
|
+
operationalError ||
|
|
270
|
+
events.some((event) => event.kind === "hook-error" || event.kind === "registered")
|
|
271
|
+
) {
|
|
272
|
+
return infrastructureFailure("INCOMPLETE_CONTROLLED_REPORT");
|
|
273
|
+
}
|
|
274
|
+
// A test Bun timed out is never assertion evidence, whatever its callback threw afterwards.
|
|
275
|
+
if (junit.timeouts > 0) return infrastructureFailure("BUN_TEST_TIMEOUT");
|
|
276
|
+
const found = new Map();
|
|
277
|
+
const ended = new Map();
|
|
278
|
+
for (const event of events) {
|
|
279
|
+
if (event.kind === "found") {
|
|
280
|
+
if (found.has(event.id)) return infrastructureFailure("DUPLICATE_TEST_ID");
|
|
281
|
+
found.set(event.id, event);
|
|
282
|
+
} else {
|
|
283
|
+
if (!found.has(event.id)) return infrastructureFailure("UNMATCHED_TEST_END");
|
|
284
|
+
if (ended.has(event.id)) return infrastructureFailure("DUPLICATE_TEST_END");
|
|
285
|
+
ended.set(event.id, event);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (found.size !== ended.size || found.size !== junit.tests - junit.skipped) {
|
|
289
|
+
return infrastructureFailure("TEST_COUNT_MISMATCH");
|
|
290
|
+
}
|
|
291
|
+
if (found.size === 0) {
|
|
292
|
+
return junit.failures === 0 && exitCode !== 0
|
|
293
|
+
? report("NO_TEST_DISCOVERED", 0, 0, false)
|
|
294
|
+
: infrastructureFailure("BUN_STATUS_MISMATCH");
|
|
295
|
+
}
|
|
296
|
+
if ([...found.values()].some((event) => !event.designated)) {
|
|
297
|
+
return infrastructureFailure("UNEXPECTED_TEST");
|
|
298
|
+
}
|
|
299
|
+
if (found.size !== 1) return infrastructureFailure("DESIGNATED_TEST_AMBIGUOUS");
|
|
300
|
+
const [end] = ended.values();
|
|
301
|
+
const failed = end.status === "fail";
|
|
302
|
+
if (junit.failures !== (failed ? 1 : 0) || (exitCode === 0) === failed) {
|
|
303
|
+
return infrastructureFailure("BUN_STATUS_MISMATCH");
|
|
304
|
+
}
|
|
305
|
+
if (!failed) return report("PASS", 1, 1, true);
|
|
306
|
+
return end.owned ? report("ASSERTION_FAILURE", 1, 1, true) : report("PROCESS_CRASH", 1, 1, false);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function escapeRegularExpression(value) {
|
|
310
|
+
return value.replace(/[\\^$.*+?()[\]{}|/-]/gu, "\\$&");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Validates a designated selection against the files the driver was asked to load or run. */
|
|
314
|
+
function parseDesignatedSelection(value, files) {
|
|
315
|
+
if (value === undefined) return undefined;
|
|
316
|
+
const selection = JSON.parse(value);
|
|
317
|
+
const validTest = (test) =>
|
|
318
|
+
typeof test === "object" &&
|
|
319
|
+
test !== null &&
|
|
320
|
+
Object.keys(test).sort().join(",") === "file,path" &&
|
|
321
|
+
normalizeRelativeFile(test.file) !== undefined &&
|
|
322
|
+
Array.isArray(test.path) &&
|
|
323
|
+
test.path.length > 0 &&
|
|
324
|
+
test.path.every((name) => typeof name === "string" && name.length > 0);
|
|
325
|
+
const fileSet = (tests) => [...new Set(tests.map((test) => normalizeRelativeFile(test.file)))];
|
|
326
|
+
const sameFiles = (tests) =>
|
|
327
|
+
JSON.stringify(fileSet(tests).sort()) === JSON.stringify([...files].sort());
|
|
328
|
+
if (
|
|
329
|
+
selection?.mode === "run" &&
|
|
330
|
+
Object.keys(selection).sort().join(",") === "expectedFailure,mode,test,testTimeoutMs" &&
|
|
331
|
+
validTest(selection.test) &&
|
|
332
|
+
(selection.expectedFailure === null || typeof selection.expectedFailure === "string") &&
|
|
333
|
+
(selection.testTimeoutMs === null ||
|
|
334
|
+
(Number.isSafeInteger(selection.testTimeoutMs) &&
|
|
335
|
+
selection.testTimeoutMs >= 1 &&
|
|
336
|
+
selection.testTimeoutMs <= 3_600_000)) &&
|
|
337
|
+
sameFiles([selection.test])
|
|
338
|
+
) {
|
|
339
|
+
return selection;
|
|
340
|
+
}
|
|
341
|
+
if (
|
|
342
|
+
selection?.mode === "load" &&
|
|
343
|
+
Object.keys(selection).sort().join(",") === "mode,tests" &&
|
|
344
|
+
Array.isArray(selection.tests) &&
|
|
345
|
+
selection.tests.length > 0 &&
|
|
346
|
+
selection.tests.every(validTest) &&
|
|
347
|
+
sameFiles(selection.tests)
|
|
348
|
+
) {
|
|
349
|
+
return selection;
|
|
350
|
+
}
|
|
351
|
+
throw new Error("INVALID_DESIGNATED_SELECTION");
|
|
352
|
+
}
|
|
353
|
+
|
|
212
354
|
async function readBoundedRegularFile(file, root) {
|
|
213
355
|
const details = await lstat(file);
|
|
214
356
|
if (!details.isFile() || details.size > MAX_REPORT_BYTES) throw new Error("INVALID_REPORT_FILE");
|
|
@@ -241,12 +383,13 @@ async function installHelper(helperSource, root) {
|
|
|
241
383
|
);
|
|
242
384
|
}
|
|
243
385
|
|
|
244
|
-
async function runBun(executable, files, junitFile, root, evidenceKey) {
|
|
386
|
+
async function runBun(executable, files, junitFile, root, evidenceKey, extraArguments = []) {
|
|
245
387
|
const exactPath = (file) => `./${file.replaceAll(path.sep, "/")}`;
|
|
246
388
|
const childEnvironment = { ...process.env };
|
|
247
389
|
delete childEnvironment.TESTFORGE_RESULT_FILE;
|
|
248
390
|
delete childEnvironment.TESTFORGE_CANDIDATE_FILES;
|
|
249
391
|
delete childEnvironment.ASSERTLEDGER_BUN_EVENTS_FILE;
|
|
392
|
+
delete childEnvironment.ASSERTLEDGER_BUN_RESULT_FILE;
|
|
250
393
|
childEnvironment.ASSERTLEDGER_BUN_ROOT = root;
|
|
251
394
|
const child = spawn(
|
|
252
395
|
executable,
|
|
@@ -258,6 +401,7 @@ async function runBun(executable, files, junitFile, root, evidenceKey) {
|
|
|
258
401
|
"./node_modules/assertledger/preload.mjs",
|
|
259
402
|
"--reporter=junit",
|
|
260
403
|
`--reporter-outfile=${junitFile}`,
|
|
404
|
+
...extraArguments,
|
|
261
405
|
...files.map(exactPath),
|
|
262
406
|
],
|
|
263
407
|
{
|
|
@@ -343,9 +487,17 @@ async function main() {
|
|
|
343
487
|
}
|
|
344
488
|
const files = [...normalizedBase, ...normalizedCandidates];
|
|
345
489
|
if (new Set(files).size !== files.length) throw new Error("DUPLICATE_TEST_FILE");
|
|
490
|
+
const selection = parseDesignatedSelection(process.env.ASSERTLEDGER_BUN_DESIGNATED, files);
|
|
491
|
+
if (selection !== undefined && normalizedCandidates.length > 0) {
|
|
492
|
+
throw new Error("INVALID_DESIGNATED_SELECTION");
|
|
493
|
+
}
|
|
346
494
|
await installHelper(await readFile(helperSourcePath, "utf8"), root);
|
|
347
495
|
const junitFile = path.join(root, `__assertledger_bun_junit_${randomUUID()}.xml`);
|
|
348
496
|
const evidenceKey = randomBytes(32);
|
|
497
|
+
if (selection !== undefined) {
|
|
498
|
+
await runDesignated(executable, baseTests, files, selection, junitFile, root, evidenceKey);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
349
501
|
const execution = await runBun(
|
|
350
502
|
executable,
|
|
351
503
|
[...baseTests, ...candidates],
|
|
@@ -370,7 +522,64 @@ async function main() {
|
|
|
370
522
|
} catch {
|
|
371
523
|
outcome = infrastructureFailure("MISSING_OR_INVALID_REPORT");
|
|
372
524
|
}
|
|
373
|
-
|
|
525
|
+
await emit(outcome);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async function runDesignated(
|
|
529
|
+
executable,
|
|
530
|
+
testFiles,
|
|
531
|
+
files,
|
|
532
|
+
selection,
|
|
533
|
+
junitFile,
|
|
534
|
+
root,
|
|
535
|
+
evidenceKey,
|
|
536
|
+
) {
|
|
537
|
+
const pattern =
|
|
538
|
+
selection.mode === "run"
|
|
539
|
+
? [
|
|
540
|
+
`--test-name-pattern=^${escapeRegularExpression(selection.test.path.join(" "))}$`,
|
|
541
|
+
...(selection.testTimeoutMs === null ? [] : [`--timeout=${selection.testTimeoutMs}`]),
|
|
542
|
+
]
|
|
543
|
+
: [];
|
|
544
|
+
const execution = await runBun(executable, testFiles, junitFile, root, evidenceKey, pattern);
|
|
545
|
+
let outcome;
|
|
546
|
+
try {
|
|
547
|
+
let junit;
|
|
548
|
+
try {
|
|
549
|
+
junit = parseJunitSummary(await readBoundedRegularFile(junitFile, root));
|
|
550
|
+
} catch (error) {
|
|
551
|
+
// A load registers no test, so Bun writes no report; a run must always have one.
|
|
552
|
+
if (selection.mode === "run" || error?.code !== "ENOENT") throw error;
|
|
553
|
+
}
|
|
554
|
+
// No registration or no matching test leaves the signed channel empty.
|
|
555
|
+
const events =
|
|
556
|
+
execution.eventContent === undefined
|
|
557
|
+
? undefined
|
|
558
|
+
: execution.eventContent === ""
|
|
559
|
+
? []
|
|
560
|
+
: parseEvents(execution.eventContent, root, new Set(files), evidenceKey, true);
|
|
561
|
+
outcome =
|
|
562
|
+
selection.mode === "run"
|
|
563
|
+
? classifyBunDesignatedRun(events, junit, execution.exitCode, execution.operationalError)
|
|
564
|
+
: classifyBunDesignatedLoad(
|
|
565
|
+
events,
|
|
566
|
+
junit,
|
|
567
|
+
selection.tests.length,
|
|
568
|
+
execution.exitCode,
|
|
569
|
+
execution.operationalError,
|
|
570
|
+
);
|
|
571
|
+
} catch {
|
|
572
|
+
outcome = infrastructureFailure("MISSING_OR_INVALID_REPORT");
|
|
573
|
+
}
|
|
574
|
+
await emit(outcome);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** Prints the report and, inside a container, also writes it where the engine reads it back. */
|
|
578
|
+
async function emit(outcome) {
|
|
579
|
+
const body = `${JSON.stringify(outcome)}\n`;
|
|
580
|
+
const resultFile = process.env.ASSERTLEDGER_BUN_RESULT_FILE;
|
|
581
|
+
if (resultFile !== undefined) await writeFile(resultFile, body, { flag: "wx" });
|
|
582
|
+
process.stdout.write(body);
|
|
374
583
|
process.exitCode = outcome.outcome === "PASS" ? 0 : 1;
|
|
375
584
|
}
|
|
376
585
|
|