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