diff --git a/packages/runtime-core/src/command-registry.ts b/packages/runtime-core/src/command-registry.ts index 4b659842..d9bff924 100644 --- a/packages/runtime-core/src/command-registry.ts +++ b/packages/runtime-core/src/command-registry.ts @@ -75,7 +75,7 @@ export const commandRegistry = [ { name: "body", description: "Optional raw request body.", format: "string" }, { name: "body-json", description: "Optional JSON request body string; takes precedence over body.", format: "JSON string" }, ], - outputShape: "JSON object with command, method, path, route, status, headers, and REST response data.", + outputShape: "JSON object with command, method, path, route, status, headers, body/data, timing, and diagnostics.", policyRequirement: "Runtime policy commands must include wordpress.rest-request.", recipe: true, handler: { kind: "playground", method: "runRestRequest" }, diff --git a/packages/runtime-core/src/runtime-action-adapter.ts b/packages/runtime-core/src/runtime-action-adapter.ts index 50145b47..fc04b970 100644 --- a/packages/runtime-core/src/runtime-action-adapter.ts +++ b/packages/runtime-core/src/runtime-action-adapter.ts @@ -160,26 +160,68 @@ async function runRuntimeRestRequestAction(episode: RuntimeEpisode, action: Runt } catch { // Keep raw stdout when a backend returns non-JSON diagnostics. } + const normalized = normalizeRuntimeRestRequestResult(action, step, stdout) return runtimeActionObservation({ type: action.type, action, step, - data: { - method: action.method ?? "GET", - path: action.path, - mappedCommand: step.execution.command, - args: step.execution.args, - exitCode: step.execution.exitCode, - stdout, - stderr: step.execution.stderr, - executionId: step.execution.id, - stepId: step.id, - }, + data: normalized, artifactRefs: step.observation?.artifactRefs, }) } +function normalizeRuntimeRestRequestResult( + action: RuntimeRestRequestAction, + step: RuntimeEpisodeStepResult, + stdout: unknown, +): Record { + const response = stdout && typeof stdout === "object" && !Array.isArray(stdout) ? stdout as Record : {} + const startedAt = Date.parse(step.execution.startedAt) + const finishedAt = Date.parse(step.execution.finishedAt) + const durationMs = Number.isFinite(startedAt) && Number.isFinite(finishedAt) ? Math.max(0, finishedAt - startedAt) : undefined + const method = stringValue(response.method) ?? action.method ?? "GET" + const path = stringValue(response.path) ?? action.path + const route = stringValue(response.route) ?? path + const headers = recordValue(response.headers) ?? {} + const body = response.body ?? response.data ?? null + const diagnostics = { + exitCode: step.execution.exitCode, + stderr: step.execution.stderr, + ...(recordValue(response.diagnostics) ?? {}), + } + + return { + method, + path, + route, + status: typeof response.status === "number" ? response.status : undefined, + headers, + body, + timing: { + startedAt: step.execution.startedAt, + finishedAt: step.execution.finishedAt, + ...(durationMs !== undefined ? { durationMs } : {}), + ...(recordValue(response.timing) ?? {}), + }, + diagnostics, + mappedCommand: step.execution.command, + args: step.execution.args, + stdout, + stderr: step.execution.stderr, + executionId: step.execution.id, + stepId: step.id, + } +} + +function stringValue(value: unknown): string | undefined { + return typeof value === "string" ? value : undefined +} + +function recordValue(value: unknown): Record | undefined { + return value && typeof value === "object" && !Array.isArray(value) ? value as Record : undefined +} + async function runRuntimeFilesystemAction( episode: RuntimeEpisode, action: RuntimeFilesystemAction, diff --git a/packages/runtime-playground/src/rest-request-command-handlers.ts b/packages/runtime-playground/src/rest-request-command-handlers.ts index 55b6440c..1518b0af 100644 --- a/packages/runtime-playground/src/rest-request-command-handlers.ts +++ b/packages/runtime-playground/src/rest-request-command-handlers.ts @@ -28,6 +28,7 @@ export function restRequestInputFromArgs(args: string[]): RestRequestCommandInpu export function restRequestPhpCode(input: RestRequestCommandInput): string { return `define( 'REST_REQUEST', true ); +$wp_codebox_started_at = microtime( true ); $wp_codebox_method = ${JSON.stringify(input.method)}; $wp_codebox_path = ${JSON.stringify(input.path)}; $wp_codebox_headers = json_decode( ${JSON.stringify(JSON.stringify(input.headers))}, true ); @@ -56,6 +57,7 @@ if ( $wp_codebox_body !== '' ) { $wp_codebox_response = rest_do_request( $wp_codebox_request ); $wp_codebox_server = rest_get_server(); $wp_codebox_data = $wp_codebox_server->response_to_data( $wp_codebox_response, false ); +$wp_codebox_finished_at = microtime( true ); echo wp_json_encode( array( 'command' => 'wordpress.rest-request', @@ -64,6 +66,11 @@ echo wp_json_encode( array( 'route' => $wp_codebox_route, 'status' => $wp_codebox_response->get_status(), 'headers' => $wp_codebox_response->get_headers(), + 'body' => $wp_codebox_data, 'data' => $wp_codebox_data, + 'timing' => array( + 'duration_ms' => (int) round( ( $wp_codebox_finished_at - $wp_codebox_started_at ) * 1000 ), + ), + 'diagnostics' => (object) array(), ), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );` } diff --git a/scripts/rest-request-runtime-smoke.ts b/scripts/rest-request-runtime-smoke.ts index 55a5f721..9cbf4857 100644 --- a/scripts/rest-request-runtime-smoke.ts +++ b/scripts/rest-request-runtime-smoke.ts @@ -40,10 +40,13 @@ try { { type: "command-result" }, ) assert.equal(direct.execution.exitCode, 0) - const directBody = JSON.parse(direct.execution.stdout) as { command: string; status: number; data: Record } + const directBody = JSON.parse(direct.execution.stdout) as { command: string; status: number; body: Record; data: Record; timing: { duration_ms: number }; diagnostics: Record } assert.equal(directBody.command, "wordpress.rest-request") assert.equal(directBody.status, 200) + assert.ok(directBody.body.post) assert.ok(directBody.data.post) + assert.equal(typeof directBody.timing.duration_ms, "number") + assert.deepEqual(directBody.diagnostics, {}) const action = await runRuntimeAction(episode, { type: "rest_request", method: "GET", path: "/wp-json/wp/v2/types", params: { context: "view" } }) assert.equal(action.schema, RUNTIME_ACTION_OBSERVATION_SCHEMA) @@ -51,6 +54,15 @@ try { assert.equal(action.step?.action.kind, "http") assert.equal(action.step?.execution.command, "wordpress.rest-request") assert.deepEqual(action.step?.execution.args, ["path=/wp-json/wp/v2/types", "method=GET", 'params-json={"context":"view"}']) + assert.equal(action.data.method, "GET") + assert.equal(action.data.path, "/wp-json/wp/v2/types") + assert.equal(action.data.route, "/wp/v2/types") + assert.equal(action.data.status, 200) + assert.equal(typeof action.data.headers, "object") + assert.ok((action.data.body as { post?: unknown }).post) + assert.equal(typeof (action.data.timing as { durationMs?: number }).durationMs, "number") + assert.equal((action.data.diagnostics as { exitCode?: number; stderr?: string }).exitCode, 0) + assert.equal((action.data.diagnostics as { exitCode?: number; stderr?: string }).stderr, "") assert.equal((action.data.stdout as { status: number }).status, 200) const trace = await episode.trace()