Files
claude-code-action/base-action/test/structured-output.test.ts
inigo 8cd2cc1236 refactor: remove individual field outputs, keep only structured_output JSON
Since GitHub Actions composite actions cannot expose dynamic outputs,
individual field outputs were not accessible anyway and only added
complexity and collision risk.

Simplified by:
- Removing individual core.setOutput() calls for each field
- Removing RESERVED_OUTPUTS check (no longer needed)
- Removing sanitizeOutputName, convertToString, MAX_OUTPUT_SIZE helpers
- Removing related unit tests for removed functionality

Users access all fields via single structured_output JSON string:
  fromJSON(steps.<id>.outputs.structured_output).field_name

Or with jq:
  echo '${{ steps.<id>.outputs.structured_output }}' | jq '.field_name'

All tests pass (462 tests).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 14:01:58 -08:00

184 lines
5.3 KiB
TypeScript

#!/usr/bin/env bun
import { describe, test, expect, afterEach } from "bun:test";
import { writeFile, unlink } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";
// Import the type for testing
type ExecutionMessage = {
type: string;
structured_output?: Record<string, unknown>;
};
// 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: ExecutionMessage[] = [
{ type: "system", subtype: "init" } as any,
{ type: "turn", content: "test" } as any,
];
if (includeResult) {
messages.push({
type: "result",
cost_usd: 0.01,
duration_ms: 1000,
structured_output: structuredOutput,
} as any);
}
await writeFile(TEST_EXECUTION_FILE, JSON.stringify(messages));
}
describe("Structured Output Parsing", () => {
afterEach(async () => {
try {
await unlink(TEST_EXECUTION_FILE);
} catch {
// Ignore if file doesn't exist
}
});
describe("parseAndSetStructuredOutputs integration", () => {
test("should handle array outputs", async () => {
await createMockExecutionFile({
affected_areas: ["auth", "database", "api"],
severity: "high",
});
const content = await Bun.file(TEST_EXECUTION_FILE).text();
const messages = JSON.parse(content) as ExecutionMessage[];
const result = messages.find(
(m) => m.type === "result" && m.structured_output,
);
expect(result?.structured_output?.affected_areas).toEqual([
"auth",
"database",
"api",
]);
});
test("should handle nested objects", async () => {
await createMockExecutionFile({
analysis: {
category: "test",
details: { count: 5, passed: true },
},
});
const content = await Bun.file(TEST_EXECUTION_FILE).text();
const messages = JSON.parse(content) as ExecutionMessage[];
const result = messages.find(
(m) => m.type === "result" && m.structured_output,
);
expect(result?.structured_output?.analysis).toEqual({
category: "test",
details: { count: 5, passed: true },
});
});
test("should handle missing structured_output", async () => {
await createMockExecutionFile(undefined, true);
const content = await Bun.file(TEST_EXECUTION_FILE).text();
const messages = JSON.parse(content) as ExecutionMessage[];
const result = messages.find(
(m) => m.type === "result" && m.structured_output,
);
expect(result).toBeUndefined();
});
test("should handle empty structured_output", async () => {
await createMockExecutionFile({});
const content = await Bun.file(TEST_EXECUTION_FILE).text();
const messages = JSON.parse(content) as ExecutionMessage[];
const result = messages.find(
(m) => m.type === "result" && m.structured_output,
);
expect(result?.structured_output).toEqual({});
});
test("should handle all supported types", async () => {
await createMockExecutionFile({
string_field: "hello",
number_field: 42,
boolean_field: true,
null_field: null,
array_field: [1, 2, 3],
object_field: { nested: "value" },
});
const content = await Bun.file(TEST_EXECUTION_FILE).text();
const messages = JSON.parse(content) as ExecutionMessage[];
const result = messages.find(
(m) => m.type === "result" && m.structured_output,
);
expect(result?.structured_output).toMatchObject({
string_field: "hello",
number_field: 42,
boolean_field: true,
null_field: null,
array_field: [1, 2, 3],
object_field: { nested: "value" },
});
});
});
describe("error scenarios", () => {
test("should handle malformed JSON", async () => {
await writeFile(TEST_EXECUTION_FILE, "invalid json {");
let error: Error | undefined;
try {
const content = await Bun.file(TEST_EXECUTION_FILE).text();
JSON.parse(content);
} catch (e) {
error = e as Error;
}
expect(error).toBeDefined();
expect(error?.message).toContain("JSON");
});
test("should handle empty execution file", async () => {
await writeFile(TEST_EXECUTION_FILE, "[]");
const content = await Bun.file(TEST_EXECUTION_FILE).text();
const messages = JSON.parse(content) as ExecutionMessage[];
const result = messages.find(
(m) => m.type === "result" && m.structured_output,
);
expect(result).toBeUndefined();
});
test("should handle missing result message", async () => {
const messages = [
{ type: "system", subtype: "init" },
{ type: "turn", content: "test" },
];
await writeFile(TEST_EXECUTION_FILE, JSON.stringify(messages));
const content = await Bun.file(TEST_EXECUTION_FILE).text();
const parsed = JSON.parse(content) as ExecutionMessage[];
const result = parsed.find(
(m) => m.type === "result" && m.structured_output,
);
expect(result).toBeUndefined();
});
});
});