From f476e4675edbe80f73a52c09fc08e90c22e5f98a Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Thu, 6 Aug 2026 20:30:10 -0400 Subject: [PATCH 1/5] fix: preserve dependency overlays in projected runtimes Fixes #2232. AI assistance: OpenAI GPT-5.6 Sol via OpenCode reviewed and validated the candidate implementation, then ran the build and targeted integration tests. Chris Huber remains responsible for every line. --- package.json | 1 + .../src/recipe-source-packages.ts | 5 + ...rlay-projected-runtime.integration.test.ts | 117 ++++++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 tests/composer-overlay-projected-runtime.integration.test.ts diff --git a/package.json b/package.json index 4a2e2715..7b3835fa 100644 --- a/package.json +++ b/package.json @@ -272,6 +272,7 @@ "test:runtime-overlay-descriptors": "tsx tests/runtime-overlay-descriptors.test.ts", "test:composer-package-overlay-revision": "tsx scripts/composer-backed-source-hydration-smoke.ts", "test:composer-package-overlay-autoload-layout": "tsx scripts/composer-package-overlay-autoload-layout-smoke.ts", + "test:composer-overlay-projected-runtime": "tsx tests/composer-overlay-projected-runtime.integration.test.ts", "test:composer-installed-versions-loader-order": "tsx scripts/composer-installed-versions-loader-order-smoke.ts", "test:recipe-extra-plugin-composer-autoloaders": "tsx tests/recipe-extra-plugin-composer-autoloaders.test.ts", "test:recipe-extra-plugin-local-zip": "tsx tests/recipe-extra-plugin-local-zip.test.ts", diff --git a/packages/runtime-core/src/recipe-source-packages.ts b/packages/runtime-core/src/recipe-source-packages.ts index f01c5190..09d1979d 100644 --- a/packages/runtime-core/src/recipe-source-packages.ts +++ b/packages/runtime-core/src/recipe-source-packages.ts @@ -208,6 +208,11 @@ export function prepareRecipeSourcePackageSync(options: PreparedRecipeSourcePack mkdirSync(preparedSource, { recursive: true }) } const originalPluginSource = join(copySource, sourceSubpath) + if (pathExists(join(originalPluginSource, "vendor", "autoload.php"))) { + preserveExistingComposerVendor(originalPluginSource, preparedPluginSource) + bridgePackageAutoloaderToComposerAutoload(preparedPluginSource) + return preparedPluginSource + } if (!pathExists(join(preparedPluginSource, "composer.json"))) { preserveExistingComposerVendor(originalPluginSource, preparedPluginSource) return preparedPluginSource diff --git a/tests/composer-overlay-projected-runtime.integration.test.ts b/tests/composer-overlay-projected-runtime.integration.test.ts new file mode 100644 index 00000000..961bb1f2 --- /dev/null +++ b/tests/composer-overlay-projected-runtime.integration.test.ts @@ -0,0 +1,117 @@ +import assert from "node:assert/strict" +import { execFile as execFileCallback } from "node:child_process" +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises" +import { tmpdir } from "node:os" +import { join } from "node:path" +import { promisify } from "node:util" + +import { buildAgentTaskRecipe } from "../packages/runtime-core/src/agent-task-recipe.js" +import { normalizeTaskInput } from "../packages/runtime-core/src/task-input.js" + +const execFile = promisify(execFileCallback) +const root = await mkdtemp(join(tmpdir(), "wp-codebox-composer-overlay-projected-runtime-")) +const consumer = join(root, "consumer-plugin") +const overlay = join(root, "overlay-package") +const artifacts = join(root, "artifacts") +const reference = "0123456789abcdef0123456789abcdef01234567" + +async function writePackage(source: string, marker: string): Promise { + await mkdir(join(source, "src"), { recursive: true }) + await mkdir(join(source, "website", "js"), { recursive: true }) + await writeFile(join(source, "composer.json"), JSON.stringify({ + name: "acme/asset", + autoload: { "psr-4": { "Acme\\Asset\\": "src/" } }, + })) + await writeFile(join(source, "src", "Asset.php"), `\n') + await writeFile(join(source, "website", "js", "site.js"), `${marker}\n`) +} + +try { + await writePackage(join(consumer, "vendor", "acme", "asset"), "base-package") + await mkdir(join(consumer, "vendor", "composer"), { recursive: true }) + await writeFile(join(consumer, "composer.json"), JSON.stringify({ name: "acme/consumer-plugin" })) + await writeFile(join(consumer, "vendor", "composer", "installed.json"), JSON.stringify({ packages: [{ + name: "acme/asset", + "install-path": "../acme/asset", + autoload: { "psr-4": { "Acme\\Asset\\": "src/" } }, + }] })) + await writeFile(join(consumer, "vendor", "composer", "autoload_psr4.php"), ` array($vendorDir . '/acme/asset/src')); +`) + await writeFile(join(consumer, "vendor", "autoload.php"), ` $directories) { + if (!str_starts_with($class, $prefix)) continue; + $relative = str_replace('\\\\', '/', substr($class, strlen($prefix))) . '.php'; + foreach ($directories as $directory) { + $file = $directory . '/' . $relative; + if (is_file($file)) { require_once $file; return; } + } + } +}); +`) + await writeFile(join(consumer, "consumer-plugin.php"), ` } + const execution = output.executions?.filter((candidate) => candidate.command === "wordpress.run-php").at(-1) + assert.equal(execution?.stdout?.trim(), "selected-overlay", "the projected Playground runtime must execute the selected dependency overlay") + + const projectedVendor = await readFile(join(artifacts, "prepared-plugins", "consumer-plugin", "vendor", "autoload.php"), "utf8") + assert.match(projectedVendor, /autoload_psr4/, "component projection preserves the hydrated Composer implementation") +} finally { + await rm(root, { recursive: true, force: true }) +} + +console.log("composer overlay projected runtime: ok") From c6ca7782f49c73b5bea03fdd2d1d62e3be2d2bfa Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Fri, 7 Aug 2026 05:01:28 -0400 Subject: [PATCH 2/5] fix(playground): default managed database diagnostics The startup diagnostic now uses the managed MySQL defaults when a service exports only its endpoint. AI assistance: OpenAI GPT-5.6 Sol via OpenCode inspected failed CI logs, identified the managed database diagnostic defaulting defect, implemented the focused correction, and ran targeted contract and overlay validation. Chris Huber remains responsible for every line. --- packages/runtime-playground/src/playground-cli-runner.ts | 2 +- tests/playground-cli-runner-bootstrap-ini.test.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/runtime-playground/src/playground-cli-runner.ts b/packages/runtime-playground/src/playground-cli-runner.ts index 3efe0be1..216664f3 100644 --- a/packages/runtime-playground/src/playground-cli-runner.ts +++ b/packages/runtime-playground/src/playground-cli-runner.ts @@ -507,7 +507,7 @@ if ($wpcb_db_endpoint['host_class'] === 'absent' || !$wpcb_db_endpoint['port'][' if (!$wpcb_db_diagnostic['transport']['stream_socket_client'] || !$wpcb_db_diagnostic['transport']['mysqli']) $wpcb_db_fail('transport_unavailable', 'Use a runtime with TCP sockets and the mysqli extension enabled.'); $wpcb_db_target = strpos($wpcb_db_host, ':') !== false ? 'tcp://[' . $wpcb_db_host . ']:' . $wpcb_db_port : 'tcp://' . $wpcb_db_host . ':' . $wpcb_db_port; $wpcb_db_diagnostic['tcp']['attempted'] = true; $wpcb_db_socket = @stream_socket_client($wpcb_db_target, $wpcb_db_tcp_errno, $wpcb_db_tcp_error, 2, STREAM_CLIENT_CONNECT); if (!$wpcb_db_socket) { $wpcb_db_diagnostic['tcp']['error_code'] = (int) $wpcb_db_tcp_errno; $wpcb_db_fail('endpoint_unreachable', 'Verify runtime network access to the managed database endpoint.'); } fclose($wpcb_db_socket); $wpcb_db_diagnostic['tcp']['connected'] = true; -$wpcb_db_diagnostic['mysqli']['attempted'] = true; $wpcb_db = @mysqli_init(); if (!$wpcb_db) $wpcb_db_fail('transport_unavailable', 'The mysqli client could not initialize in this runtime.'); @mysqli_options($wpcb_db, MYSQLI_OPT_CONNECT_TIMEOUT, 2); $wpcb_db_connected = @mysqli_real_connect($wpcb_db, $wpcb_db_host, getenv('DB_USER'), getenv('DB_PASSWORD'), getenv('DB_NAME'), (int) $wpcb_db_port); if (!$wpcb_db_connected) { $wpcb_db_errno = (int) mysqli_connect_errno(); $wpcb_db_diagnostic['mysqli']['error_code'] = $wpcb_db_errno; $wpcb_db_fail($wpcb_db_errno === 1045 ? 'authentication_failed' : ($wpcb_db_errno === 1049 ? 'database_missing' : 'endpoint_unreachable'), 'Verify the managed database credentials and selected database.'); } mysqli_close($wpcb_db); $wpcb_db_diagnostic['mysqli']['connected'] = true; +$wpcb_db_diagnostic['mysqli']['attempted'] = true; $wpcb_db = @mysqli_init(); if (!$wpcb_db) $wpcb_db_fail('transport_unavailable', 'The mysqli client could not initialize in this runtime.'); @mysqli_options($wpcb_db, MYSQLI_OPT_CONNECT_TIMEOUT, 2); $wpcb_db_user = getenv('DB_USER') ?: 'root'; $wpcb_db_name = getenv('DB_NAME') ?: 'runtime'; $wpcb_db_connected = @mysqli_real_connect($wpcb_db, $wpcb_db_host, $wpcb_db_user, getenv('DB_PASSWORD'), $wpcb_db_name, (int) $wpcb_db_port); if (!$wpcb_db_connected) { $wpcb_db_errno = (int) mysqli_connect_errno(); $wpcb_db_diagnostic['mysqli']['error_code'] = $wpcb_db_errno; $wpcb_db_fail($wpcb_db_errno === 1045 ? 'authentication_failed' : ($wpcb_db_errno === 1049 ? 'database_missing' : 'endpoint_unreachable'), 'Verify the managed database credentials and selected database.'); } mysqli_close($wpcb_db); $wpcb_db_diagnostic['mysqli']['connected'] = true; ` } diff --git a/tests/playground-cli-runner-bootstrap-ini.test.ts b/tests/playground-cli-runner-bootstrap-ini.test.ts index 4464181b..0b8b69d7 100644 --- a/tests/playground-cli-runner-bootstrap-ini.test.ts +++ b/tests/playground-cli-runner-bootstrap-ini.test.ts @@ -116,7 +116,9 @@ try { const sharedAutoPrepend = await readFile(sharedAutoPrependPath as string, "utf8") assert.match(sharedAutoPrepend, /require_once '\/internal\/shared\/auto_prepend_file\.php'/) assert.match(sharedAutoPrepend, /putenv\("TC_MYSQL_PORT=33060"\);/) - assert.doesNotMatch(sharedAutoPrepend, /secret|DB_PASSWORD/) + assert.match(sharedAutoPrepend, /getenv\('DB_USER'\) \?: 'root'/) + assert.match(sharedAutoPrepend, /getenv\('DB_NAME'\) \?: 'runtime'/) + assert.doesNotMatch(sharedAutoPrepend, /secret/) assert.equal(runs[0]?.env?.DB_PASSWORD, undefined) const requestWorkerPath = calls[0]["mount-before-install"]?.[3]?.hostPath assert.equal(typeof requestWorkerPath, "string") From f3ab58e3839329cfae93de8720e9280411b609ff Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Fri, 7 Aug 2026 05:06:53 -0400 Subject: [PATCH 3/5] test: use the recipe PHP version field The MariaDB integration recipe now uses the schema-supported runtime.phpVersion property. AI assistance: OpenAI GPT-5.6 Sol via OpenCode inspected the failed contracts logs, identified the invalid recipe fixture field, and applied the schema-aligned correction. Chris Huber remains responsible for every line. --- tests/disposable-mysql-mysqli.integration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/disposable-mysql-mysqli.integration.test.ts b/tests/disposable-mysql-mysqli.integration.test.ts index a158cd00..9f0ff773 100644 --- a/tests/disposable-mysql-mysqli.integration.test.ts +++ b/tests/disposable-mysql-mysqli.integration.test.ts @@ -49,7 +49,7 @@ if (!await dockerAvailable()) { const mariaDbCode = "if (!function_exists('mysqli_init')) { throw new RuntimeException('mysqli is unavailable'); } mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); $connect = static function (string $port): mysqli { $db = mysqli_init(); if (!mysqli_real_connect($db, getenv('DB_HOST'), getenv('DB_USER'), getenv('DB_PASSWORD'), getenv('DB_NAME'), (int) $port)) { throw new RuntimeException(mysqli_connect_error()); } return $db; }; $db = $connect((string) getenv('DB_PORT')); $compatibility = $connect((string) getenv('TC_MYSQL_PORT')); mysqli_query($db, 'CREATE TABLE mariadb_bridge (id INT PRIMARY KEY, value VARCHAR(32) NOT NULL) ENGINE=InnoDB'); mysqli_query($db, \"INSERT INTO mariadb_bridge (id, value) VALUES (1, 'reachable')\"); $row = mysqli_fetch_assoc(mysqli_query($compatibility, 'SELECT value FROM mariadb_bridge WHERE id = 1')); if (($row['value'] ?? null) !== 'reachable') { throw new RuntimeException('MariaDB read failed'); } mysqli_query($db, 'DROP TABLE mariadb_bridge'); echo getenv('DB_PORT') . ':' . getenv('TC_MYSQL_PORT');" await writeFile(mariaDbRecipePath, JSON.stringify({ schema: "wp-codebox/workspace-recipe/v1", - runtime: { php: "8.4" }, + runtime: { phpVersion: "8.4" }, inputs: { services: [{ id: "mariadb", kind: "mysql", configuration: { engine: "mariadb", rootAuthentication: "empty-password" }, outputs: { host: "DB_HOST", port: ["DB_PORT", "TC_MYSQL_PORT"], username: "DB_USER", password: "DB_PASSWORD", database: "DB_NAME" } }], }, From 2dbf12bb4f4ce3d7f20f9c7dbab5bc7a737027a8 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Fri, 7 Aug 2026 05:11:54 -0400 Subject: [PATCH 4/5] fix(playground): support custom managed database mappings Only run canonical endpoint diagnostics when the runtime exports DB_HOST or DB_PORT. AI assistance: OpenAI GPT-5.6 Sol via OpenCode analyzed contracts failures, identified the custom database mapping boundary, and implemented focused coverage. Chris Huber remains responsible for every line. --- .../runtime-playground/src/playground-cli-runner.ts | 2 +- tests/playground-cli-runner-bootstrap-ini.test.ts | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/runtime-playground/src/playground-cli-runner.ts b/packages/runtime-playground/src/playground-cli-runner.ts index 216664f3..29529f49 100644 --- a/packages/runtime-playground/src/playground-cli-runner.ts +++ b/packages/runtime-playground/src/playground-cli-runner.ts @@ -485,7 +485,7 @@ export function classifyManagedDatabaseMysqliError(errorCode: number): "authenti } export function managedDatabaseDiagnosticsPhp(spec: RuntimeCreateSpec): string { - if (spec.environment.databaseSetup !== "external") return "" + if (spec.environment.databaseSetup !== "external" || (!spec.runtimeEnv?.DB_HOST && !spec.runtimeEnv?.DB_PORT)) return "" const services = Array.isArray(spec.metadata?.managedRuntimeServices) ? spec.metadata.managedRuntimeServices : [] const mysql = services.find((service): service is Record => typeof service === "object" && service !== null && (service as { kind?: unknown }).kind === "mysql") const receipt = mysql ? { diff --git a/tests/playground-cli-runner-bootstrap-ini.test.ts b/tests/playground-cli-runner-bootstrap-ini.test.ts index 0b8b69d7..1c2946dd 100644 --- a/tests/playground-cli-runner-bootstrap-ini.test.ts +++ b/tests/playground-cli-runner-bootstrap-ini.test.ts @@ -144,6 +144,18 @@ try { assert.equal(calls[0]?.["mount-before-install"]?.some((mount) => mount.vfsPath === "/internal/wp-codebox"), true, "passwordless external databases retain isolated request workers") assert.equal(calls[0]?.["mount-before-install"]?.some((mount) => /^\/wordpress\/wp-codebox-execute-[a-f0-9]{24}\.php$/.test(mount.vfsPath)), true) + calls.length = 0 + const customDatabaseServer = await startPlaygroundCliServer({ + ...spec, + runtimeEnv: { TC_MYSQL_PORT: "33060" }, + secretEnv: { DB_PASSWORD: "secret" }, + secretEnvTargets: { DB_PASSWORD: "DB_PASSWORD" }, + }, [], { cliModule }) + await customDatabaseServer[Symbol.asyncDispose]() + const customDatabaseAutoPrependPath = calls[0]?.["mount-before-install"]?.[1]?.hostPath + assert.equal(typeof customDatabaseAutoPrependPath, "string") + assert.doesNotMatch(await readFile(customDatabaseAutoPrependPath as string, "utf8"), /WP_CODEBOX_MANAGED_DB_DIAGNOSTIC/, "custom database mappings bypass the canonical endpoint diagnostic") + calls.length = 0 const defaultRuntimeIniSpec: RuntimeCreateSpec = { ...spec, From 0681e6011a1827a4f37aa7fbf313cfbb106fba07 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Fri, 7 Aug 2026 05:29:49 -0400 Subject: [PATCH 5/5] fix(playground): repair readonly mount cache races Clone cached readonly mounts while locked, rebuild stale snapshots, and invalidate corrupt v1 entries. Native agent task failures now retain redacted child output tails for diagnosis. AI assistance: OpenAI GPT-5.6 Sol via OpenCode analyzed the failing native Playground E2E, reproduced the readonly mount cache failures, implemented the cache repair, and ran repeated E2E verification. Chris Huber remains responsible for every line. --- .../execute-native-agent-task.mjs | 9 +++- .../src/mount-materialization.ts | 46 ++++++++++++------- ...e-native-agent-task-playground-e2e.test.ts | 9 +++- 3 files changed, 45 insertions(+), 19 deletions(-) diff --git a/.github/scripts/run-agent-task/execute-native-agent-task.mjs b/.github/scripts/run-agent-task/execute-native-agent-task.mjs index 352c3313..fc9abcdc 100644 --- a/.github/scripts/run-agent-task/execute-native-agent-task.mjs +++ b/.github/scripts/run-agent-task/execute-native-agent-task.mjs @@ -927,7 +927,14 @@ const result = { success, request_path: requestPath, runtime_input_path: ".codebox/native-agent-task-input.json", - execution: { stdout_truncated: execution.stdout_truncated, stderr_truncated: execution.stderr_truncated }, + execution: { + stdout_truncated: execution.stdout_truncated, + stderr_truncated: execution.stderr_truncated, + ...(execution.code !== 0 ? { + stdout_tail: bounded(redact(execution.stdout), MAX_WORKFLOW_OUTPUT_BYTES), + stderr_tail: bounded(redact(execution.stderr), MAX_WORKFLOW_OUTPUT_BYTES), + } : {}), + }, runtime_result: redact(runtimeRecord), ...(reviewerEvidence ? { reviewer_evidence: reviewerEvidence } : {}), verification, diff --git a/packages/runtime-playground/src/mount-materialization.ts b/packages/runtime-playground/src/mount-materialization.ts index 63487936..ddc5bd6c 100644 --- a/packages/runtime-playground/src/mount-materialization.ts +++ b/packages/runtime-playground/src/mount-materialization.ts @@ -123,32 +123,44 @@ export async function stageReadonlyPlaygroundMounts(mounts: MountSpec[]): Promis async function prepareReadonlyDirectory(mount: MountSpec, mountIndex: number, sourceRoot: string, destination: string, diagnostics: MaterializationDiagnostic[]): Promise { const startedAt = Date.now() const generation = await readonlyMountSourceGeneration(mount, mountIndex, sourceRoot, diagnostics) - const cacheRoot = join(tmpdir(), "wp-codebox-readonly-mount-cache-v1") + const cacheRoot = join(tmpdir(), "wp-codebox-readonly-mount-cache-v2") const cachePath = join(cacheRoot, generation.fingerprint) await mkdir(cacheRoot, { recursive: true, mode: 0o700 }) let mode: ReadonlyMountPreparation["mode"] = "cache-hit" await withPlaygroundArchiveCacheLock(cacheRoot, `readonly-mount-${generation.fingerprint}`, async () => { - if (!await directoryExists(cachePath)) { - mode = "cache-miss" - const temporary = await mkdtemp(join(cacheRoot, ".prepare-")) - try { - const prepared = join(temporary, "tree") - await stageReadonlyDirectory(mount, mountIndex, sourceRoot, prepared, []) - const verified = await readonlyMountSourceGeneration(mount, mountIndex, sourceRoot, []) - if (verified.fingerprint !== generation.fingerprint) { - throw new Error(`Readonly mount source changed while preparing snapshot: ${mount.target}`) + for (let attempt = 0; attempt < 2; attempt++) { + if (!await directoryExists(cachePath)) { + mode = "cache-miss" + const temporary = await mkdtemp(join(cacheRoot, ".prepare-")) + try { + const prepared = join(temporary, "tree") + await stageReadonlyDirectory(mount, mountIndex, sourceRoot, prepared, []) + const verified = await readonlyMountSourceGeneration(mount, mountIndex, sourceRoot, []) + if (verified.fingerprint !== generation.fingerprint) { + throw new Error(`Readonly mount source changed while preparing snapshot: ${mount.target}`) + } + await rename(prepared, cachePath) + } finally { + await rm(temporary, { recursive: true, force: true }) } - await rename(prepared, cachePath) - } finally { - await rm(temporary, { recursive: true, force: true }) + await retainReadonlyMountCache(cacheRoot, cachePath) + } + // Cache retention can evict a different entry. Clone while holding the + // shared lock so another preparation cannot evict this tree first. + try { + await cp(cachePath, destination, { recursive: true, dereference: true, mode: constants.COPYFILE_FICLONE }) + return + } catch (error) { + if (attempt !== 0 || (error as { code?: unknown }).code !== "ENOENT") throw error + // A prior interrupted or raced preparation can leave a cache root with + // missing descendants. Rebuild it once while still holding the lock. + mode = "cache-miss" + await rm(cachePath, { recursive: true, force: true }) + await rm(destination, { recursive: true, force: true }) } - await retainReadonlyMountCache(cacheRoot, cachePath) } }) - // Clone the immutable cache tree for this writable Playground mount. APFS and - // other supporting filesystems make this copy-on-write; other filesystems copy safely. - await cp(cachePath, destination, { recursive: true, dereference: true, mode: constants.COPYFILE_FICLONE }) return { mode, bytes: generation.bytes, files: generation.files, elapsedMs: Date.now() - startedAt } } diff --git a/tests/execute-native-agent-task-playground-e2e.test.ts b/tests/execute-native-agent-task-playground-e2e.test.ts index bf0ee7ca..3371b27f 100644 --- a/tests/execute-native-agent-task-playground-e2e.test.ts +++ b/tests/execute-native-agent-task-playground-e2e.test.ts @@ -79,7 +79,14 @@ add_filter( 'pre_http_request', static function( $preempt, $args, $url ) { assert.equal(seedProvenance.files, 2, "only README and the explicit .env.example template are copied") assert.equal(seedProvenance.excluded.files, 7) assert.deepEqual(seedProvenance.excluded.categories, [{ category: "credentials", count: 2 }, { category: "environment", count: 1 }, { category: "generated-tree", count: 3 }, { category: "private-key", count: 1 }]) - assert.equal(execution.code, 0, `${execution.stderr ?? ""}\n${JSON.stringify(result)}`) + assert.equal(execution.code, 0, JSON.stringify({ + execution: result.execution, + runtimeError: result.runtime_result?.error, + agentError: result.runtime_result?.agent_task_run_result?.error, + agentResult: result.runtime_result?.agent_result, + agentTaskResult: result.runtime_result?.agent_task_run_result, + failure: result.failure, + })) assert.equal(result.success, true) assert.equal(await readFile(join(workspace, "README.md"), "utf8"), "after\n") assert.equal(result.runtime_result.agent_result?.changedFiles?.count, 1)