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.
Files changed (107) hide show
  1. package/README.fr.md +5 -2
  2. package/README.md +5 -2
  3. package/conformance/schema-extensions.json +15 -0
  4. package/dist/build-info.json +1 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +213 -8
  7. package/dist/cli.js.map +1 -1
  8. package/dist/contracts/index.d.ts +467 -4
  9. package/dist/contracts/index.d.ts.map +1 -1
  10. package/dist/contracts/index.js +256 -3
  11. package/dist/contracts/index.js.map +1 -1
  12. package/dist/contracts/qualification.d.ts +528 -0
  13. package/dist/contracts/qualification.d.ts.map +1 -0
  14. package/dist/contracts/qualification.js +357 -0
  15. package/dist/contracts/qualification.js.map +1 -0
  16. package/dist/core/index.d.ts +9 -0
  17. package/dist/core/index.d.ts.map +1 -1
  18. package/dist/core/index.js +46 -2
  19. package/dist/core/index.js.map +1 -1
  20. package/dist/core/qualification.d.ts +17 -0
  21. package/dist/core/qualification.d.ts.map +1 -0
  22. package/dist/core/qualification.js +571 -0
  23. package/dist/core/qualification.js.map +1 -0
  24. package/dist/diagnostics.d.ts.map +1 -1
  25. package/dist/diagnostics.js +81 -4
  26. package/dist/diagnostics.js.map +1 -1
  27. package/dist/engine/adapters/bun-native.d.ts +35 -0
  28. package/dist/engine/adapters/bun-native.d.ts.map +1 -0
  29. package/dist/engine/adapters/bun-native.js +179 -0
  30. package/dist/engine/adapters/bun-native.js.map +1 -0
  31. package/dist/engine/adapters/bun-test-designated-profile.d.ts +23 -0
  32. package/dist/engine/adapters/bun-test-designated-profile.d.ts.map +1 -0
  33. package/dist/engine/adapters/bun-test-designated-profile.js +24 -0
  34. package/dist/engine/adapters/bun-test-designated-profile.js.map +1 -0
  35. package/dist/engine/container.d.ts +7 -0
  36. package/dist/engine/container.d.ts.map +1 -1
  37. package/dist/engine/container.js +22 -7
  38. package/dist/engine/container.js.map +1 -1
  39. package/dist/engine/git-regression.d.ts.map +1 -1
  40. package/dist/engine/git-regression.js +9 -2
  41. package/dist/engine/git-regression.js.map +1 -1
  42. package/dist/engine/index.d.ts +14 -0
  43. package/dist/engine/index.d.ts.map +1 -1
  44. package/dist/engine/index.js +828 -97
  45. package/dist/engine/index.js.map +1 -1
  46. package/dist/engine/qualification.d.ts +154 -0
  47. package/dist/engine/qualification.d.ts.map +1 -0
  48. package/dist/engine/qualification.js +725 -0
  49. package/dist/engine/qualification.js.map +1 -0
  50. package/dist/engine/test-closure.d.ts +39 -0
  51. package/dist/engine/test-closure.d.ts.map +1 -0
  52. package/dist/engine/test-closure.js +513 -0
  53. package/dist/engine/test-closure.js.map +1 -0
  54. package/dist/engine/witness-import.d.ts +31 -0
  55. package/dist/engine/witness-import.d.ts.map +1 -0
  56. package/dist/engine/witness-import.js +224 -0
  57. package/dist/engine/witness-import.js.map +1 -0
  58. package/dist/index.d.ts +3 -0
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +3 -0
  61. package/dist/index.js.map +1 -1
  62. package/dist/mcp/index.d.ts.map +1 -1
  63. package/dist/mcp/index.js +65 -0
  64. package/dist/mcp/index.js.map +1 -1
  65. package/dist/sdk/index.d.ts +292 -3
  66. package/dist/sdk/index.d.ts.map +1 -1
  67. package/dist/sdk/index.js +39 -1
  68. package/dist/sdk/index.js.map +1 -1
  69. package/docs/container-isolation.md +6 -0
  70. package/docs/migration-orchestration-qualification-v1.md +109 -0
  71. package/docs/migration-verification-v4.md +94 -0
  72. package/docs/orchestration-qualification.md +167 -0
  73. package/docs/reference.md +7 -0
  74. package/docs/witness-import.md +67 -0
  75. package/examples/orchestration/README.md +68 -0
  76. package/examples/orchestration/consumer-obligations.json +101 -0
  77. package/examples/orchestration/workspace/bitbucket-pipelines.yml +10 -0
  78. package/examples/orchestration/workspace/command-shape.test.mjs +25 -0
  79. package/examples/orchestration/workspace/commands.mjs +14 -0
  80. package/examples/orchestration/workspace/completion.mjs +39 -0
  81. package/examples/orchestration/workspace/faults.json +1 -0
  82. package/examples/orchestration/workspace/native.test.ts +4 -0
  83. package/examples/orchestration/workspace/package.json +6 -0
  84. package/examples/orchestration/workspace/packages/app/package.json +12 -0
  85. package/examples/orchestration/workspace/packages/app/verdict.txt +1 -0
  86. package/examples/orchestration/workspace/packages/leaf/package.json +9 -0
  87. package/examples/orchestration/workspace/packages/leaf/verdict.txt +1 -0
  88. package/examples/orchestration/workspace/pnpm-lock.yaml +170 -0
  89. package/examples/orchestration/workspace/pnpm-workspace.yaml +2 -0
  90. package/examples/orchestration/workspace/task.mjs +23 -0
  91. package/examples/orchestration/workspace/turbo.json +38 -0
  92. package/examples/orchestration/workspace/wrapper.mjs +45 -0
  93. package/integrations/bun/driver.d.mts +30 -1
  94. package/integrations/bun/driver.mjs +215 -6
  95. package/integrations/bun/preload.mjs +186 -5
  96. package/integrations/bun-native/README.md +86 -0
  97. package/integrations/bun-native/driver.mjs +396 -0
  98. package/integrations/bun-native/preload.mjs +375 -0
  99. package/integrations/skill/SKILL.md +25 -5
  100. package/package.json +1 -1
  101. package/schemas/evidence-manifest.v4.json +995 -0
  102. package/schemas/qualification-execution-request.v1.json +571 -0
  103. package/schemas/qualification-plan.v1.json +528 -0
  104. package/schemas/qualification-receipt.v1.json +821 -0
  105. package/schemas/qualification-replay-result.v1.json +37 -0
  106. package/schemas/verification-request.v4.json +590 -0
  107. package/schemas/witness-import-request.v1.json +231 -0
