@ai-agent-forge/plugin-test-runner 0.85.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 (52) hide show
  1. package/README.md +44 -0
  2. package/agent-forge.json +12 -0
  3. package/dist/capability.d.ts +5 -0
  4. package/dist/capability.d.ts.map +1 -0
  5. package/dist/capability.js +138 -0
  6. package/dist/capability.js.map +1 -0
  7. package/dist/entry.d.ts +13 -0
  8. package/dist/entry.d.ts.map +1 -0
  9. package/dist/entry.js +14 -0
  10. package/dist/entry.js.map +1 -0
  11. package/dist/index.d.ts +12 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +10 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/node-adapter.d.ts +24 -0
  16. package/dist/node-adapter.d.ts.map +1 -0
  17. package/dist/node-adapter.js +448 -0
  18. package/dist/node-adapter.js.map +1 -0
  19. package/dist/node-jest-json-report.d.ts +12 -0
  20. package/dist/node-jest-json-report.d.ts.map +1 -0
  21. package/dist/node-jest-json-report.js +52 -0
  22. package/dist/node-jest-json-report.js.map +1 -0
  23. package/dist/node-jest-runner.d.ts +26 -0
  24. package/dist/node-jest-runner.d.ts.map +1 -0
  25. package/dist/node-jest-runner.js +51 -0
  26. package/dist/node-jest-runner.js.map +1 -0
  27. package/dist/node-junit-xml-report.d.ts +11 -0
  28. package/dist/node-junit-xml-report.d.ts.map +1 -0
  29. package/dist/node-junit-xml-report.js +76 -0
  30. package/dist/node-junit-xml-report.js.map +1 -0
  31. package/dist/node-pytest-runner.d.ts +34 -0
  32. package/dist/node-pytest-runner.d.ts.map +1 -0
  33. package/dist/node-pytest-runner.js +111 -0
  34. package/dist/node-pytest-runner.js.map +1 -0
  35. package/dist/node-test-process.d.ts +24 -0
  36. package/dist/node-test-process.d.ts.map +1 -0
  37. package/dist/node-test-process.js +89 -0
  38. package/dist/node-test-process.js.map +1 -0
  39. package/dist/node-vitest-runner.d.ts +24 -0
  40. package/dist/node-vitest-runner.d.ts.map +1 -0
  41. package/dist/node-vitest-runner.js +49 -0
  42. package/dist/node-vitest-runner.js.map +1 -0
  43. package/dist/shell-utils.d.ts +15 -0
  44. package/dist/shell-utils.d.ts.map +1 -0
  45. package/dist/shell-utils.js +43 -0
  46. package/dist/shell-utils.js.map +1 -0
  47. package/dist/types.d.ts +93 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +5 -0
  50. package/dist/types.js.map +1 -0
  51. package/package.json +55 -0
  52. package/plugin.json +9 -0
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # @agent-forge/plugin-test-runner
2
+
3
+ Test runner capability plugin for [Agent Forge](https://github.com/agent-forge/agent-forge), extracted from the host's
4
+ `src/plugins/test-runner` (D-075 S3). It registers one full-control tool, `test_run`, which asks the host to run a
5
+ framework test target (vitest/jest/node-test/pytest) and returns bounded structured results; framework stdout/stderr
6
+ stays host-owned.
7
+
8
+ The plugin has no standalone command binding at load time. Without host-injected command resolver and runner seams the
9
+ tool reports an explicit `unsupported` result; hosts that want real execution bind the per-framework adapters
10
+ (`createVitestRealTestRunnerAdapterV1`, `createJestRealTestRunnerAdapterV1`, `createPytestRealTestRunnerAdapterV1`)
11
+ against a workspace directory. Readonly hosts get no tool registration at all.
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ # from a local checkout
17
+ agent-forge plugin install ./plugins/test-runner
18
+
19
+ # from npm
20
+ agent-forge plugin install @agent-forge/plugin-test-runner
21
+ ```
22
+
23
+ The package ships two manifests: `agent-forge.json` (ecosystem manifest for the deterministic plugin resolver) and
24
+ `plugin.json` (host-load manifest consumed by `loadCapabilityPluginFromManifest`, entry `./dist/entry.js`).
25
+
26
+ ## Library surface
27
+
28
+ `import { createNodeTestRunnerAdapterV1, createTestRunnerCapabilityFactoryV1 } from "@agent-forge/plugin-test-runner";`
29
+
30
+ - `createNodeTestRunnerAdapterV1(options?)` — host-injection adapter; requires `commandResolver`/`runner` to produce
31
+ results, otherwise returns `unsupported`.
32
+ - `createDeterministicTestRunnerAdapterV1(fixtures)` — deterministic fixture adapter for tests.
33
+ - `createVitestRealTestRunnerAdapterV1({ workspaceDirectory })` / Jest / pytest variants — real per-framework runners
34
+ with JSON/JUnit report parsing.
35
+ - `createTestRunnerCapabilityFactoryV1(adapter)` — registers `test_run` on a full-control `CapabilityAPI`.
36
+ - Default export of `dist/entry.js` — the precomposed factory used by external plugin loading.
37
+
38
+ ## Development
39
+
40
+ ```sh
41
+ npm install --ignore-scripts # repo root, creates the workspace symlink
42
+ npm run build # plugins/test-runner (tsgo)
43
+ npm test # vitest; the entry fidelity test requires `npm run build` first
44
+ ```
@@ -0,0 +1,12 @@
1
+ {
2
+ "packageFormatVersion": 1,
3
+ "id": "agent-forge.plugin.test-runner",
4
+ "pluginVersion": "0.84.4",
5
+ "apiVersion": "1",
6
+ "minHostVersion": "0.84.4",
7
+ "capabilityRefs": [{ "id": "test.run", "version": 1 }],
8
+ "platform": {
9
+ "os": ["windows", "macos", "linux"],
10
+ "runtime": [{ "name": "node" }]
11
+ }
12
+ }
@@ -0,0 +1,5 @@
1
+ import type { CapabilityPluginSyncFactory } from "@agent-forge/plugin-sdk";
2
+ import type { TestRunnerAdapterV1 } from "./types.ts";
3
+ /** Registers the bounded test operation only for a full-control host. */
4
+ export declare function createTestRunnerCapabilityFactoryV1(runtime: TestRunnerAdapterV1): CapabilityPluginSyncFactory;
5
+ //# sourceMappingURL=capability.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capability.d.ts","sourceRoot":"","sources":["../src/capability.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,yBAAyB,CAAC;AAI3E,OAAO,KAAK,EAAE,mBAAmB,EAAmB,MAAM,YAAY,CAAC;AAiGvE,yEAAyE;AACzE,wBAAgB,mCAAmC,CAAC,OAAO,EAAE,mBAAmB,GAAG,2BAA2B,CAsD7G","sourcesContent":["import type { CapabilityPluginSyncFactory } from \"@agent-forge/plugin-sdk\";\nimport { Type } from \"typebox\";\nimport { Value } from \"typebox/value\";\nimport { projectTestRunResultV1, TEST_RUNNER_MAX_OPAQUE_REF_CHARS_V1 } from \"./node-adapter.ts\";\nimport type { TestRunnerAdapterV1, TestRunResultV1 } from \"./types.ts\";\n\nconst requestSchema = Type.Object(\n\t{\n\t\tworkspaceRef: Type.String({\n\t\t\tminLength: 1,\n\t\t\tmaxLength: TEST_RUNNER_MAX_OPAQUE_REF_CHARS_V1,\n\t\t\tpattern: \"^[A-Za-z0-9][A-Za-z0-9._:-]*$\",\n\t\t}),\n\t\tframework: Type.Union([\n\t\t\tType.Literal(\"vitest\"),\n\t\t\tType.Literal(\"jest\"),\n\t\t\tType.Literal(\"node-test\"),\n\t\t\tType.Literal(\"pytest\"),\n\t\t]),\n\t\ttarget: Type.String({ minLength: 1, maxLength: 1024, pattern: \"^[^\\\\u0000\\\\r\\\\n]+$\" }),\n\t\ttimeoutMs: Type.Optional(Type.Integer({ minimum: 1, maximum: 120_000 })),\n\t\tmaxFailures: Type.Optional(Type.Integer({ minimum: 1, maximum: 1_000 })),\n\t},\n\t{ additionalProperties: false },\n);\nconst inputSchema = Type.Object({ request: requestSchema }, { additionalProperties: false });\n\nfunction objectInput(first: unknown, second: unknown): Record<string, unknown> {\n\tconst value = second === undefined ? first : second;\n\tif (value === null || typeof value !== \"object\" || Array.isArray(value))\n\t\tthrow new Error(\"Test runner input must be an object\");\n\treturn value as Record<string, unknown>;\n}\n\nfunction invocation(\n\tfirst: unknown,\n\tsecond: unknown,\n): { readonly input: Record<string, unknown>; readonly operationId: string } {\n\tconst candidate = typeof first === \"string\" && first.trim() !== \"\" ? first : \"profile-runtime-tool-call\";\n\treturn {\n\t\tinput: objectInput(first, second),\n\t\toperationId: candidate.length <= 256 ? candidate : \"profile-runtime-tool-call\",\n\t};\n}\n\nfunction signalFrom(value: unknown): AbortSignal | undefined {\n\treturn value instanceof AbortSignal ? value : undefined;\n}\n\nfunction adapterFailure(operationId: string): TestRunResultV1 {\n\treturn {\n\t\tversion: 1,\n\t\toperationId,\n\t\tstatus: \"failed\",\n\t\tsummary: { total: 0, passed: 0, failed: 0, skipped: 0, flaky: 0 },\n\t\tfailures: [],\n\t\tfailuresTruncated: false,\n\t\terror: { version: 1, code: \"adapter_failed\", message: \"Test runner adapter returned an invalid result\" },\n\t};\n}\n\nfunction invalidInput(operationId: string, message: string): TestRunResultV1 {\n\treturn {\n\t\tversion: 1,\n\t\toperationId,\n\t\tstatus: \"failed\",\n\t\tsummary: { total: 0, passed: 0, failed: 0, skipped: 0, flaky: 0 },\n\t\tfailures: [],\n\t\tfailuresTruncated: false,\n\t\terror: { version: 1, code: \"invalid_input\", message },\n\t};\n}\n\nfunction cancelled(operationId: string): TestRunResultV1 {\n\treturn {\n\t\tversion: 1,\n\t\toperationId,\n\t\tstatus: \"cancelled\",\n\t\tsummary: { total: 0, passed: 0, failed: 0, skipped: 0, flaky: 0 },\n\t\tfailures: [],\n\t\tfailuresTruncated: false,\n\t\terror: { version: 1, code: \"cancelled\", message: \"Test run was cancelled\" },\n\t};\n}\n\nconst CANCELLATION_V1 = Symbol(\"test-runner-capability-cancelled\");\n\nfunction runWithCancellation(\n\trun: () => Promise<TestRunResultV1>,\n\tsignal: AbortSignal | undefined,\n): Promise<TestRunResultV1 | typeof CANCELLATION_V1> {\n\tif (signal === undefined) return run();\n\tlet onAbort!: () => void;\n\tconst cancellation = new Promise<typeof CANCELLATION_V1>((resolve) => {\n\t\tonAbort = () => resolve(CANCELLATION_V1);\n\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t});\n\tconst pending = run();\n\treturn Promise.race([pending, cancellation]).finally(() => signal.removeEventListener(\"abort\", onAbort));\n}\n\n/** Registers the bounded test operation only for a full-control host. */\nexport function createTestRunnerCapabilityFactoryV1(runtime: TestRunnerAdapterV1): CapabilityPluginSyncFactory {\n\treturn (api) => {\n\t\tif (api.host.mode === \"readonly\") return;\n\t\tapi.registerTool({\n\t\t\tname: \"test_run\",\n\t\t\tlabel: \"Run tests\",\n\t\t\tdescription: \"Run a host-resolved test target and return bounded structured test results\",\n\t\t\tparameters: inputSchema,\n\t\t\texecute: async (first: unknown, second?: unknown, third?: unknown) => {\n\t\t\t\tlet invocationResult: { readonly input: Record<string, unknown>; readonly operationId: string };\n\t\t\t\ttry {\n\t\t\t\t\tinvocationResult = invocation(first, second);\n\t\t\t\t} catch {\n\t\t\t\t\treturn invalidInput(\"profile-runtime-tool-call\", \"Test runner input must be an object\");\n\t\t\t\t}\n\t\t\t\tconst { input, operationId } = invocationResult;\n\t\t\t\tconst signal = signalFrom(third);\n\t\t\t\tif (signal?.aborted) return cancelled(operationId);\n\t\t\t\tlet requestInput: Record<string, unknown>;\n\t\t\t\ttry {\n\t\t\t\t\tif (!Value.Check(inputSchema, input)) return invalidInput(operationId, \"Test runner request is invalid\");\n\t\t\t\t\trequestInput = objectInput(input.request, undefined);\n\t\t\t\t} catch {\n\t\t\t\t\treturn invalidInput(operationId, \"Test runner request is invalid\");\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tconst output = await runWithCancellation(\n\t\t\t\t\t\t() =>\n\t\t\t\t\t\t\truntime.run(\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tversion: 1,\n\t\t\t\t\t\t\t\t\toperationId,\n\t\t\t\t\t\t\t\t\tworkspaceRef: requestInput.workspaceRef as string,\n\t\t\t\t\t\t\t\t\tframework: requestInput.framework as \"vitest\" | \"jest\" | \"node-test\" | \"pytest\",\n\t\t\t\t\t\t\t\t\ttarget: requestInput.target as string,\n\t\t\t\t\t\t\t\t\t...(requestInput.timeoutMs === undefined\n\t\t\t\t\t\t\t\t\t\t? {}\n\t\t\t\t\t\t\t\t\t\t: { timeoutMs: requestInput.timeoutMs as number }),\n\t\t\t\t\t\t\t\t\t...(requestInput.maxFailures === undefined\n\t\t\t\t\t\t\t\t\t\t? {}\n\t\t\t\t\t\t\t\t\t\t: { maxFailures: requestInput.maxFailures as number }),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tsignal,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\tsignal,\n\t\t\t\t\t);\n\t\t\t\t\tif (output === CANCELLATION_V1) return cancelled(operationId);\n\t\t\t\t\treturn projectTestRunResultV1(output, operationId) ?? adapterFailure(operationId);\n\t\t\t\t} catch {\n\t\t\t\t\treturn adapterFailure(operationId);\n\t\t\t\t}\n\t\t\t},\n\t\t});\n\t};\n}\n"]}
@@ -0,0 +1,138 @@
1
+ import { Type } from "typebox";
2
+ import { Value } from "typebox/value";
3
+ import { projectTestRunResultV1, TEST_RUNNER_MAX_OPAQUE_REF_CHARS_V1 } from "./node-adapter.js";
4
+ const requestSchema = Type.Object({
5
+ workspaceRef: Type.String({
6
+ minLength: 1,
7
+ maxLength: TEST_RUNNER_MAX_OPAQUE_REF_CHARS_V1,
8
+ pattern: "^[A-Za-z0-9][A-Za-z0-9._:-]*$",
9
+ }),
10
+ framework: Type.Union([
11
+ Type.Literal("vitest"),
12
+ Type.Literal("jest"),
13
+ Type.Literal("node-test"),
14
+ Type.Literal("pytest"),
15
+ ]),
16
+ target: Type.String({ minLength: 1, maxLength: 1024, pattern: "^[^\\u0000\\r\\n]+$" }),
17
+ timeoutMs: Type.Optional(Type.Integer({ minimum: 1, maximum: 120_000 })),
18
+ maxFailures: Type.Optional(Type.Integer({ minimum: 1, maximum: 1_000 })),
19
+ }, { additionalProperties: false });
20
+ const inputSchema = Type.Object({ request: requestSchema }, { additionalProperties: false });
21
+ function objectInput(first, second) {
22
+ const value = second === undefined ? first : second;
23
+ if (value === null || typeof value !== "object" || Array.isArray(value))
24
+ throw new Error("Test runner input must be an object");
25
+ return value;
26
+ }
27
+ function invocation(first, second) {
28
+ const candidate = typeof first === "string" && first.trim() !== "" ? first : "profile-runtime-tool-call";
29
+ return {
30
+ input: objectInput(first, second),
31
+ operationId: candidate.length <= 256 ? candidate : "profile-runtime-tool-call",
32
+ };
33
+ }
34
+ function signalFrom(value) {
35
+ return value instanceof AbortSignal ? value : undefined;
36
+ }
37
+ function adapterFailure(operationId) {
38
+ return {
39
+ version: 1,
40
+ operationId,
41
+ status: "failed",
42
+ summary: { total: 0, passed: 0, failed: 0, skipped: 0, flaky: 0 },
43
+ failures: [],
44
+ failuresTruncated: false,
45
+ error: { version: 1, code: "adapter_failed", message: "Test runner adapter returned an invalid result" },
46
+ };
47
+ }
48
+ function invalidInput(operationId, message) {
49
+ return {
50
+ version: 1,
51
+ operationId,
52
+ status: "failed",
53
+ summary: { total: 0, passed: 0, failed: 0, skipped: 0, flaky: 0 },
54
+ failures: [],
55
+ failuresTruncated: false,
56
+ error: { version: 1, code: "invalid_input", message },
57
+ };
58
+ }
59
+ function cancelled(operationId) {
60
+ return {
61
+ version: 1,
62
+ operationId,
63
+ status: "cancelled",
64
+ summary: { total: 0, passed: 0, failed: 0, skipped: 0, flaky: 0 },
65
+ failures: [],
66
+ failuresTruncated: false,
67
+ error: { version: 1, code: "cancelled", message: "Test run was cancelled" },
68
+ };
69
+ }
70
+ const CANCELLATION_V1 = Symbol("test-runner-capability-cancelled");
71
+ function runWithCancellation(run, signal) {
72
+ if (signal === undefined)
73
+ return run();
74
+ let onAbort;
75
+ const cancellation = new Promise((resolve) => {
76
+ onAbort = () => resolve(CANCELLATION_V1);
77
+ signal.addEventListener("abort", onAbort, { once: true });
78
+ });
79
+ const pending = run();
80
+ return Promise.race([pending, cancellation]).finally(() => signal.removeEventListener("abort", onAbort));
81
+ }
82
+ /** Registers the bounded test operation only for a full-control host. */
83
+ export function createTestRunnerCapabilityFactoryV1(runtime) {
84
+ return (api) => {
85
+ if (api.host.mode === "readonly")
86
+ return;
87
+ api.registerTool({
88
+ name: "test_run",
89
+ label: "Run tests",
90
+ description: "Run a host-resolved test target and return bounded structured test results",
91
+ parameters: inputSchema,
92
+ execute: async (first, second, third) => {
93
+ let invocationResult;
94
+ try {
95
+ invocationResult = invocation(first, second);
96
+ }
97
+ catch {
98
+ return invalidInput("profile-runtime-tool-call", "Test runner input must be an object");
99
+ }
100
+ const { input, operationId } = invocationResult;
101
+ const signal = signalFrom(third);
102
+ if (signal?.aborted)
103
+ return cancelled(operationId);
104
+ let requestInput;
105
+ try {
106
+ if (!Value.Check(inputSchema, input))
107
+ return invalidInput(operationId, "Test runner request is invalid");
108
+ requestInput = objectInput(input.request, undefined);
109
+ }
110
+ catch {
111
+ return invalidInput(operationId, "Test runner request is invalid");
112
+ }
113
+ try {
114
+ const output = await runWithCancellation(() => runtime.run({
115
+ version: 1,
116
+ operationId,
117
+ workspaceRef: requestInput.workspaceRef,
118
+ framework: requestInput.framework,
119
+ target: requestInput.target,
120
+ ...(requestInput.timeoutMs === undefined
121
+ ? {}
122
+ : { timeoutMs: requestInput.timeoutMs }),
123
+ ...(requestInput.maxFailures === undefined
124
+ ? {}
125
+ : { maxFailures: requestInput.maxFailures }),
126
+ }, signal), signal);
127
+ if (output === CANCELLATION_V1)
128
+ return cancelled(operationId);
129
+ return projectTestRunResultV1(output, operationId) ?? adapterFailure(operationId);
130
+ }
131
+ catch {
132
+ return adapterFailure(operationId);
133
+ }
134
+ },
135
+ });
136
+ };
137
+ }
138
+ //# sourceMappingURL=capability.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capability.js","sourceRoot":"","sources":["../src/capability.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CapabilityPluginSyncFactory } from \"@agent-forge/plugin-sdk\";\nimport { Type } from \"typebox\";\nimport { Value } from \"typebox/value\";\nimport { projectTestRunResultV1, TEST_RUNNER_MAX_OPAQUE_REF_CHARS_V1 } from \"./node-adapter.ts\";\nimport type { TestRunnerAdapterV1, TestRunResultV1 } from \"./types.ts\";\n\nconst requestSchema = Type.Object(\n\t{\n\t\tworkspaceRef: Type.String({\n\t\t\tminLength: 1,\n\t\t\tmaxLength: TEST_RUNNER_MAX_OPAQUE_REF_CHARS_V1,\n\t\t\tpattern: \"^[A-Za-z0-9][A-Za-z0-9._:-]*$\",\n\t\t}),\n\t\tframework: Type.Union([\n\t\t\tType.Literal(\"vitest\"),\n\t\t\tType.Literal(\"jest\"),\n\t\t\tType.Literal(\"node-test\"),\n\t\t\tType.Literal(\"pytest\"),\n\t\t]),\n\t\ttarget: Type.String({ minLength: 1, maxLength: 1024, pattern: \"^[^\\\\u0000\\\\r\\\\n]+$\" }),\n\t\ttimeoutMs: Type.Optional(Type.Integer({ minimum: 1, maximum: 120_000 })),\n\t\tmaxFailures: Type.Optional(Type.Integer({ minimum: 1, maximum: 1_000 })),\n\t},\n\t{ additionalProperties: false },\n);\nconst inputSchema = Type.Object({ request: requestSchema }, { additionalProperties: false });\n\nfunction objectInput(first: unknown, second: unknown): Record<string, unknown> {\n\tconst value = second === undefined ? first : second;\n\tif (value === null || typeof value !== \"object\" || Array.isArray(value))\n\t\tthrow new Error(\"Test runner input must be an object\");\n\treturn value as Record<string, unknown>;\n}\n\nfunction invocation(\n\tfirst: unknown,\n\tsecond: unknown,\n): { readonly input: Record<string, unknown>; readonly operationId: string } {\n\tconst candidate = typeof first === \"string\" && first.trim() !== \"\" ? first : \"profile-runtime-tool-call\";\n\treturn {\n\t\tinput: objectInput(first, second),\n\t\toperationId: candidate.length <= 256 ? candidate : \"profile-runtime-tool-call\",\n\t};\n}\n\nfunction signalFrom(value: unknown): AbortSignal | undefined {\n\treturn value instanceof AbortSignal ? value : undefined;\n}\n\nfunction adapterFailure(operationId: string): TestRunResultV1 {\n\treturn {\n\t\tversion: 1,\n\t\toperationId,\n\t\tstatus: \"failed\",\n\t\tsummary: { total: 0, passed: 0, failed: 0, skipped: 0, flaky: 0 },\n\t\tfailures: [],\n\t\tfailuresTruncated: false,\n\t\terror: { version: 1, code: \"adapter_failed\", message: \"Test runner adapter returned an invalid result\" },\n\t};\n}\n\nfunction invalidInput(operationId: string, message: string): TestRunResultV1 {\n\treturn {\n\t\tversion: 1,\n\t\toperationId,\n\t\tstatus: \"failed\",\n\t\tsummary: { total: 0, passed: 0, failed: 0, skipped: 0, flaky: 0 },\n\t\tfailures: [],\n\t\tfailuresTruncated: false,\n\t\terror: { version: 1, code: \"invalid_input\", message },\n\t};\n}\n\nfunction cancelled(operationId: string): TestRunResultV1 {\n\treturn {\n\t\tversion: 1,\n\t\toperationId,\n\t\tstatus: \"cancelled\",\n\t\tsummary: { total: 0, passed: 0, failed: 0, skipped: 0, flaky: 0 },\n\t\tfailures: [],\n\t\tfailuresTruncated: false,\n\t\terror: { version: 1, code: \"cancelled\", message: \"Test run was cancelled\" },\n\t};\n}\n\nconst CANCELLATION_V1 = Symbol(\"test-runner-capability-cancelled\");\n\nfunction runWithCancellation(\n\trun: () => Promise<TestRunResultV1>,\n\tsignal: AbortSignal | undefined,\n): Promise<TestRunResultV1 | typeof CANCELLATION_V1> {\n\tif (signal === undefined) return run();\n\tlet onAbort!: () => void;\n\tconst cancellation = new Promise<typeof CANCELLATION_V1>((resolve) => {\n\t\tonAbort = () => resolve(CANCELLATION_V1);\n\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t});\n\tconst pending = run();\n\treturn Promise.race([pending, cancellation]).finally(() => signal.removeEventListener(\"abort\", onAbort));\n}\n\n/** Registers the bounded test operation only for a full-control host. */\nexport function createTestRunnerCapabilityFactoryV1(runtime: TestRunnerAdapterV1): CapabilityPluginSyncFactory {\n\treturn (api) => {\n\t\tif (api.host.mode === \"readonly\") return;\n\t\tapi.registerTool({\n\t\t\tname: \"test_run\",\n\t\t\tlabel: \"Run tests\",\n\t\t\tdescription: \"Run a host-resolved test target and return bounded structured test results\",\n\t\t\tparameters: inputSchema,\n\t\t\texecute: async (first: unknown, second?: unknown, third?: unknown) => {\n\t\t\t\tlet invocationResult: { readonly input: Record<string, unknown>; readonly operationId: string };\n\t\t\t\ttry {\n\t\t\t\t\tinvocationResult = invocation(first, second);\n\t\t\t\t} catch {\n\t\t\t\t\treturn invalidInput(\"profile-runtime-tool-call\", \"Test runner input must be an object\");\n\t\t\t\t}\n\t\t\t\tconst { input, operationId } = invocationResult;\n\t\t\t\tconst signal = signalFrom(third);\n\t\t\t\tif (signal?.aborted) return cancelled(operationId);\n\t\t\t\tlet requestInput: Record<string, unknown>;\n\t\t\t\ttry {\n\t\t\t\t\tif (!Value.Check(inputSchema, input)) return invalidInput(operationId, \"Test runner request is invalid\");\n\t\t\t\t\trequestInput = objectInput(input.request, undefined);\n\t\t\t\t} catch {\n\t\t\t\t\treturn invalidInput(operationId, \"Test runner request is invalid\");\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tconst output = await runWithCancellation(\n\t\t\t\t\t\t() =>\n\t\t\t\t\t\t\truntime.run(\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tversion: 1,\n\t\t\t\t\t\t\t\t\toperationId,\n\t\t\t\t\t\t\t\t\tworkspaceRef: requestInput.workspaceRef as string,\n\t\t\t\t\t\t\t\t\tframework: requestInput.framework as \"vitest\" | \"jest\" | \"node-test\" | \"pytest\",\n\t\t\t\t\t\t\t\t\ttarget: requestInput.target as string,\n\t\t\t\t\t\t\t\t\t...(requestInput.timeoutMs === undefined\n\t\t\t\t\t\t\t\t\t\t? {}\n\t\t\t\t\t\t\t\t\t\t: { timeoutMs: requestInput.timeoutMs as number }),\n\t\t\t\t\t\t\t\t\t...(requestInput.maxFailures === undefined\n\t\t\t\t\t\t\t\t\t\t? {}\n\t\t\t\t\t\t\t\t\t\t: { maxFailures: requestInput.maxFailures as number }),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tsignal,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\tsignal,\n\t\t\t\t\t);\n\t\t\t\t\tif (output === CANCELLATION_V1) return cancelled(operationId);\n\t\t\t\t\treturn projectTestRunResultV1(output, operationId) ?? adapterFailure(operationId);\n\t\t\t\t} catch {\n\t\t\t\t\treturn adapterFailure(operationId);\n\t\t\t\t}\n\t\t\t},\n\t\t});\n\t};\n}\n"]}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Default-export factory consumed by the host's `loadCapabilityPluginFromManifest`.
3
+ *
4
+ * The adapter is deliberately constructed without host injection: command
5
+ * resolution and execution are host-owned per workspace (resolver + runner
6
+ * seams), so the registered `test_run` tool reports an explicit `unsupported`
7
+ * result until a host binds a real runner through its own profile machinery
8
+ * (fail-closed by design, same contract as the embedded build). Readonly hosts
9
+ * get no tool registration at all.
10
+ */
11
+ declare const _default: import("@agent-forge/plugin-sdk").CapabilityPluginSyncFactory;
12
+ export default _default;
13
+ //# sourceMappingURL=entry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry.d.ts","sourceRoot":"","sources":["../src/entry.ts"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;;AACH,wBAAoF","sourcesContent":["import { createTestRunnerCapabilityFactoryV1 } from \"./capability.ts\";\nimport { createNodeTestRunnerAdapterV1 } from \"./node-adapter.ts\";\n\n/**\n * Default-export factory consumed by the host's `loadCapabilityPluginFromManifest`.\n *\n * The adapter is deliberately constructed without host injection: command\n * resolution and execution are host-owned per workspace (resolver + runner\n * seams), so the registered `test_run` tool reports an explicit `unsupported`\n * result until a host binds a real runner through its own profile machinery\n * (fail-closed by design, same contract as the embedded build). Readonly hosts\n * get no tool registration at all.\n */\nexport default createTestRunnerCapabilityFactoryV1(createNodeTestRunnerAdapterV1());\n"]}
package/dist/entry.js ADDED
@@ -0,0 +1,14 @@
1
+ import { createTestRunnerCapabilityFactoryV1 } from "./capability.js";
2
+ import { createNodeTestRunnerAdapterV1 } from "./node-adapter.js";
3
+ /**
4
+ * Default-export factory consumed by the host's `loadCapabilityPluginFromManifest`.
5
+ *
6
+ * The adapter is deliberately constructed without host injection: command
7
+ * resolution and execution are host-owned per workspace (resolver + runner
8
+ * seams), so the registered `test_run` tool reports an explicit `unsupported`
9
+ * result until a host binds a real runner through its own profile machinery
10
+ * (fail-closed by design, same contract as the embedded build). Readonly hosts
11
+ * get no tool registration at all.
12
+ */
13
+ export default createTestRunnerCapabilityFactoryV1(createNodeTestRunnerAdapterV1());
14
+ //# sourceMappingURL=entry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry.js","sourceRoot":"","sources":["../src/entry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mCAAmC,EAAE,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAE,6BAA6B,EAAE,MAAM,mBAAmB,CAAC;AAElE;;;;;;;;;GASG;AACH,eAAe,mCAAmC,CAAC,6BAA6B,EAAE,CAAC,CAAC","sourcesContent":["import { createTestRunnerCapabilityFactoryV1 } from \"./capability.ts\";\nimport { createNodeTestRunnerAdapterV1 } from \"./node-adapter.ts\";\n\n/**\n * Default-export factory consumed by the host's `loadCapabilityPluginFromManifest`.\n *\n * The adapter is deliberately constructed without host injection: command\n * resolution and execution are host-owned per workspace (resolver + runner\n * seams), so the registered `test_run` tool reports an explicit `unsupported`\n * result until a host binds a real runner through its own profile machinery\n * (fail-closed by design, same contract as the embedded build). Readonly hosts\n * get no tool registration at all.\n */\nexport default createTestRunnerCapabilityFactoryV1(createNodeTestRunnerAdapterV1());\n"]}
@@ -0,0 +1,12 @@
1
+ export { createTestRunnerCapabilityFactoryV1 } from "./capability.ts";
2
+ export type { NodeTestRunnerAdapterOptionsV1 } from "./node-adapter.ts";
3
+ export { createDeterministicTestRunnerAdapterV1, createNodeTestRunnerAdapterV1, projectTestRunResultV1, sanitizeTestRunnerTextV1, TEST_RUNNER_MAX_FAILURE_MESSAGE_CHARS_V1, TEST_RUNNER_MAX_FAILURES_V1, TEST_RUNNER_MAX_OPAQUE_REF_CHARS_V1, } from "./node-adapter.ts";
4
+ export { parseJestJsonReportV1 } from "./node-jest-json-report.ts";
5
+ export { createJestRealTestRunnerAdapterV1, createJestTestRunnerCommandResolverV1, resolveJestEntryV1, } from "./node-jest-runner.ts";
6
+ export { parseJunitXmlReportV1 } from "./node-junit-xml-report.ts";
7
+ export { createPytestRealTestRunnerAdapterV1, createPytestTestRunnerCommandResolverV1, createPytestTestRunnerCommandRunnerV1, resolvePytestLauncherV1, } from "./node-pytest-runner.ts";
8
+ export { boundedNodeTestEnvV1, createJsonReportTestRunnerCommandRunnerV1 } from "./node-test-process.ts";
9
+ export { createVitestRealTestRunnerAdapterV1, createVitestTestRunnerCommandResolverV1, resolveVitestEntryV1, } from "./node-vitest-runner.ts";
10
+ export { sanitizeBinaryOutput } from "./shell-utils.ts";
11
+ export type * from "./types.ts";
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mCAAmC,EAAE,MAAM,iBAAiB,CAAC;AACtE,YAAY,EAAE,8BAA8B,EAAE,MAAM,mBAAmB,CAAC;AACxE,OAAO,EACN,sCAAsC,EACtC,6BAA6B,EAC7B,sBAAsB,EACtB,wBAAwB,EACxB,wCAAwC,EACxC,2BAA2B,EAC3B,mCAAmC,GACnC,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,EACN,iCAAiC,EACjC,qCAAqC,EACrC,kBAAkB,GAClB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,EACN,mCAAmC,EACnC,uCAAuC,EACvC,qCAAqC,EACrC,uBAAuB,GACvB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,oBAAoB,EAAE,yCAAyC,EAAE,MAAM,wBAAwB,CAAC;AACzG,OAAO,EACN,mCAAmC,EACnC,uCAAuC,EACvC,oBAAoB,GACpB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACxD,mBAAmB,YAAY,CAAC","sourcesContent":["export { createTestRunnerCapabilityFactoryV1 } from \"./capability.ts\";\nexport type { NodeTestRunnerAdapterOptionsV1 } from \"./node-adapter.ts\";\nexport {\n\tcreateDeterministicTestRunnerAdapterV1,\n\tcreateNodeTestRunnerAdapterV1,\n\tprojectTestRunResultV1,\n\tsanitizeTestRunnerTextV1,\n\tTEST_RUNNER_MAX_FAILURE_MESSAGE_CHARS_V1,\n\tTEST_RUNNER_MAX_FAILURES_V1,\n\tTEST_RUNNER_MAX_OPAQUE_REF_CHARS_V1,\n} from \"./node-adapter.ts\";\nexport { parseJestJsonReportV1 } from \"./node-jest-json-report.ts\";\nexport {\n\tcreateJestRealTestRunnerAdapterV1,\n\tcreateJestTestRunnerCommandResolverV1,\n\tresolveJestEntryV1,\n} from \"./node-jest-runner.ts\";\nexport { parseJunitXmlReportV1 } from \"./node-junit-xml-report.ts\";\nexport {\n\tcreatePytestRealTestRunnerAdapterV1,\n\tcreatePytestTestRunnerCommandResolverV1,\n\tcreatePytestTestRunnerCommandRunnerV1,\n\tresolvePytestLauncherV1,\n} from \"./node-pytest-runner.ts\";\nexport { boundedNodeTestEnvV1, createJsonReportTestRunnerCommandRunnerV1 } from \"./node-test-process.ts\";\nexport {\n\tcreateVitestRealTestRunnerAdapterV1,\n\tcreateVitestTestRunnerCommandResolverV1,\n\tresolveVitestEntryV1,\n} from \"./node-vitest-runner.ts\";\nexport { sanitizeBinaryOutput } from \"./shell-utils.ts\";\nexport type * from \"./types.ts\";\n"]}
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export { createTestRunnerCapabilityFactoryV1 } from "./capability.js";
2
+ export { createDeterministicTestRunnerAdapterV1, createNodeTestRunnerAdapterV1, projectTestRunResultV1, sanitizeTestRunnerTextV1, TEST_RUNNER_MAX_FAILURE_MESSAGE_CHARS_V1, TEST_RUNNER_MAX_FAILURES_V1, TEST_RUNNER_MAX_OPAQUE_REF_CHARS_V1, } from "./node-adapter.js";
3
+ export { parseJestJsonReportV1 } from "./node-jest-json-report.js";
4
+ export { createJestRealTestRunnerAdapterV1, createJestTestRunnerCommandResolverV1, resolveJestEntryV1, } from "./node-jest-runner.js";
5
+ export { parseJunitXmlReportV1 } from "./node-junit-xml-report.js";
6
+ export { createPytestRealTestRunnerAdapterV1, createPytestTestRunnerCommandResolverV1, createPytestTestRunnerCommandRunnerV1, resolvePytestLauncherV1, } from "./node-pytest-runner.js";
7
+ export { boundedNodeTestEnvV1, createJsonReportTestRunnerCommandRunnerV1 } from "./node-test-process.js";
8
+ export { createVitestRealTestRunnerAdapterV1, createVitestTestRunnerCommandResolverV1, resolveVitestEntryV1, } from "./node-vitest-runner.js";
9
+ export { sanitizeBinaryOutput } from "./shell-utils.js";
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mCAAmC,EAAE,MAAM,iBAAiB,CAAC;AAEtE,OAAO,EACN,sCAAsC,EACtC,6BAA6B,EAC7B,sBAAsB,EACtB,wBAAwB,EACxB,wCAAwC,EACxC,2BAA2B,EAC3B,mCAAmC,GACnC,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,EACN,iCAAiC,EACjC,qCAAqC,EACrC,kBAAkB,GAClB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,EACN,mCAAmC,EACnC,uCAAuC,EACvC,qCAAqC,EACrC,uBAAuB,GACvB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,oBAAoB,EAAE,yCAAyC,EAAE,MAAM,wBAAwB,CAAC;AACzG,OAAO,EACN,mCAAmC,EACnC,uCAAuC,EACvC,oBAAoB,GACpB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC","sourcesContent":["export { createTestRunnerCapabilityFactoryV1 } from \"./capability.ts\";\nexport type { NodeTestRunnerAdapterOptionsV1 } from \"./node-adapter.ts\";\nexport {\n\tcreateDeterministicTestRunnerAdapterV1,\n\tcreateNodeTestRunnerAdapterV1,\n\tprojectTestRunResultV1,\n\tsanitizeTestRunnerTextV1,\n\tTEST_RUNNER_MAX_FAILURE_MESSAGE_CHARS_V1,\n\tTEST_RUNNER_MAX_FAILURES_V1,\n\tTEST_RUNNER_MAX_OPAQUE_REF_CHARS_V1,\n} from \"./node-adapter.ts\";\nexport { parseJestJsonReportV1 } from \"./node-jest-json-report.ts\";\nexport {\n\tcreateJestRealTestRunnerAdapterV1,\n\tcreateJestTestRunnerCommandResolverV1,\n\tresolveJestEntryV1,\n} from \"./node-jest-runner.ts\";\nexport { parseJunitXmlReportV1 } from \"./node-junit-xml-report.ts\";\nexport {\n\tcreatePytestRealTestRunnerAdapterV1,\n\tcreatePytestTestRunnerCommandResolverV1,\n\tcreatePytestTestRunnerCommandRunnerV1,\n\tresolvePytestLauncherV1,\n} from \"./node-pytest-runner.ts\";\nexport { boundedNodeTestEnvV1, createJsonReportTestRunnerCommandRunnerV1 } from \"./node-test-process.ts\";\nexport {\n\tcreateVitestRealTestRunnerAdapterV1,\n\tcreateVitestTestRunnerCommandResolverV1,\n\tresolveVitestEntryV1,\n} from \"./node-vitest-runner.ts\";\nexport { sanitizeBinaryOutput } from \"./shell-utils.ts\";\nexport type * from \"./types.ts\";\n"]}
@@ -0,0 +1,24 @@
1
+ import { type TestRunnerAdapterV1, type TestRunnerCommandResolverV1, type TestRunnerCommandRunnerV1, type TestRunnerDeterministicFixtureV1, type TestRunResultV1 } from "./types.ts";
2
+ export declare const TEST_RUNNER_MAX_FAILURES_V1 = 64;
3
+ export declare const TEST_RUNNER_MAX_FAILURE_MESSAGE_CHARS_V1 = 512;
4
+ export declare const TEST_RUNNER_MAX_OPAQUE_REF_CHARS_V1 = 256;
5
+ export interface NodeTestRunnerAdapterOptionsV1 {
6
+ /** Resolves a request to host-owned command identity and argv. */
7
+ readonly commandResolver?: TestRunnerCommandResolverV1;
8
+ /** Runs a resolved command without accepting cwd, environment, or shell input. */
9
+ readonly runner?: TestRunnerCommandRunnerV1;
10
+ readonly defaultTimeoutMs?: number;
11
+ }
12
+ /** Removes credentials and absolute host paths before text crosses the model boundary. */
13
+ export declare function sanitizeTestRunnerTextV1(value: string, maximum?: number): string;
14
+ /** Strictly projects arbitrary adapter output into the bounded public test DTO. */
15
+ export declare function projectTestRunResultV1(value: unknown, operationId: string): TestRunResultV1 | undefined;
16
+ /**
17
+ * Creates a Node host adapter without a shell/cwd/environment surface. A host
18
+ * must inject both resolver and runner for real execution; the default is a
19
+ * deterministic `unsupported` result rather than a synthetic passing run.
20
+ */
21
+ export declare function createNodeTestRunnerAdapterV1(options?: NodeTestRunnerAdapterOptionsV1): TestRunnerAdapterV1;
22
+ /** Deterministic test-only fixture adapter; it never starts a process. */
23
+ export declare function createDeterministicTestRunnerAdapterV1(fixtures: readonly TestRunnerDeterministicFixtureV1[]): TestRunnerAdapterV1;
24
+ //# sourceMappingURL=node-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-adapter.d.ts","sourceRoot":"","sources":["../src/node-adapter.ts"],"names":[],"mappings":"AAGA,OAAO,EAIN,KAAK,mBAAmB,EACxB,KAAK,2BAA2B,EAEhC,KAAK,yBAAyB,EAC9B,KAAK,gCAAgC,EAErC,KAAK,eAAe,EAEpB,MAAM,YAAY,CAAC;AAUpB,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAC9C,eAAO,MAAM,wCAAwC,MAAM,CAAC;AAO5D,eAAO,MAAM,mCAAmC,MAAM,CAAC;AAOvD,MAAM,WAAW,8BAA8B;IAC9C,kEAAkE;IAClE,QAAQ,CAAC,eAAe,CAAC,EAAE,2BAA2B,CAAC;IACvD,kFAAkF;IAClF,QAAQ,CAAC,MAAM,CAAC,EAAE,yBAAyB,CAAC;IAC5C,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CACnC;AAkBD,0FAA0F;AAC1F,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,SAA2C,GAAG,MAAM,CASlH;AA0LD,mFAAmF;AACnF,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAwDvG;AA0HD;;;;GAIG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,GAAE,8BAAmC,GAAG,mBAAmB,CAqC/G;AAED,0EAA0E;AAC1E,wBAAgB,sCAAsC,CACrD,QAAQ,EAAE,SAAS,gCAAgC,EAAE,GACnD,mBAAmB,CA2BrB","sourcesContent":["import type { JsonValue } from \"@agent-forge/plugin-sdk\";\nimport { projectPluginContextV1, validatePluginErrorV1 } from \"@agent-forge/plugin-sdk\";\nimport { sanitizeBinaryOutput } from \"./shell-utils.ts\";\nimport {\n\tTEST_RUNNER_HOST_ADAPTER_ID_V1,\n\tTEST_RUNNER_HOST_ADAPTER_VERSION_V1,\n\ttype TestRunFailureV1,\n\ttype TestRunnerAdapterV1,\n\ttype TestRunnerCommandResolverV1,\n\ttype TestRunnerCommandResultV1,\n\ttype TestRunnerCommandRunnerV1,\n\ttype TestRunnerDeterministicFixtureV1,\n\ttype TestRunRequestV1,\n\ttype TestRunResultV1,\n\ttype TestRunSummaryV1,\n} from \"./types.ts\";\n\nconst DEFAULT_TIMEOUT_MS_V1 = 30_000;\nconst MAX_TIMEOUT_MS_V1 = 120_000;\nconst MAX_MAX_FAILURES_V1 = 1_000;\nconst MAX_COMMAND_ID_CHARS_V1 = 256;\nconst MAX_COMMAND_ARGS_V1 = 128;\nconst MAX_COMMAND_ARG_CHARS_V1 = 2048;\nconst MAX_TEST_COUNT_V1 = 10_000_000;\nconst MAX_HOST_FAILURES_V1 = 4096;\nexport const TEST_RUNNER_MAX_FAILURES_V1 = 64;\nexport const TEST_RUNNER_MAX_FAILURE_MESSAGE_CHARS_V1 = 512;\nconst MAX_HOST_FAILURE_MESSAGE_CHARS_V1 = 8192;\nconst MAX_FILE_IDENTIFIER_CHARS_V1 = 512;\nconst MAX_ERROR_CODE_CHARS_V1 = 128;\nconst MAX_ERROR_MESSAGE_CHARS_V1 = 512;\nconst MAX_OPERATION_ID_CHARS_V1 = 256;\nconst MAX_REPORT_ARTIFACT_REF_CHARS_V1 = 256;\nexport const TEST_RUNNER_MAX_OPAQUE_REF_CHARS_V1 = 256;\n\nconst FRAMEWORKS_V1 = new Set([\"vitest\", \"jest\", \"node-test\", \"pytest\"]);\nconst OPAQUE_REF_V1 = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/u;\nconst FILE_IDENTIFIER_V1 = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/u;\nconst SAFE_ERROR_CODE_V1 = /^[a-z][a-z0-9_]{0,127}$/u;\n\nexport interface NodeTestRunnerAdapterOptionsV1 {\n\t/** Resolves a request to host-owned command identity and argv. */\n\treadonly commandResolver?: TestRunnerCommandResolverV1;\n\t/** Runs a resolved command without accepting cwd, environment, or shell input. */\n\treadonly runner?: TestRunnerCommandRunnerV1;\n\treadonly defaultTimeoutMs?: number;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction hasOnlyKeys(value: Record<string, unknown>, allowed: readonly string[]): boolean {\n\treturn Object.keys(value).every((key) => allowed.includes(key));\n}\n\nfunction isSafeOpaqueRef(value: string): boolean {\n\treturn value.length <= TEST_RUNNER_MAX_OPAQUE_REF_CHARS_V1 && OPAQUE_REF_V1.test(value);\n}\n\nfunction isSafeFileIdentifier(value: string): boolean {\n\treturn FILE_IDENTIFIER_V1.test(value) && !value.split(\"/\").some((segment) => segment === \".\" || segment === \"..\");\n}\n\n/** Removes credentials and absolute host paths before text crosses the model boundary. */\nexport function sanitizeTestRunnerTextV1(value: string, maximum = TEST_RUNNER_MAX_FAILURE_MESSAGE_CHARS_V1): string {\n\tlet text = sanitizeBinaryOutput(value);\n\ttext = text\n\t\t.replace(\n\t\t\t/((?:api[-_ ]?key|access[-_ ]?token|auth(?:orization)?|bearer|password|passwd|secret|token)\\s*[:=]\\s*)([^\\s,;]+)/giu,\n\t\t\t\"$1[redacted-secret]\",\n\t\t)\n\t\t.replace(/(?:[A-Za-z]:[\\\\/]|\\\\\\\\|\\/(?:home|Users|private|tmp|var|workspace)\\/)[^\\s\"'<>]*/gu, \"[redacted-path]\");\n\treturn text.length <= maximum ? text : text.slice(0, maximum);\n}\n\nfunction validPositiveInteger(value: unknown, maximum: number): value is number {\n\treturn Number.isSafeInteger(value) && (value as number) >= 1 && (value as number) <= maximum;\n}\n\nfunction invalidInput(operationId: string, message: string): TestRunResultV1 {\n\treturn {\n\t\tversion: 1,\n\t\toperationId,\n\t\tstatus: \"failed\",\n\t\tsummary: emptySummary(),\n\t\tfailures: [],\n\t\tfailuresTruncated: false,\n\t\terror: { version: 1, code: \"invalid_input\", message },\n\t};\n}\n\nfunction adapterFailure(operationId: string): TestRunResultV1 {\n\treturn {\n\t\tversion: 1,\n\t\toperationId,\n\t\tstatus: \"failed\",\n\t\tsummary: emptySummary(),\n\t\tfailures: [],\n\t\tfailuresTruncated: false,\n\t\terror: { version: 1, code: \"adapter_failed\", message: \"Test runner adapter returned an invalid result\" },\n\t};\n}\n\nfunction unsupported(operationId: string): TestRunResultV1 {\n\treturn {\n\t\tversion: 1,\n\t\toperationId,\n\t\tstatus: \"unsupported\",\n\t\tsummary: emptySummary(),\n\t\tfailures: [],\n\t\tfailuresTruncated: false,\n\t\terror: { version: 1, code: \"test_runner_unavailable\", message: \"No test runner is bound by this host\" },\n\t};\n}\n\nfunction cancelled(operationId: string): TestRunResultV1 {\n\treturn {\n\t\tversion: 1,\n\t\toperationId,\n\t\tstatus: \"cancelled\",\n\t\tsummary: emptySummary(),\n\t\tfailures: [],\n\t\tfailuresTruncated: false,\n\t\terror: { version: 1, code: \"cancelled\", message: \"Test run was cancelled\" },\n\t};\n}\n\nfunction timedOut(operationId: string): TestRunResultV1 {\n\treturn {\n\t\tversion: 1,\n\t\toperationId,\n\t\tstatus: \"failed\",\n\t\tsummary: emptySummary(),\n\t\tfailures: [],\n\t\tfailuresTruncated: false,\n\t\terror: { version: 1, code: \"timeout\", message: \"Test run timed out\" },\n\t};\n}\n\nfunction emptySummary(): TestRunSummaryV1 {\n\treturn { total: 0, passed: 0, failed: 0, skipped: 0, flaky: 0 };\n}\n\nfunction validateRequest(request: TestRunRequestV1): string | undefined {\n\tif (request.version !== 1) return \"version must be 1\";\n\tif (\n\t\ttypeof request.operationId !== \"string\" ||\n\t\trequest.operationId.trim() === \"\" ||\n\t\trequest.operationId.length > MAX_OPERATION_ID_CHARS_V1\n\t)\n\t\treturn \"operationId is invalid\";\n\tif (typeof request.workspaceRef !== \"string\" || !isSafeOpaqueRef(request.workspaceRef))\n\t\treturn \"workspaceRef must be a safe opaque host handle\";\n\tif (typeof request.framework !== \"string\" || !FRAMEWORKS_V1.has(request.framework)) return \"framework is invalid\";\n\tif (\n\t\ttypeof request.target !== \"string\" ||\n\t\trequest.target.trim() === \"\" ||\n\t\trequest.target.length > 1024 ||\n\t\t/[\\u0000\\r\\n]/u.test(request.target)\n\t)\n\t\treturn \"target is invalid\";\n\tif (request.timeoutMs !== undefined && !validPositiveInteger(request.timeoutMs, MAX_TIMEOUT_MS_V1))\n\t\treturn `timeoutMs must be a positive integer at most ${MAX_TIMEOUT_MS_V1}`;\n\tif (request.maxFailures !== undefined && !validPositiveInteger(request.maxFailures, MAX_MAX_FAILURES_V1))\n\t\treturn `maxFailures must be a positive integer at most ${MAX_MAX_FAILURES_V1}`;\n\treturn undefined;\n}\n\nfunction validCommand(value: unknown): value is { readonly commandId: string; readonly args: readonly string[] } {\n\tif (!isRecord(value) || !hasOnlyKeys(value, [\"commandId\", \"args\"])) return false;\n\treturn (\n\t\ttypeof value.commandId === \"string\" &&\n\t\tvalue.commandId.length > 0 &&\n\t\tvalue.commandId.length <= MAX_COMMAND_ID_CHARS_V1 &&\n\t\tisSafeOpaqueRef(value.commandId) &&\n\t\tArray.isArray(value.args) &&\n\t\tvalue.args.length <= MAX_COMMAND_ARGS_V1 &&\n\t\tvalue.args.every(\n\t\t\t(arg) => typeof arg === \"string\" && arg.length <= MAX_COMMAND_ARG_CHARS_V1 && !/[\\u0000\\r\\n]/u.test(arg),\n\t\t)\n\t);\n}\n\nfunction validateSummary(value: unknown): TestRunSummaryV1 | undefined {\n\tif (!isRecord(value) || !hasOnlyKeys(value, [\"total\", \"passed\", \"failed\", \"skipped\", \"flaky\"])) return undefined;\n\tconst total = value.total;\n\tconst passed = value.passed;\n\tconst failed = value.failed;\n\tconst skipped = value.skipped;\n\tconst flaky = value.flaky;\n\tconst isCount = (item: unknown): item is number => Number.isSafeInteger(item) && (item as number) >= 0;\n\tif (\n\t\t!isCount(total) ||\n\t\t!isCount(passed) ||\n\t\t!isCount(failed) ||\n\t\t!isCount(skipped) ||\n\t\t!isCount(flaky) ||\n\t\ttotal > MAX_TEST_COUNT_V1 ||\n\t\tpassed > MAX_TEST_COUNT_V1 ||\n\t\tfailed > MAX_TEST_COUNT_V1 ||\n\t\tskipped > MAX_TEST_COUNT_V1 ||\n\t\tflaky > MAX_TEST_COUNT_V1 ||\n\t\tpassed + failed + skipped > total ||\n\t\tflaky > total\n\t)\n\t\treturn undefined;\n\treturn {\n\t\ttotal,\n\t\tpassed,\n\t\tfailed,\n\t\tskipped,\n\t\tflaky,\n\t};\n}\n\nfunction projectFailure(value: unknown): TestRunFailureV1 | undefined {\n\tif (!isRecord(value) || !hasOnlyKeys(value, [\"file\", \"line\", \"column\", \"message\"])) return undefined;\n\tif (\n\t\ttypeof value.file !== \"string\" ||\n\t\tvalue.file.length > MAX_FILE_IDENTIFIER_CHARS_V1 ||\n\t\t!isSafeFileIdentifier(value.file) ||\n\t\ttypeof value.message !== \"string\" ||\n\t\tvalue.message.trim() === \"\" ||\n\t\tvalue.message.length > MAX_HOST_FAILURE_MESSAGE_CHARS_V1 ||\n\t\t(value.line !== undefined && !validPositiveInteger(value.line, MAX_TEST_COUNT_V1)) ||\n\t\t(value.column !== undefined && !validPositiveInteger(value.column, MAX_TEST_COUNT_V1))\n\t)\n\t\treturn undefined;\n\treturn {\n\t\tfile: value.file,\n\t\t...(value.line === undefined ? {} : { line: value.line }),\n\t\t...(value.column === undefined ? {} : { column: value.column }),\n\t\tmessage: sanitizeTestRunnerTextV1(value.message),\n\t};\n}\n\nfunction projectError(value: unknown): TestRunResultV1[\"error\"] | undefined {\n\tif (!isRecord(value) || !hasOnlyKeys(value, [\"version\", \"code\", \"message\", \"causeCode\"])) return undefined;\n\ttry {\n\t\tconst error = validatePluginErrorV1(value, \"Test runner error\");\n\t\tif (\n\t\t\terror.code.length > MAX_ERROR_CODE_CHARS_V1 ||\n\t\t\t!SAFE_ERROR_CODE_V1.test(error.code) ||\n\t\t\terror.message.length > MAX_HOST_FAILURE_MESSAGE_CHARS_V1 ||\n\t\t\t(error.causeCode !== undefined &&\n\t\t\t\t(error.causeCode.length > MAX_ERROR_CODE_CHARS_V1 || !SAFE_ERROR_CODE_V1.test(error.causeCode)))\n\t\t)\n\t\t\treturn undefined;\n\t\treturn {\n\t\t\tversion: 1,\n\t\t\tcode: error.code,\n\t\t\tmessage: sanitizeTestRunnerTextV1(error.message, MAX_ERROR_MESSAGE_CHARS_V1),\n\t\t\t...(error.causeCode === undefined ? {} : { causeCode: error.causeCode }),\n\t\t};\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** Strictly projects arbitrary adapter output into the bounded public test DTO. */\nexport function projectTestRunResultV1(value: unknown, operationId: string): TestRunResultV1 | undefined {\n\tif (\n\t\t!isRecord(value) ||\n\t\t!hasOnlyKeys(value, [\n\t\t\t\"version\",\n\t\t\t\"operationId\",\n\t\t\t\"status\",\n\t\t\t\"summary\",\n\t\t\t\"failures\",\n\t\t\t\"failuresTruncated\",\n\t\t\t\"exitCode\",\n\t\t\t\"reportArtifactRef\",\n\t\t\t\"error\",\n\t\t]) ||\n\t\tvalue.version !== 1 ||\n\t\tvalue.operationId !== operationId ||\n\t\ttypeof value.status !== \"string\" ||\n\t\t![\"succeeded\", \"failed\", \"cancelled\", \"needs_user\", \"unsupported\"].includes(value.status) ||\n\t\t!Array.isArray(value.failures) ||\n\t\tvalue.failures.length > TEST_RUNNER_MAX_FAILURES_V1 ||\n\t\ttypeof value.failuresTruncated !== \"boolean\"\n\t)\n\t\treturn undefined;\n\tconst summary = validateSummary(value.summary);\n\tif (summary === undefined) return undefined;\n\tconst failures = value.failures.map(projectFailure);\n\tif (failures.some((failure) => failure === undefined)) return undefined;\n\tconst exitCode = value.exitCode;\n\tif (\n\t\texitCode !== undefined &&\n\t\t(typeof exitCode !== \"number\" || !Number.isSafeInteger(exitCode) || exitCode < 0 || exitCode > 255)\n\t)\n\t\treturn undefined;\n\tif (\n\t\tvalue.reportArtifactRef !== undefined &&\n\t\t(typeof value.reportArtifactRef !== \"string\" ||\n\t\t\tvalue.reportArtifactRef.length > MAX_REPORT_ARTIFACT_REF_CHARS_V1 ||\n\t\t\t!isSafeOpaqueRef(value.reportArtifactRef))\n\t)\n\t\treturn undefined;\n\tconst error = value.error === undefined ? undefined : projectError(value.error);\n\tif ((value.status === \"succeeded\" && error !== undefined) || (value.status !== \"succeeded\" && error === undefined))\n\t\treturn undefined;\n\tif (value.status === \"succeeded\" && (summary.failed !== 0 || exitCode !== 0)) return undefined;\n\tconst projected: TestRunResultV1 = {\n\t\tversion: 1,\n\t\toperationId,\n\t\tstatus: value.status as TestRunResultV1[\"status\"],\n\t\tsummary,\n\t\tfailures: failures as TestRunFailureV1[],\n\t\tfailuresTruncated: value.failuresTruncated,\n\t\t...(exitCode === undefined ? {} : { exitCode }),\n\t\t...(value.reportArtifactRef === undefined ? {} : { reportArtifactRef: value.reportArtifactRef }),\n\t\t...(error === undefined ? {} : { error }),\n\t};\n\treturn contextBoundResult(projected) ?? adapterFailure(operationId);\n}\n\nfunction completeResult(\n\toperationId: string,\n\tvalue: TestRunnerCommandResultV1,\n\tmaxFailures: number,\n): TestRunResultV1 | undefined {\n\tif (!isRecord(value) || !hasOnlyKeys(value, [\"status\", \"exitCode\", \"summary\", \"failures\", \"reportArtifactRef\"]))\n\t\treturn undefined;\n\tif (value.status === \"unsupported\") return unsupported(operationId);\n\tif (value.status !== \"completed\") return undefined;\n\tif (!Number.isSafeInteger(value.exitCode) || value.exitCode < 0 || value.exitCode > 255) return undefined;\n\tconst summary = validateSummary(value.summary);\n\tif (summary === undefined) return undefined;\n\tif (value.failures !== undefined && (!Array.isArray(value.failures) || value.failures.length > MAX_HOST_FAILURES_V1))\n\t\treturn undefined;\n\tconst rawFailures = value.failures ?? [];\n\tif (rawFailures.length > summary.failed) return undefined;\n\tconst failureLimit = Math.min(maxFailures, TEST_RUNNER_MAX_FAILURES_V1);\n\tconst failures = rawFailures.slice(0, failureLimit).map(projectFailure);\n\tif (failures.some((failure) => failure === undefined)) return undefined;\n\tif (\n\t\tvalue.reportArtifactRef !== undefined &&\n\t\t(typeof value.reportArtifactRef !== \"string\" ||\n\t\t\tvalue.reportArtifactRef.length > MAX_REPORT_ARTIFACT_REF_CHARS_V1 ||\n\t\t\t!isSafeOpaqueRef(value.reportArtifactRef))\n\t)\n\t\treturn undefined;\n\tconst succeeded = value.exitCode === 0 && summary.failed === 0;\n\tconst projected: TestRunResultV1 = {\n\t\tversion: 1,\n\t\toperationId,\n\t\tstatus: succeeded ? \"succeeded\" : \"failed\",\n\t\tsummary,\n\t\tfailures: failures as TestRunFailureV1[],\n\t\tfailuresTruncated: rawFailures.length > failureLimit || summary.failed > failures.length,\n\t\texitCode: value.exitCode,\n\t\t...(value.reportArtifactRef === undefined ? {} : { reportArtifactRef: value.reportArtifactRef }),\n\t\t...(succeeded\n\t\t\t? {}\n\t\t\t: {\n\t\t\t\t\terror: {\n\t\t\t\t\t\tversion: 1 as const,\n\t\t\t\t\t\tcode: summary.failed > 0 ? \"test_failures\" : \"exit_nonzero\",\n\t\t\t\t\t\tmessage:\n\t\t\t\t\t\t\tsummary.failed > 0 ? \"One or more tests failed\" : \"Test runner exited with a non-zero status\",\n\t\t\t\t\t},\n\t\t\t\t}),\n\t};\n\treturn contextBoundResult(projected) ?? adapterFailure(operationId);\n}\n\nfunction contextBoundResult(result: TestRunResultV1): TestRunResultV1 | undefined {\n\tconst outputArtifactRef = result.reportArtifactRef;\n\tfor (let failureCount = result.failures.length; failureCount >= 0; failureCount--) {\n\t\tconst failures = result.failures.slice(0, failureCount);\n\t\tconst projection = projectPluginContextV1(\n\t\t\t{\n\t\t\t\tversion: 1,\n\t\t\t\toperationId: result.operationId,\n\t\t\t\titems: [\n\t\t\t\t\t{ ...result.summary } as unknown as JsonValue,\n\t\t\t\t\t...failures.map((failure) => ({ ...failure }) as unknown as JsonValue),\n\t\t\t\t],\n\t\t\t\t...(outputArtifactRef === undefined ? {} : { outputArtifactRef }),\n\t\t\t\ttruncated: result.failuresTruncated || failureCount < result.failures.length,\n\t\t\t},\n\t\t\t{ maxOutputBytes: 64 * 1024, maxInlineItems: 65, maxCellChars: 512 },\n\t\t);\n\t\tif (projection.status === \"succeeded\") {\n\t\t\tconst candidate: TestRunResultV1 = {\n\t\t\t\t...result,\n\t\t\t\tfailures,\n\t\t\t\tfailuresTruncated: result.failuresTruncated || failureCount < result.failures.length,\n\t\t\t};\n\t\t\tif (new TextEncoder().encode(JSON.stringify(candidate)).byteLength <= 64 * 1024) return candidate;\n\t\t}\n\t}\n\treturn undefined;\n}\n\nconst CANCELLATION_V1 = Symbol(\"test-runner-cancelled\");\nconst TIMEOUT_V1 = Symbol(\"test-runner-timeout\");\n\nasync function runBounded(\n\trunner: TestRunnerCommandRunnerV1,\n\tcommand: { readonly commandId: string; readonly args: readonly string[] },\n\ttimeoutMs: number,\n\tsignal: AbortSignal | undefined,\n): Promise<TestRunnerCommandResultV1 | typeof CANCELLATION_V1 | typeof TIMEOUT_V1 | undefined> {\n\tconst controller = new AbortController();\n\tlet stop: typeof CANCELLATION_V1 | typeof TIMEOUT_V1 | undefined;\n\tconst abort = () => {\n\t\tstop ??= CANCELLATION_V1;\n\t\tcontroller.abort();\n\t};\n\tif (signal?.aborted) return CANCELLATION_V1;\n\tsignal?.addEventListener(\"abort\", abort, { once: true });\n\tconst timeout = setTimeout(() => {\n\t\tstop ??= TIMEOUT_V1;\n\t\tcontroller.abort();\n\t}, timeoutMs);\n\tconst completed = Promise.resolve()\n\t\t.then(() => runner.run({ command, timeoutMs, signal: controller.signal }))\n\t\t.then(\n\t\t\t(value) => value,\n\t\t\t() => undefined,\n\t\t);\n\tconst stopped = new Promise<typeof CANCELLATION_V1 | typeof TIMEOUT_V1>((resolve) => {\n\t\tconst observe = () => {\n\t\t\tif (stop !== undefined) resolve(stop);\n\t\t};\n\t\tcontroller.signal.addEventListener(\"abort\", observe, { once: true });\n\t});\n\ttry {\n\t\treturn await Promise.race([completed, stopped]);\n\t} finally {\n\t\tclearTimeout(timeout);\n\t\tsignal?.removeEventListener(\"abort\", abort);\n\t}\n}\n\n/**\n * Creates a Node host adapter without a shell/cwd/environment surface. A host\n * must inject both resolver and runner for real execution; the default is a\n * deterministic `unsupported` result rather than a synthetic passing run.\n */\nexport function createNodeTestRunnerAdapterV1(options: NodeTestRunnerAdapterOptionsV1 = {}): TestRunnerAdapterV1 {\n\tconst defaultTimeoutMs = options.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS_V1;\n\tif (!validPositiveInteger(defaultTimeoutMs, MAX_TIMEOUT_MS_V1))\n\t\tthrow new Error(`defaultTimeoutMs must be a positive integer at most ${MAX_TIMEOUT_MS_V1}`);\n\tconst resolver = options.commandResolver;\n\tconst runner = options.runner;\n\treturn {\n\t\tadapterId: TEST_RUNNER_HOST_ADAPTER_ID_V1,\n\t\tadapterVersion: TEST_RUNNER_HOST_ADAPTER_VERSION_V1,\n\t\tasync run(request, signal) {\n\t\t\tconst operationId =\n\t\t\t\ttypeof request.operationId === \"string\" && request.operationId.trim() !== \"\"\n\t\t\t\t\t? request.operationId\n\t\t\t\t\t: \"invalid-test-operation\";\n\t\t\tconst invalid = validateRequest(request);\n\t\t\tif (invalid !== undefined) return invalidInput(operationId, invalid);\n\t\t\tif (signal?.aborted) return cancelled(operationId);\n\t\t\tif (resolver === undefined || runner === undefined) return unsupported(operationId);\n\t\t\tlet command: Awaited<ReturnType<TestRunnerCommandResolverV1[\"resolve\"]>>;\n\t\t\ttry {\n\t\t\t\tcommand = await resolver.resolve(request, signal);\n\t\t\t} catch {\n\t\t\t\treturn adapterFailure(operationId);\n\t\t\t}\n\t\t\tif (signal?.aborted) return cancelled(operationId);\n\t\t\tif (command === undefined) return unsupported(operationId);\n\t\t\tif (!validCommand(command)) return adapterFailure(operationId);\n\t\t\tconst rawResult = await runBounded(runner, command, request.timeoutMs ?? defaultTimeoutMs, signal);\n\t\t\tif (rawResult === CANCELLATION_V1) return cancelled(operationId);\n\t\t\tif (rawResult === TIMEOUT_V1) return timedOut(operationId);\n\t\t\tif (rawResult === undefined) return adapterFailure(operationId);\n\t\t\treturn (\n\t\t\t\tcompleteResult(operationId, rawResult, request.maxFailures ?? TEST_RUNNER_MAX_FAILURES_V1) ??\n\t\t\t\tadapterFailure(operationId)\n\t\t\t);\n\t\t},\n\t};\n}\n\n/** Deterministic test-only fixture adapter; it never starts a process. */\nexport function createDeterministicTestRunnerAdapterV1(\n\tfixtures: readonly TestRunnerDeterministicFixtureV1[],\n): TestRunnerAdapterV1 {\n\tconst fixtureByKey = new Map<\n\t\tstring,\n\t\t{ readonly commandId: string; readonly fixture: TestRunnerDeterministicFixtureV1 }\n\t>();\n\tfor (const [index, fixture] of fixtures.entries()) {\n\t\tconst key = `${fixture.workspaceRef}\\u0000${fixture.framework}\\u0000${fixture.target}`;\n\t\tif (fixtureByKey.has(key)) throw new Error(\"Deterministic test runner fixtures must be unique\");\n\t\tfixtureByKey.set(key, { commandId: `fixture-${index + 1}`, fixture });\n\t}\n\treturn createNodeTestRunnerAdapterV1({\n\t\tcommandResolver: {\n\t\t\tasync resolve(request) {\n\t\t\t\tconst key = `${request.workspaceRef}\\u0000${request.framework}\\u0000${request.target}`;\n\t\t\t\tconst fixture = fixtureByKey.get(key);\n\t\t\t\treturn fixture === undefined ? undefined : { commandId: fixture.commandId, args: [] };\n\t\t\t},\n\t\t},\n\t\trunner: {\n\t\t\tasync run(input) {\n\t\t\t\tconst fixture = [...fixtureByKey.values()].find(\n\t\t\t\t\t(candidate) => candidate.commandId === input.command.commandId,\n\t\t\t\t);\n\t\t\t\treturn fixture?.fixture.result ?? { status: \"unsupported\" };\n\t\t\t},\n\t\t},\n\t});\n}\n"]}