| @@ -5,7 +5,7 @@ import { createExecutor, definePlugin } from "@executor-js/sdk"; · 4 unmodified lines |
| 5 | import { makeTestConfig } from "@executor-js/sdk/testing"; | 5 | import { makeTestConfig } from "@executor-js/sdk/testing"; |
| 6 | import type { CodeExecutor, ExecuteResult } from "@executor-js/codemode-core"; | 6 | import type { CodeExecutor, ExecuteResult } from "@executor-js/codemode-core"; |
| 7 | | 7 | |
| 8 | import { createExecutionEngine, formatPausedExecution } from "./engine"; | 8 | import { createExecutionEngine, formatExecuteResult, formatPausedExecution } from "./engine"; |
| 9 | import { FormElicitation } from "@executor-js/sdk/core"; | 9 | import { FormElicitation } from "@executor-js/sdk/core"; |
| 10 | | 10 | |
| 11 | // Regression for the hang reported as the executor-MCP "180s timeout" against | 11 | // Regression for the hang reported as the executor-MCP "180s timeout" against |
| @@ -140,3 +140,139 @@ describe("formatPausedExecution approval terms", () => { · 128 unmodified lines |
| 140 | expect((result.structured["interaction"] as Record<string, unknown>)["meta"]).toBeUndefined(); | 140 | expect((result.structured["interaction"] as Record<string, unknown>)["meta"]).toBeUndefined(); |
| 141 | }); | 141 | }); |
| 142 | }); | 142 | }); |
| | 143 | |
| | 144 | // Pins the exact preview and structured shapes so the serialization dedupe in |
| | 145 | // the engine cannot drift them: the preview stays pretty-printed (indent 2), |
| | 146 | // truncation keeps its exact suffix, and structured carries the raw value. |
| | 147 | describe("formatExecuteResult output identity", () => { |
| | 148 | const MAX_PREVIEW_CHARS = 30_000; |
| | 149 | |
| | 150 | it("renders an object result as pretty-printed JSON and keeps the raw value in structured", () => { |
| | 151 | const value = { café: "naïve — ✓", emoji: "🎉", nested: { π: 3.14159 } }; |
| | 152 | const formatted = formatExecuteResult({ result: value, logs: [] }); |
| | 153 | |
| | 154 | expect(formatted.text).toBe(JSON.stringify(value, null, 2)); |
| | 155 | expect(formatted.structured).toEqual({ status: "completed", result: value, logs: [] }); |
| | 156 | expect(formatted.structured["result"]).toBe(value); |
| | 157 | expect(formatted.isError).toBe(false); |
| | 158 | }); |
| | 159 | |
| | 160 | it("truncates a long preview with the exact suffix and untouched structured value", () => { |
| | 161 | const value = { data: "é🎉".repeat(12_000) }; |
| | 162 | const pretty = JSON.stringify(value, null, 2); |
| | 163 | expect(pretty.length).toBeGreaterThan(MAX_PREVIEW_CHARS); |
| | 164 | |
| | 165 | const formatted = formatExecuteResult({ result: value }); |
| | 166 | |
| | 167 | expect(formatted.text).toBe( |
| | 168 | `${pretty.slice(0, MAX_PREVIEW_CHARS)}\n... [truncated ${pretty.length - MAX_PREVIEW_CHARS} chars]`, |
| | 169 | ); |
| | 170 | expect(formatted.structured["result"]).toBe(value); |
| | 171 | }); |
| | 172 | |
| | 173 | it("returns a string result verbatim", () => { |
| | 174 | const formatted = formatExecuteResult({ result: "plain — ✓", logs: ["l1", "l2"] }); |
| | 175 | |
| | 176 | expect(formatted.text).toBe("plain — ✓\n\nLogs:\nl1\nl2"); |
| | 177 | expect(formatted.structured).toEqual({ |
| | 178 | status: "completed", |
| | 179 | result: "plain — ✓", |
| | 180 | logs: ["l1", "l2"], |
| | 181 | }); |
| | 182 | }); |
| | 183 | |
| | 184 | it("renders an error result with logs and truncation", () => { |
| | 185 | const formatted = formatExecuteResult({ |
| | 186 | result: null, |
| | 187 | error: "boom", |
| | 188 | errorKind: "tool_error", |
| | 189 | logs: ["x".repeat(MAX_PREVIEW_CHARS)], |
| | 190 | }); |
| | 191 | |
| | 192 | const untruncated = `Error: boom\n\nLogs:\n${"x".repeat(MAX_PREVIEW_CHARS)}`; |
| | 193 | expect(formatted.text).toBe( |
| | 194 | `${untruncated.slice(0, MAX_PREVIEW_CHARS)}\n... [truncated ${untruncated.length - MAX_PREVIEW_CHARS} chars]`, |
| | 195 | ); |
| | 196 | expect(formatted.isError).toBe(true); |
| | 197 | expect(formatted.structured).toEqual({ |
| | 198 | status: "error", |
| | 199 | error: "boom", |
| | 200 | logs: ["x".repeat(MAX_PREVIEW_CHARS)], |
| | 201 | }); |
| | 202 | }); |
| | 203 | }); |
| | 204 | |
| | 205 | // The result-size span metric walks the whole result value with |
| | 206 | // `JSON.stringify`. A `toJSON` probe inside the fixture counts full walks: |
| | 207 | // every complete stringify of the value visits the probe exactly once. |
| | 208 | describe("execute outcome measurement cost", () => { |
| | 209 | const instrumented = () => { |
| | 210 | let walks = 0; |
| | 211 | const probe = { |
| | 212 | toJSON: () => { |
| | 213 | walks += 1; |
| | 214 | return "probe"; |
| | 215 | }, |
| | 216 | }; |
| | 217 | return { value: { data: [1, 2, 3], probe }, walks: () => walks }; |
| | 218 | }; |
| | 219 | |
| | 220 | const executorFor = (result: ExecuteResult): CodeExecutor<FakeRuntimeError> => ({ |
| | 221 | execute: () => Effect.succeed(result), |
| | 222 | }); |
| | 223 | |
| | 224 | it.effect("walks the result value once on the pausable path", () => |
| | 225 | Effect.gen(function* () { |
| | 226 | const executor = yield* makeExecutor(); |
| | 227 | const fixture = instrumented(); |
| | 228 | const engine = createExecutionEngine({ |
| | 229 | executor, |
| | 230 | codeExecutor: executorFor({ result: fixture.value, logs: [] }), |
| | 231 | }); |
| | 232 | |
| | 233 | const outcome = yield* engine.executeWithPause("noop"); |
| | 234 | |
| | 235 | expect(outcome.status).toBe("completed"); |
| | 236 | expect(fixture.walks()).toBe(1); |
| | 237 | }), |
| | 238 | ); |
| | 239 | |
| | 240 | it.effect("walks the result value once even when autoApprove stamps two spans", () => |
| | 241 | Effect.gen(function* () { |
| | 242 | const executor = yield* makeExecutor(); |
| | 243 | const fixture = instrumented(); |
| | 244 | const engine = createExecutionEngine({ |
| | 245 | executor, |
| | 246 | codeExecutor: executorFor({ result: fixture.value, logs: [] }), |
| | 247 | }); |
| | 248 | |
| | 249 | // autoApprove runs the inline path (inner span annotation) and then |
| | 250 | // annotates the outer pausable span with the same result. |
| | 251 | const outcome = yield* engine.executeWithPause("noop", { autoApprove: true }); |
| | 252 | |
| | 253 | expect(outcome.status).toBe("completed"); |
| | 254 | expect(fixture.walks()).toBe(1); |
| | 255 | }), |
| | 256 | ); |
| | 257 | |
| | 258 | it.effect("a completed execution plus its preview pays two walks total", () => |
| | 259 | Effect.gen(function* () { |
| | 260 | const executor = yield* makeExecutor(); |
| | 261 | const fixture = instrumented(); |
| | 262 | const engine = createExecutionEngine({ |
| | 263 | executor, |
| | 264 | codeExecutor: executorFor({ result: fixture.value, logs: [] }), |
| | 265 | }); |
| | 266 | |
| | 267 | const outcome = yield* engine.executeWithPause("noop"); |
| | 268 | expect(outcome.status).toBe("completed"); |
| | 269 | if (outcome.status !== "completed") return; |
| | 270 | |
| | 271 | // One compact walk for the span size metric, one pretty walk for the |
| | 272 | // preview. They produce different strings (compact vs indent 2), so |
| | 273 | // neither can be derived from the other. |
| | 274 | formatExecuteResult(outcome.result); |
| | 275 | expect(fixture.walks()).toBe(2); |
| | 276 | }), |
| | 277 | ); |
| | 278 | }); |