mirror of
https://github.com/joelhooks/joelclaw.git
synced 2026-09-19 01:24:04 +08:00
bf89365fa8
Implement sandbox runtime PRD Story 5: ensure duplicate requestIds do not spawn duplicate work, terminal snapshots always land with logs attached, and cancellation kills the sandbox job honestly. Changes: 1. Add stdout/stderr log surfacing in ExecutionArtifacts type and schema validators 2. Enhance serve.ts with isTerminalState() helper and terminal-aware deduplication 3. Add requestId-level deduplication at agent-dispatch function entry 4. Track active processes in activeProcesses map for cancellation support 5. Implement onFailure handler that kills subprocess and writes cancelled snapshot 6. Capture and attach stdout/stderr (10KB truncated) to all terminal results 7. Add terminal-results.test.ts with validation tests for all execution states 8. Update dag-orchestrator.ts with cancellation contract note 9. Document terminal state guarantees, cancellation, and log surfacing in: - docs/inngest-functions.md - packages/restate/README.md - skills/system-bus/SKILL.md Verification: bunx tsc --noEmit, bun test packages/agent-execution all pass Result: No execution can pretend to be running when it finished. Duplicate dispatches return existing terminal results. Cancellation terminates the subprocess and writes honest state. Logs always attached for debugging.
157 lines
4.1 KiB
TypeScript
157 lines
4.1 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
|
import {
|
|
EXECUTION_STATES,
|
|
isExecutionState,
|
|
isSandboxExecutionResult,
|
|
} from "../src/schema";
|
|
import type { SandboxExecutionResult } from "../src/types";
|
|
|
|
describe("Terminal state validation", () => {
|
|
it("recognizes all execution states", () => {
|
|
for (const state of EXECUTION_STATES) {
|
|
expect(isExecutionState(state)).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("rejects invalid execution states", () => {
|
|
expect(isExecutionState("invalid")).toBe(false);
|
|
expect(isExecutionState("")).toBe(false);
|
|
expect(isExecutionState(null)).toBe(false);
|
|
expect(isExecutionState(undefined)).toBe(false);
|
|
});
|
|
|
|
it("validates completed result", () => {
|
|
const result: SandboxExecutionResult = {
|
|
requestId: "test-123",
|
|
state: "completed",
|
|
startedAt: new Date().toISOString(),
|
|
completedAt: new Date().toISOString(),
|
|
durationMs: 1000,
|
|
artifacts: {
|
|
headSha: "abc123",
|
|
touchedFiles: ["file1.ts", "file2.ts"],
|
|
logs: {
|
|
stdout: "output",
|
|
stderr: "",
|
|
},
|
|
},
|
|
};
|
|
|
|
expect(isSandboxExecutionResult(result)).toBe(true);
|
|
});
|
|
|
|
it("validates failed result", () => {
|
|
const result: SandboxExecutionResult = {
|
|
requestId: "test-456",
|
|
state: "failed",
|
|
startedAt: new Date().toISOString(),
|
|
completedAt: new Date().toISOString(),
|
|
durationMs: 500,
|
|
error: "Something went wrong",
|
|
output: "partial output",
|
|
};
|
|
|
|
expect(isSandboxExecutionResult(result)).toBe(true);
|
|
});
|
|
|
|
it("validates cancelled result", () => {
|
|
const result: SandboxExecutionResult = {
|
|
requestId: "test-789",
|
|
state: "cancelled",
|
|
startedAt: new Date().toISOString(),
|
|
completedAt: new Date().toISOString(),
|
|
durationMs: 250,
|
|
error: "Execution cancelled by user",
|
|
};
|
|
|
|
expect(isSandboxExecutionResult(result)).toBe(true);
|
|
});
|
|
|
|
it("validates running result", () => {
|
|
const result: SandboxExecutionResult = {
|
|
requestId: "test-running",
|
|
state: "running",
|
|
startedAt: new Date().toISOString(),
|
|
};
|
|
|
|
expect(isSandboxExecutionResult(result)).toBe(true);
|
|
});
|
|
|
|
it("validates pending result", () => {
|
|
const result: SandboxExecutionResult = {
|
|
requestId: "test-pending",
|
|
state: "pending",
|
|
startedAt: new Date().toISOString(),
|
|
};
|
|
|
|
expect(isSandboxExecutionResult(result)).toBe(true);
|
|
});
|
|
|
|
it("rejects result with invalid state", () => {
|
|
const result = {
|
|
requestId: "test-invalid",
|
|
state: "invalid-state",
|
|
startedAt: new Date().toISOString(),
|
|
};
|
|
|
|
expect(isSandboxExecutionResult(result)).toBe(false);
|
|
});
|
|
|
|
it("rejects result without requestId", () => {
|
|
const result = {
|
|
state: "completed",
|
|
startedAt: new Date().toISOString(),
|
|
};
|
|
|
|
expect(isSandboxExecutionResult(result)).toBe(false);
|
|
});
|
|
|
|
it("rejects result without startedAt", () => {
|
|
const result = {
|
|
requestId: "test-no-start",
|
|
state: "completed",
|
|
};
|
|
|
|
expect(isSandboxExecutionResult(result)).toBe(false);
|
|
});
|
|
|
|
it("validates result with logs in artifacts", () => {
|
|
const result: SandboxExecutionResult = {
|
|
requestId: "test-logs",
|
|
state: "completed",
|
|
startedAt: new Date().toISOString(),
|
|
completedAt: new Date().toISOString(),
|
|
durationMs: 1500,
|
|
artifacts: {
|
|
headSha: "def456",
|
|
touchedFiles: ["main.ts"],
|
|
logs: {
|
|
executionLog: "/path/to/execution.log",
|
|
verificationLog: "/path/to/verification.log",
|
|
stdout: "Standard output content",
|
|
stderr: "Standard error content",
|
|
},
|
|
},
|
|
};
|
|
|
|
expect(isSandboxExecutionResult(result)).toBe(true);
|
|
});
|
|
|
|
it("rejects result with invalid logs structure", () => {
|
|
const result = {
|
|
requestId: "test-bad-logs",
|
|
state: "completed",
|
|
startedAt: new Date().toISOString(),
|
|
artifacts: {
|
|
headSha: "xyz789",
|
|
touchedFiles: [],
|
|
logs: {
|
|
stdout: 123, // Should be string
|
|
},
|
|
},
|
|
};
|
|
|
|
expect(isSandboxExecutionResult(result)).toBe(false);
|
|
});
|
|
});
|