mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-22 22:44:13 +08:00
Fixed test coverage gap where tests were only parsing JSON manually without actually invoking the parseAndSetStructuredOutputs function. Changes: - Export parseAndSetStructuredOutputs for testing - Rewrite tests to use spyOn() to mock @actions/core functions - Add tests that actually call the function and verify: - core.setOutput() called with correct JSON string - core.info() called with correct field count - Error thrown when result exists but structured_output undefined - Error thrown when no result message exists - Handles special characters in field names (hyphens, dots, @ symbols) - Handles arrays and nested objects correctly - File errors propagate correctly All 8 tests now properly test the actual implementation with full coverage of success and error paths. Addresses review comment: https://github.com/anthropics/claude-code-action/pull/683#discussion_r2539770213 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
159 lines
4.6 KiB
TypeScript
159 lines
4.6 KiB
TypeScript
#!/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 } 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;
|
|
|
|
beforeEach(() => {
|
|
setOutputSpy = spyOn(core, "setOutput").mockImplementation(() => {});
|
|
infoSpy = spyOn(core, "info").mockImplementation(() => {});
|
|
});
|
|
|
|
describe("parseAndSetStructuredOutputs", () => {
|
|
afterEach(async () => {
|
|
setOutputSpy?.mockRestore();
|
|
infoSpy?.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)",
|
|
);
|
|
});
|
|
});
|