-
Notifications
You must be signed in to change notification settings - Fork 408
Fix smoke-antigravity: replace npm install with GCS binary download, add missing log parser #34768
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8d8e9d1
06133fa
a596c64
2f35f86
fcd39a9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| // @ts-check | ||
| /// <reference types="@actions/github-script" /> | ||
|
|
||
| const { createEngineLogParser, generateInformationSection } = require("./log_parser_shared.cjs"); | ||
|
|
||
| const main = createEngineLogParser({ | ||
| parserName: "Antigravity", | ||
| parseFunction: parseAntigravityLog, | ||
| supportsDirectories: false, | ||
| }); | ||
|
|
||
| /** | ||
| * Parse Antigravity CLI stream-json log output and format as markdown. | ||
| * Antigravity CLI emits one JSON object per line (JSONL) with the following structure: | ||
| * - Each line contains an accumulated response up to that point: | ||
| * {"response": "<accumulated text>", "stats": {"models": {...}, "tools": {...}}} | ||
| * - Each new line supersedes the previous (the response field grows incrementally). | ||
| * - The last valid JSON line contains the complete final response and final stats. | ||
| * | ||
| * Stats structure: | ||
| * - stats.models: map of model name → {input_tokens, output_tokens} | ||
| * - stats.tools: map of tool name → call count | ||
| * | ||
| * @param {string} logContent - The raw log content to parse | ||
| * @returns {{markdown: string, logEntries: Array, mcpFailures: Array<string>, maxTurnsHit: boolean}} Parsed log data | ||
| */ | ||
| function parseAntigravityLog(logContent) { | ||
| if (!logContent) { | ||
| return { | ||
| markdown: "## 🤖 Antigravity\n\nNo log content provided.\n\n", | ||
| logEntries: [], | ||
| mcpFailures: [], | ||
| maxTurnsHit: false, | ||
| }; | ||
| } | ||
|
|
||
| /** @type {Array<{response: string, stats: any}>} */ | ||
| const parsedLines = []; | ||
| for (const line of logContent.split("\n")) { | ||
| const trimmed = line.trim(); | ||
| if (!trimmed || !trimmed.startsWith("{")) { | ||
| continue; | ||
| } | ||
| try { | ||
| const parsed = JSON.parse(trimmed); | ||
| if (parsed && typeof parsed.response === "string") { | ||
| parsedLines.push(parsed); | ||
| } | ||
| } catch (_e) { | ||
| // Skip non-JSON lines | ||
| } | ||
| } | ||
|
|
||
| if (parsedLines.length === 0) { | ||
| return { | ||
| markdown: "## 🤖 Antigravity\n\nLog format not recognized as Antigravity stream-json.\n\n", | ||
| logEntries: [], | ||
| mcpFailures: [], | ||
| maxTurnsHit: false, | ||
| }; | ||
| } | ||
|
Comment on lines
+12
to
+61
|
||
|
|
||
| // The last valid JSON line contains the complete final response and stats | ||
| const lastEntry = parsedLines[parsedLines.length - 1]; | ||
| const finalResponse = lastEntry.response || ""; | ||
| const stats = lastEntry.stats || {}; | ||
|
|
||
| // Build markdown output | ||
| let markdown = "## 🤖 Antigravity\n\n"; | ||
|
|
||
| if (finalResponse.trim()) { | ||
| markdown += finalResponse.trim() + "\n\n"; | ||
| } | ||
|
|
||
| // Compute aggregated token usage from all models | ||
| let totalInputTokens = 0; | ||
| let totalOutputTokens = 0; | ||
| if (stats.models && typeof stats.models === "object") { | ||
| for (const modelStats of Object.values(stats.models)) { | ||
| if (modelStats && typeof modelStats === "object") { | ||
| const { input_tokens = 0, output_tokens = 0 } = /** @type {any} */ (modelStats); | ||
| totalInputTokens += input_tokens; | ||
| totalOutputTokens += output_tokens; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Build a synthetic entry compatible with generateInformationSection | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/diagnose] 💡 SuggestionInspect whether the Antigravity JSONL output includes timing information (e.g. const syntheticEntry = (totalInputTokens > 0 || totalOutputTokens > 0) ? {
usage: { input_tokens: totalInputTokens, output_tokens: totalOutputTokens },
duration_ms: stats.duration_ms ?? 0,
num_turns: finalResponse.trim() ? 1 : 0,
} : null;If timing is genuinely unavailable, add a brief comment explaining why
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Smoke bot reply. Me read latest review thread. Warning Firewall blocked 6 domainsThe following domains were blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "accounts.google.com"
- "android.clients.google.com"
- "clients2.google.com"
- "contentautofill.googleapis.com"
- "safebrowsingohttpgateway.googleapis.com"
- "www.google.com"See Network Configuration for more information.
|
||
| const syntheticEntry = | ||
| totalInputTokens > 0 || totalOutputTokens > 0 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Smoke review: parser keeps synthetic usage only when tokens exist. Me see clear path. |
||
| ? { | ||
| usage: { | ||
| input_tokens: totalInputTokens, | ||
| output_tokens: totalOutputTokens, | ||
| }, | ||
| duration_ms: 0, | ||
| num_turns: finalResponse.trim() ? 1 : 0, | ||
| } | ||
| : null; | ||
|
|
||
| markdown += generateInformationSection(syntheticEntry); | ||
|
|
||
| // Build logEntries for compatibility with createEngineLogParser contract | ||
| /** @type {Array<any>} */ | ||
| const logEntries = []; | ||
| if (finalResponse.trim()) { | ||
| logEntries.push({ | ||
| type: "assistant", | ||
| message: { | ||
| content: [{ type: "text", text: finalResponse.trim() }], | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| return { | ||
| markdown, | ||
| logEntries, | ||
| mcpFailures: [], | ||
| maxTurnsHit: false, | ||
| }; | ||
| } | ||
|
|
||
| // Export for testing | ||
| if (typeof module !== "undefined" && module.exports) { | ||
| module.exports = { | ||
| main, | ||
| parseAntigravityLog, | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; | ||
|
|
||
| describe("parse_antigravity_log.cjs", () => { | ||
| let mockCore; | ||
| let parseAntigravityLog; | ||
|
|
||
| beforeEach(async () => { | ||
| mockCore = { | ||
| debug: vi.fn(), | ||
| info: vi.fn(), | ||
| warning: vi.fn(), | ||
| error: vi.fn(), | ||
| setFailed: vi.fn(), | ||
| setOutput: vi.fn(), | ||
| summary: { | ||
| addRaw: vi.fn().mockReturnThis(), | ||
| write: vi.fn().mockResolvedValue(), | ||
| }, | ||
| }; | ||
| global.core = mockCore; | ||
|
|
||
| const module = await import("./parse_antigravity_log.cjs?" + Date.now()); | ||
| parseAntigravityLog = module.parseAntigravityLog; | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| delete global.core; | ||
| }); | ||
|
|
||
| describe("parseAntigravityLog function", () => { | ||
| it("should return a default message for empty string input", () => { | ||
| const result = parseAntigravityLog(""); | ||
|
|
||
| expect(result.markdown).toContain("No log content provided"); | ||
| expect(result.logEntries).toEqual([]); | ||
| expect(result.mcpFailures).toEqual([]); | ||
| expect(result.maxTurnsHit).toBe(false); | ||
| }); | ||
|
|
||
| it("should return a default message for null input", () => { | ||
| const result = parseAntigravityLog(null); | ||
|
|
||
| expect(result.markdown).toContain("No log content provided"); | ||
| expect(result.logEntries).toEqual([]); | ||
| expect(result.mcpFailures).toEqual([]); | ||
| expect(result.maxTurnsHit).toBe(false); | ||
| }); | ||
|
|
||
| it("should return unrecognized format message for non-JSON lines only", () => { | ||
| const logContent = "plain text line\nnot json at all\ndebug: some message"; | ||
|
|
||
| const result = parseAntigravityLog(logContent); | ||
|
|
||
| expect(result.markdown).toContain("Log format not recognized as Antigravity stream-json"); | ||
| expect(result.logEntries).toEqual([]); | ||
| }); | ||
|
|
||
| it("should skip non-JSON lines mixed with valid JSONL", () => { | ||
| const logContent = [ | ||
| "DEBUG: starting antigravity", | ||
| JSON.stringify({ response: "Final answer", stats: {} }), | ||
| "DEBUG: done", | ||
| ].join("\n"); | ||
|
|
||
| const result = parseAntigravityLog(logContent); | ||
|
|
||
| expect(result.markdown).toContain("Final answer"); | ||
| expect(result.logEntries).toHaveLength(1); | ||
| }); | ||
|
|
||
| it("should use the last valid JSON line as the final response", () => { | ||
| const logContent = [ | ||
| JSON.stringify({ response: "Partial answer", stats: {} }), | ||
| JSON.stringify({ response: "Complete final answer", stats: {} }), | ||
| ].join("\n"); | ||
|
|
||
| const result = parseAntigravityLog(logContent); | ||
|
|
||
| expect(result.markdown).toContain("Complete final answer"); | ||
| expect(result.markdown).not.toContain("Partial answer"); | ||
| expect(result.logEntries).toHaveLength(1); | ||
| expect(result.logEntries[0].message.content[0].text).toBe("Complete final answer"); | ||
| }); | ||
|
|
||
| it("should aggregate token counts across multiple models", () => { | ||
| const logContent = JSON.stringify({ | ||
| response: "Done", | ||
| stats: { | ||
| models: { | ||
| "model-a": { input_tokens: 100, output_tokens: 50 }, | ||
| "model-b": { input_tokens: 200, output_tokens: 75 }, | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| const result = parseAntigravityLog(logContent); | ||
|
|
||
| // Total: 300 input, 125 output | ||
| expect(result.markdown).toContain("300"); | ||
| expect(result.markdown).toContain("125"); | ||
| }); | ||
|
|
||
| it("should handle single JSONL line with response and stats", () => { | ||
| const logContent = JSON.stringify({ | ||
| response: "Hello from Antigravity", | ||
| stats: { | ||
| models: { | ||
| "gemini-2.0-flash": { input_tokens: 500, output_tokens: 200 }, | ||
| }, | ||
| tools: { bash: 3 }, | ||
| }, | ||
| }); | ||
|
|
||
| const result = parseAntigravityLog(logContent); | ||
|
|
||
| expect(result.markdown).toContain("## 🤖 Antigravity"); | ||
| expect(result.markdown).toContain("Hello from Antigravity"); | ||
| expect(result.markdown).toContain("500"); | ||
| expect(result.markdown).toContain("200"); | ||
| expect(result.logEntries).toHaveLength(1); | ||
| expect(result.logEntries[0].type).toBe("assistant"); | ||
| }); | ||
|
|
||
| it("should handle missing stats gracefully", () => { | ||
| const logContent = JSON.stringify({ response: "Response without stats" }); | ||
|
|
||
| const result = parseAntigravityLog(logContent); | ||
|
|
||
| expect(result.markdown).toContain("Response without stats"); | ||
| expect(result.logEntries).toHaveLength(1); | ||
| }); | ||
|
|
||
| it("should handle empty response in the last JSONL entry", () => { | ||
| const logContent = JSON.stringify({ response: "", stats: { models: {} } }); | ||
|
|
||
| const result = parseAntigravityLog(logContent); | ||
|
|
||
| expect(result.markdown).toContain("## 🤖 Antigravity"); | ||
| expect(result.logEntries).toHaveLength(0); | ||
| }); | ||
|
|
||
| it("should skip JSON lines that do not have a response string field", () => { | ||
| const logContent = [ | ||
| JSON.stringify({ type: "debug", message: "starting" }), | ||
| JSON.stringify({ response: "Real response", stats: {} }), | ||
| ].join("\n"); | ||
|
|
||
| const result = parseAntigravityLog(logContent); | ||
|
|
||
| expect(result.markdown).toContain("Real response"); | ||
| }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[/tdd] No co-located test file. Other log parsers in
actions/setup/js/(e.g.parse_claude_log.cjs,parse_copilot_log.cjs) each have a*.test.cjscompanion with unit tests for theirparseFunction.parseAntigravityLogis the first parser without one, which means the JSONL parsing logic, the last-line accumulation semantics, and the empty-log fallback paths are untested.💡 Suggested test cases
Create
parse_antigravity_log.test.cjscovering at minimum:stats.modelswith multiple models → aggregatesinput_tokens+output_tokenscorrectlystatsfield → does not crash, returns zero token counts