@@ -0,0 +1,396 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
3
+ import { lstat, readFile, realpath } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+
7
+ const RESULT_VERSION = "1.0.0";
8
+ const BUN_REVISION = "1.4.2+744846f84";
9
+ const MAX_REPORT_BYTES = 8 * 1024 * 1024;
10
+ const MAX_OUTPUT_BYTES = 64 * 1024;
11
+ const ANSI_SGR = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "gu");
12
+ const PRELOAD_SOURCE_PATH = fileURLToPath(new URL("./preload.mjs", import.meta.url));
13
+
14
+ function report(
15
+ outcome,
16
+ testsDiscovered = 0,
17
+ candidateTestsDiscovered = 0,
18
+ attributed = false,
19
+ testFiles = [],
20
+ assertionFailureFiles = [],
21
+ ) {
22
+ return {
23
+ protocolVersion: RESULT_VERSION,
24
+ outcome,
25
+ testsDiscovered,
26
+ candidateTestsDiscovered,
27
+ attributed,
28
+ testFiles,
29
+ assertionFailureFiles,
30
+ };
31
+ }
32
+
33
+ function infrastructureFailure(reason) {
34
+ console.error(`BUN_TEST_INFRA_ERROR:${reason}`);
35
+ return report("INFRA_ERROR");
36
+ }
37
+
38
+ function normalizeRelativeFile(value) {
39
+ if (
40
+ typeof value !== "string" ||
41
+ value.length === 0 ||
42
+ value.startsWith("-") ||
43
+ path.isAbsolute(value)
44
+ )
45
+ return undefined;
46
+ const normalized = path.normalize(value);
47
+ if (normalized === ".." || normalized.startsWith(`..${path.sep}`)) return undefined;
48
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
49
+ }
50
+
51
+ function normalizeAbsoluteFile(value, root) {
52
+ if (typeof value !== "string" || !path.isAbsolute(value)) return undefined;
53
+ const relative = path.relative(root, path.resolve(value));
54
+ return normalizeRelativeFile(relative);
55
+ }
56
+
57
+ function strictInteger(value) {
58
+ if (typeof value !== "string" || !/^(0|[1-9][0-9]*)$/u.test(value)) return undefined;
59
+ const parsed = Number(value);
60
+ return Number.isSafeInteger(parsed) ? parsed : undefined;
61
+ }
62
+
63
+ function parseJunitSummary(xml) {
64
+ if (
65
+ typeof xml !== "string" ||
66
+ !xml.startsWith('<?xml version="1.0" encoding="UTF-8"?>') ||
67
+ !xml.trimEnd().endsWith("</testsuites>") ||
68
+ xml.includes("<!DOCTYPE")
69
+ )
70
+ return undefined;
71
+ const root = xml.match(/<testsuites\b([^>]*)>/u);
72
+ if (root === null) return undefined;
73
+ const attributes = new Map();
74
+ const matches = [...root[1].matchAll(/\s+([A-Za-z][A-Za-z0-9-]*)="([^"]*)"/gu)];
75
+ if (root[1].replace(/\s+([A-Za-z][A-Za-z0-9-]*)="([^"]*)"/gu, "").trim() !== "") {
76
+ return undefined;
77
+ }
78
+ for (const match of matches) {
79
+ if (attributes.has(match[1])) return undefined;
80
+ attributes.set(match[1], match[2]);
81
+ }
82
+ if (attributes.get("name") !== "bun test") return undefined;
83
+ const tests = strictInteger(attributes.get("tests"));
84
+ const failures = strictInteger(attributes.get("failures"));
85
+ const skipped = strictInteger(attributes.get("skipped"));
86
+ if (
87
+ tests === undefined ||
88
+ failures === undefined ||
89
+ skipped === undefined ||
90
+ failures > tests ||
91
+ skipped > tests
92
+ ) {
93
+ return undefined;
94
+ }
95
+ // Same report observation as the v4 Bun driver: a late callback failure does not
96
+ // erase Bun's per-test TimeoutError. This is operational evidence, not attribution.
97
+ const timeouts = [...xml.matchAll(/<failure\b[^>]*\btype="TimeoutError"/gu)].length;
98
+ if (timeouts > failures) return undefined;
99
+ return { tests, failures, skipped, timeouts };
100
+ }
101
+
102
+ function parseEvents(content, root, allowedFiles, evidenceKey) {
103
+ if (!content.endsWith("\n")) return undefined;
104
+ const lines = content.trimEnd().split("\n");
105
+ if (lines.length === 1 && lines[0] === "") return [];
106
+ const events = [];
107
+ let ready = 0;
108
+ for (const line of lines) {
109
+ let envelope;
110
+ try {
111
+ envelope = JSON.parse(line);
112
+ } catch {
113
+ return undefined;
114
+ }
115
+ if (
116
+ typeof envelope !== "object" ||
117
+ envelope === null ||
118
+ Array.isArray(envelope) ||
119
+ Object.keys(envelope).sort().join(",") !== "event,mac" ||
120
+ typeof envelope.mac !== "string" ||
121
+ !/^[0-9a-f]{64}$/u.test(envelope.mac)
122
+ )
123
+ return undefined;
124
+ const expectedMac = createHmac("sha256", evidenceKey)
125
+ .update(JSON.stringify(envelope.event))
126
+ .digest();
127
+ if (!timingSafeEqual(expectedMac, Buffer.from(envelope.mac, "hex"))) return undefined;
128
+ const value = envelope.event;
129
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
130
+ return undefined;
131
+ }
132
+ if (value.kind === "ready") {
133
+ if (Object.keys(value).join(",") !== "kind" || ++ready !== 1) return undefined;
134
+ continue;
135
+ }
136
+ if (value.kind === "hook-error") {
137
+ if (Object.keys(value).join(",") !== "kind") return undefined;
138
+ events.push({ kind: "hook-error" });
139
+ continue;
140
+ }
141
+ if (typeof value.id !== "string" || !/^[0-9a-f-]{36}$/u.test(value.id)) {
142
+ return undefined;
143
+ }
144
+ if (value.kind === "found") {
145
+ if (Object.keys(value).sort().join(",") !== "file,id,kind") return undefined;
146
+ const file = normalizeAbsoluteFile(value.file, root);
147
+ if (file === undefined || !allowedFiles.has(file)) return undefined;
148
+ events.push({ kind: "found", id: value.id, file });
149
+ } else if (value.kind === "end") {
150
+ if (value.status !== "pass" && value.status !== "fail") return undefined;
151
+ const expectedKeys = value.status === "pass" ? "id,kind,status" : "id,kind,owned,status";
152
+ if (Object.keys(value).sort().join(",") !== expectedKeys) return undefined;
153
+ if (value.status === "fail" && typeof value.owned !== "boolean") return undefined;
154
+ events.push({ kind: "end", id: value.id, status: value.status, owned: value.owned ?? false });
155
+ } else return undefined;
156
+ }
157
+ return ready === 1 ? events : undefined;
158
+ }
159
+
160
+ export function classifyBunInstrumentedEvidence(
161
+ events,
162
+ junit,
163
+ baseFiles,
164
+ candidateFiles,
165
+ exitCode,
166
+ operationalError = false,
167
+ ) {
168
+ if (
169
+ events === undefined ||
170
+ junit === undefined ||
171
+ exitCode === null ||
172
+ junit.skipped !== 0 ||
173
+ operationalError ||
174
+ events.some((event) => event.kind === "hook-error")
175
+ ) {
176
+ return infrastructureFailure("INCOMPLETE_CONTROLLED_REPORT");
177
+ }
178
+ if (junit.timeouts > 0) return infrastructureFailure("BUN_TEST_TIMEOUT");
179
+ const found = new Map();
180
+ const ended = new Map();
181
+ for (const event of events) {
182
+ if (event.kind === "found") {
183
+ if (found.has(event.id)) return infrastructureFailure("DUPLICATE_TEST_ID");
184
+ found.set(event.id, event.file);
185
+ } else {
186
+ if (!found.has(event.id)) return infrastructureFailure("UNMATCHED_TEST_END");
187
+ if (ended.has(event.id)) return infrastructureFailure("DUPLICATE_TEST_END");
188
+ ended.set(event.id, event);
189
+ }
190
+ }
191
+ const completed = [...ended.values()];
192
+ if (found.size === 0 && junit.tests === 0 && junit.failures === 0)
193
+ return report("NO_TEST_DISCOVERED");
194
+ if (found.size === 0 || found.size !== ended.size || completed.length !== junit.tests) {
195
+ return infrastructureFailure("TEST_COUNT_MISMATCH");
196
+ }
197
+ const files = new Set(found.values());
198
+ const testFiles = [...files].sort();
199
+ if ([...baseFiles].some((file) => !files.has(file))) {
200
+ return infrastructureFailure("BASE_TEST_FILE_NOT_STARTED");
201
+ }
202
+ if ([...candidateFiles].some((file) => !files.has(file)))
203
+ return infrastructureFailure("DECLARED_TEST_FILE_NOT_STARTED");
204
+ if ([...files].some((file) => !baseFiles.has(file) && !candidateFiles.has(file))) {
205
+ return infrastructureFailure("UNEXPECTED_TEST_FILE");
206
+ }
207
+ const failures = completed.filter((entry) => entry.status === "fail");
208
+ if (failures.length !== junit.failures || (exitCode === 0) !== (junit.failures === 0)) {
209
+ return infrastructureFailure("BUN_STATUS_MISMATCH");
210
+ }
211
+ const candidateIds = completed
212
+ .filter((entry) => candidateFiles.has(found.get(entry.id)))
213
+ .map((entry) => entry.id);
214
+ if (exitCode === 0) {
215
+ return candidateFiles.size > 0 && candidateIds.length === 0
216
+ ? report("NO_TEST_DISCOVERED", completed.length, 0, false)
217
+ : report("PASS", completed.length, candidateIds.length, candidateFiles.size > 0, testFiles);
218
+ }
219
+ const candidateFailures = failures.filter((entry) => candidateFiles.has(found.get(entry.id)));
220
+ const baseFailures = failures.filter((entry) => baseFiles.has(found.get(entry.id)));
221
+ if (baseFailures.length > 0 || candidateIds.length === 0) {
222
+ return report("PROCESS_CRASH", completed.length, candidateIds.length, false, testFiles);
223
+ }
224
+ const attributed =
225
+ candidateFailures.length > 0 &&
226
+ failures.length === candidateFailures.length &&
227
+ candidateFailures.every((entry) => entry.owned);
228
+ return report(
229
+ attributed ? "ASSERTION_FAILURE" : "PROCESS_CRASH",
230
+ completed.length,
231
+ candidateIds.length,
232
+ attributed,
233
+ testFiles,
234
+ attributed ? [...new Set(candidateFailures.map((entry) => found.get(entry.id)))].sort() : [],
235
+ );
236
+ }
237
+
238
+ async function readBoundedRegularFile(file, root) {
239
+ const details = await lstat(file);
240
+ if (!details.isFile() || details.size > MAX_REPORT_BYTES) throw new Error("INVALID_REPORT_FILE");
241
+ const resolved = await realpath(file);
242
+ const relative = path.relative(root, resolved);
243
+ if (
244
+ relative === "" ||
245
+ relative === ".." ||
246
+ relative.startsWith(`..${path.sep}`) ||
247
+ path.isAbsolute(relative)
248
+ ) {
249
+ throw new Error("REPORT_PATH_ESCAPE");
250
+ }
251
+ return readFile(file, "utf8");
252
+ }
253
+
254
+ async function runBun(executable, files, junitFile, root, evidenceKey) {
255
+ const exactPath = (file) => `./${file.replaceAll(path.sep, "/")}`;
256
+ const childEnvironment = { ...process.env };
257
+ delete childEnvironment.TESTFORGE_RESULT_FILE;
258
+ delete childEnvironment.TESTFORGE_CANDIDATE_FILES;
259
+ delete childEnvironment.ASSERTLEDGER_BUN_EVENTS_FILE;
260
+ childEnvironment.ASSERTLEDGER_BUN_ROOT = root;
261
+ const child = spawn(
262
+ executable,
263
+ [
264
+ "test",
265
+ "--max-concurrency=1",
266
+ "--retry=0",
267
+ "--preload",
268
+ PRELOAD_SOURCE_PATH,
269
+ "--reporter=junit",
270
+ `--reporter-outfile=${junitFile}`,
271
+ ...files.map(exactPath),
272
+ ],
273
+ {
274
+ cwd: root,
275
+ env: childEnvironment,
276
+ shell: false,
277
+ stdio: ["ignore", "pipe", "pipe", "pipe", "pipe"],
278
+ },
279
+ );
280
+ let keyDeliveryError = false;
281
+ child.stdio[4].on("error", () => {
282
+ keyDeliveryError = true;
283
+ });
284
+ child.stdio[4].end(evidenceKey);
285
+ let outputBytes = 0;
286
+ let overflow = false;
287
+ let stderr = "";
288
+ let evidenceBytes = 0;
289
+ const evidenceChunks = [];
290
+ const collect = (chunk) => {
291
+ outputBytes += chunk.length;
292
+ if (outputBytes > MAX_OUTPUT_BYTES) {
293
+ overflow = true;
294
+ child.kill();
295
+ }
296
+ };
297
+ child.stdout.on("data", collect);
298
+ child.stderr.on("data", (chunk) => {
299
+ collect(chunk);
300
+ if (!overflow) stderr += chunk.toString("utf8");
301
+ });
302
+ child.stdio[3].on("data", (chunk) => {
303
+ evidenceBytes += chunk.length;
304
+ if (evidenceBytes > MAX_REPORT_BYTES) {
305
+ overflow = true;
306
+ child.kill();
307
+ } else {
308
+ evidenceChunks.push(chunk);
309
+ }
310
+ });
311
+ const exitCode = await new Promise((resolve) => {
312
+ child.once("error", () => resolve(null));
313
+ child.once("close", (code) => resolve(code));
314
+ });
315
+ const plainStderr = stderr.replace(ANSI_SGR, "");
316
+ const operationalError =
317
+ plainStderr.includes("Unhandled error between tests") ||
318
+ /(?:^|\r?\n)\s*[1-9][0-9]*\s+errors?\s*(?:\r?\n|$)/u.test(plainStderr);
319
+ return {
320
+ exitCode: overflow || keyDeliveryError ? null : exitCode,
321
+ operationalError,
322
+ eventContent: overflow ? undefined : Buffer.concat(evidenceChunks).toString("utf8"),
323
+ };
324
+ }
325
+
326
+ async function main() {
327
+ const root = await realpath(process.cwd());
328
+ const [executable, ...baseTests] = process.argv.slice(2);
329
+ if (!path.isAbsolute(executable ?? "") || baseTests.length === 0) {
330
+ throw new Error("INVALID_ARGUMENTS");
331
+ }
332
+ const revision = spawnSync(executable, ["--revision"], {
333
+ cwd: process.cwd(),
334
+ encoding: "utf8",
335
+ shell: false,
336
+ windowsHide: true,
337
+ timeout: 5_000,
338
+ maxBuffer: MAX_OUTPUT_BYTES,
339
+ });
340
+ if (revision.status !== 0 || revision.stderr !== "" || revision.stdout.trim() !== BUN_REVISION) {
341
+ throw new Error("UNSUPPORTED_BUN_REVISION");
342
+ }
343
+ const candidates = [];
344
+ if (!Array.isArray(candidates)) throw new Error("INVALID_CANDIDATES");
345
+ const normalizedBase = baseTests.map(normalizeRelativeFile);
346
+ const normalizedCandidates = candidates.map(normalizeRelativeFile);
347
+ if ([...normalizedBase, ...normalizedCandidates].some((file) => file === undefined)) {
348
+ throw new Error("INVALID_PATH");
349
+ }
350
+ const files = [...normalizedBase, ...normalizedCandidates];
351
+ if (new Set(files).size !== files.length) throw new Error("DUPLICATE_TEST_FILE");
352
+ const junitFile = path.join(root, `__assertledger_bun_junit_${randomUUID()}.xml`);
353
+ const evidenceKey = randomBytes(32);
354
+ const execution = await runBun(
355
+ executable,
356
+ [...baseTests, ...candidates],
357
+ junitFile,
358
+ root,
359
+ evidenceKey,
360
+ );
361
+ let outcome;
362
+ const events =
363
+ execution.eventContent === undefined
364
+ ? undefined
365
+ : parseEvents(execution.eventContent, root, new Set(files), evidenceKey);
366
+ try {
367
+ const junitContent = await readBoundedRegularFile(junitFile, root);
368
+ outcome = classifyBunInstrumentedEvidence(
369
+ events,
370
+ parseJunitSummary(junitContent),
371
+ new Set(),
372
+ new Set(normalizedBase),
373
+ execution.exitCode,
374
+ execution.operationalError,
375
+ );
376
+ } catch (error) {
377
+ outcome =
378
+ error?.code === "ENOENT" &&
379
+ events?.length === 0 &&
380
+ execution.exitCode === 0 &&
381
+ !execution.operationalError
382
+ ? report("NO_TEST_DISCOVERED")
383
+ : infrastructureFailure("MISSING_OR_INVALID_REPORT");
384
+ }
385
+ process.stdout.write(`${JSON.stringify({ ...outcome, exitCode: execution.exitCode })}\n`);
386
+ // Transport completion is independent of the inner Bun semantic outcome.
387
+ process.exitCode = 0;
388
+ }
389
+
390
+ if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
391
+ main().catch((error) => {
392
+ console.error(error instanceof Error ? error.message : "UNKNOWN_DRIVER_ERROR");
393
+ process.stdout.write(`${JSON.stringify(report("INFRA_ERROR"))}\n`);
394
+ process.exitCode = 1;
395
+ });
396
+ }