mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-22 22:44:13 +08:00
refactor: remove CLI path, use Agent SDK exclusively (#849)
* refactor: remove CLI path, use Agent SDK exclusively - Remove CLI-based Claude execution in favor of Agent SDK - Delete prepareRunConfig, parseAndSetSessionId, parseAndSetStructuredOutputs functions - Remove named pipe IPC and sanitizeJsonOutput helper - Remove test-agent-sdk job from test-base-action workflow (SDK is now default) - Delete run-claude.test.ts and structured-output.test.ts (testing removed CLI code) - Update CLAUDE.md to remove named pipe references Co-Authored-By: Claude <noreply@anthropic.com> Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%) Claude-Steers: 2 Claude-Permission-Prompts: 1 Claude-Escapes: 0 Claude-Plan: <claude-plan> # Plan: Remove Non-Agent SDK Code Path ## Overview Since `use_agent_sdk` defaults to `true`, remove the legacy CLI code path entirely from `base-action/src/run-claude.ts`. ## Files to Modify ### 1. `base-action/src/run-claude.ts` - Main Cleanup **Remove imports:** - `exec` from `child_process` - `promisify` from `util` - `unlink`, `writeFile`, `stat` from `fs/promises` (keep `readFile` - check if needed) - `createWriteStream` from `fs` - `spawn` from `child_process` - `parseShellArgs` from `shell-quote` (still used in `parse-sdk-options.ts`, keep package) **Remove constants:** - `execAsync` - `PIPE_PATH` - `EXECUTION_FILE` (defined in both files, keep in SDK file) - `BASE_ARGS` **Remove types:** - `PreparedConfig` type (lines 85-89) - only used by `prepareRunConfig()` **Remove functions:** - `sanitizeJsonOutput()` (lines 21-68) - `prepareRunConfig()` (lines 91-125) - also remove export - `parseAndSetSessionId()` (lines 131-155) - also remove export - `parseAndSetStructuredOutputs()` (lines 162-197) - also remove export **Simplify `runClaude()`:** - Remove `useAgentSdk` flag check and logging (lines 200-204) - Remove the `if (useAgentSdk)` block, make SDK call direct - Remove entire CLI path (lines 211-438) - Resulting function becomes just: ```typescript export async function runClaude(promptPath: string, options: ClaudeOptions) { const parsedOptions = parseSdkOptions(options); return runClaudeWithSdk(promptPath, parsedOptions); } ``` ### 2. Delete Test Files **`base-action/test/run-claude.test.ts`:** - Delete entire file (only tests `prepareRunConfig()`) **`base-action/test/structured-output.test.ts`:** - Delete entire file (only tests `parseAndSetStructuredOutputs()` and `parseAndSetSessionId()`) ### 3. Workflow Update **`.github/workflows/test-base-action.yml`:** - Remove `test-agent-sdk` job (lines 120-176) - redundant now ### 4. Documentation Update **`base-action/CLAUDE.md`:** - Line 30: Remove "- Named pipes for IPC between prompt input and Claude process" - Line 57: Remove "- Uses `mkfifo` to create named pipes for prompt input" ## Verification 1. Run `bun run typecheck` to ensure no type errors 2. Run `bun test` to ensure remaining tests pass 3. Run `bun run format` to fix any formatting issues </claude-plan> * fix: address PR review comments - Add session_id output handling in run-claude-sdk.ts (critical) - Remove unused claudeEnv parameter from ClaudeOptions and index.ts - Update stale CLI path comment in parse-sdk-options.ts Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%) Claude-Steers: 0 Claude-Permission-Prompts: 0 Claude-Escapes: 0 Claude-Plan: <claude-plan> # Plan: Remove Non-Agent SDK Code Path ## Overview Since `use_agent_sdk` defaults to `true`, remove the legacy CLI code path entirely from `base-action/src/run-claude.ts`. ## Files to Modify ### 1. `base-action/src/run-claude.ts` - Main Cleanup **Remove imports:** - `exec` from `child_process` - `promisify` from `util` - `unlink`, `writeFile`, `stat` from `fs/promises` (keep `readFile` - check if needed) - `createWriteStream` from `fs` - `spawn` from `child_process` - `parseShellArgs` from `shell-quote` (still used in `parse-sdk-options.ts`, keep package) **Remove constants:** - `execAsync` - `PIPE_PATH` - `EXECUTION_FILE` (defined in both files, keep in SDK file) - `BASE_ARGS` **Remove types:** - `PreparedConfig` type (lines 85-89) - only used by `prepareRunConfig()` **Remove functions:** - `sanitizeJsonOutput()` (lines 21-68) - `prepareRunConfig()` (lines 91-125) - also remove export - `parseAndSetSessionId()` (lines 131-155) - also remove export - `parseAndSetStructuredOutputs()` (lines 162-197) - also remove export **Simplify `runClaude()`:** - Remove `useAgentSdk` flag check and logging (lines 200-204) - Remove the `if (useAgentSdk)` block, make SDK call direct - Remove entire CLI path (lines 211-438) - Resulting function becomes just: ```typescript export async function runClaude(promptPath: string, options: ClaudeOptions) { const parsedOptions = parseSdkOptions(options); return runClaudeWithSdk(promptPath, parsedOptions); } ``` ### 2. Delete Test Files **`base-action/test/run-claude.test.ts`:** - Delete entire file (only tests `prepareRunConfig()`) **`base-action/test/structured-output.test.ts`:** - Delete entire file (only tests `parseAndSetStructuredOutputs()` and `parseAndSetSessionId()`) ### 3. Workflow Update **`.github/workflows/test-base-action.yml`:** - Remove `test-agent-sdk` job (lines 120-176) - redundant now ### 4. Documentation Update **`base-action/CLAUDE.md`:** - Line 30: Remove "- Named pipes for IPC between prompt input and Claude process" - Line 57: Remove "- Uses `mkfifo` to create named pipes for prompt input" ## Verification 1. Run `bun run typecheck` to ensure no type errors 2. Run `bun test` to ensure remaining tests pass 3. Run `bun run format` to fix any formatting issues </claude-plan>
This commit is contained in:
58
.github/workflows/test-base-action.yml
vendored
58
.github/workflows/test-base-action.yml
vendored
@@ -116,61 +116,3 @@ jobs:
|
|||||||
echo "❌ Execution log file not found"
|
echo "❌ Execution log file not found"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
test-agent-sdk:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
|
||||||
|
|
||||||
- name: Test with Agent SDK
|
|
||||||
id: sdk-test
|
|
||||||
uses: ./base-action
|
|
||||||
env:
|
|
||||||
USE_AGENT_SDK: "true"
|
|
||||||
with:
|
|
||||||
prompt: ${{ github.event.inputs.test_prompt || 'List the files in the current directory starting with "package"' }}
|
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
||||||
allowed_tools: "LS,Read"
|
|
||||||
|
|
||||||
- name: Verify SDK output
|
|
||||||
run: |
|
|
||||||
OUTPUT_FILE="${{ steps.sdk-test.outputs.execution_file }}"
|
|
||||||
CONCLUSION="${{ steps.sdk-test.outputs.conclusion }}"
|
|
||||||
|
|
||||||
echo "Conclusion: $CONCLUSION"
|
|
||||||
echo "Output file: $OUTPUT_FILE"
|
|
||||||
|
|
||||||
if [ "$CONCLUSION" = "success" ]; then
|
|
||||||
echo "✅ Action completed successfully with Agent SDK"
|
|
||||||
else
|
|
||||||
echo "❌ Action failed with Agent SDK"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "$OUTPUT_FILE" ]; then
|
|
||||||
if [ -s "$OUTPUT_FILE" ]; then
|
|
||||||
echo "✅ Execution log file created successfully with content"
|
|
||||||
echo "Validating JSON format:"
|
|
||||||
if jq . "$OUTPUT_FILE" > /dev/null 2>&1; then
|
|
||||||
echo "✅ Output is valid JSON"
|
|
||||||
# Verify SDK output contains total_cost_usd (SDK field name)
|
|
||||||
if jq -e '.[] | select(.type == "result") | .total_cost_usd' "$OUTPUT_FILE" > /dev/null 2>&1; then
|
|
||||||
echo "✅ SDK output contains total_cost_usd field"
|
|
||||||
else
|
|
||||||
echo "❌ SDK output missing total_cost_usd field"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "Content preview:"
|
|
||||||
head -c 500 "$OUTPUT_FILE"
|
|
||||||
else
|
|
||||||
echo "❌ Output is not valid JSON"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "❌ Execution log file is empty"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "❌ Execution log file not found"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ This is a GitHub Action that allows running Claude Code within GitHub workflows.
|
|||||||
### Key Design Patterns
|
### Key Design Patterns
|
||||||
|
|
||||||
- Uses Bun runtime for development and execution
|
- Uses Bun runtime for development and execution
|
||||||
- Named pipes for IPC between prompt input and Claude process
|
|
||||||
- JSON streaming output format for execution logs
|
- JSON streaming output format for execution logs
|
||||||
- Composite action pattern to orchestrate multiple steps
|
- Composite action pattern to orchestrate multiple steps
|
||||||
- Provider-agnostic design supporting Anthropic API, AWS Bedrock, and Google Vertex AI
|
- Provider-agnostic design supporting Anthropic API, AWS Bedrock, and Google Vertex AI
|
||||||
@@ -54,7 +53,6 @@ This is a GitHub Action that allows running Claude Code within GitHub workflows.
|
|||||||
|
|
||||||
## Important Technical Details
|
## Important Technical Details
|
||||||
|
|
||||||
- Uses `mkfifo` to create named pipes for prompt input
|
|
||||||
- Outputs execution logs as JSON to `/tmp/claude-execution-output.json`
|
- Outputs execution logs as JSON to `/tmp/claude-execution-output.json`
|
||||||
- Timeout enforcement via `timeout` command wrapper
|
- Timeout enforcement via `timeout` command wrapper
|
||||||
- Strict TypeScript configuration with Bun-specific settings
|
- Strict TypeScript configuration with Bun-specific settings
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ async function run() {
|
|||||||
mcpConfig: process.env.INPUT_MCP_CONFIG,
|
mcpConfig: process.env.INPUT_MCP_CONFIG,
|
||||||
systemPrompt: process.env.INPUT_SYSTEM_PROMPT,
|
systemPrompt: process.env.INPUT_SYSTEM_PROMPT,
|
||||||
appendSystemPrompt: process.env.INPUT_APPEND_SYSTEM_PROMPT,
|
appendSystemPrompt: process.env.INPUT_APPEND_SYSTEM_PROMPT,
|
||||||
claudeEnv: process.env.INPUT_CLAUDE_ENV,
|
|
||||||
fallbackModel: process.env.INPUT_FALLBACK_MODEL,
|
fallbackModel: process.env.INPUT_FALLBACK_MODEL,
|
||||||
model: process.env.ANTHROPIC_MODEL,
|
model: process.env.ANTHROPIC_MODEL,
|
||||||
pathToClaudeCodeExecutable:
|
pathToClaudeCodeExecutable:
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ export function parseSdkOptions(options: ClaudeOptions): ParsedSdkOptions {
|
|||||||
if (process.env.INPUT_ACTION_INPUTS_PRESENT) {
|
if (process.env.INPUT_ACTION_INPUTS_PRESENT) {
|
||||||
env.GITHUB_ACTION_INPUTS = process.env.INPUT_ACTION_INPUTS_PRESENT;
|
env.GITHUB_ACTION_INPUTS = process.env.INPUT_ACTION_INPUTS_PRESENT;
|
||||||
}
|
}
|
||||||
// Ensure SDK path uses the same entrypoint as the CLI path
|
// Set the entrypoint for Claude Code to identify this as the GitHub Action
|
||||||
env.CLAUDE_CODE_ENTRYPOINT = "claude-code-github-action";
|
env.CLAUDE_CODE_ENTRYPOINT = "claude-code-github-action";
|
||||||
|
|
||||||
// Build system prompt option - default to claude_code preset
|
// Build system prompt option - default to claude_code preset
|
||||||
|
|||||||
@@ -178,6 +178,15 @@ export async function runClaudeWithSdk(
|
|||||||
core.warning(`Failed to write execution file: ${error}`);
|
core.warning(`Failed to write execution file: ${error}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract and set session_id from system.init message
|
||||||
|
const initMessage = messages.find(
|
||||||
|
(m) => m.type === "system" && "subtype" in m && m.subtype === "init",
|
||||||
|
);
|
||||||
|
if (initMessage && "session_id" in initMessage && initMessage.session_id) {
|
||||||
|
core.setOutput("session_id", initMessage.session_id);
|
||||||
|
core.info(`Set session_id: ${initMessage.session_id}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (!resultMessage) {
|
if (!resultMessage) {
|
||||||
core.setOutput("conclusion", "failure");
|
core.setOutput("conclusion", "failure");
|
||||||
core.error("No result message received from Claude");
|
core.error("No result message received from Claude");
|
||||||
|
|||||||
@@ -1,72 +1,6 @@
|
|||||||
import * as core from "@actions/core";
|
|
||||||
import { exec } from "child_process";
|
|
||||||
import { promisify } from "util";
|
|
||||||
import { unlink, writeFile, stat, readFile } from "fs/promises";
|
|
||||||
import { createWriteStream } from "fs";
|
|
||||||
import { spawn } from "child_process";
|
|
||||||
import { parse as parseShellArgs } from "shell-quote";
|
|
||||||
import { runClaudeWithSdk } from "./run-claude-sdk";
|
import { runClaudeWithSdk } from "./run-claude-sdk";
|
||||||
import { parseSdkOptions } from "./parse-sdk-options";
|
import { parseSdkOptions } from "./parse-sdk-options";
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
|
||||||
|
|
||||||
const PIPE_PATH = `${process.env.RUNNER_TEMP}/claude_prompt_pipe`;
|
|
||||||
const EXECUTION_FILE = `${process.env.RUNNER_TEMP}/claude-execution-output.json`;
|
|
||||||
const BASE_ARGS = ["--verbose", "--output-format", "stream-json"];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sanitizes JSON output to remove sensitive information when full output is disabled
|
|
||||||
* Returns a safe summary message or null if the message should be completely suppressed
|
|
||||||
*/
|
|
||||||
function sanitizeJsonOutput(
|
|
||||||
jsonObj: any,
|
|
||||||
showFullOutput: boolean,
|
|
||||||
): string | null {
|
|
||||||
if (showFullOutput) {
|
|
||||||
// In full output mode, return the full JSON
|
|
||||||
return JSON.stringify(jsonObj, null, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// In non-full-output mode, provide minimal safe output
|
|
||||||
const type = jsonObj.type;
|
|
||||||
const subtype = jsonObj.subtype;
|
|
||||||
|
|
||||||
// System initialization - safe to show
|
|
||||||
if (type === "system" && subtype === "init") {
|
|
||||||
return JSON.stringify(
|
|
||||||
{
|
|
||||||
type: "system",
|
|
||||||
subtype: "init",
|
|
||||||
message: "Claude Code initialized",
|
|
||||||
model: jsonObj.model || "unknown",
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Result messages - Always show the final result
|
|
||||||
if (type === "result") {
|
|
||||||
// These messages contain the final result and should always be visible
|
|
||||||
return JSON.stringify(
|
|
||||||
{
|
|
||||||
type: "result",
|
|
||||||
subtype: jsonObj.subtype,
|
|
||||||
is_error: jsonObj.is_error,
|
|
||||||
duration_ms: jsonObj.duration_ms,
|
|
||||||
num_turns: jsonObj.num_turns,
|
|
||||||
total_cost_usd: jsonObj.total_cost_usd,
|
|
||||||
permission_denials: jsonObj.permission_denials,
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For any other message types, suppress completely in non-full-output mode
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ClaudeOptions = {
|
export type ClaudeOptions = {
|
||||||
claudeArgs?: string;
|
claudeArgs?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
@@ -77,363 +11,11 @@ export type ClaudeOptions = {
|
|||||||
mcpConfig?: string;
|
mcpConfig?: string;
|
||||||
systemPrompt?: string;
|
systemPrompt?: string;
|
||||||
appendSystemPrompt?: string;
|
appendSystemPrompt?: string;
|
||||||
claudeEnv?: string;
|
|
||||||
fallbackModel?: string;
|
fallbackModel?: string;
|
||||||
showFullOutput?: string;
|
showFullOutput?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PreparedConfig = {
|
|
||||||
claudeArgs: string[];
|
|
||||||
promptPath: string;
|
|
||||||
env: Record<string, string>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function prepareRunConfig(
|
|
||||||
promptPath: string,
|
|
||||||
options: ClaudeOptions,
|
|
||||||
): PreparedConfig {
|
|
||||||
// Build Claude CLI arguments:
|
|
||||||
// 1. Prompt flag (always first)
|
|
||||||
// 2. User's claudeArgs (full control)
|
|
||||||
// 3. BASE_ARGS (always last, cannot be overridden)
|
|
||||||
|
|
||||||
const claudeArgs = ["-p"];
|
|
||||||
|
|
||||||
// Parse and add user's custom Claude arguments
|
|
||||||
if (options.claudeArgs?.trim()) {
|
|
||||||
const parsed = parseShellArgs(options.claudeArgs);
|
|
||||||
const customArgs = parsed.filter(
|
|
||||||
(arg): arg is string => typeof arg === "string",
|
|
||||||
);
|
|
||||||
claudeArgs.push(...customArgs);
|
|
||||||
}
|
|
||||||
|
|
||||||
// BASE_ARGS are always appended last (cannot be overridden)
|
|
||||||
claudeArgs.push(...BASE_ARGS);
|
|
||||||
|
|
||||||
const customEnv: Record<string, string> = {};
|
|
||||||
|
|
||||||
if (process.env.INPUT_ACTION_INPUTS_PRESENT) {
|
|
||||||
customEnv.GITHUB_ACTION_INPUTS = process.env.INPUT_ACTION_INPUTS_PRESENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
claudeArgs,
|
|
||||||
promptPath,
|
|
||||||
env: customEnv,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses session_id from execution file and sets GitHub Action output
|
|
||||||
* Exported for testing
|
|
||||||
*/
|
|
||||||
export async function parseAndSetSessionId(
|
|
||||||
executionFile: string,
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
const content = await readFile(executionFile, "utf-8");
|
|
||||||
const messages = JSON.parse(content) as {
|
|
||||||
type: string;
|
|
||||||
subtype?: string;
|
|
||||||
session_id?: string;
|
|
||||||
}[];
|
|
||||||
|
|
||||||
// Find the system.init message which contains session_id
|
|
||||||
const initMessage = messages.find(
|
|
||||||
(m) => m.type === "system" && m.subtype === "init",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (initMessage?.session_id) {
|
|
||||||
core.setOutput("session_id", initMessage.session_id);
|
|
||||||
core.info(`Set session_id: ${initMessage.session_id}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Don't fail the action if session_id extraction fails
|
|
||||||
core.warning(`Failed to extract session_id: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses structured_output from execution file and sets GitHub Action outputs
|
|
||||||
* Only runs if --json-schema was explicitly provided in claude_args
|
|
||||||
* Exported for testing
|
|
||||||
*/
|
|
||||||
export async function parseAndSetStructuredOutputs(
|
|
||||||
executionFile: string,
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
const content = await readFile(executionFile, "utf-8");
|
|
||||||
const messages = JSON.parse(content) as {
|
|
||||||
type: string;
|
|
||||||
structured_output?: Record<string, unknown>;
|
|
||||||
}[];
|
|
||||||
|
|
||||||
// Search backwards - result is typically last or second-to-last message
|
|
||||||
const result = messages.findLast(
|
|
||||||
(m) => m.type === "result" && m.structured_output,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!result?.structured_output) {
|
|
||||||
throw new Error(
|
|
||||||
`--json-schema was provided but Claude did not return structured_output.\n` +
|
|
||||||
`Found ${messages.length} messages. Result exists: ${!!result}\n`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the complete structured output as a single JSON string
|
|
||||||
// This works around GitHub Actions limitation that composite actions can't have dynamic outputs
|
|
||||||
const structuredOutputJson = JSON.stringify(result.structured_output);
|
|
||||||
core.setOutput("structured_output", structuredOutputJson);
|
|
||||||
core.info(
|
|
||||||
`Set structured_output with ${Object.keys(result.structured_output).length} field(s)`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
throw error; // Preserve original error and stack trace
|
|
||||||
}
|
|
||||||
throw new Error(`Failed to parse structured outputs: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function runClaude(promptPath: string, options: ClaudeOptions) {
|
export async function runClaude(promptPath: string, options: ClaudeOptions) {
|
||||||
// Feature flag: use SDK path by default, set USE_AGENT_SDK=false to use CLI
|
|
||||||
const useAgentSdk = process.env.USE_AGENT_SDK !== "false";
|
|
||||||
console.log(
|
|
||||||
`Using ${useAgentSdk ? "Agent SDK" : "CLI"} path (USE_AGENT_SDK=${process.env.USE_AGENT_SDK ?? "unset"})`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (useAgentSdk) {
|
|
||||||
const parsedOptions = parseSdkOptions(options);
|
const parsedOptions = parseSdkOptions(options);
|
||||||
return runClaudeWithSdk(promptPath, parsedOptions);
|
return runClaudeWithSdk(promptPath, parsedOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = prepareRunConfig(promptPath, options);
|
|
||||||
|
|
||||||
// Detect if --json-schema is present in claude args
|
|
||||||
const hasJsonSchema = options.claudeArgs?.includes("--json-schema") ?? false;
|
|
||||||
|
|
||||||
// Create a named pipe
|
|
||||||
try {
|
|
||||||
await unlink(PIPE_PATH);
|
|
||||||
} catch (e) {
|
|
||||||
// Ignore if file doesn't exist
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the named pipe
|
|
||||||
await execAsync(`mkfifo "${PIPE_PATH}"`);
|
|
||||||
|
|
||||||
// Log prompt file size
|
|
||||||
let promptSize = "unknown";
|
|
||||||
try {
|
|
||||||
const stats = await stat(config.promptPath);
|
|
||||||
promptSize = stats.size.toString();
|
|
||||||
} catch (e) {
|
|
||||||
// Ignore error
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Prompt file size: ${promptSize} bytes`);
|
|
||||||
|
|
||||||
// Log custom environment variables if any
|
|
||||||
const customEnvKeys = Object.keys(config.env).filter(
|
|
||||||
(key) => key !== "CLAUDE_ACTION_INPUTS_PRESENT",
|
|
||||||
);
|
|
||||||
if (customEnvKeys.length > 0) {
|
|
||||||
console.log(`Custom environment variables: ${customEnvKeys.join(", ")}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log custom arguments if any
|
|
||||||
if (options.claudeArgs && options.claudeArgs.trim() !== "") {
|
|
||||||
console.log(`Custom Claude arguments: ${options.claudeArgs}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output to console
|
|
||||||
console.log(`Running Claude with prompt from file: ${config.promptPath}`);
|
|
||||||
console.log(`Full command: claude ${config.claudeArgs.join(" ")}`);
|
|
||||||
|
|
||||||
// Start sending prompt to pipe in background
|
|
||||||
const catProcess = spawn("cat", [config.promptPath], {
|
|
||||||
stdio: ["ignore", "pipe", "inherit"],
|
|
||||||
});
|
|
||||||
const pipeStream = createWriteStream(PIPE_PATH);
|
|
||||||
catProcess.stdout.pipe(pipeStream);
|
|
||||||
|
|
||||||
catProcess.on("error", (error) => {
|
|
||||||
console.error("Error reading prompt file:", error);
|
|
||||||
pipeStream.destroy();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use custom executable path if provided, otherwise default to "claude"
|
|
||||||
const claudeExecutable = options.pathToClaudeCodeExecutable || "claude";
|
|
||||||
|
|
||||||
const claudeProcess = spawn(claudeExecutable, config.claudeArgs, {
|
|
||||||
stdio: ["pipe", "pipe", "inherit"],
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
...config.env,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle Claude process errors
|
|
||||||
claudeProcess.on("error", (error) => {
|
|
||||||
console.error("Error spawning Claude process:", error);
|
|
||||||
pipeStream.destroy();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Determine if full output should be shown
|
|
||||||
// Show full output if explicitly set to "true" OR if GitHub Actions debug mode is enabled
|
|
||||||
const isDebugMode = process.env.ACTIONS_STEP_DEBUG === "true";
|
|
||||||
let showFullOutput = options.showFullOutput === "true" || isDebugMode;
|
|
||||||
|
|
||||||
if (isDebugMode && options.showFullOutput !== "false") {
|
|
||||||
console.log("Debug mode detected - showing full output");
|
|
||||||
showFullOutput = true;
|
|
||||||
} else if (!showFullOutput) {
|
|
||||||
console.log("Running Claude Code (full output hidden for security)...");
|
|
||||||
console.log(
|
|
||||||
"Rerun in debug mode or enable `show_full_output: true` in your workflow file for full output.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Capture output for parsing execution metrics
|
|
||||||
let output = "";
|
|
||||||
claudeProcess.stdout.on("data", (data) => {
|
|
||||||
const text = data.toString();
|
|
||||||
|
|
||||||
// Try to parse as JSON and handle based on verbose setting
|
|
||||||
const lines = text.split("\n");
|
|
||||||
lines.forEach((line: string, index: number) => {
|
|
||||||
if (line.trim() === "") return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Check if this line is a JSON object
|
|
||||||
const parsed = JSON.parse(line);
|
|
||||||
const sanitizedOutput = sanitizeJsonOutput(parsed, showFullOutput);
|
|
||||||
|
|
||||||
if (sanitizedOutput) {
|
|
||||||
process.stdout.write(sanitizedOutput);
|
|
||||||
if (index < lines.length - 1 || text.endsWith("\n")) {
|
|
||||||
process.stdout.write("\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Not a JSON object
|
|
||||||
if (showFullOutput) {
|
|
||||||
// In full output mode, print as is
|
|
||||||
process.stdout.write(line);
|
|
||||||
if (index < lines.length - 1 || text.endsWith("\n")) {
|
|
||||||
process.stdout.write("\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// In non-full-output mode, suppress non-JSON output
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
output += text;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle stdout errors
|
|
||||||
claudeProcess.stdout.on("error", (error) => {
|
|
||||||
console.error("Error reading Claude stdout:", error);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Pipe from named pipe to Claude
|
|
||||||
const pipeProcess = spawn("cat", [PIPE_PATH]);
|
|
||||||
pipeProcess.stdout.pipe(claudeProcess.stdin);
|
|
||||||
|
|
||||||
// Handle pipe process errors
|
|
||||||
pipeProcess.on("error", (error) => {
|
|
||||||
console.error("Error reading from named pipe:", error);
|
|
||||||
claudeProcess.kill("SIGTERM");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for Claude to finish
|
|
||||||
const exitCode = await new Promise<number>((resolve) => {
|
|
||||||
claudeProcess.on("close", (code) => {
|
|
||||||
resolve(code || 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
claudeProcess.on("error", (error) => {
|
|
||||||
console.error("Claude process error:", error);
|
|
||||||
resolve(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clean up processes
|
|
||||||
try {
|
|
||||||
catProcess.kill("SIGTERM");
|
|
||||||
} catch (e) {
|
|
||||||
// Process may already be dead
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
pipeProcess.kill("SIGTERM");
|
|
||||||
} catch (e) {
|
|
||||||
// Process may already be dead
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up pipe file
|
|
||||||
try {
|
|
||||||
await unlink(PIPE_PATH);
|
|
||||||
} catch (e) {
|
|
||||||
// Ignore errors during cleanup
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set conclusion based on exit code
|
|
||||||
if (exitCode === 0) {
|
|
||||||
// Try to process the output and save execution metrics
|
|
||||||
try {
|
|
||||||
await writeFile("output.txt", output);
|
|
||||||
|
|
||||||
// Process output.txt into JSON and save to execution file
|
|
||||||
// Increase maxBuffer from Node.js default of 1MB to 10MB to handle large Claude outputs
|
|
||||||
const { stdout: jsonOutput } = await execAsync("jq -s '.' output.txt", {
|
|
||||||
maxBuffer: 10 * 1024 * 1024,
|
|
||||||
});
|
|
||||||
await writeFile(EXECUTION_FILE, jsonOutput);
|
|
||||||
|
|
||||||
console.log(`Log saved to ${EXECUTION_FILE}`);
|
|
||||||
} catch (e) {
|
|
||||||
core.warning(`Failed to process output for execution metrics: ${e}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
core.setOutput("execution_file", EXECUTION_FILE);
|
|
||||||
|
|
||||||
// Extract and set session_id
|
|
||||||
await parseAndSetSessionId(EXECUTION_FILE);
|
|
||||||
|
|
||||||
// Parse and set structured outputs only if user provided --json-schema in claude_args
|
|
||||||
if (hasJsonSchema) {
|
|
||||||
try {
|
|
||||||
await parseAndSetStructuredOutputs(EXECUTION_FILE);
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage =
|
|
||||||
error instanceof Error ? error.message : String(error);
|
|
||||||
core.setFailed(errorMessage);
|
|
||||||
core.setOutput("conclusion", "failure");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set conclusion to success if we reached here
|
|
||||||
core.setOutput("conclusion", "success");
|
|
||||||
} else {
|
|
||||||
core.setOutput("conclusion", "failure");
|
|
||||||
|
|
||||||
// Still try to save execution file if we have output
|
|
||||||
if (output) {
|
|
||||||
try {
|
|
||||||
await writeFile("output.txt", output);
|
|
||||||
// Increase maxBuffer from Node.js default of 1MB to 10MB to handle large Claude outputs
|
|
||||||
const { stdout: jsonOutput } = await execAsync("jq -s '.' output.txt", {
|
|
||||||
maxBuffer: 10 * 1024 * 1024,
|
|
||||||
});
|
|
||||||
await writeFile(EXECUTION_FILE, jsonOutput);
|
|
||||||
core.setOutput("execution_file", EXECUTION_FILE);
|
|
||||||
} catch (e) {
|
|
||||||
// Ignore errors when processing output during failure
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
process.exit(exitCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
#!/usr/bin/env bun
|
|
||||||
|
|
||||||
import { describe, test, expect } from "bun:test";
|
|
||||||
import { prepareRunConfig, type ClaudeOptions } from "../src/run-claude";
|
|
||||||
|
|
||||||
describe("prepareRunConfig", () => {
|
|
||||||
test("should prepare config with basic arguments", () => {
|
|
||||||
const options: ClaudeOptions = {};
|
|
||||||
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
|
|
||||||
|
|
||||||
expect(prepared.claudeArgs).toEqual([
|
|
||||||
"-p",
|
|
||||||
"--verbose",
|
|
||||||
"--output-format",
|
|
||||||
"stream-json",
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should include promptPath", () => {
|
|
||||||
const options: ClaudeOptions = {};
|
|
||||||
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
|
|
||||||
|
|
||||||
expect(prepared.promptPath).toBe("/tmp/test-prompt.txt");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should use provided prompt path", () => {
|
|
||||||
const options: ClaudeOptions = {};
|
|
||||||
const prepared = prepareRunConfig("/custom/prompt/path.txt", options);
|
|
||||||
|
|
||||||
expect(prepared.promptPath).toBe("/custom/prompt/path.txt");
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("claudeArgs handling", () => {
|
|
||||||
test("should parse and include custom claude arguments", () => {
|
|
||||||
const options: ClaudeOptions = {
|
|
||||||
claudeArgs: "--max-turns 10 --model claude-3-opus-20240229",
|
|
||||||
};
|
|
||||||
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
|
|
||||||
|
|
||||||
expect(prepared.claudeArgs).toEqual([
|
|
||||||
"-p",
|
|
||||||
"--max-turns",
|
|
||||||
"10",
|
|
||||||
"--model",
|
|
||||||
"claude-3-opus-20240229",
|
|
||||||
"--verbose",
|
|
||||||
"--output-format",
|
|
||||||
"stream-json",
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should handle empty claudeArgs", () => {
|
|
||||||
const options: ClaudeOptions = {
|
|
||||||
claudeArgs: "",
|
|
||||||
};
|
|
||||||
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
|
|
||||||
|
|
||||||
expect(prepared.claudeArgs).toEqual([
|
|
||||||
"-p",
|
|
||||||
"--verbose",
|
|
||||||
"--output-format",
|
|
||||||
"stream-json",
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should handle claudeArgs with quoted strings", () => {
|
|
||||||
const options: ClaudeOptions = {
|
|
||||||
claudeArgs: '--system-prompt "You are a helpful assistant"',
|
|
||||||
};
|
|
||||||
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
|
|
||||||
|
|
||||||
expect(prepared.claudeArgs).toEqual([
|
|
||||||
"-p",
|
|
||||||
"--system-prompt",
|
|
||||||
"You are a helpful assistant",
|
|
||||||
"--verbose",
|
|
||||||
"--output-format",
|
|
||||||
"stream-json",
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should include json-schema flag when provided", () => {
|
|
||||||
const options: ClaudeOptions = {
|
|
||||||
claudeArgs:
|
|
||||||
'--json-schema \'{"type":"object","properties":{"result":{"type":"boolean"}}}\'',
|
|
||||||
};
|
|
||||||
|
|
||||||
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
|
|
||||||
|
|
||||||
expect(prepared.claudeArgs).toContain("--json-schema");
|
|
||||||
expect(prepared.claudeArgs).toContain(
|
|
||||||
'{"type":"object","properties":{"result":{"type":"boolean"}}}',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
#!/usr/bin/env bun
|
|
||||||
|
|
||||||
import { describe, test, expect, afterEach, beforeEach, spyOn } from "bun:test";
|
|
||||||
import { writeFile, unlink } from "fs/promises";
|
|
||||||
import { tmpdir } from "os";
|
|
||||||
import { join } from "path";
|
|
||||||
import {
|
|
||||||
parseAndSetStructuredOutputs,
|
|
||||||
parseAndSetSessionId,
|
|
||||||
} from "../src/run-claude";
|
|
||||||
import * as core from "@actions/core";
|
|
||||||
|
|
||||||
// Mock execution file path
|
|
||||||
const TEST_EXECUTION_FILE = join(tmpdir(), "test-execution-output.json");
|
|
||||||
|
|
||||||
// Helper to create mock execution file with structured output
|
|
||||||
async function createMockExecutionFile(
|
|
||||||
structuredOutput?: Record<string, unknown>,
|
|
||||||
includeResult: boolean = true,
|
|
||||||
): Promise<void> {
|
|
||||||
const messages: any[] = [
|
|
||||||
{ type: "system", subtype: "init" },
|
|
||||||
{ type: "turn", content: "test" },
|
|
||||||
];
|
|
||||||
|
|
||||||
if (includeResult) {
|
|
||||||
messages.push({
|
|
||||||
type: "result",
|
|
||||||
cost_usd: 0.01,
|
|
||||||
duration_ms: 1000,
|
|
||||||
structured_output: structuredOutput,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await writeFile(TEST_EXECUTION_FILE, JSON.stringify(messages));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spy on core functions
|
|
||||||
let setOutputSpy: any;
|
|
||||||
let infoSpy: any;
|
|
||||||
let warningSpy: any;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
setOutputSpy = spyOn(core, "setOutput").mockImplementation(() => {});
|
|
||||||
infoSpy = spyOn(core, "info").mockImplementation(() => {});
|
|
||||||
warningSpy = spyOn(core, "warning").mockImplementation(() => {});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("parseAndSetStructuredOutputs", () => {
|
|
||||||
afterEach(async () => {
|
|
||||||
setOutputSpy?.mockRestore();
|
|
||||||
infoSpy?.mockRestore();
|
|
||||||
warningSpy?.mockRestore();
|
|
||||||
try {
|
|
||||||
await unlink(TEST_EXECUTION_FILE);
|
|
||||||
} catch {
|
|
||||||
// Ignore if file doesn't exist
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should set structured_output with valid data", async () => {
|
|
||||||
await createMockExecutionFile({
|
|
||||||
is_flaky: true,
|
|
||||||
confidence: 0.85,
|
|
||||||
summary: "Test looks flaky",
|
|
||||||
});
|
|
||||||
|
|
||||||
await parseAndSetStructuredOutputs(TEST_EXECUTION_FILE);
|
|
||||||
|
|
||||||
expect(setOutputSpy).toHaveBeenCalledWith(
|
|
||||||
"structured_output",
|
|
||||||
'{"is_flaky":true,"confidence":0.85,"summary":"Test looks flaky"}',
|
|
||||||
);
|
|
||||||
expect(infoSpy).toHaveBeenCalledWith(
|
|
||||||
"Set structured_output with 3 field(s)",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should handle arrays and nested objects", async () => {
|
|
||||||
await createMockExecutionFile({
|
|
||||||
items: ["a", "b", "c"],
|
|
||||||
config: { key: "value", nested: { deep: true } },
|
|
||||||
});
|
|
||||||
|
|
||||||
await parseAndSetStructuredOutputs(TEST_EXECUTION_FILE);
|
|
||||||
|
|
||||||
const callArgs = setOutputSpy.mock.calls[0];
|
|
||||||
expect(callArgs[0]).toBe("structured_output");
|
|
||||||
const parsed = JSON.parse(callArgs[1]);
|
|
||||||
expect(parsed).toEqual({
|
|
||||||
items: ["a", "b", "c"],
|
|
||||||
config: { key: "value", nested: { deep: true } },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should handle special characters in field names", async () => {
|
|
||||||
await createMockExecutionFile({
|
|
||||||
"test-result": "passed",
|
|
||||||
"item.count": 10,
|
|
||||||
"user@email": "test",
|
|
||||||
});
|
|
||||||
|
|
||||||
await parseAndSetStructuredOutputs(TEST_EXECUTION_FILE);
|
|
||||||
|
|
||||||
const callArgs = setOutputSpy.mock.calls[0];
|
|
||||||
const parsed = JSON.parse(callArgs[1]);
|
|
||||||
expect(parsed["test-result"]).toBe("passed");
|
|
||||||
expect(parsed["item.count"]).toBe(10);
|
|
||||||
expect(parsed["user@email"]).toBe("test");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should throw error when result exists but structured_output is undefined", async () => {
|
|
||||||
const messages = [
|
|
||||||
{ type: "system", subtype: "init" },
|
|
||||||
{ type: "result", cost_usd: 0.01, duration_ms: 1000 },
|
|
||||||
];
|
|
||||||
await writeFile(TEST_EXECUTION_FILE, JSON.stringify(messages));
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
parseAndSetStructuredOutputs(TEST_EXECUTION_FILE),
|
|
||||||
).rejects.toThrow(
|
|
||||||
"--json-schema was provided but Claude did not return structured_output",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should throw error when no result message exists", async () => {
|
|
||||||
const messages = [
|
|
||||||
{ type: "system", subtype: "init" },
|
|
||||||
{ type: "turn", content: "test" },
|
|
||||||
];
|
|
||||||
await writeFile(TEST_EXECUTION_FILE, JSON.stringify(messages));
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
parseAndSetStructuredOutputs(TEST_EXECUTION_FILE),
|
|
||||||
).rejects.toThrow(
|
|
||||||
"--json-schema was provided but Claude did not return structured_output",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should throw error with malformed JSON", async () => {
|
|
||||||
await writeFile(TEST_EXECUTION_FILE, "{ invalid json");
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
parseAndSetStructuredOutputs(TEST_EXECUTION_FILE),
|
|
||||||
).rejects.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should throw error when file does not exist", async () => {
|
|
||||||
await expect(
|
|
||||||
parseAndSetStructuredOutputs("/nonexistent/file.json"),
|
|
||||||
).rejects.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should handle empty structured_output object", async () => {
|
|
||||||
await createMockExecutionFile({});
|
|
||||||
|
|
||||||
await parseAndSetStructuredOutputs(TEST_EXECUTION_FILE);
|
|
||||||
|
|
||||||
expect(setOutputSpy).toHaveBeenCalledWith("structured_output", "{}");
|
|
||||||
expect(infoSpy).toHaveBeenCalledWith(
|
|
||||||
"Set structured_output with 0 field(s)",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("parseAndSetSessionId", () => {
|
|
||||||
afterEach(async () => {
|
|
||||||
setOutputSpy?.mockRestore();
|
|
||||||
infoSpy?.mockRestore();
|
|
||||||
warningSpy?.mockRestore();
|
|
||||||
try {
|
|
||||||
await unlink(TEST_EXECUTION_FILE);
|
|
||||||
} catch {
|
|
||||||
// Ignore if file doesn't exist
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should extract session_id from system.init message", async () => {
|
|
||||||
const messages = [
|
|
||||||
{ type: "system", subtype: "init", session_id: "test-session-123" },
|
|
||||||
{ type: "result", cost_usd: 0.01 },
|
|
||||||
];
|
|
||||||
await writeFile(TEST_EXECUTION_FILE, JSON.stringify(messages));
|
|
||||||
|
|
||||||
await parseAndSetSessionId(TEST_EXECUTION_FILE);
|
|
||||||
|
|
||||||
expect(setOutputSpy).toHaveBeenCalledWith("session_id", "test-session-123");
|
|
||||||
expect(infoSpy).toHaveBeenCalledWith("Set session_id: test-session-123");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should handle missing session_id gracefully", async () => {
|
|
||||||
const messages = [
|
|
||||||
{ type: "system", subtype: "init" },
|
|
||||||
{ type: "result", cost_usd: 0.01 },
|
|
||||||
];
|
|
||||||
await writeFile(TEST_EXECUTION_FILE, JSON.stringify(messages));
|
|
||||||
|
|
||||||
await parseAndSetSessionId(TEST_EXECUTION_FILE);
|
|
||||||
|
|
||||||
expect(setOutputSpy).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should handle missing system.init message gracefully", async () => {
|
|
||||||
const messages = [{ type: "result", cost_usd: 0.01 }];
|
|
||||||
await writeFile(TEST_EXECUTION_FILE, JSON.stringify(messages));
|
|
||||||
|
|
||||||
await parseAndSetSessionId(TEST_EXECUTION_FILE);
|
|
||||||
|
|
||||||
expect(setOutputSpy).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should handle malformed JSON gracefully with warning", async () => {
|
|
||||||
await writeFile(TEST_EXECUTION_FILE, "{ invalid json");
|
|
||||||
|
|
||||||
await parseAndSetSessionId(TEST_EXECUTION_FILE);
|
|
||||||
|
|
||||||
expect(setOutputSpy).not.toHaveBeenCalled();
|
|
||||||
expect(warningSpy).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should handle non-existent file gracefully with warning", async () => {
|
|
||||||
await parseAndSetSessionId("/nonexistent/file.json");
|
|
||||||
|
|
||||||
expect(setOutputSpy).not.toHaveBeenCalled();
|
|
||||||
expect(warningSpy).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user