diff --git a/.gitignore b/.gitignore
index a5d6bf5a29..acbab7ab2e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -136,3 +136,4 @@ actions/setup-safe-outputs/js/
# License compliance reports
licenses.csv
*.bck.yml
+actions/setup/js/test-*/
diff --git a/actions/setup/js/parse_claude_log.test.cjs b/actions/setup/js/parse_claude_log.test.cjs
index ea1d92f01b..9682664bef 100644
--- a/actions/setup/js/parse_claude_log.test.cjs
+++ b/actions/setup/js/parse_claude_log.test.cjs
@@ -1,472 +1,530 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import fs from "fs";
import path from "path";
+
describe("parse_claude_log.cjs", () => {
- let mockCore, parseClaudeLogScript, originalConsole, originalProcess;
- (beforeEach(() => {
- ((originalConsole = global.console),
- (originalProcess = { ...process }),
- (global.console = { log: vi.fn(), error: vi.fn() }),
- (mockCore = {
- debug: vi.fn(),
- info: vi.fn(),
- notice: vi.fn(),
- warning: vi.fn(),
- error: vi.fn(),
- setFailed: vi.fn(),
- setOutput: vi.fn(),
- exportVariable: vi.fn(),
- setSecret: vi.fn(),
- getInput: vi.fn(),
- getBooleanInput: vi.fn(),
- getMultilineInput: vi.fn(),
- getState: vi.fn(),
- saveState: vi.fn(),
- startGroup: vi.fn(),
- endGroup: vi.fn(),
- group: vi.fn(),
- addPath: vi.fn(),
- setCommandEcho: vi.fn(),
- isDebug: vi.fn().mockReturnValue(!1),
- getIDToken: vi.fn(),
- toPlatformPath: vi.fn(),
- toPosixPath: vi.fn(),
- toWin32Path: vi.fn(),
- summary: { addRaw: vi.fn().mockReturnThis(), write: vi.fn().mockResolvedValue() },
- }),
- (global.core = mockCore),
- (global.require = vi.fn().mockImplementation(module => {
- if ("fs" === module) return fs;
- if ("@actions/core" === module) return mockCore;
- if ("./log_parser_bootstrap.cjs" === module) return require("./log_parser_bootstrap.cjs");
- if ("./log_parser_shared.cjs" === module) return require("./log_parser_shared.cjs");
- throw new Error(`Module not found: ${module}`);
- })));
- const scriptPath = path.join(__dirname, "parse_claude_log.cjs");
- parseClaudeLogScript = fs.readFileSync(scriptPath, "utf8");
- }),
- afterEach(() => {
- (delete process.env.GH_AW_AGENT_OUTPUT, (global.console = originalConsole), (process.env = originalProcess.env), delete global.core, delete global.require);
- }));
+ let mockCore, originalConsole, originalProcess;
+ let main, parseClaudeLog;
+
+ beforeEach(async () => {
+ originalConsole = global.console;
+ originalProcess = { ...process };
+ global.console = { log: vi.fn(), error: vi.fn() };
+
+ mockCore = {
+ debug: vi.fn(),
+ info: vi.fn(),
+ notice: vi.fn(),
+ warning: vi.fn(),
+ error: vi.fn(),
+ setFailed: vi.fn(),
+ setOutput: vi.fn(),
+ exportVariable: vi.fn(),
+ setSecret: vi.fn(),
+ getInput: vi.fn(),
+ getBooleanInput: vi.fn(),
+ getMultilineInput: vi.fn(),
+ getState: vi.fn(),
+ saveState: vi.fn(),
+ startGroup: vi.fn(),
+ endGroup: vi.fn(),
+ group: vi.fn(),
+ addPath: vi.fn(),
+ setCommandEcho: vi.fn(),
+ isDebug: vi.fn().mockReturnValue(false),
+ getIDToken: vi.fn(),
+ toPlatformPath: vi.fn(),
+ toPosixPath: vi.fn(),
+ toWin32Path: vi.fn(),
+ summary: { addRaw: vi.fn().mockReturnThis(), write: vi.fn().mockResolvedValue() },
+ };
+
+ global.core = mockCore;
+
+ // Import the module to get the exported functions
+ const module = await import("./parse_claude_log.cjs?" + Date.now());
+ main = module.main;
+ parseClaudeLog = module.parseClaudeLog;
+ });
+
+ afterEach(() => {
+ delete process.env.GH_AW_AGENT_OUTPUT;
+ delete process.env.GH_AW_MAX_TURNS;
+ global.console = originalConsole;
+ process.env = originalProcess.env;
+ delete global.core;
+ });
+
const runScript = async logContent => {
- const tempFile = path.join(process.cwd(), `test_log_${Date.now()}.txt`);
- (fs.writeFileSync(tempFile, logContent), (process.env.GH_AW_AGENT_OUTPUT = tempFile));
- try {
- const scriptWithExports = parseClaudeLogScript.replace("main();", "global.testParseClaudeLog = parseClaudeLog; global.testMain = main; main();"),
- scriptFunction = new Function(scriptWithExports);
- await scriptFunction();
- } finally {
- fs.existsSync(tempFile) && fs.unlinkSync(tempFile);
+ const tempFile = path.join(process.cwd(), `test_log_${Date.now()}.txt`);
+ fs.writeFileSync(tempFile, logContent);
+ process.env.GH_AW_AGENT_OUTPUT = tempFile;
+ try {
+ await main();
+ } finally {
+ if (fs.existsSync(tempFile)) {
+ fs.unlinkSync(tempFile);
}
- },
- extractParseFunction = () => {
- const scriptWithExport = parseClaudeLogScript.replace("main();", "global.testParseClaudeLog = parseClaudeLog;");
- return (new Function(scriptWithExport)(), global.testParseClaudeLog);
- };
- (describe("parseClaudeLog function", () => {
- let parseClaudeLog;
- (beforeEach(() => {
- parseClaudeLog = extractParseFunction();
- }),
- it("should parse old JSON array format", () => {
- const jsonArrayLog = JSON.stringify([
- { type: "system", subtype: "init", session_id: "test-123", tools: ["Bash", "Read"], model: "claude-sonnet-4-20250514" },
+ }
+ };
+
+ describe("parseClaudeLog function", () => {
+ it("should parse old JSON array format", () => {
+ const jsonArrayLog = JSON.stringify([
+ { type: "system", subtype: "init", session_id: "test-123", tools: ["Bash", "Read"], model: "claude-sonnet-4-20250514" },
+ {
+ type: "assistant",
+ message: {
+ content: [
+ { type: "text", text: "I'll help you with this task." },
+ { type: "tool_use", id: "tool_123", name: "Bash", input: { command: "echo 'Hello World'" } },
+ ],
+ },
+ },
+ { type: "result", total_cost_usd: 0.0015, usage: { input_tokens: 150, output_tokens: 50 }, num_turns: 1 },
+ ]);
+ const result = parseClaudeLog(jsonArrayLog);
+
+ expect(result.markdown).toContain("🚀 Initialization");
+ expect(result.markdown).toContain("🤖 Commands and Tools");
+ expect(result.markdown).toContain("test-123");
+ expect(result.markdown).toContain("echo 'Hello World'");
+ expect(result.markdown).toContain("Total Cost");
+ expect(result.mcpFailures).toEqual([]);
+ });
+
+ it("should parse new mixed format with debug logs and JSON array", () => {
+ const result = parseClaudeLog(
+ '[DEBUG] Starting Claude Code CLI\n[ERROR] Some error occurred\nnpm warn exec The following package was not found\n[{"type":"system","subtype":"init","session_id":"29d324d8-1a92-43c6-8740-babc2875a1d6","tools":["Task","Bash","mcp__safe_outputs__missing-tool"],"model":"claude-sonnet-4-20250514"},{"type":"assistant","message":{"content":[{"type":"tool_use","id":"tool_123","name":"mcp__safe_outputs__missing-tool","input":{"tool":"draw_pelican","reason":"Tool needed to draw pelican artwork"}}]}},{"type":"result","total_cost_usd":0.1789264,"usage":{"input_tokens":25,"output_tokens":832},"num_turns":10}]\n[DEBUG] Session completed'
+ );
+
+ expect(result.markdown).toContain("🚀 Initialization");
+ expect(result.markdown).toContain("🤖 Commands and Tools");
+ expect(result.markdown).toContain("29d324d8-1a92-43c6-8740-babc2875a1d6");
+ expect(result.markdown).toContain("safe_outputs::missing-tool");
+ expect(result.markdown).toContain("Total Cost");
+ expect(result.mcpFailures).toEqual([]);
+ });
+
+ it("should parse mixed format with individual JSON lines", () => {
+ const result = parseClaudeLog(
+ '[DEBUG] Starting Claude Code CLI\n{"type":"system","subtype":"init","session_id":"test-456","tools":["Bash","Read"],"model":"claude-sonnet-4-20250514"}\n[DEBUG] Processing user prompt\n{"type":"assistant","message":{"content":[{"type":"text","text":"I\'ll help you."},{"type":"tool_use","id":"tool_123","name":"Bash","input":{"command":"ls -la"}}]}}\n{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"tool_123","content":"file1.txt\\nfile2.txt"}]}}\n{"type":"result","total_cost_usd":0.002,"usage":{"input_tokens":100,"output_tokens":25},"num_turns":2}\n[DEBUG] Workflow completed'
+ );
+
+ expect(result.markdown).toContain("🚀 Initialization");
+ expect(result.markdown).toContain("🤖 Commands and Tools");
+ expect(result.markdown).toContain("test-456");
+ expect(result.markdown).toContain("ls -la");
+ expect(result.markdown).toContain("Total Cost");
+ expect(result.mcpFailures).toEqual([]);
+ });
+
+ it("should handle MCP server failures", () => {
+ const logWithFailures = JSON.stringify([
+ {
+ type: "system",
+ subtype: "init",
+ session_id: "test-789",
+ tools: ["Bash"],
+ mcp_servers: [
+ { name: "github", status: "connected" },
+ { name: "failed_server", status: "failed" },
+ ],
+ model: "claude-sonnet-4-20250514",
+ },
+ ]);
+ const result = parseClaudeLog(logWithFailures);
+
+ expect(result.markdown).toContain("🚀 Initialization");
+ expect(result.markdown).toContain("failed_server (failed)");
+ expect(result.mcpFailures).toEqual(["failed_server"]);
+ });
+
+ it("should display detailed error information for failed MCP servers", () => {
+ const logWithDetailedErrors = JSON.stringify([
+ {
+ type: "system",
+ subtype: "init",
+ session_id: "test-detailed-errors",
+ tools: ["Bash"],
+ mcp_servers: [
+ { name: "working_server", status: "connected" },
{
- type: "assistant",
- message: {
- content: [
- { type: "text", text: "I'll help you with this task." },
- { type: "tool_use", id: "tool_123", name: "Bash", input: { command: "echo 'Hello World'" } },
- ],
- },
- },
- { type: "result", total_cost_usd: 0.0015, usage: { input_tokens: 150, output_tokens: 50 }, num_turns: 1 },
- ]),
- result = parseClaudeLog(jsonArrayLog);
- (expect(result.markdown).toContain("🚀 Initialization"),
- expect(result.markdown).toContain("🤖 Commands and Tools"),
- expect(result.markdown).toContain("test-123"),
- expect(result.markdown).toContain("echo 'Hello World'"),
- expect(result.markdown).toContain("Total Cost"),
- expect(result.mcpFailures).toEqual([]));
- }),
- it("should parse new mixed format with debug logs and JSON array", () => {
- const result = parseClaudeLog(
- '[DEBUG] Starting Claude Code CLI\n[ERROR] Some error occurred\nnpm warn exec The following package was not found\n[{"type":"system","subtype":"init","session_id":"29d324d8-1a92-43c6-8740-babc2875a1d6","tools":["Task","Bash","mcp__safe_outputs__missing-tool"],"model":"claude-sonnet-4-20250514"},{"type":"assistant","message":{"content":[{"type":"tool_use","id":"tool_123","name":"mcp__safe_outputs__missing-tool","input":{"tool":"draw_pelican","reason":"Tool needed to draw pelican artwork"}}]}},{"type":"result","total_cost_usd":0.1789264,"usage":{"input_tokens":25,"output_tokens":832},"num_turns":10}]\n[DEBUG] Session completed'
- );
- (expect(result.markdown).toContain("🚀 Initialization"),
- expect(result.markdown).toContain("🤖 Commands and Tools"),
- expect(result.markdown).toContain("29d324d8-1a92-43c6-8740-babc2875a1d6"),
- expect(result.markdown).toContain("safe_outputs::missing-tool"),
- expect(result.markdown).toContain("Total Cost"),
- expect(result.mcpFailures).toEqual([]));
- }),
- it("should parse mixed format with individual JSON lines", () => {
- const result = parseClaudeLog(
- '[DEBUG] Starting Claude Code CLI\n{"type":"system","subtype":"init","session_id":"test-456","tools":["Bash","Read"],"model":"claude-sonnet-4-20250514"}\n[DEBUG] Processing user prompt\n{"type":"assistant","message":{"content":[{"type":"text","text":"I\'ll help you."},{"type":"tool_use","id":"tool_123","name":"Bash","input":{"command":"ls -la"}}]}}\n{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"tool_123","content":"file1.txt\\nfile2.txt"}]}}\n{"type":"result","total_cost_usd":0.002,"usage":{"input_tokens":100,"output_tokens":25},"num_turns":2}\n[DEBUG] Workflow completed'
- );
- (expect(result.markdown).toContain("🚀 Initialization"),
- expect(result.markdown).toContain("🤖 Commands and Tools"),
- expect(result.markdown).toContain("test-456"),
- expect(result.markdown).toContain("ls -la"),
- expect(result.markdown).toContain("Total Cost"),
- expect(result.mcpFailures).toEqual([]));
- }),
- it("should handle MCP server failures", () => {
- const logWithFailures = JSON.stringify([
- {
- type: "system",
- subtype: "init",
- session_id: "test-789",
- tools: ["Bash"],
- mcp_servers: [
- { name: "github", status: "connected" },
- { name: "failed_server", status: "failed" },
- ],
- model: "claude-sonnet-4-20250514",
+ name: "failed_with_error",
+ status: "failed",
+ error: "Connection timeout after 30s",
+ stderr: "Error: ECONNREFUSED connect ECONNREFUSED 127.0.0.1:3000\n at TCPConnectWrap.afterConnect",
+ exitCode: 1,
+ command: "npx @github/github-mcp-server",
},
- ]),
- result = parseClaudeLog(logWithFailures);
- (expect(result.markdown).toContain("🚀 Initialization"), expect(result.markdown).toContain("failed_server (failed)"), expect(result.mcpFailures).toEqual(["failed_server"]));
- }),
- it("should display detailed error information for failed MCP servers", () => {
- const logWithDetailedErrors = JSON.stringify([
- {
- type: "system",
- subtype: "init",
- session_id: "test-detailed-errors",
- tools: ["Bash"],
- mcp_servers: [
- { name: "working_server", status: "connected" },
+ ],
+ model: "claude-sonnet-4-20250514",
+ },
+ ]);
+ const result = parseClaudeLog(logWithDetailedErrors);
+
+ expect(result.markdown).toContain("🚀 Initialization");
+ expect(result.markdown).toContain("failed_with_error (failed)");
+ expect(result.markdown).toContain("**Error:** Connection timeout after 30s");
+ expect(result.markdown).toContain("**Stderr:**");
+ expect(result.markdown).toContain("**Exit Code:** 1");
+ expect(result.markdown).toContain("**Command:** `npx @github/github-mcp-server`");
+ expect(result.mcpFailures).toEqual(["failed_with_error"]);
+ });
+
+ it("should handle MCP server failures with message and reason fields", () => {
+ const logWithMessageAndReason = JSON.stringify([
+ {
+ type: "system",
+ subtype: "init",
+ session_id: "test-message-reason",
+ tools: ["Bash"],
+ mcp_servers: [{ name: "failed_server", status: "failed", message: "Failed to initialize", reason: "Network error" }],
+ model: "claude-sonnet-4-20250514",
+ },
+ ]);
+ const result = parseClaudeLog(logWithMessageAndReason);
+
+ expect(result.markdown).toContain("failed_server (failed)");
+ expect(result.markdown).toContain("**Message:** Failed to initialize");
+ expect(result.markdown).toContain("**Reason:** Network error");
+ });
+
+ it("should truncate long stderr output", () => {
+ const longStderr = "x".repeat(600);
+ const logWithLongStderr = JSON.stringify([
+ {
+ type: "system",
+ subtype: "init",
+ session_id: "test-long-stderr",
+ tools: ["Bash"],
+ mcp_servers: [{ name: "failed_server", status: "failed", stderr: longStderr }],
+ model: "claude-sonnet-4-20250514",
+ },
+ ]);
+ const result = parseClaudeLog(logWithLongStderr);
+
+ expect(result.markdown).toContain("...");
+ expect(result.markdown).not.toContain(longStderr);
+ });
+
+ it("should handle MCP server failures with partial error information", () => {
+ const logWithPartialInfo = JSON.stringify([
+ {
+ type: "system",
+ subtype: "init",
+ session_id: "test-partial",
+ tools: ["Bash"],
+ mcp_servers: [
+ { name: "partial_error_1", status: "failed", error: "Connection refused" },
+ { name: "partial_error_2", status: "failed", stderr: "Something went wrong" },
+ { name: "partial_error_3", status: "failed", exitCode: 127 },
+ ],
+ model: "claude-sonnet-4-20250514",
+ },
+ ]);
+ const result = parseClaudeLog(logWithPartialInfo);
+
+ expect(result.markdown).toContain("partial_error_1 (failed)");
+ expect(result.markdown).toContain("**Error:** Connection refused");
+ expect(result.markdown).toContain("partial_error_2 (failed)");
+ expect(result.markdown).toContain("**Stderr:**");
+ expect(result.markdown).toContain("partial_error_3 (failed)");
+ expect(result.markdown).toContain("**Exit Code:** 127");
+ });
+
+ it("should handle exitCode zero for failed servers", () => {
+ const logWithExitCodeZero = JSON.stringify([
+ {
+ type: "system",
+ subtype: "init",
+ session_id: "test-exit-zero",
+ tools: ["Bash"],
+ mcp_servers: [{ name: "failed_but_exit_zero", status: "failed", exitCode: 0, error: "Server exited unexpectedly" }],
+ model: "claude-sonnet-4-20250514",
+ },
+ ]);
+ const result = parseClaudeLog(logWithExitCodeZero);
+
+ expect(result.markdown).toContain("failed_but_exit_zero (failed)");
+ expect(result.markdown).toContain("**Error:** Server exited unexpectedly");
+ expect(result.markdown).toContain("**Exit Code:** 0");
+ });
+
+ it("should handle unrecognized log format", () => {
+ const result = parseClaudeLog("This is not JSON or valid format");
+ expect(result.markdown).toContain("Log format not recognized");
+ });
+
+ it("should handle empty log content", () => {
+ const result = parseClaudeLog("");
+ expect(result.markdown).toContain("Log format not recognized");
+ });
+
+ it("should skip debug lines that look like arrays but aren't JSON", () => {
+ const result = parseClaudeLog(
+ '[DEBUG] Starting\n[INFO] Processing\n[{"type":"system","subtype":"init","session_id":"test","tools":["Bash"],"model":"claude-sonnet-4-20250514"}]\n[DEBUG] Done'
+ );
+ expect(result.markdown).toContain("🚀 Initialization");
+ });
+
+ it("should handle tool use with MCP tools", () => {
+ const logWithMcpTools = JSON.stringify([
+ {
+ type: "system",
+ subtype: "init",
+ session_id: "mcp-test",
+ tools: ["Bash", "mcp__github__create_issue"],
+ model: "claude-sonnet-4-20250514",
+ },
+ { type: "assistant", message: { content: [{ type: "tool_use", id: "tool_1", name: "mcp__github__create_issue", input: { title: "Test" } }] } },
+ { type: "result", total_cost_usd: 0.01, usage: { input_tokens: 100, output_tokens: 50 }, num_turns: 1 },
+ ]);
+ const result = parseClaudeLog(logWithMcpTools);
+
+ expect(result.markdown).toContain("github::create_issue");
+ expect(result.mcpFailures).toEqual([]);
+ });
+
+ it("should detect when max-turns limit is hit", () => {
+ process.env.GH_AW_MAX_TURNS = "5";
+ const logWithMaxTurns = JSON.stringify([
+ { type: "system", subtype: "init", session_id: "max-turns", tools: ["Bash"], model: "claude-sonnet-4-20250514" },
+ { type: "result", total_cost_usd: 0.05, usage: { input_tokens: 500, output_tokens: 250 }, num_turns: 5 },
+ ]);
+ const result = parseClaudeLog(logWithMaxTurns);
+
+ expect(result.markdown).toContain("**Turns:** 5");
+ expect(result.maxTurnsHit).toBe(true);
+ });
+
+ it("should not flag max-turns when turns is less than limit", () => {
+ process.env.GH_AW_MAX_TURNS = "5";
+ const logBelowMaxTurns = JSON.stringify([
+ { type: "system", subtype: "init", session_id: "below-max", tools: ["Bash"], model: "claude-sonnet-4-20250514" },
+ { type: "result", total_cost_usd: 0.01, usage: { input_tokens: 100, output_tokens: 50 }, num_turns: 3 },
+ ]);
+ const result = parseClaudeLog(logBelowMaxTurns);
+
+ expect(result.markdown).toContain("**Turns:** 3");
+ expect(result.maxTurnsHit).toBe(false);
+ });
+
+ it("should not flag max-turns when environment variable is not set", () => {
+ delete process.env.GH_AW_MAX_TURNS;
+ const logWithoutMaxTurnsEnv = JSON.stringify([
+ { type: "system", subtype: "init", session_id: "no-env", tools: ["Bash"], model: "claude-sonnet-4-20250514" },
+ { type: "result", total_cost_usd: 0.01, usage: { input_tokens: 100, output_tokens: 50 }, num_turns: 10 },
+ ]);
+ const result = parseClaudeLog(logWithoutMaxTurnsEnv);
+
+ expect(result.markdown).toContain("**Turns:** 10");
+ expect(result.maxTurnsHit).toBe(false);
+ });
+ });
+
+ describe("main function integration", () => {
+ it("should handle valid log file", async () => {
+ const validLog = JSON.stringify([
+ { type: "system", subtype: "init", session_id: "integration-test", tools: ["Bash"], model: "claude-sonnet-4-20250514" },
+ { type: "result", total_cost_usd: 0.001, usage: { input_tokens: 50, output_tokens: 25 }, num_turns: 1 },
+ ]);
+
+ await runScript(validLog);
+
+ expect(mockCore.summary.addRaw).toHaveBeenCalled();
+ expect(mockCore.summary.write).toHaveBeenCalled();
+ expect(mockCore.setFailed).not.toHaveBeenCalled();
+
+ const markdownCall = mockCore.summary.addRaw.mock.calls[0];
+ expect(markdownCall[0]).toContain("```");
+ expect(markdownCall[0]).toContain("Conversation:");
+ expect(markdownCall[0]).toContain("Statistics:");
+ expect(mockCore.info).toHaveBeenCalled();
+
+ const infoCall = mockCore.info.mock.calls.find(call => call[0].includes("=== Claude Execution Summary ==="));
+ expect(infoCall).toBeDefined();
+ expect(infoCall[0]).toContain("Model: claude-sonnet-4-20250514");
+ });
+
+ it("should handle log with MCP failures", async () => {
+ const logWithFailures = JSON.stringify([
+ {
+ type: "system",
+ subtype: "init",
+ session_id: "failure-test",
+ mcp_servers: [
+ { name: "working_server", status: "connected" },
+ { name: "broken_server", status: "failed" },
+ ],
+ tools: ["Bash"],
+ model: "claude-sonnet-4-20250514",
+ },
+ ]);
+
+ await runScript(logWithFailures);
+
+ expect(mockCore.summary.addRaw).toHaveBeenCalled();
+ expect(mockCore.summary.write).toHaveBeenCalled();
+ expect(mockCore.setFailed).toHaveBeenCalledWith("MCP server(s) failed to launch: broken_server");
+ });
+
+ it("should call setFailed when max-turns limit is hit", async () => {
+ process.env.GH_AW_MAX_TURNS = "3";
+ const logHittingMaxTurns = JSON.stringify([
+ { type: "system", subtype: "init", session_id: "max-turns-test", tools: ["Bash"], model: "claude-sonnet-4-20250514" },
+ { type: "result", total_cost_usd: 0.02, usage: { input_tokens: 200, output_tokens: 100 }, num_turns: 3 },
+ ]);
+
+ await runScript(logHittingMaxTurns);
+
+ expect(mockCore.summary.addRaw).toHaveBeenCalled();
+ expect(mockCore.summary.write).toHaveBeenCalled();
+ expect(mockCore.setFailed).toHaveBeenCalledWith("Agent execution stopped: max-turns limit reached. The agent did not complete its task successfully.");
+ });
+
+ it("should handle missing log file", async () => {
+ process.env.GH_AW_AGENT_OUTPUT = "/nonexistent/file.log";
+ await main();
+ expect(mockCore.info).toHaveBeenCalledWith("Log path not found: /nonexistent/file.log");
+ expect(mockCore.setFailed).not.toHaveBeenCalled();
+ });
+
+ it("should handle missing environment variable", async () => {
+ delete process.env.GH_AW_AGENT_OUTPUT;
+ await main();
+ expect(mockCore.info).toHaveBeenCalledWith("No agent log file specified");
+ expect(mockCore.setFailed).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("helper function tests", () => {
+ it("should format bash commands correctly", () => {
+ const result = parseClaudeLog(JSON.stringify([{ type: "assistant", message: { content: [{ type: "tool_use", id: "tool_1", name: "Bash", input: { command: "echo 'hello world'\n && ls -la\n && pwd" } }] } }]));
+ expect(result.markdown).toContain("echo 'hello world' && ls -la && pwd");
+ });
+
+ it("should truncate long strings appropriately", () => {
+ const longCommand = "a".repeat(400);
+ const result = parseClaudeLog(JSON.stringify([{ type: "assistant", message: { content: [{ type: "tool_use", id: "tool_1", name: "Bash", input: { command: longCommand } }] } }]));
+ expect(result.markdown).toContain("...");
+ });
+
+ it("should format MCP tool names correctly", () => {
+ const result = parseClaudeLog(JSON.stringify([{ type: "assistant", message: { content: [{ type: "tool_use", id: "tool_1", name: "mcp__github__create_pull_request", input: { title: "Test PR" } }] } }]));
+ expect(result.markdown).toContain("github::create_pull_request");
+ });
+
+ it("should render tool outputs in collapsible HTML details elements", () => {
+ const result = parseClaudeLog(
+ JSON.stringify([
+ { type: "assistant", message: { content: [{ type: "tool_use", id: "tool_1", name: "Bash", input: { command: "ls -la", description: "List files" } }] } },
+ {
+ type: "user",
+ message: {
+ content: [
{
- name: "failed_with_error",
- status: "failed",
- error: "Connection timeout after 30s",
- stderr: "Error: ECONNREFUSED connect ECONNREFUSED 127.0.0.1:3000\n at TCPConnectWrap.afterConnect",
- exitCode: 1,
- command: "npx @github/github-mcp-server",
+ type: "tool_result",
+ tool_use_id: "tool_1",
+ content: "total 24\ndrwxr-xr-x 6 user staff 192 Jan 1 12:00 .\ndrwxr-xr-x 10 user staff 320 Jan 1 12:00 ..",
},
],
- model: "claude-sonnet-4-20250514",
- },
- ]),
- result = parseClaudeLog(logWithDetailedErrors);
- (expect(result.markdown).toContain("🚀 Initialization"),
- expect(result.markdown).toContain("failed_with_error (failed)"),
- expect(result.markdown).toContain("**Error:** Connection timeout after 30s"),
- expect(result.markdown).toContain("**Stderr:** `Error: ECONNREFUSED"),
- expect(result.markdown).toContain("**Exit Code:** 1"),
- expect(result.markdown).toContain("**Command:** `npx @github/github-mcp-server`"),
- expect(result.mcpFailures).toEqual(["failed_with_error"]));
- }),
- it("should handle MCP server failures with message and reason fields", () => {
- const logWithMessageAndReason = JSON.stringify([
- {
- type: "system",
- subtype: "init",
- session_id: "test-message-reason",
- tools: ["Bash"],
- mcp_servers: [{ name: "failed_server", status: "failed", message: "Failed to initialize MCP server", reason: "Server binary not found in PATH" }],
- model: "claude-sonnet-4-20250514",
},
- ]),
- result = parseClaudeLog(logWithMessageAndReason);
- (expect(result.markdown).toContain("failed_server (failed)"),
- expect(result.markdown).toContain("**Message:** Failed to initialize MCP server"),
- expect(result.markdown).toContain("**Reason:** Server binary not found in PATH"),
- expect(result.mcpFailures).toEqual(["failed_server"]));
- }),
- it("should truncate long stderr output", () => {
- const longStderr = "x".repeat(1e3),
- logWithLongStderr = JSON.stringify([
- { type: "system", subtype: "init", session_id: "test-long-stderr", tools: ["Bash"], mcp_servers: [{ name: "verbose_failure", status: "failed", stderr: longStderr }], model: "claude-sonnet-4-20250514" },
- ]),
- result = parseClaudeLog(logWithLongStderr);
- (expect(result.markdown).toContain("verbose_failure (failed)"), expect(result.markdown).toContain("**Stderr:**"), expect(result.markdown).toMatch(/Stderr:.*x{500}\.\.\./), expect(result.mcpFailures).toEqual(["verbose_failure"]));
- }),
- it("should handle MCP server failures with partial error information", () => {
- const logWithPartialInfo = JSON.stringify([
- {
- type: "system",
- subtype: "init",
- session_id: "test-partial",
- tools: ["Bash"],
- mcp_servers: [
- { name: "partial_error_1", status: "failed", error: "Connection refused" },
- { name: "partial_error_2", status: "failed", exitCode: 127 },
- { name: "partial_error_3", status: "failed", stderr: "Command not found" },
- ],
- model: "claude-sonnet-4-20250514",
- },
- ]),
- result = parseClaudeLog(logWithPartialInfo);
- (expect(result.markdown).toContain("partial_error_1 (failed)"),
- expect(result.markdown).toContain("**Error:** Connection refused"),
- expect(result.markdown).toContain("partial_error_2 (failed)"),
- expect(result.markdown).toContain("**Exit Code:** 127"),
- expect(result.markdown).toContain("partial_error_3 (failed)"),
- expect(result.markdown).toContain("**Stderr:** `Command not found`"),
- expect(result.mcpFailures).toEqual(["partial_error_1", "partial_error_2", "partial_error_3"]));
- }),
- it("should handle exitCode zero for failed servers", () => {
- const logWithExitCodeZero = JSON.stringify([
- {
- type: "system",
- subtype: "init",
- session_id: "test-exitcode-zero",
- tools: ["Bash"],
- mcp_servers: [{ name: "failed_but_exit_zero", status: "failed", error: "Server exited unexpectedly", exitCode: 0 }],
- model: "claude-sonnet-4-20250514",
- },
- ]),
- result = parseClaudeLog(logWithExitCodeZero);
- (expect(result.markdown).toContain("failed_but_exit_zero (failed)"),
- expect(result.markdown).toContain("**Error:** Server exited unexpectedly"),
- expect(result.markdown).toContain("**Exit Code:** 0"),
- expect(result.mcpFailures).toEqual(["failed_but_exit_zero"]));
- }),
- it("should handle unrecognized log format", () => {
- const result = parseClaudeLog("This is not JSON or valid format");
- (expect(result.markdown).toContain("Log format not recognized"), expect(result.mcpFailures).toEqual([]));
- }),
- it("should handle empty log content", () => {
- const result = parseClaudeLog("");
- (expect(result.markdown).toContain("Log format not recognized"), expect(result.mcpFailures).toEqual([]));
- }),
- it("should skip debug lines that look like arrays but aren't JSON", () => {
- const result = parseClaudeLog(
- '[DEBUG] Starting process\n[ERROR] Failed with error\n[INFO] Some information\n[{"type":"system","subtype":"init","session_id":"test-999","tools":["Bash"],"model":"claude-sonnet-4-20250514"}]\n[DEBUG] Process completed'
- );
- (expect(result.markdown).toContain("🚀 Initialization"), expect(result.markdown).toContain("test-999"), expect(result.mcpFailures).toEqual([]));
- }),
- it("should handle tool use with MCP tools", () => {
- const logWithMcpTools = JSON.stringify([
- { type: "system", subtype: "init", session_id: "mcp-test", tools: ["mcp__github__create_issue", "mcp__safe_outputs__missing-tool"], model: "claude-sonnet-4-20250514" },
- {
- type: "assistant",
- message: {
- content: [
- { type: "tool_use", id: "tool_1", name: "mcp__github__create_issue", input: { title: "Test Issue", body: "Test description" } },
- { type: "tool_use", id: "tool_2", name: "mcp__safe_outputs__missing-tool", input: { tool: "missing_tool", reason: "Not available" } },
- ],
- },
- },
- ]),
- result = parseClaudeLog(logWithMcpTools);
- (expect(result.markdown).toContain("github::create_issue"), expect(result.markdown).toContain("safe_outputs::missing-tool"), expect(result.mcpFailures).toEqual([]));
- }),
- it("should detect when max-turns limit is hit", () => {
- process.env.GH_AW_MAX_TURNS = "5";
- const logWithMaxTurns = JSON.stringify([
- { type: "system", subtype: "init", session_id: "test-789", tools: ["Bash"], model: "claude-sonnet-4-20250514" },
- { type: "assistant", message: { content: [{ type: "text", text: "Task in progress" }] } },
- { type: "result", total_cost_usd: 0.05, usage: { input_tokens: 500, output_tokens: 200 }, num_turns: 5 },
- ]),
- result = parseClaudeLog(logWithMaxTurns);
- (expect(result.markdown).toContain("**Turns:** 5"), expect(result.maxTurnsHit).toBe(!0), delete process.env.GH_AW_MAX_TURNS);
- }),
- it("should not flag max-turns when turns is less than limit", () => {
- process.env.GH_AW_MAX_TURNS = "10";
- const logBelowMaxTurns = JSON.stringify([
- { type: "system", subtype: "init", session_id: "test-890", tools: ["Bash"], model: "claude-sonnet-4-20250514" },
- { type: "result", total_cost_usd: 0.01, usage: { input_tokens: 100, output_tokens: 50 }, num_turns: 3 },
- ]),
- result = parseClaudeLog(logBelowMaxTurns);
- (expect(result.markdown).toContain("**Turns:** 3"), expect(result.maxTurnsHit).toBe(!1), delete process.env.GH_AW_MAX_TURNS);
- }),
- it("should not flag max-turns when environment variable is not set", () => {
- const logWithoutMaxTurnsEnv = JSON.stringify([
- { type: "system", subtype: "init", session_id: "test-901", tools: ["Bash"], model: "claude-sonnet-4-20250514" },
- { type: "result", total_cost_usd: 0.01, usage: { input_tokens: 100, output_tokens: 50 }, num_turns: 10 },
- ]),
- result = parseClaudeLog(logWithoutMaxTurnsEnv);
- (expect(result.markdown).toContain("**Turns:** 10"), expect(result.maxTurnsHit).toBe(!1));
- }));
- }),
- describe("main function integration", () => {
- (it("should handle valid log file", async () => {
- const validLog = JSON.stringify([
- { type: "system", subtype: "init", session_id: "integration-test", tools: ["Bash"], model: "claude-sonnet-4-20250514" },
- { type: "result", total_cost_usd: 0.001, usage: { input_tokens: 50, output_tokens: 25 }, num_turns: 1 },
- ]);
- (await runScript(validLog), expect(mockCore.summary.addRaw).toHaveBeenCalled(), expect(mockCore.summary.write).toHaveBeenCalled(), expect(mockCore.setFailed).not.toHaveBeenCalled());
- const markdownCall = mockCore.summary.addRaw.mock.calls[0];
- (expect(markdownCall[0]).toContain("```"), expect(markdownCall[0]).toContain("Conversation:"), expect(markdownCall[0]).toContain("Statistics:"), expect(mockCore.info).toHaveBeenCalled());
- const infoCall = mockCore.info.mock.calls.find(call => call[0].includes("=== Claude Execution Summary ==="));
- (expect(infoCall).toBeDefined(), expect(infoCall[0]).toContain("Model: claude-sonnet-4-20250514"));
- }),
- it("should handle log with MCP failures", async () => {
- const logWithFailures = JSON.stringify([
- {
- type: "system",
- subtype: "init",
- session_id: "failure-test",
- mcp_servers: [
- { name: "working_server", status: "connected" },
- { name: "broken_server", status: "failed" },
- ],
- tools: ["Bash"],
- model: "claude-sonnet-4-20250514",
- },
- ]);
- (await runScript(logWithFailures),
- expect(mockCore.summary.addRaw).toHaveBeenCalled(),
- expect(mockCore.summary.write).toHaveBeenCalled(),
- expect(mockCore.setFailed).toHaveBeenCalledWith("MCP server(s) failed to launch: broken_server"));
- }),
- it("should call setFailed when max-turns limit is hit", async () => {
- process.env.GH_AW_MAX_TURNS = "3";
- const logHittingMaxTurns = JSON.stringify([
- { type: "system", subtype: "init", session_id: "max-turns-test", tools: ["Bash"], model: "claude-sonnet-4-20250514" },
- { type: "result", total_cost_usd: 0.02, usage: { input_tokens: 200, output_tokens: 100 }, num_turns: 3 },
- ]);
- (await runScript(logHittingMaxTurns),
- expect(mockCore.summary.addRaw).toHaveBeenCalled(),
- expect(mockCore.summary.write).toHaveBeenCalled(),
- expect(mockCore.setFailed).toHaveBeenCalledWith("Agent execution stopped: max-turns limit reached. The agent did not complete its task successfully."),
- delete process.env.GH_AW_MAX_TURNS);
- }),
- it("should handle missing log file", async () => {
- process.env.GH_AW_AGENT_OUTPUT = "/nonexistent/file.log";
- const scriptWithExport = parseClaudeLogScript.replace("main();", "global.testMain = main;");
- (new Function(scriptWithExport)(), await global.testMain(), expect(mockCore.info).toHaveBeenCalledWith("Log path not found: /nonexistent/file.log"), expect(mockCore.setFailed).not.toHaveBeenCalled());
- }),
- it("should handle missing environment variable", async () => {
- delete process.env.GH_AW_AGENT_OUTPUT;
- const scriptWithExport = parseClaudeLogScript.replace("main();", "global.testMain = main;");
- (new Function(scriptWithExport)(), await global.testMain(), expect(mockCore.info).toHaveBeenCalledWith("No agent log file specified"), expect(mockCore.setFailed).not.toHaveBeenCalled());
- }));
- }),
- describe("helper function tests", () => {
- (it("should format bash commands correctly", () => {
- const result = extractParseFunction()(JSON.stringify([{ type: "assistant", message: { content: [{ type: "tool_use", id: "tool_1", name: "Bash", input: { command: "echo 'hello world'\n && ls -la\n && pwd" } }] } }]));
- expect(result.markdown).toContain("echo 'hello world' && ls -la && pwd");
- }),
- it("should truncate long strings appropriately", () => {
- const parseClaudeLog = extractParseFunction(),
- longCommand = "a".repeat(400),
- result = parseClaudeLog(JSON.stringify([{ type: "assistant", message: { content: [{ type: "tool_use", id: "tool_1", name: "Bash", input: { command: longCommand } }] } }]));
- expect(result.markdown).toContain("...");
- }),
- it("should format MCP tool names correctly", () => {
- const result = extractParseFunction()(JSON.stringify([{ type: "assistant", message: { content: [{ type: "tool_use", id: "tool_1", name: "mcp__github__create_pull_request", input: { title: "Test PR" } }] } }]));
- expect(result.markdown).toContain("github::create_pull_request");
- }),
- it("should render tool outputs in collapsible HTML details elements", () => {
- const result = extractParseFunction()(
- JSON.stringify([
- { type: "assistant", message: { content: [{ type: "tool_use", id: "tool_1", name: "Bash", input: { command: "ls -la", description: "List files" } }] } },
- {
- type: "user",
- message: {
- content: [
- {
- type: "tool_result",
- tool_use_id: "tool_1",
- content: "total 48\ndrwxr-xr-x 5 user user 4096 Jan 1 00:00 .\ndrwxr-xr-x 3 user user 4096 Jan 1 00:00 ..\n-rw-r--r-- 1 user user 123 Jan 1 00:00 file1.txt\n-rw-r--r-- 1 user user 456 Jan 1 00:00 file2.txt",
- is_error: !1,
- },
- ],
- },
- },
- ])
- );
- (expect(result.markdown).toContain(""),
- expect(result.markdown).toContain(""),
- expect(result.markdown).toContain("
"),
- expect(result.markdown).toContain(" "),
- expect(result.markdown).toContain("List files: ls -la"),
- expect(result.markdown).toMatch(/~\d+t/),
- expect(result.markdown).toContain("```"),
- expect(result.markdown).toContain("total 48"),
- expect(result.markdown).toContain("file1.txt"));
- }),
- it("should include token estimates in tool call rendering", () => {
- const result = extractParseFunction()(
- JSON.stringify([
- { type: "assistant", message: { content: [{ type: "tool_use", id: "tool_1", name: "mcp__github__create_issue", input: { title: "Test Issue", body: "Test description that is long enough to generate some tokens" } }] } },
- { type: "user", message: { content: [{ type: "tool_result", tool_use_id: "tool_1", content: "Issue created successfully with number 123", is_error: !1 }] } },
- ])
- );
- (expect(result.markdown).toMatch(/~\d+t/), expect(result.markdown).toContain("github::create_issue"));
- }),
- it("should include duration when available in tool_result", () => {
- const result = extractParseFunction()(
- JSON.stringify([
- { type: "assistant", message: { content: [{ type: "tool_use", id: "tool_1", name: "Bash", input: { command: "sleep 2" } }] } },
- { type: "user", message: { content: [{ type: "tool_result", tool_use_id: "tool_1", content: "", is_error: !1, duration_ms: 2500 }] } },
- ])
- );
- (expect(result.markdown).toMatch(/\d+s<\/code>/), expect(result.markdown).toMatch(/~\d+t/));
- }),
- it("should truncate long tool outputs", () => {
- const parseClaudeLog = extractParseFunction(),
- longOutput = "x".repeat(600),
- result = parseClaudeLog(
- JSON.stringify([
- { type: "assistant", message: { content: [{ type: "tool_use", id: "tool_1", name: "Bash", input: { command: "cat large_file.txt" } }] } },
- { type: "user", message: { content: [{ type: "tool_result", tool_use_id: "tool_1", content: longOutput, is_error: !1 }] } },
- ])
- );
- (expect(result.markdown).toContain("..."), expect(result.markdown).not.toContain("x".repeat(600)));
- }),
- it("should show summary only when no tool output", () => {
- const result = extractParseFunction()(
- JSON.stringify([
- { type: "assistant", message: { content: [{ type: "tool_use", id: "tool_1", name: "Bash", input: { command: "mkdir test_dir" } }] } },
- { type: "user", message: { content: [{ type: "tool_result", tool_use_id: "tool_1", content: "", is_error: !1 }] } },
- ])
- );
- (expect(result.markdown).not.toContain(""), expect(result.markdown).toContain("mkdir test_dir"));
- }),
- it("should display all tools even when there are many (more than 5)", () => {
- const result = extractParseFunction()(
- JSON.stringify([
- {
- type: "system",
- subtype: "init",
- session_id: "many-tools-test",
- tools: [
- "Bash",
- "Read",
- "Write",
- "Edit",
- "LS",
- "Grep",
- "mcp__github__create_issue",
- "mcp__github__list_issues",
- "mcp__github__get_issue",
- "mcp__github__create_pull_request",
- "mcp__github__list_pull_requests",
- "mcp__github__get_pull_request",
- "mcp__github__create_discussion",
- "mcp__github__list_discussions",
- "safe_outputs-create_issue",
- "safe_outputs-add-comment",
- ],
- model: "claude-sonnet-4",
- },
- ])
- );
- (expect(result.markdown).toContain("github::create_issue"),
- expect(result.markdown).toContain("github::list_issues"),
- expect(result.markdown).toContain("github::get_issue"),
- expect(result.markdown).toContain("github::create_pull_request"),
- expect(result.markdown).toContain("github::list_pull_requests"),
- expect(result.markdown).toContain("github::get_pull_request"),
- expect(result.markdown).toContain("github::create_discussion"),
- expect(result.markdown).toContain("github::list_discussions"),
- expect(result.markdown).toContain("**Safe Outputs:**"),
- expect(result.markdown).toContain("create_issue"),
- expect(result.markdown).toContain("add-comment"),
- expect(result.markdown).toContain("Read"),
- expect(result.markdown).toContain("Write"),
- expect(result.markdown).toContain("Edit"),
- expect(result.markdown).toContain("LS"),
- expect(result.markdown).toContain("Grep"),
- expect(result.markdown).toContain("Bash"));
- const toolsSection = result.markdown.split("## 🤖 Reasoning")[0];
- expect(toolsSection).not.toMatch(/and \d+ more/);
- }));
- }));
+ },
+ ])
+ );
+
+ expect(result.markdown).toContain("");
+ expect(result.markdown).toContain(" ");
+ expect(result.markdown).toContain("total 24");
+ });
+
+ it("should include token estimates in tool call rendering", () => {
+ const result = parseClaudeLog(
+ JSON.stringify([{ type: "assistant", message: { content: [{ type: "tool_use", id: "tool_1", name: "Bash", input: { command: "echo 'test'" } }] } }])
+ );
+ expect(result.markdown).toContain("~");
+ });
+
+ it("should include duration when available in tool_result", () => {
+ const result = parseClaudeLog(
+ JSON.stringify([
+ { type: "assistant", message: { content: [{ type: "tool_use", id: "tool_1", name: "Bash", input: { command: "sleep 1" } }] } },
+ { type: "user", message: { content: [{ type: "tool_result", tool_use_id: "tool_1", content: "done", duration_ms: 1234 }] } },
+ ])
+ );
+ expect(result.markdown).toContain("1s"); // Duration is shown in summary
+ });
+
+ it("should truncate long tool outputs", () => {
+ const longOutput = "x".repeat(2000);
+ const result = parseClaudeLog(
+ JSON.stringify([
+ { type: "assistant", message: { content: [{ type: "tool_use", id: "tool_1", name: "Bash", input: { command: "cat bigfile" } }] } },
+ { type: "user", message: { content: [{ type: "tool_result", tool_use_id: "tool_1", content: longOutput }] } },
+ ])
+ );
+ expect(result.markdown).toContain("...");
+ expect(result.markdown).not.toContain("x".repeat(2000));
+ });
+
+ it("should show summary only when no tool output", () => {
+ const result = parseClaudeLog(
+ JSON.stringify([
+ { type: "assistant", message: { content: [{ type: "tool_use", id: "tool_1", name: "Bash", input: { command: "echo test" } }] } },
+ { type: "user", message: { content: [{ type: "tool_result", tool_use_id: "tool_1", content: "" }] } },
+ ])
+ );
+ expect(result.markdown).toContain("echo test"); // Tool is shown even with empty output
+ });
+
+ it("should display all tools even when there are many (more than 5)", () => {
+ const result = parseClaudeLog(
+ JSON.stringify([
+ {
+ type: "system",
+ subtype: "init",
+ session_id: "many-tools",
+ tools: [
+ "Bash",
+ "Read",
+ "Write",
+ "Edit",
+ "LS",
+ "Grep",
+ "mcp__github__list_issues",
+ "mcp__github__get_issue",
+ "mcp__github__create_pull_request",
+ "mcp__github__list_pull_requests",
+ "mcp__github__get_pull_request",
+ "mcp__github__create_discussion",
+ "mcp__github__list_discussions",
+ "mcp__safe_outputs__create_issue",
+ "mcp__safe_outputs__add-comment",
+ ],
+ model: "claude-sonnet-4-20250514",
+ },
+ ])
+ );
+
+ expect(result.markdown).toContain("github::list_issues");
+ expect(result.markdown).toContain("github::get_issue");
+ expect(result.markdown).toContain("github::create_pull_request");
+ expect(result.markdown).toContain("github::list_pull_requests");
+ expect(result.markdown).toContain("github::get_pull_request");
+ expect(result.markdown).toContain("github::create_discussion");
+ expect(result.markdown).toContain("github::list_discussions");
+ expect(result.markdown).toContain("safe_outputs::create_issue");
+ expect(result.markdown).toContain("safe_outputs::add-comment");
+ expect(result.markdown).toContain("Read");
+ expect(result.markdown).toContain("Write");
+ expect(result.markdown).toContain("Edit");
+ expect(result.markdown).toContain("LS");
+ expect(result.markdown).toContain("Grep");
+ expect(result.markdown).toContain("Bash");
+
+ const toolsSection = result.markdown.split("## 🤖 Reasoning")[0];
+ expect(toolsSection).not.toMatch(/and \d+ more/);
+ });
+ });
});
diff --git a/actions/setup/js/parse_copilot_log.test.cjs b/actions/setup/js/parse_copilot_log.test.cjs
index a73c510e90..f59d81f786 100644
--- a/actions/setup/js/parse_copilot_log.test.cjs
+++ b/actions/setup/js/parse_copilot_log.test.cjs
@@ -1,425 +1,291 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import fs from "fs";
import path from "path";
+
describe("parse_copilot_log.cjs", () => {
- let mockCore, parseCopilotLogScript, originalConsole, originalProcess;
- (beforeEach(() => {
- ((originalConsole = global.console),
- (originalProcess = { ...process }),
- (global.console = { log: vi.fn(), error: vi.fn() }),
- (mockCore = {
- debug: vi.fn(),
- info: vi.fn(),
- notice: vi.fn(),
- warning: vi.fn(),
- error: vi.fn(),
- setFailed: vi.fn(),
- setOutput: vi.fn(),
- exportVariable: vi.fn(),
- setSecret: vi.fn(),
- getInput: vi.fn(),
- getBooleanInput: vi.fn(),
- getMultilineInput: vi.fn(),
- getState: vi.fn(),
- saveState: vi.fn(),
- startGroup: vi.fn(),
- endGroup: vi.fn(),
- group: vi.fn(),
- addPath: vi.fn(),
- setCommandEcho: vi.fn(),
- isDebug: vi.fn().mockReturnValue(!1),
- getIDToken: vi.fn(),
- toPlatformPath: vi.fn(),
- toPosixPath: vi.fn(),
- toWin32Path: vi.fn(),
- summary: { addRaw: vi.fn().mockReturnThis(), write: vi.fn().mockResolvedValue() },
- }),
- (global.core = mockCore),
- (global.require = vi.fn().mockImplementation(module => {
- if ("fs" === module) return fs;
- if ("path" === module) return path;
- if ("@actions/core" === module) return mockCore;
- if ("./log_parser_bootstrap.cjs" === module) return require("./log_parser_bootstrap.cjs");
- if ("./log_parser_shared.cjs" === module) return require("./log_parser_shared.cjs");
- throw new Error(`Module not found: ${module}`);
- })));
- const scriptPath = path.join(__dirname, "parse_copilot_log.cjs");
- parseCopilotLogScript = fs.readFileSync(scriptPath, "utf8");
- }),
- afterEach(() => {
- (delete process.env.GH_AW_AGENT_OUTPUT, (global.console = originalConsole), (process.env = originalProcess.env), delete global.core, delete global.require);
- }));
- const extractParseFunction = () => {
- const scriptWithExport = parseCopilotLogScript.replace("main();", "global.testParseCopilotLog = parseCopilotLog;");
- return (new Function(scriptWithExport)(), global.testParseCopilotLog);
- };
- (describe("parseCopilotLog function", () => {
- let parseCopilotLog;
- (beforeEach(() => {
- parseCopilotLog = extractParseFunction();
- }),
- it("should parse JSON array format", () => {
- const jsonArrayLog = JSON.stringify([
- { type: "system", subtype: "init", session_id: "copilot-test-123", tools: ["Bash", "Read", "mcp__github__create_issue"], model: "gpt-5" },
- {
- type: "assistant",
- message: {
- content: [
- { type: "text", text: "I'll help you with this task." },
- { type: "tool_use", id: "tool_123", name: "Bash", input: { command: "echo 'Hello World'", description: "Print greeting" } },
- ],
- },
- },
- { type: "user", message: { content: [{ type: "tool_result", tool_use_id: "tool_123", content: "Hello World\n" }] } },
- { type: "result", total_cost_usd: 0.0015, usage: { input_tokens: 150, output_tokens: 50 }, num_turns: 1 },
- ]),
- result = parseCopilotLog(jsonArrayLog);
- (expect(result.markdown).toContain("🚀 Initialization"),
- expect(result.markdown).toContain("🤖 Commands and Tools"),
- expect(result.markdown).toContain("copilot-test-123"),
- expect(result.markdown).toContain("echo 'Hello World'"),
- expect(result.markdown).toContain("Total Cost"),
- expect(result.markdown).toContain(""),
- expect(result.markdown).toContain(""));
- }),
- it("should parse mixed format with debug logs and JSON array", () => {
- const result = parseCopilotLog(
- '[DEBUG] Starting Copilot CLI\n[ERROR] Some error occurred\n[{"type":"system","subtype":"init","session_id":"copilot-456","tools":["Bash","mcp__safe_outputs__missing-tool"],"model":"gpt-5"},{"type":"assistant","message":{"content":[{"type":"tool_use","id":"tool_123","name":"mcp__safe_outputs__missing-tool","input":{"tool":"draw_pelican","reason":"Tool needed to draw pelican artwork"}}]}},{"type":"result","total_cost_usd":0.1789264,"usage":{"input_tokens":25,"output_tokens":832},"num_turns":10}]\n[DEBUG] Session completed'
- );
- (expect(result.markdown).toContain("🚀 Initialization"),
- expect(result.markdown).toContain("🤖 Commands and Tools"),
- expect(result.markdown).toContain("copilot-456"),
- expect(result.markdown).toContain("safe_outputs::missing-tool"),
- expect(result.markdown).toContain("Total Cost"));
- }),
- it("should parse mixed format with individual JSON lines (JSONL)", () => {
- const result = parseCopilotLog(
- '[DEBUG] Starting Copilot CLI\n{"type":"system","subtype":"init","session_id":"copilot-789","tools":["Bash","Read"],"model":"gpt-5"}\n[DEBUG] Processing user prompt\n{"type":"assistant","message":{"content":[{"type":"text","text":"I\'ll help you."},{"type":"tool_use","id":"tool_123","name":"Bash","input":{"command":"ls -la"}}]}}\n{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"tool_123","content":"file1.txt\\nfile2.txt"}]}}\n{"type":"result","total_cost_usd":0.002,"usage":{"input_tokens":100,"output_tokens":25},"num_turns":2}\n[DEBUG] Workflow completed'
- );
- (expect(result.markdown).toContain("🚀 Initialization"),
- expect(result.markdown).toContain("🤖 Commands and Tools"),
- expect(result.markdown).toContain("copilot-789"),
- expect(result.markdown).toContain("ls -la"),
- expect(result.markdown).toContain("Total Cost"));
- }),
- it("should handle tool calls with details in HTML format", () => {
- const logWithToolOutput = JSON.stringify([
- { type: "system", subtype: "init", session_id: "test-details", tools: ["Bash"], model: "gpt-5" },
- { type: "assistant", message: { content: [{ type: "tool_use", id: "tool_1", name: "Bash", input: { command: "cat README.md", description: "Read README" } }] } },
- { type: "user", message: { content: [{ type: "tool_result", tool_use_id: "tool_1", content: "# Project Title\n\nProject description here." }] } },
- ]),
- result = parseCopilotLog(logWithToolOutput);
- (expect(result.markdown).toContain(""),
- expect(result.markdown).toContain(""),
- expect(result.markdown).toContain("
"),
- expect(result.markdown).toContain(" "),
- expect(result.markdown).toContain("cat README.md"),
- expect(result.markdown).toContain("Project Title"),
- expect(result.markdown).toContain("``````json"),
- expect(result.markdown).toMatch(/``````\n#/),
- expect(result.markdown).toContain("**Parameters:**"),
- expect(result.markdown).toContain("**Response:**"),
- expect(result.markdown).toContain("``````json"));
- const detailsMatch = result.markdown.match(/[\s\S]*?<\/details>/);
- expect(detailsMatch).toBeDefined();
- const detailsContent = detailsMatch[0];
- (expect(detailsContent).toContain("**Parameters:**"), expect(detailsContent).toContain("**Response:**"), expect(detailsContent).toContain('"command": "cat README.md"'), expect(detailsContent).toContain("Project description here"));
- }),
- it("should handle MCP tools", () => {
- const logWithMcpTools = JSON.stringify([
- { type: "system", subtype: "init", session_id: "mcp-test", tools: ["mcp__github__create_issue", "mcp__safe_outputs__missing-tool"], model: "gpt-5" },
- {
- type: "assistant",
- message: {
- content: [
- { type: "tool_use", id: "tool_1", name: "mcp__github__create_issue", input: { title: "Test Issue", body: "Test description" } },
- { type: "tool_use", id: "tool_2", name: "mcp__safe_outputs__missing-tool", input: { tool: "missing_tool", reason: "Not available" } },
- ],
- },
- },
- ]),
- result = parseCopilotLog(logWithMcpTools);
- (expect(result.markdown).toContain("github::create_issue"), expect(result.markdown).toContain("safe_outputs::missing-tool"));
- }),
- it("should handle unrecognized log format", () => {
- const result = parseCopilotLog("This is not JSON or valid format");
- expect(result.markdown).toContain("Log format not recognized");
- }),
- it("should handle empty log content", () => {
- const result = parseCopilotLog("");
- expect(result.markdown).toContain("Log format not recognized");
- }),
- it("should handle empty JSON array", () => {
- const result = parseCopilotLog("[]");
- expect(result.markdown).toContain("Log format not recognized");
- }),
- it("should skip internal file operations in summary", () => {
- const logWithInternalTools = JSON.stringify([
- {
- type: "assistant",
- message: {
- content: [
- { type: "tool_use", id: "tool_1", name: "Read", input: { file_path: "/tmp/gh-aw/test.txt" } },
- { type: "tool_use", id: "tool_2", name: "Write", input: { file_path: "/tmp/gh-aw/output.txt" } },
- { type: "tool_use", id: "tool_3", name: "Bash", input: { command: "echo test" } },
- ],
+ let mockCore, originalConsole, originalProcess;
+ let main, parseCopilotLog, extractPremiumRequestCount;
+
+ beforeEach(async () => {
+ originalConsole = global.console;
+ originalProcess = { ...process };
+ global.console = { log: vi.fn(), error: vi.fn() };
+
+ mockCore = {
+ debug: vi.fn(),
+ info: vi.fn(),
+ notice: vi.fn(),
+ warning: vi.fn(),
+ error: vi.fn(),
+ setFailed: vi.fn(),
+ setOutput: vi.fn(),
+ exportVariable: vi.fn(),
+ setSecret: vi.fn(),
+ getInput: vi.fn(),
+ getBooleanInput: vi.fn(),
+ getMultilineInput: vi.fn(),
+ getState: vi.fn(),
+ saveState: vi.fn(),
+ startGroup: vi.fn(),
+ endGroup: vi.fn(),
+ group: vi.fn(),
+ addPath: vi.fn(),
+ setCommandEcho: vi.fn(),
+ isDebug: vi.fn().mockReturnValue(false),
+ getIDToken: vi.fn(),
+ toPlatformPath: vi.fn(),
+ toPosixPath: vi.fn(),
+ toWin32Path: vi.fn(),
+ summary: { addRaw: vi.fn().mockReturnThis(), write: vi.fn().mockResolvedValue() },
+ };
+
+ global.core = mockCore;
+
+ // Import the module to get the exported functions
+ const module = await import("./parse_copilot_log.cjs?" + Date.now());
+ main = module.main;
+ parseCopilotLog = module.parseCopilotLog;
+ extractPremiumRequestCount = module.extractPremiumRequestCount;
+ });
+
+ afterEach(() => {
+ delete process.env.GH_AW_AGENT_OUTPUT;
+ global.console = originalConsole;
+ process.env = originalProcess.env;
+ delete global.core;
+ });
+
+ describe("parseCopilotLog function", () => {
+ it("should parse JSON array format", () => {
+ const jsonArrayLog = JSON.stringify([
+ { type: "system", subtype: "init", session_id: "copilot-test-123", tools: ["Bash", "Read", "mcp__github__create_issue"], model: "gpt-5" },
+ {
+ type: "assistant",
+ message: {
+ content: [
+ { type: "text", text: "I'll help you with this task." },
+ { type: "tool_use", id: "tool_123", name: "Bash", input: { command: "echo 'Hello World'", description: "Print greeting" } },
+ ],
+ },
+ },
+ { type: "user", message: { content: [{ type: "tool_result", tool_use_id: "tool_123", content: "Hello World\n" }] } },
+ { type: "result", total_cost_usd: 0.0015, usage: { input_tokens: 150, output_tokens: 50 }, num_turns: 1 },
+ ]);
+ const result = parseCopilotLog(jsonArrayLog);
+
+ expect(result.markdown).toContain("🚀 Initialization");
+ expect(result.markdown).toContain("🤖 Commands and Tools");
+ expect(result.markdown).toContain("copilot-test-123");
+ expect(result.markdown).toContain("echo 'Hello World'");
+ expect(result.markdown).toContain("Total Cost");
+ expect(result.markdown).toContain("");
+ expect(result.markdown).toContain("");
+ });
+
+ it("should parse mixed format with debug logs and JSON array", () => {
+ const result = parseCopilotLog(
+ '[DEBUG] Starting Copilot CLI\n[ERROR] Some error occurred\n[{"type":"system","subtype":"init","session_id":"copilot-456","tools":["Bash","mcp__safe_outputs__missing-tool"],"model":"gpt-5"},{"type":"assistant","message":{"content":[{"type":"tool_use","id":"tool_123","name":"mcp__safe_outputs__missing-tool","input":{"tool":"draw_pelican","reason":"Tool needed to draw pelican artwork"}}]}},{"type":"result","total_cost_usd":0.1789264,"usage":{"input_tokens":25,"output_tokens":832},"num_turns":10}]\n[DEBUG] Session completed'
+ );
+
+ expect(result.markdown).toContain("🚀 Initialization");
+ expect(result.markdown).toContain("🤖 Commands and Tools");
+ expect(result.markdown).toContain("copilot-456");
+ expect(result.markdown).toContain("safe_outputs::missing-tool");
+ expect(result.markdown).toContain("Total Cost");
+ });
+
+ it("should parse mixed format with individual JSON lines (JSONL)", () => {
+ const result = parseCopilotLog(
+ '[DEBUG] Starting Copilot CLI\n{"type":"system","subtype":"init","session_id":"copilot-789","tools":["Bash","Read"],"model":"gpt-5"}\n[DEBUG] Processing user prompt\n{"type":"assistant","message":{"content":[{"type":"text","text":"I\'ll help you."},{"type":"tool_use","id":"tool_123","name":"Bash","input":{"command":"ls -la"}}]}}\n{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"tool_123","content":"file1.txt\\nfile2.txt"}]}}\n{"type":"result","total_cost_usd":0.002,"usage":{"input_tokens":100,"output_tokens":25},"num_turns":2}\n[DEBUG] Workflow completed'
+ );
+
+ expect(result.markdown).toContain("🚀 Initialization");
+ expect(result.markdown).toContain("🤖 Commands and Tools");
+ expect(result.markdown).toContain("copilot-789");
+ expect(result.markdown).toContain("ls -la");
+ expect(result.markdown).toContain("Total Cost");
+ });
+
+ it("should handle tool calls with details in HTML format", () => {
+ const logWithHtmlDetails = JSON.stringify([
+ { type: "system", subtype: "init", session_id: "html-test", tools: ["Bash"], model: "gpt-5" },
+ {
+ type: "assistant",
+ message: {
+ content: [
+ {
+ type: "tool_use",
+ id: "tool_1",
+ name: "Bash",
+ input: { command: "cat file.txt", description: "Read file contents" },
},
- },
- ]),
- result = parseCopilotLog(logWithInternalTools);
- expect(result.markdown).toContain("🤖 Commands and Tools");
- const commandsSection = result.markdown.split("📊 Information")[0];
- (expect(commandsSection).toContain("echo test"), expect(commandsSection.split("🤖 Reasoning")[0]).not.toContain("Read"), expect(commandsSection.split("🤖 Reasoning")[0]).not.toContain("Write"));
- }),
- it("should render user text messages as markdown", () => {
- const logWithTextMessage = JSON.stringify([
- { type: "assistant", message: { content: [{ type: "text", text: "Let me analyze the code and provide feedback.\n\n## Analysis\n\nThe code looks good but could use some improvements." }] } },
- ]),
- result = parseCopilotLog(logWithTextMessage);
- (expect(result.markdown).toContain("🤖 Reasoning"), expect(result.markdown).toContain("Let me analyze the code"), expect(result.markdown).toContain("## Analysis"), expect(result.markdown).toContain("could use some improvements"));
- }),
- it("should parse debug log format with tool calls and mark them as successful", () => {
- const result = parseCopilotLog(
- '2025-09-26T11:13:11.798Z [DEBUG] Using model: claude-sonnet-4\n2025-09-26T11:13:12.575Z [START-GROUP] Sending request to the AI model\n2025-09-26T11:13:17.989Z [DEBUG] response (Request-ID test-123):\n2025-09-26T11:13:17.989Z [DEBUG] data:\n{\n "id": "chatcmpl-test",\n "object": "chat.completion",\n "model": "claude-sonnet-4",\n "choices": [\n {\n "index": 0,\n "message": {\n "role": "assistant",\n "content": "I\'ll help you with this task.",\n "tool_calls": [\n {\n "id": "call_abc123",\n "type": "function",\n "function": {\n "name": "bash",\n "arguments": "{\\"command\\":\\"echo \'Hello World\'\\",\\"description\\":\\"Print greeting\\",\\"sessionId\\":\\"main\\",\\"async\\":false}"\n }\n },\n {\n "id": "call_def456",\n "type": "function",\n "function": {\n "name": "github-search_issues",\n "arguments": "{\\"query\\":\\"is:open label:bug\\"}"\n }\n }\n ]\n },\n "finish_reason": "tool_calls"\n }\n ],\n "usage": {\n "prompt_tokens": 100,\n "completion_tokens": 50,\n "total_tokens": 150\n }\n}\n2025-09-26T11:13:18.000Z [END-GROUP]'
- );
- (expect(result.markdown).toContain("🤖 Commands and Tools"),
- expect(result.markdown).toContain("echo 'Hello World'"),
- expect(result.markdown).toContain("github::search_issues"),
- expect(result.markdown).toContain("✅"),
- expect(result.markdown).not.toContain("❓ `echo"),
- expect(result.markdown).not.toContain("❓ `github::search_issues"));
- const commandsSection = result.markdown.split("📊 Information")[0];
- (expect(commandsSection).toContain("✅ `echo 'Hello World'`"), expect(commandsSection).toContain("✅ `github::search_issues(...)`"));
- }),
- it("should extract and display premium model information from debug logs", () => {
- const result = parseCopilotLog(
- '2025-09-26T11:13:11.798Z [DEBUG] Using model: claude-sonnet-4\n2025-09-26T11:13:11.944Z [DEBUG] Got model info: {\n "billing": {\n "is_premium": true,\n "multiplier": 1,\n "restricted_to": [\n "pro",\n "pro_plus",\n "max",\n "business",\n "enterprise"\n ]\n },\n "capabilities": {\n "family": "claude-sonnet-4",\n "limits": {\n "max_context_window_tokens": 200000,\n "max_output_tokens": 16000\n }\n },\n "id": "claude-sonnet-4",\n "name": "Claude Sonnet 4",\n "vendor": "Anthropic",\n "version": "claude-sonnet-4"\n}\n2025-09-26T11:13:12.575Z [START-GROUP] Sending request to the AI model\n2025-09-26T11:13:17.989Z [DEBUG] response (Request-ID test-123):\n2025-09-26T11:13:17.989Z [DEBUG] data:\n{\n "id": "chatcmpl-test",\n "object": "chat.completion",\n "model": "claude-sonnet-4",\n "choices": [\n {\n "index": 0,\n "message": {\n "role": "assistant",\n "content": "I\'ll help you with this task."\n },\n "finish_reason": "stop"\n }\n ],\n "usage": {\n "prompt_tokens": 100,\n "completion_tokens": 50,\n "total_tokens": 150\n }\n}\n2025-09-26T11:13:18.000Z [END-GROUP]'
- );
- (expect(result.markdown).toContain("🚀 Initialization"),
- expect(result.markdown).toContain("**Model Name:** Claude Sonnet 4 (Anthropic)"),
- expect(result.markdown).toContain("**Premium Model:** Yes"),
- expect(result.markdown).toContain("**Required Plans:** pro, pro_plus, max, business, enterprise"));
- }),
- it("should handle non-premium models in debug logs", () => {
- const result = parseCopilotLog(
- '2025-09-26T11:13:11.798Z [DEBUG] Using model: gpt-4o\n2025-09-26T11:13:11.944Z [DEBUG] Got model info: {\n "billing": {\n "is_premium": false,\n "multiplier": 1\n },\n "id": "gpt-4o",\n "name": "GPT-4o",\n "vendor": "OpenAI"\n}\n2025-09-26T11:13:12.575Z [DEBUG] response (Request-ID test-123):\n2025-09-26T11:13:12.575Z [DEBUG] data:\n{\n "id": "chatcmpl-test",\n "model": "gpt-4o",\n "choices": [{"index": 0, "message": {"role": "assistant", "content": "Hello"}, "finish_reason": "stop"}],\n "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}\n}'
- );
- (expect(result.markdown).toContain("**Model Name:** GPT-4o (OpenAI)"), expect(result.markdown).toContain("**Premium Model:** No"));
- }),
- it("should handle model info with cost multiplier", () => {
- const result = parseCopilotLog(
- '2025-09-26T11:13:11.798Z [DEBUG] Using model: claude-opus\n2025-09-26T11:13:11.944Z [DEBUG] Got model info: {\n "billing": {\n "is_premium": true,\n "multiplier": 2.5,\n "restricted_to": ["enterprise"]\n },\n "id": "claude-opus",\n "name": "Claude Opus",\n "vendor": "Anthropic"\n}\n2025-09-26T11:13:12.575Z [DEBUG] response (Request-ID test-123):\n2025-09-26T11:13:12.575Z [DEBUG] data:\n{\n "id": "chatcmpl-test",\n "model": "claude-opus",\n "choices": [{"index": 0, "message": {"role": "assistant", "content": "Hello"}, "finish_reason": "stop"}],\n "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}\n}'
- );
- (expect(result.markdown).toContain("**Premium Model:** Yes (2.5x cost multiplier)"), expect(result.markdown).toContain("**Required Plans:** enterprise"));
- }),
- it("should display premium requests consumed for premium models", () => {
- const structuredLog = JSON.stringify([
- {
- type: "system",
- subtype: "init",
- session_id: "test-premium",
- model: "claude-sonnet-4",
- tools: [],
- model_info: { billing: { is_premium: !0, multiplier: 1, restricted_to: ["pro", "pro_plus", "max"] }, id: "claude-sonnet-4", name: "Claude Sonnet 4", vendor: "Anthropic" },
- },
- { type: "assistant", message: { content: [{ type: "text", text: "Hello" }] } },
- { type: "result", num_turns: 5, usage: { input_tokens: 1e3, output_tokens: 250 } },
- ]),
- result = parseCopilotLog(structuredLog);
- (expect(result.markdown).toContain("**Premium Requests Consumed:** 1"),
- expect(result.markdown).toContain("**Turns:** 5"),
- expect(result.markdown).toContain("**Token Usage:**"),
- expect(result.markdown).toContain("- Input: 1,000"),
- expect(result.markdown).toContain("- Output: 250"));
- }),
- it("should not display premium requests for non-premium models", () => {
- const structuredLog = JSON.stringify([
- { type: "system", subtype: "init", session_id: "test-non-premium", model: "gpt-4o", tools: [], model_info: { billing: { is_premium: !1, multiplier: 1 }, id: "gpt-4o", name: "GPT-4o", vendor: "OpenAI" } },
- { type: "result", num_turns: 3, usage: { input_tokens: 500, output_tokens: 100 } },
- ]),
- result = parseCopilotLog(structuredLog);
- (expect(result.markdown).not.toContain("Premium Requests Consumed"), expect(result.markdown).toContain("**Turns:** 3"), expect(result.markdown).toContain("**Token Usage:**"));
- }),
- it("should display 1 premium request consumed regardless of number of turns", () => {
- const structuredLog = JSON.stringify([
- {
- type: "system",
- subtype: "init",
- session_id: "test-multiple-turns",
- model: "claude-sonnet-4",
- tools: [],
- model_info: { billing: { is_premium: !0, multiplier: 1, restricted_to: ["pro", "pro_plus", "max"] }, id: "claude-sonnet-4", name: "Claude Sonnet 4", vendor: "Anthropic" },
- },
- { type: "assistant", message: { content: [{ type: "text", text: "Response 1" }] } },
- { type: "assistant", message: { content: [{ type: "text", text: "Response 2" }] } },
- { type: "result", num_turns: 17, usage: { input_tokens: 5e3, output_tokens: 1e3 } },
- ]),
- result = parseCopilotLog(structuredLog);
- (expect(result.markdown).toContain("**Premium Requests Consumed:** 1"), expect(result.markdown).toContain("**Turns:** 17"), expect(result.markdown).toContain("**Token Usage:**"));
- }),
- it("should accumulate token usage across multiple API responses in debug logs", () => {
- const result = parseCopilotLog(
- '2025-10-21T01:00:00.000Z [INFO] Starting Copilot CLI: 0.0.350\n2025-10-21T01:00:01.000Z [DEBUG] response (Request-ID test-1):\n2025-10-21T01:00:01.000Z [DEBUG] data:\n{\n "id": "chatcmpl-1",\n "model": "claude-sonnet-4",\n "choices": [{\n "message": {\n "role": "assistant",\n "content": "I\'ll help you."\n },\n "finish_reason": "stop"\n }],\n "usage": {\n "prompt_tokens": 100,\n "completion_tokens": 50,\n "total_tokens": 150\n }\n}\n2025-10-21T01:00:02.000Z [DEBUG] response (Request-ID test-2):\n2025-10-21T01:00:02.000Z [DEBUG] data:\n{\n "id": "chatcmpl-2",\n "model": "claude-sonnet-4",\n "choices": [{\n "message": {\n "role": "assistant",\n "content": "Done!"\n },\n "finish_reason": "stop"\n }],\n "usage": {\n "prompt_tokens": 200,\n "completion_tokens": 10,\n "total_tokens": 210\n }\n}'
- );
- (expect(result.markdown).toContain("**Token Usage:**"), expect(result.markdown).toContain("- Input: 300"), expect(result.markdown).toContain("- Output: 60"), expect(result.markdown).toContain("**Turns:** 2"));
- }),
- it("should extract premium request count from log content using regex", () => {
- const logWithPremiumInfo =
- "\nSome log output here\n[INFO] Premium requests consumed: 3\nMore log content\n" +
- JSON.stringify([
- { type: "system", subtype: "init", session_id: "test-regex", model: "claude-sonnet-4", tools: [], model_info: { billing: { is_premium: !0 }, id: "claude-sonnet-4", name: "Claude Sonnet 4", vendor: "Anthropic" } },
- { type: "result", num_turns: 10, usage: { input_tokens: 1e3, output_tokens: 200 } },
- ]),
- result = parseCopilotLog(logWithPremiumInfo);
- (expect(result.markdown).toContain("**Premium Requests Consumed:** 3"), expect(result.markdown).toContain("**Turns:** 10"));
- }));
- }),
- describe("extractPremiumRequestCount function", () => {
- let extractPremiumRequestCount;
- (beforeEach(() => {
- const scriptWithExport = fs.readFileSync(path.join(__dirname, "parse_copilot_log.cjs"), "utf8").replace("main();", "global.testExtractPremiumRequestCount = extractPremiumRequestCount;");
- (new Function(scriptWithExport)(), (extractPremiumRequestCount = global.testExtractPremiumRequestCount));
- }),
- it("should extract premium request count from various formats", () => {
- (expect(extractPremiumRequestCount("Premium requests consumed: 5")).toBe(5),
- expect(extractPremiumRequestCount("3 premium requests consumed")).toBe(3),
- expect(extractPremiumRequestCount("Consumed 7 premium requests")).toBe(7),
- expect(extractPremiumRequestCount("[INFO] Premium request consumed: 1")).toBe(1));
- }),
- it("should default to 1 if no match found", () => {
- (expect(extractPremiumRequestCount("No premium info here")).toBe(1), expect(extractPremiumRequestCount("")).toBe(1), expect(extractPremiumRequestCount("Some random log content")).toBe(1));
- }),
- it("should handle case-insensitive matching", () => {
- (expect(extractPremiumRequestCount("PREMIUM REQUESTS CONSUMED: 4")).toBe(4), expect(extractPremiumRequestCount("premium Request Consumed: 2")).toBe(2));
- }),
- it("should ignore invalid numbers", () => {
- (expect(extractPremiumRequestCount("Premium requests consumed: 0")).toBe(1),
- expect(extractPremiumRequestCount("Premium requests consumed: -5")).toBe(1),
- expect(extractPremiumRequestCount("Premium requests consumed: abc")).toBe(1));
- }));
- }),
- describe("main function integration", () => {
- (it("should handle valid log file", async () => {
- const validLog = JSON.stringify([
- { type: "system", subtype: "init", session_id: "integration-test", tools: ["Bash"], model: "gpt-5" },
- { type: "result", total_cost_usd: 0.001, usage: { input_tokens: 50, output_tokens: 25 }, num_turns: 1 },
- ]);
- (await (async logContent => {
- const tempFile = path.join(process.cwd(), `test_log_${Date.now()}.txt`);
- (fs.writeFileSync(tempFile, logContent), (process.env.GH_AW_AGENT_OUTPUT = tempFile));
- try {
- const scriptWithExports = parseCopilotLogScript.replace("main();", "global.testParseCopilotLog = parseCopilotLog; global.testMain = main; main();"),
- scriptFunction = new Function(scriptWithExports);
- await scriptFunction();
- } finally {
- fs.existsSync(tempFile) && fs.unlinkSync(tempFile);
- }
- })(validLog),
- expect(mockCore.summary.addRaw).toHaveBeenCalled(),
- expect(mockCore.summary.write).toHaveBeenCalled(),
- expect(mockCore.setFailed).not.toHaveBeenCalled());
- const markdownCall = mockCore.summary.addRaw.mock.calls[0];
- (expect(markdownCall[0]).toContain("```"), expect(markdownCall[0]).toContain("Conversation:"), expect(markdownCall[0]).toContain("Statistics:"), expect(mockCore.info).toHaveBeenCalled());
- const infoCall = mockCore.info.mock.calls.find(call => call[0].includes("=== Copilot Execution Summary ==="));
- (expect(infoCall).toBeDefined(), expect(infoCall[0]).toContain("Model: gpt-5"));
- }),
- it("should handle missing log file", async () => {
- process.env.GH_AW_AGENT_OUTPUT = "/nonexistent/file.log";
- const scriptWithExport = parseCopilotLogScript.replace("main();", "global.testMain = main;");
- (new Function(scriptWithExport)(), await global.testMain(), expect(mockCore.info).toHaveBeenCalledWith("Log path not found: /nonexistent/file.log"), expect(mockCore.setFailed).not.toHaveBeenCalled());
- }),
- it("should handle missing environment variable", async () => {
- delete process.env.GH_AW_AGENT_OUTPUT;
- const scriptWithExport = parseCopilotLogScript.replace("main();", "global.testMain = main;");
- (new Function(scriptWithExport)(), await global.testMain(), expect(mockCore.info).toHaveBeenCalledWith("No agent log file specified"), expect(mockCore.setFailed).not.toHaveBeenCalled());
- }));
- }),
- describe("helper function tests", () => {
- (it("should format bash commands correctly", () => {
- const result = extractParseFunction()(JSON.stringify([{ type: "assistant", message: { content: [{ type: "tool_use", id: "tool_1", name: "Bash", input: { command: "echo 'hello world'\n && ls -la\n && pwd" } }] } }]));
- expect(result.markdown).toContain("echo 'hello world' && ls -la && pwd");
- }),
- it("should truncate long strings appropriately", () => {
- const parseCopilotLog = extractParseFunction(),
- longCommand = "a".repeat(400),
- result = parseCopilotLog(JSON.stringify([{ type: "assistant", message: { content: [{ type: "tool_use", id: "tool_1", name: "Bash", input: { command: longCommand } }] } }]));
- expect(result.markdown).toContain("...");
- }),
- it("should format MCP tool names correctly", () => {
- const result = extractParseFunction()(JSON.stringify([{ type: "assistant", message: { content: [{ type: "tool_use", id: "tool_1", name: "mcp__github__create_pull_request", input: { title: "Test PR" } }] } }]));
- expect(result.markdown).toContain("github::create_pull_request");
- }),
- it("should extract tools from debug log format", () => {
- const result = extractParseFunction()(
- '2025-10-18T01:34:52.534Z [INFO] Starting Copilot CLI: 0.0.343\n2025-10-18T01:34:55.314Z [DEBUG] Got model info: {\n "id": "claude-sonnet-4.5",\n "name": "Claude Sonnet 4.5",\n "vendor": "Anthropic",\n "billing": {\n "is_premium": true,\n "multiplier": 1,\n "restricted_to": ["pro", "pro_plus", "max"]\n }\n}\n2025-10-18T01:34:55.407Z [DEBUG] Tools:\n2025-10-18T01:34:55.412Z [DEBUG] [\n {\n "type": "function",\n "function": {\n "name": "bash",\n "description": "Runs a Bash command"\n }\n },\n {\n "type": "function",\n "function": {\n "name": "github-create_issue",\n "description": "Creates a GitHub issue"\n }\n },\n {\n "type": "function",\n "function": {\n "name": "safe_outputs-create_issue",\n "description": "Safe output create issue"\n }\n }\n2025-10-18T01:34:55.500Z [DEBUG] ]\n2025-10-18T01:35:00.739Z [DEBUG] data:\n2025-10-18T01:35:00.739Z [DEBUG] {\n "choices": [\n {\n "finish_reason": "tool_calls",\n "message": {\n "content": "I\'ll help you with this task.",\n "role": "assistant"\n }\n },\n {\n "finish_reason": "tool_calls",\n "message": {\n "role": "assistant",\n "tool_calls": [\n {\n "function": {\n "arguments": "{\\"command\\":\\"echo test\\"}",\n "name": "bash"\n },\n "id": "tool_123",\n "type": "function"\n }\n ]\n }\n }\n ],\n "model": "Claude Sonnet 4.5",\n "usage": {\n "completion_tokens": 50,\n "prompt_tokens": 100\n }\n2025-10-18T01:35:00.800Z [DEBUG] }'
- );
- (expect(result.markdown).toContain("**Available Tools:**"),
- expect(result.markdown).toContain("bash"),
- expect(result.markdown).toContain("github::create_issue"),
- expect(result.markdown).toContain("**Safe Outputs:**"),
- expect(result.markdown).toContain("create_issue"),
- expect(result.markdown).toContain("**Git/GitHub:**"),
- expect(result.markdown).toContain("**Builtin:**"),
- expect(result.markdown).toContain("Claude Sonnet 4.5"),
- expect(result.markdown).toContain("**Premium Model:** Yes"),
- expect(result.markdown).toContain("echo test"));
- }),
- it("should detect permission denied errors in tool calls from debug logs", () => {
- const result = extractParseFunction()(
- '2025-10-24T16:00:00.000Z [INFO] Starting Copilot CLI: 0.0.350\n2025-10-24T16:00:01.000Z [DEBUG] response (Request-ID test-1):\n2025-10-24T16:00:01.000Z [DEBUG] data:\n{\n "id": "chatcmpl-1",\n "model": "claude-sonnet-4",\n "choices": [{\n "message": {\n "role": "assistant",\n "content": "I\'ll create an issue for you.",\n "tool_calls": [\n {\n "id": "call_create_issue_123",\n "type": "function",\n "function": {\n "name": "github-create_issue",\n "arguments": "{\\"title\\":\\"Test Issue\\",\\"body\\":\\"Test body\\"}"\n }\n }\n ]\n },\n "finish_reason": "tool_calls"\n }],\n "usage": {\n "prompt_tokens": 100,\n "completion_tokens": 50\n }\n}\n2025-10-24T16:00:02.000Z [ERROR] Tool execution failed: github-create_issue\n2025-10-24T16:00:02.000Z [ERROR] Permission denied: Resource not accessible by integration\n2025-10-24T16:00:02.000Z [DEBUG] response (Request-ID test-2):\n2025-10-24T16:00:02.000Z [DEBUG] data:\n{\n "id": "chatcmpl-2",\n "model": "claude-sonnet-4",\n "choices": [{\n "message": {\n "role": "assistant",\n "content": "I encountered a permission error."\n },\n "finish_reason": "stop"\n }],\n "usage": {\n "prompt_tokens": 200,\n "completion_tokens": 10\n }\n}'
- );
- expect(result.markdown).toContain("github::create_issue");
- const commandsSection = result.markdown.split("📊 Information")[0];
- (expect(commandsSection).toContain("❌"), expect(commandsSection).toContain("❌ `github::create_issue(...)`"), expect(commandsSection).not.toContain("✅ `github::create_issue(...)`"));
- }),
- it("should display all tools even when there are many (more than 5)", () => {
- const result = extractParseFunction()(
- JSON.stringify([
+ ],
+ },
+ },
+ {
+ type: "user",
+ message: {
+ content: [
{
- type: "system",
- subtype: "init",
- session_id: "many-tools-test",
- tools: [
- "Bash",
- "Read",
- "Write",
- "Edit",
- "LS",
- "Grep",
- "mcp__github__create_issue",
- "mcp__github__list_issues",
- "mcp__github__get_issue",
- "mcp__github__create_pull_request",
- "mcp__github__list_pull_requests",
- "mcp__github__get_pull_request",
- "mcp__github__create_discussion",
- "mcp__github__list_discussions",
- "safe_outputs-create_issue",
- "safe_outputs-add-comment",
- ],
- model: "gpt-5",
+ type: "tool_result",
+ tool_use_id: "tool_1",
+ content: "File contents here",
},
- ])
- );
- (expect(result.markdown).toContain("github::create_issue"),
- expect(result.markdown).toContain("github::list_issues"),
- expect(result.markdown).toContain("github::get_issue"),
- expect(result.markdown).toContain("github::create_pull_request"),
- expect(result.markdown).toContain("github::list_pull_requests"),
- expect(result.markdown).toContain("github::get_pull_request"),
- expect(result.markdown).toContain("github::create_discussion"),
- expect(result.markdown).toContain("github::list_discussions"),
- expect(result.markdown).toContain("**Safe Outputs:**"),
- expect(result.markdown).toContain("create_issue"),
- expect(result.markdown).toContain("add-comment"),
- expect(result.markdown).toContain("Read"),
- expect(result.markdown).toContain("Write"),
- expect(result.markdown).toContain("Edit"),
- expect(result.markdown).toContain("LS"),
- expect(result.markdown).toContain("Grep"),
- expect(result.markdown).toContain("Bash"));
- const toolsSection = result.markdown.split("## 🤖 Reasoning")[0];
- expect(toolsSection).not.toMatch(/and \d+ more/);
- }));
- }));
+ ],
+ },
+ },
+ ]);
+ const result = parseCopilotLog(logWithHtmlDetails);
+
+ expect(result.markdown).toContain("");
+ expect(result.markdown).toContain(" ");
+ expect(result.markdown).toContain("File contents here");
+ });
+
+ it("should handle MCP tools", () => {
+ const logWithMcpTools = JSON.stringify([
+ {
+ type: "system",
+ subtype: "init",
+ session_id: "mcp-test",
+ tools: ["Bash", "mcp__github__create_issue", "mcp__github__list_pull_requests"],
+ model: "gpt-5",
+ },
+ { type: "assistant", message: { content: [{ type: "tool_use", id: "tool_1", name: "mcp__github__create_issue", input: { title: "Test" } }] } },
+ { type: "result", total_cost_usd: 0.01, usage: { input_tokens: 100, output_tokens: 50 }, num_turns: 1 },
+ ]);
+ const result = parseCopilotLog(logWithMcpTools);
+
+ expect(result.markdown).toContain("github::create_issue");
+ expect(result.markdown).toContain("github::list_pull_requests");
+ });
+
+ it("should handle unrecognized log format", () => {
+ const result = parseCopilotLog("This is not JSON or valid format");
+ expect(result.markdown).toContain("Log format not recognized");
+ });
+
+ it("should handle empty log content", () => {
+ const result = parseCopilotLog("");
+ expect(result.markdown).toContain("Log format not recognized");
+ });
+
+ it("should handle model info with cost multiplier", () => {
+ const structuredLog = JSON.stringify([
+ { type: "system", subtype: "init", session_id: "cost-test", tools: ["Bash"], model: "gpt-4", model_info: { is_premium: true, cost_multiplier: 3 } },
+ { type: "result", num_turns: 2, usage: { input_tokens: 500, output_tokens: 200 } },
+ ]);
+ const result = parseCopilotLog(structuredLog);
+
+ expect(result.markdown).toContain("gpt-4");
+ });
+
+ it("should not display premium requests for non-premium models", () => {
+ const structuredLog = JSON.stringify([
+ { type: "system", subtype: "init", session_id: "non-premium-test", tools: ["Bash"], model: "gpt-3.5-turbo", model_info: { is_premium: false } },
+ { type: "result", num_turns: 3, usage: { input_tokens: 500, output_tokens: 200 } },
+ ]);
+ const result = parseCopilotLog(structuredLog);
+
+ expect(result.markdown).not.toContain("**Premium Requests:**");
+ });
+ });
+
+ describe("extractPremiumRequestCount function", () => {
+ it("should default to 1 if no match found", () => {
+ expect(extractPremiumRequestCount("No premium info here")).toBe(1);
+ });
+
+ it("should ignore invalid numbers", () => {
+ expect(extractPremiumRequestCount("Premium requests: abc")).toBe(1);
+ });
+ });
+
+ describe("main function integration", () => {
+ it("should handle valid log file", async () => {
+ const validLog = JSON.stringify([
+ { type: "system", subtype: "init", session_id: "integration-test", tools: ["Bash"], model: "gpt-5" },
+ { type: "result", total_cost_usd: 0.001, usage: { input_tokens: 50, output_tokens: 25 }, num_turns: 1 },
+ ]);
+
+ const tempFile = path.join(process.cwd(), `test_log_${Date.now()}.txt`);
+ fs.writeFileSync(tempFile, validLog);
+ process.env.GH_AW_AGENT_OUTPUT = tempFile;
+
+ try {
+ await main();
+
+ expect(mockCore.summary.addRaw).toHaveBeenCalled();
+ expect(mockCore.summary.write).toHaveBeenCalled();
+ } finally {
+ if (fs.existsSync(tempFile)) {
+ fs.unlinkSync(tempFile);
+ }
+ }
+ });
+
+ it("should handle missing log file", async () => {
+ process.env.GH_AW_AGENT_OUTPUT = "/nonexistent/file.log";
+ await main();
+ expect(mockCore.info).toHaveBeenCalledWith("Log path not found: /nonexistent/file.log");
+ });
+
+ it("should handle missing environment variable", async () => {
+ delete process.env.GH_AW_AGENT_OUTPUT;
+ await main();
+ expect(mockCore.info).toHaveBeenCalledWith("No agent log file specified");
+ });
+ });
+
+ describe("helper function tests", () => {
+ it("should format bash commands correctly", () => {
+ const result = parseCopilotLog(JSON.stringify([{ type: "assistant", message: { content: [{ type: "tool_use", id: "tool_1", name: "Bash", input: { command: "echo 'hello world'\n && ls -la\n && pwd" } }] } }]));
+ expect(result.markdown).toContain("echo 'hello world' && ls -la && pwd");
+ });
+
+ it("should truncate long strings appropriately", () => {
+ const longCommand = "a".repeat(400);
+ const result = parseCopilotLog(JSON.stringify([{ type: "assistant", message: { content: [{ type: "tool_use", id: "tool_1", name: "Bash", input: { command: longCommand } }] } }]));
+ expect(result.markdown).toContain("...");
+ });
+
+ it("should format MCP tool names correctly", () => {
+ const result = parseCopilotLog(JSON.stringify([{ type: "assistant", message: { content: [{ type: "tool_use", id: "tool_1", name: "mcp__github__create_pull_request", input: { title: "Test PR" } }] } }]));
+ expect(result.markdown).toContain("github::create_pull_request");
+ });
+
+ it("should display all tool types correctly", () => {
+ const result = parseCopilotLog(
+ JSON.stringify([
+ {
+ type: "system",
+ subtype: "init",
+ session_id: "all-tools",
+ tools: [
+ "Bash",
+ "Read",
+ "Write",
+ "Edit",
+ "LS",
+ "Grep",
+ "mcp__github__list_issues",
+ "mcp__github__create_pull_request",
+ "mcp__safe_outputs__create_issue",
+ ],
+ model: "gpt-5",
+ },
+ ])
+ );
+
+ expect(result.markdown).toContain("Bash");
+ expect(result.markdown).toContain("Read");
+ expect(result.markdown).toContain("Write");
+ expect(result.markdown).toContain("Edit");
+ expect(result.markdown).toContain("LS");
+ expect(result.markdown).toContain("Grep");
+ expect(result.markdown).toContain("github::list_issues");
+ expect(result.markdown).toContain("github::create_pull_request");
+ expect(result.markdown).toContain("safe_outputs::create_issue");
+ });
+ });
});
diff --git a/actions/setup/js/test-2on4AU/test.log b/actions/setup/js/test-2on4AU/test.log
deleted file mode 100644
index 6b584e8ece..0000000000
--- a/actions/setup/js/test-2on4AU/test.log
+++ /dev/null
@@ -1 +0,0 @@
-content
\ No newline at end of file
diff --git a/actions/setup/js/test-60MCzS/test.log b/actions/setup/js/test-60MCzS/test.log
deleted file mode 100644
index 6b584e8ece..0000000000
--- a/actions/setup/js/test-60MCzS/test.log
+++ /dev/null
@@ -1 +0,0 @@
-content
\ No newline at end of file
diff --git a/actions/setup/js/test-7j9GSH/test.log b/actions/setup/js/test-7j9GSH/test.log
deleted file mode 100644
index 6b584e8ece..0000000000
--- a/actions/setup/js/test-7j9GSH/test.log
+++ /dev/null
@@ -1 +0,0 @@
-content
\ No newline at end of file
diff --git a/actions/setup/js/test-9AX7fX/test.log b/actions/setup/js/test-9AX7fX/test.log
deleted file mode 100644
index 6b584e8ece..0000000000
--- a/actions/setup/js/test-9AX7fX/test.log
+++ /dev/null
@@ -1 +0,0 @@
-content
\ No newline at end of file
diff --git a/actions/setup/js/test-GNKId1/test.log b/actions/setup/js/test-GNKId1/test.log
deleted file mode 100644
index 6b584e8ece..0000000000
--- a/actions/setup/js/test-GNKId1/test.log
+++ /dev/null
@@ -1 +0,0 @@
-content
\ No newline at end of file
diff --git a/actions/setup/js/test-Hj2ZYD/test.log b/actions/setup/js/test-Hj2ZYD/test.log
deleted file mode 100644
index 6b584e8ece..0000000000
--- a/actions/setup/js/test-Hj2ZYD/test.log
+++ /dev/null
@@ -1 +0,0 @@
-content
\ No newline at end of file
diff --git a/actions/setup/js/test-IRzCio/test.log b/actions/setup/js/test-IRzCio/test.log
deleted file mode 100644
index 6b584e8ece..0000000000
--- a/actions/setup/js/test-IRzCio/test.log
+++ /dev/null
@@ -1 +0,0 @@
-content
\ No newline at end of file
diff --git a/actions/setup/js/test-IXfc6q/test.log b/actions/setup/js/test-IXfc6q/test.log
deleted file mode 100644
index 6b584e8ece..0000000000
--- a/actions/setup/js/test-IXfc6q/test.log
+++ /dev/null
@@ -1 +0,0 @@
-content
\ No newline at end of file
diff --git a/actions/setup/js/test-SYvzgU/test.log b/actions/setup/js/test-SYvzgU/test.log
deleted file mode 100644
index 6b584e8ece..0000000000
--- a/actions/setup/js/test-SYvzgU/test.log
+++ /dev/null
@@ -1 +0,0 @@
-content
\ No newline at end of file
diff --git a/actions/setup/js/test-WRSOIK/test.log b/actions/setup/js/test-WRSOIK/test.log
deleted file mode 100644
index 6b584e8ece..0000000000
--- a/actions/setup/js/test-WRSOIK/test.log
+++ /dev/null
@@ -1 +0,0 @@
-content
\ No newline at end of file
diff --git a/actions/setup/js/test-data/FINDINGS.md b/actions/setup/js/test-data/FINDINGS.md
deleted file mode 100644
index 1c50315f2e..0000000000
--- a/actions/setup/js/test-data/FINDINGS.md
+++ /dev/null
@@ -1,103 +0,0 @@
-# Copilot Log Parser - Format Mismatch Discovery
-
-## Summary
-
-Downloaded actual Copilot CLI logs from workflow run [#18296543175](https://github.com/githubnext/gh-aw/actions/runs/18296543175/job/52095988377#step:19:1) and discovered a format mismatch between the parser's expectations and actual Copilot CLI output.
-
-## What Was Done
-
-1. ✅ Downloaded artifacts from the specified workflow run
-2. ✅ Extracted the Copilot CLI logs (`9843a065-22e7-4b08-a00f-5cf5b804aaad.log`)
-3. ✅ Stored as sample logs in `pkg/workflow/js/test-data/copilot-raw-logs-run-18296543175.log`
-4. ✅ Applied the transform using `parse_copilot_log.cjs`
-5. ✅ Stored transformed output in `pkg/workflow/js/test-data/copilot-transformed-run-18296543175.md`
-6. ✅ Created README documenting the format mismatch
-
-## Key Finding
-
-**The parser expects structured JSON format (like Claude logs), but Copilot CLI outputs debug logs.**
-
-### Expected Format (What the Parser Looks For)
-```json
-[
- {"type": "system", "subtype": "init", "session_id": "...", "tools": [...]},
- {"type": "assistant", "message": {"content": [...]}},
- {"type": "user", "message": {"content": [...]}}
-]
-```
-
-### Actual Format (What Copilot CLI Produces)
-```
-2025-10-06T22:48:23.111Z [INFO] Starting Copilot CLI: 0.0.335
-2025-10-06T22:49:18.969Z [DEBUG] response (Request-ID ...):
-2025-10-06T22:49:18.969Z [DEBUG] data:
-2025-10-06T22:49:18.969Z [DEBUG] {
- "choices": [
- {
- "finish_reason": "tool_calls",
- "message": {
- "content": "Perfect! The workflows have been recompiled...",
- "role": "assistant",
- "tool_calls": [...]
- }
- }
- ],
- "usage": {...}
-}
-```
-
-## Transform Result
-
-The parser now successfully handles both formats:
-
-**Structured JSON format** (original implementation):
-```json
-[
- {"type": "system", "subtype": "init", ...},
- {"type": "assistant", "message": {...}},
- ...
-]
-```
-
-**Debug log format** (newly implemented - Option 1):
-- Extracts JSON responses from `[DEBUG] data:` blocks
-- Reconstructs the conversation flow from API responses
-- Maintains backward compatibility with JSON format
-
-The updated parser automatically detects which format is being used and processes it accordingly.
-
-## Next Steps (Options)
-
-### ✅ Option 1: Update Parser to Handle Debug Logs (IMPLEMENTED)
-- ✅ Add support for parsing Copilot CLI debug log format
-- ✅ Extract JSON responses from `[DEBUG] data:` blocks
-- ✅ Reconstruct the conversation flow from API responses
-- ✅ Maintain backward compatibility with JSON format
-
-**Status**: The parser now supports both structured JSON and debug log formats. It automatically detects which format is being used and processes it accordingly.
-
-### Option 2: Update Copilot CLI to Output Structured Logs
-- Modify Copilot CLI to output structured JSON logs
-- Match the format used by Claude for consistency
-- Easier for parser to handle
-
-### Option 3: Parser Detects and Handles Both Formats
-- ✅ Auto-detect which format is being used (IMPLEMENTED)
-- ✅ Handle both structured JSON and debug logs (IMPLEMENTED)
-- Provides maximum flexibility
-
-## Files Created
-
-- `sample-logs/copilot-raw-logs-run-18296543175.log` - Raw Copilot CLI logs (152KB, excluded from git via .gitignore)
-- `pkg/workflow/js/test-data/copilot-transformed-run-18296543175.md` - Transform result showing format mismatch (committed for review)
-- `pkg/workflow/js/test-data/README.md` - Documentation of the format mismatch
-- `pkg/workflow/js/test-data/FINDINGS.md` - This file with analysis and recommendations
-
-**Note**: The raw log file is 152KB and excluded from git. To obtain it, download the artifact from the [workflow run](https://github.com/githubnext/gh-aw/actions/runs/18296543175/job/52095988377) or regenerate by running a Copilot CLI workflow.
-
-## Recommendation
-
-I recommend **Option 3** (dual format support) as it provides the most robust solution:
-1. Parser can handle both current debug logs and future structured logs
-2. Backward compatible with any existing implementations
-3. Future-proof if Copilot CLI output format changes
diff --git a/actions/setup/js/test-data/README.md b/actions/setup/js/test-data/README.md
deleted file mode 100644
index a0731c3bee..0000000000
--- a/actions/setup/js/test-data/README.md
+++ /dev/null
@@ -1,78 +0,0 @@
-# Copilot Log Test Data
-
-This directory contains sample logs from actual Copilot CLI workflow runs for testing and documentation purposes.
-
-## Files
-
-### Raw Logs (Not in Git)
-These files are excluded from git via `.gitignore` due to their size. They are stored locally in `sample-logs/` directory.
-
-#### sample-logs/copilot-raw-logs-run-18296543175.log
-- **Source**: https://github.com/githubnext/gh-aw/actions/runs/18296543175/job/52095988377
-- **Workflow**: Tidy workflow (automatic code formatting and linting)
-- **Format**: Raw Copilot CLI debug logs with timestamps and embedded JSON API responses
-- **Size**: ~152KB, 4429 lines
-- **Download**: Get from workflow artifacts or run `gh aw audit 18296543175`
-
-### Transformed Output (In Git)
-
-#### copilot-transformed-run-18296543175.md
-- **Description**: Output from applying `parse_copilot_log.cjs` to workflow run 18296543175 (tidy workflow)
-- **Result**: Successfully parsed debug logs showing code formatting and linting workflow
-- **Location**: Committed to git for review (small file, ~1.4KB)
-
-#### copilot-transformed-run-18296916269.md
-- **Description**: Output from applying `parse_copilot_log.cjs` to workflow run 18296916269 (MCP imports research)
-- **Result**: Successfully parsed debug logs showing research workflow with web search and file editing
-- **Location**: Committed to git for review (small file, ~6.9KB)
-- **Workflow**: Research task involving MCP server configuration and imports
-
-## Format Analysis
-
-The current `parse_copilot_log.cjs` implementation expects structured JSON logs similar to Claude:
-```json
-[
- {"type": "system", "subtype": "init", ...},
- {"type": "assistant", "message": {...}},
- {"type": "user", "message": {...}}
-]
-```
-
-However, the actual Copilot CLI produces debug logs with this structure:
-```
-2025-10-06T22:48:23.111Z [INFO] Starting Copilot CLI: 0.0.335
-2025-10-06T22:48:27.031Z [DEBUG] AnthropicTokenLimitErrorTruncator will truncate...
-2025-10-06T22:49:18.969Z [DEBUG] response (Request-ID ...):
-2025-10-06T22:49:18.969Z [DEBUG] data:
-2025-10-06T22:49:18.969Z [DEBUG] {
- "choices": [
- {
- "finish_reason": "tool_calls",
- "message": {
- "content": "...",
- "role": "assistant"
- }
- }
- ],
- "usage": {...}
-}
-```
-
-## Next Steps
-
-The parser needs to be updated to handle the actual Copilot CLI debug log format, or Copilot CLI needs to output structured JSON logs similar to Claude.
-
-### Option 1: Update Parser to Handle Debug Logs
-- Parse timestamped log lines
-- Extract JSON responses from `[DEBUG] data:` blocks
-- Reconstruct conversation flow from API responses
-
-### Option 2: Update Copilot CLI Output
-- Have Copilot CLI output structured JSON logs
-- Match the format used by Claude for consistency
-- Include system init, assistant messages, and user responses in a structured array
-
-### Option 3: Dual Format Support
-- Support both formats in the parser
-- Auto-detect which format is being used
-- Handle both structured JSON and debug logs
diff --git a/actions/setup/js/test-data/copilot-transformed-run-18296543175.md b/actions/setup/js/test-data/copilot-transformed-run-18296543175.md
deleted file mode 100644
index b7b430f9aa..0000000000
--- a/actions/setup/js/test-data/copilot-transformed-run-18296543175.md
+++ /dev/null
@@ -1,47 +0,0 @@
-## 🚀 Initialization
-
-**Model:** claude-sonnet-4.5
-
-**Session ID:** copilot-0.0.335-1759792930654
-
-**Available Tools:**
-
-
-## 🤖 Commands and Tools
-
-* ❓ `github::search_pull_requests(...)`
-* ❓ `make fmt`
-* ❓ `make lint`
-* ❓ `make recompile`
-* ❓ `make test`
-* ❓ `git status --porcelain`
-
-## 📊 Information
-
-**Turns:** 11
-
-
-## 🤖 Reasoning
-
-I'll help you tidy up the codebase by formatting, linting, fixing issues, recompiling workflows, and creating or updating a pull request if needed. Let me start by checking for existing tidy pull requests.
-
-❓ github::search_pull_requests(query: repo:githubnext/gh-aw is:open is:pr labe...)
-
-No existing tidy pull requests found with the automation label. Now let's proceed with the tidying process.
-
-❓ Format all Go code: `make fmt`
-
-❓ Run linting checks: `make lint`
-
-Great! The code is already properly formatted and passes all linting checks. Now let's recompile the workflows to ensure they're up to date.
-
-❓ Recompile all workflow files: `make recompile`
-
-Perfect! The workflows have been recompiled successfully. Now let's run the tests to make sure everything is working correctly.
-
-❓ Run all tests: `make test`
-
-Excellent! All tests pass. Now let's check if there are any changes that need to be committed.
-
-❓ Check for uncommitted changes: `git status --porcelain`
-
diff --git a/actions/setup/js/test-data/copilot-transformed-run-18296916269.md b/actions/setup/js/test-data/copilot-transformed-run-18296916269.md
deleted file mode 100644
index 8f95121f95..0000000000
--- a/actions/setup/js/test-data/copilot-transformed-run-18296916269.md
+++ /dev/null
@@ -1,162 +0,0 @@
-## 🚀 Initialization
-
-**Model:** claude-sonnet-4.5
-
-**Session ID:** copilot-0.0.335-1759793448632
-
-**Available Tools:**
-
-
-## 🤖 Commands and Tools
-
-* ❓ tavily-tavily_search
-* ❓ tavily-tavily_search
-* ❓ tavily-tavily_search
-* ❓ str_replace_editor
-* ❓ str_replace_editor
-* ❓ `find /home/runner/work/gh-aw/gh-aw -name "*.md" -path "*/workflows/*" | head -20`
-* ❓ `grep -r "tavily" /home/runner/work/gh-aw/gh-aw/.github/workflows/ --include="*.m...`
-* ❓ str_replace_editor
-* ❓ str_replace_editor
-* ❓ str_replace_editor
-* ❓ `find /home/runner/work/gh-aw/gh-aw/pkg -name "*.go" | grep -E "(parser|workflow|...`
-* ❓ tavily-tavily_search
-* ❓ `find /home/runner/work/gh-aw/gh-aw/pkg -type f -name "*.go" | xargs grep -l "inc...`
-* ❓ str_replace_editor
-* ❓ tavily-tavily_search
-* ❓ tavily-tavily_search
-* ❓ `cd /home/runner/work/gh-aw/gh-aw && find pkg/parser -name "*.go" -exec grep -l "...`
-* ❓ str_replace_editor
-* ❓ `cd /home/runner/work/gh-aw/gh-aw && grep -n "imports" pkg/parser/frontmatter.go ...`
-* ❓ `cd /home/runner/work/gh-aw/gh-aw && grep -A 20 "ProcessImports" pkg/parser/front...`
-* ❓ str_replace_editor
-* ❓ tavily-tavily_search
-* ❓ str_replace_editor
-* ❓ `cd /home/runner/work/gh-aw/gh-aw && grep -n "mcp-servers\|mcpServers" pkg/parser...`
-* ❓ str_replace_editor
-* ❓ `cd /home/runner/work/gh-aw/gh-aw && grep -A 50 "ExtractMCPConfigurations" pkg/pa...`
-* ❓ str_replace_editor
-* ❓ tavily-tavily_search
-* ❓ `cd /home/runner/work/gh-aw/gh-aw && cat .github/workflows/scout.md | head -25`
-* ❓ `cd /home/runner/work/gh-aw/gh-aw && find .github/workflows -name "*.md" -exec gr...`
-* ❓ `cd /home/runner/work/gh-aw/gh-aw && find pkg/cli/workflows -name "*.md" -exec gr...`
-* ❓ tavily-tavily_search
-* ❓ `cd /home/runner/work/gh-aw/gh-aw && cat .github/workflows/shared/tavily-mcp.md`
-* ❓ `cd /home/runner/work/gh-aw/gh-aw && find .github/workflows -name "*.md" -type f ...`
-* ❓ `cd /home/runner/work/gh-aw/gh-aw && grep -r "ProcessImports" pkg/ --include="*.g...`
-* ❓ tavily-tavily_search
-* ❓ `cd /home/runner/work/gh-aw/gh-aw && ls -la .github/instructions/`
-* ❓ str_replace_editor
-* ❓ `cat > /tmp/gh-aw/cache-memory/research-mcp-imports.md << 'EOF' # MCP Server Imports Re...`
-* ❓ safe_outputs-add_comment
-* ❓ `cat > /tmp/gh-aw/cache-memory/mcp-imports-summary.txt << 'EOF' MCP Server Imports Rese...`
-
-## 📊 Information
-
-**Turns:** 48
-
-
-## 🤖 Reasoning
-
-I'll help research MCP server imports in compilers, focusing on how to support importing mcp-servers entries and understanding best practices for this feature.
-
-❓ tavily-tavily_search: MCP server Model Context Protocol import configuration compiler
-
-❓ tavily-tavily_search: Model Context Protocol mcp-servers configuration schema format
-
-❓ tavily-tavily_search: compiler import external configuration YAML frontmatter include directive
-
-❓ str_replace_editor: view
-
-Let me explore the repository structure and current implementation to better understand the context:
-
-❓ str_replace_editor: view
-
-❓ Find workflow markdown files: `find /home/runner/work/gh-aw/gh-aw -name "*.md" -path "*/workflows/*" | head -20`
-
-❓ Search for tavily references in workflows: `grep -r "tavily" /home/runner/work/gh-aw/gh-aw/.github/workflows/ --include="*.m...`
-
-❓ str_replace_editor: view
-
-❓ str_replace_editor: view
-
-❓ str_replace_editor: view
-
-❓ Find compiler and parser files: `find /home/runner/work/gh-aw/gh-aw/pkg -name "*.go" | grep -E "(parser|workflow|...`
-
-❓ tavily-tavily_search: GitHub Actions workflow YAML include import external configuration best practices
-
-❓ Find Go files with include/import logic: `find /home/runner/work/gh-aw/gh-aw/pkg -type f -name "*.go" | xargs grep -l "inc...`
-
-❓ str_replace_editor: view
-
-❓ tavily-tavily_search: MCP server configuration inheritance merge composition pattern
-
-❓ tavily-tavily_search: "mcp-servers" "tools" configuration YAML merge strategy deepmerge
-
-❓ Find parser files with import handling: `cd /home/runner/work/gh-aw/gh-aw && find pkg/parser -name "*.go" -exec grep -l "...`
-
-❓ str_replace_editor: view
-
-❓ Search for imports handling in frontmatter parser: `cd /home/runner/work/gh-aw/gh-aw && grep -n "imports" pkg/parser/frontmatter.go ...`
-
-❓ Look for ProcessImports function: `cd /home/runner/work/gh-aw/gh-aw && grep -A 20 "ProcessImports" pkg/parser/front...`
-
-❓ str_replace_editor: view
-
-❓ tavily-tavily_search: YAML deep merge strategy tools configuration inheritance frontmatter
-
-❓ str_replace_editor: view
-
-❓ Search for mcp-servers references: `cd /home/runner/work/gh-aw/gh-aw && grep -n "mcp-servers\|mcpServers" pkg/parser...`
-
-❓ str_replace_editor: view
-
-❓ Look for mcp-servers and tools handling in MCP extraction: `cd /home/runner/work/gh-aw/gh-aw && grep -A 50 "ExtractMCPConfigurations" pkg/pa...`
-
-❓ str_replace_editor: view
-
-❓ tavily-tavily_search: compiler configuration import reusable modules composition pattern best practices
-
-Let me now look at example workflow files and understand the current structure better:
-
-❓ View scout workflow example: `cd /home/runner/work/gh-aw/gh-aw && cat .github/workflows/scout.md | head -25`
-
-❓ Find workflows using mcp-servers: `cd /home/runner/work/gh-aw/gh-aw && find .github/workflows -name "*.md" -exec gr...`
-
-❓ Find test workflows using mcp-servers: `cd /home/runner/work/gh-aw/gh-aw && find pkg/cli/workflows -name "*.md" -exec gr...`
-
-❓ tavily-tavily_search: "import directive" "shared configuration" markdown frontmatter YAML merge deep
-
-Now let me check the documentation to understand the issue better:
-
-❓ View the tavily MCP include file: `cd /home/runner/work/gh-aw/gh-aw && cat .github/workflows/shared/tavily-mcp.md`
-
-❓ Find workflows with tools section: `cd /home/runner/work/gh-aw/gh-aw && find .github/workflows -name "*.md" -type f ...`
-
-❓ Find usages of ProcessImports function: `cd /home/runner/work/gh-aw/gh-aw && grep -r "ProcessImports" pkg/ --include="*.g...`
-
-Perfect! Now I have a good understanding. Let me search for more specific information about merge strategies and then compile the research report:
-
-❓ tavily-tavily_search: GitHub Actions reusable workflow configuration merge strategy composition
-
-❓ Check instructions directory: `cd /home/runner/work/gh-aw/gh-aw && ls -la .github/instructions/`
-
-❓ str_replace_editor: view
-
-Now let me write the memory cache and prepare the comprehensive research report:
-
-❓ Write research summary to cache memory: `cat > /tmp/gh-aw/cache-memory/research-mcp-imports.md << 'EOF' # MCP Server Imports Re...`
-
-❓ safe_outputs-add_comment: # 🔍 Scout Research Report
-
-*Triggered by @pelikhan*
-
-## Executive Summary
-
-The request is to move T...
-
-Perfect! The research report has been successfully posted. Let me also create a summary in the cache memory for future reference:
-
-❓ Create summary file in cache memory: `cat > /tmp/gh-aw/cache-memory/mcp-imports-summary.txt << 'EOF' MCP Server Imports Rese...`
-
diff --git a/actions/setup/js/test-fWIxhd/safe-outputs.jsonl b/actions/setup/js/test-fWIxhd/safe-outputs.jsonl
deleted file mode 100644
index 003597e20d..0000000000
--- a/actions/setup/js/test-fWIxhd/safe-outputs.jsonl
+++ /dev/null
@@ -1 +0,0 @@
-{"type":"create_issue","title":"Test Issue","body":"Test body"}
\ No newline at end of file
diff --git a/actions/setup/js/test-fWIxhd/test.log b/actions/setup/js/test-fWIxhd/test.log
deleted file mode 100644
index 6b584e8ece..0000000000
--- a/actions/setup/js/test-fWIxhd/test.log
+++ /dev/null
@@ -1 +0,0 @@
-content
\ No newline at end of file
diff --git a/actions/setup/js/test-i7RCld/test.log b/actions/setup/js/test-i7RCld/test.log
deleted file mode 100644
index 6b584e8ece..0000000000
--- a/actions/setup/js/test-i7RCld/test.log
+++ /dev/null
@@ -1 +0,0 @@
-content
\ No newline at end of file
diff --git a/actions/setup/js/test-lq2w64/test.log b/actions/setup/js/test-lq2w64/test.log
deleted file mode 100644
index 6b584e8ece..0000000000
--- a/actions/setup/js/test-lq2w64/test.log
+++ /dev/null
@@ -1 +0,0 @@
-content
\ No newline at end of file
diff --git a/actions/setup/js/test-noI9xR/test.log b/actions/setup/js/test-noI9xR/test.log
deleted file mode 100644
index 6b584e8ece..0000000000
--- a/actions/setup/js/test-noI9xR/test.log
+++ /dev/null
@@ -1 +0,0 @@
-content
\ No newline at end of file