Skip to content

Commit b01c7cc

Browse files
Copilotpelikhan
andauthored
Fix smoke-antigravity: replace npm install with GCS binary download, add missing log parser (#34768)
* Initial plan * Fix smoke-antigravity: replace npm install with GCS binary download, add log parser Root cause 1: @google/antigravity-cli does not exist on npm - the Antigravity CLI is a Go binary distributed from Google Cloud Storage. Replace with a shell script that downloads from storage.googleapis.com/antigravity-public/antigravity-cli/. Root cause 2: parse_antigravity_log.cjs was missing. Create the log parser for Antigravity's stream-json JSONL format. Also fix DefaultAntigravityVersion from 0.39.1 (incorrect, copied from Gemini) to 1.0.2-6113393518706688 (actual current version). Recompile smoke-antigravity.lock.yml with the corrected steps. Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Fix review comments: mark antigravity experimental, remove Node.js setup, fix shell injection, add tests Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Co-authored-by: Peli de Halleux <pelikhan@users.noreply.github.com>
1 parent 6c49a68 commit b01c7cc

8 files changed

Lines changed: 433 additions & 47 deletions

File tree

.github/workflows/smoke-antigravity.lock.yml

Lines changed: 10 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// @ts-check
2+
/// <reference types="@actions/github-script" />
3+
4+
const { createEngineLogParser, generateInformationSection } = require("./log_parser_shared.cjs");
5+
6+
const main = createEngineLogParser({
7+
parserName: "Antigravity",
8+
parseFunction: parseAntigravityLog,
9+
supportsDirectories: false,
10+
});
11+
12+
/**
13+
* Parse Antigravity CLI stream-json log output and format as markdown.
14+
* Antigravity CLI emits one JSON object per line (JSONL) with the following structure:
15+
* - Each line contains an accumulated response up to that point:
16+
* {"response": "<accumulated text>", "stats": {"models": {...}, "tools": {...}}}
17+
* - Each new line supersedes the previous (the response field grows incrementally).
18+
* - The last valid JSON line contains the complete final response and final stats.
19+
*
20+
* Stats structure:
21+
* - stats.models: map of model name → {input_tokens, output_tokens}
22+
* - stats.tools: map of tool name → call count
23+
*
24+
* @param {string} logContent - The raw log content to parse
25+
* @returns {{markdown: string, logEntries: Array, mcpFailures: Array<string>, maxTurnsHit: boolean}} Parsed log data
26+
*/
27+
function parseAntigravityLog(logContent) {
28+
if (!logContent) {
29+
return {
30+
markdown: "## 🤖 Antigravity\n\nNo log content provided.\n\n",
31+
logEntries: [],
32+
mcpFailures: [],
33+
maxTurnsHit: false,
34+
};
35+
}
36+
37+
/** @type {Array<{response: string, stats: any}>} */
38+
const parsedLines = [];
39+
for (const line of logContent.split("\n")) {
40+
const trimmed = line.trim();
41+
if (!trimmed || !trimmed.startsWith("{")) {
42+
continue;
43+
}
44+
try {
45+
const parsed = JSON.parse(trimmed);
46+
if (parsed && typeof parsed.response === "string") {
47+
parsedLines.push(parsed);
48+
}
49+
} catch (_e) {
50+
// Skip non-JSON lines
51+
}
52+
}
53+
54+
if (parsedLines.length === 0) {
55+
return {
56+
markdown: "## 🤖 Antigravity\n\nLog format not recognized as Antigravity stream-json.\n\n",
57+
logEntries: [],
58+
mcpFailures: [],
59+
maxTurnsHit: false,
60+
};
61+
}
62+
63+
// The last valid JSON line contains the complete final response and stats
64+
const lastEntry = parsedLines[parsedLines.length - 1];
65+
const finalResponse = lastEntry.response || "";
66+
const stats = lastEntry.stats || {};
67+
68+
// Build markdown output
69+
let markdown = "## 🤖 Antigravity\n\n";
70+
71+
if (finalResponse.trim()) {
72+
markdown += finalResponse.trim() + "\n\n";
73+
}
74+
75+
// Compute aggregated token usage from all models
76+
let totalInputTokens = 0;
77+
let totalOutputTokens = 0;
78+
if (stats.models && typeof stats.models === "object") {
79+
for (const modelStats of Object.values(stats.models)) {
80+
if (modelStats && typeof modelStats === "object") {
81+
const { input_tokens = 0, output_tokens = 0 } = /** @type {any} */ (modelStats);
82+
totalInputTokens += input_tokens;
83+
totalOutputTokens += output_tokens;
84+
}
85+
}
86+
}
87+
88+
// Build a synthetic entry compatible with generateInformationSection
89+
const syntheticEntry =
90+
totalInputTokens > 0 || totalOutputTokens > 0
91+
? {
92+
usage: {
93+
input_tokens: totalInputTokens,
94+
output_tokens: totalOutputTokens,
95+
},
96+
duration_ms: 0,
97+
num_turns: finalResponse.trim() ? 1 : 0,
98+
}
99+
: null;
100+
101+
markdown += generateInformationSection(syntheticEntry);
102+
103+
// Build logEntries for compatibility with createEngineLogParser contract
104+
/** @type {Array<any>} */
105+
const logEntries = [];
106+
if (finalResponse.trim()) {
107+
logEntries.push({
108+
type: "assistant",
109+
message: {
110+
content: [{ type: "text", text: finalResponse.trim() }],
111+
},
112+
});
113+
}
114+
115+
return {
116+
markdown,
117+
logEntries,
118+
mcpFailures: [],
119+
maxTurnsHit: false,
120+
};
121+
}
122+
123+
// Export for testing
124+
if (typeof module !== "undefined" && module.exports) {
125+
module.exports = {
126+
main,
127+
parseAntigravityLog,
128+
};
129+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2+
3+
describe("parse_antigravity_log.cjs", () => {
4+
let mockCore;
5+
let parseAntigravityLog;
6+
7+
beforeEach(async () => {
8+
mockCore = {
9+
debug: vi.fn(),
10+
info: vi.fn(),
11+
warning: vi.fn(),
12+
error: vi.fn(),
13+
setFailed: vi.fn(),
14+
setOutput: vi.fn(),
15+
summary: {
16+
addRaw: vi.fn().mockReturnThis(),
17+
write: vi.fn().mockResolvedValue(),
18+
},
19+
};
20+
global.core = mockCore;
21+
22+
const module = await import("./parse_antigravity_log.cjs?" + Date.now());
23+
parseAntigravityLog = module.parseAntigravityLog;
24+
});
25+
26+
afterEach(() => {
27+
delete global.core;
28+
});
29+
30+
describe("parseAntigravityLog function", () => {
31+
it("should return a default message for empty string input", () => {
32+
const result = parseAntigravityLog("");
33+
34+
expect(result.markdown).toContain("No log content provided");
35+
expect(result.logEntries).toEqual([]);
36+
expect(result.mcpFailures).toEqual([]);
37+
expect(result.maxTurnsHit).toBe(false);
38+
});
39+
40+
it("should return a default message for null input", () => {
41+
const result = parseAntigravityLog(null);
42+
43+
expect(result.markdown).toContain("No log content provided");
44+
expect(result.logEntries).toEqual([]);
45+
expect(result.mcpFailures).toEqual([]);
46+
expect(result.maxTurnsHit).toBe(false);
47+
});
48+
49+
it("should return unrecognized format message for non-JSON lines only", () => {
50+
const logContent = "plain text line\nnot json at all\ndebug: some message";
51+
52+
const result = parseAntigravityLog(logContent);
53+
54+
expect(result.markdown).toContain("Log format not recognized as Antigravity stream-json");
55+
expect(result.logEntries).toEqual([]);
56+
});
57+
58+
it("should skip non-JSON lines mixed with valid JSONL", () => {
59+
const logContent = [
60+
"DEBUG: starting antigravity",
61+
JSON.stringify({ response: "Final answer", stats: {} }),
62+
"DEBUG: done",
63+
].join("\n");
64+
65+
const result = parseAntigravityLog(logContent);
66+
67+
expect(result.markdown).toContain("Final answer");
68+
expect(result.logEntries).toHaveLength(1);
69+
});
70+
71+
it("should use the last valid JSON line as the final response", () => {
72+
const logContent = [
73+
JSON.stringify({ response: "Partial answer", stats: {} }),
74+
JSON.stringify({ response: "Complete final answer", stats: {} }),
75+
].join("\n");
76+
77+
const result = parseAntigravityLog(logContent);
78+
79+
expect(result.markdown).toContain("Complete final answer");
80+
expect(result.markdown).not.toContain("Partial answer");
81+
expect(result.logEntries).toHaveLength(1);
82+
expect(result.logEntries[0].message.content[0].text).toBe("Complete final answer");
83+
});
84+
85+
it("should aggregate token counts across multiple models", () => {
86+
const logContent = JSON.stringify({
87+
response: "Done",
88+
stats: {
89+
models: {
90+
"model-a": { input_tokens: 100, output_tokens: 50 },
91+
"model-b": { input_tokens: 200, output_tokens: 75 },
92+
},
93+
},
94+
});
95+
96+
const result = parseAntigravityLog(logContent);
97+
98+
// Total: 300 input, 125 output
99+
expect(result.markdown).toContain("300");
100+
expect(result.markdown).toContain("125");
101+
});
102+
103+
it("should handle single JSONL line with response and stats", () => {
104+
const logContent = JSON.stringify({
105+
response: "Hello from Antigravity",
106+
stats: {
107+
models: {
108+
"gemini-2.0-flash": { input_tokens: 500, output_tokens: 200 },
109+
},
110+
tools: { bash: 3 },
111+
},
112+
});
113+
114+
const result = parseAntigravityLog(logContent);
115+
116+
expect(result.markdown).toContain("## 🤖 Antigravity");
117+
expect(result.markdown).toContain("Hello from Antigravity");
118+
expect(result.markdown).toContain("500");
119+
expect(result.markdown).toContain("200");
120+
expect(result.logEntries).toHaveLength(1);
121+
expect(result.logEntries[0].type).toBe("assistant");
122+
});
123+
124+
it("should handle missing stats gracefully", () => {
125+
const logContent = JSON.stringify({ response: "Response without stats" });
126+
127+
const result = parseAntigravityLog(logContent);
128+
129+
expect(result.markdown).toContain("Response without stats");
130+
expect(result.logEntries).toHaveLength(1);
131+
});
132+
133+
it("should handle empty response in the last JSONL entry", () => {
134+
const logContent = JSON.stringify({ response: "", stats: { models: {} } });
135+
136+
const result = parseAntigravityLog(logContent);
137+
138+
expect(result.markdown).toContain("## 🤖 Antigravity");
139+
expect(result.logEntries).toHaveLength(0);
140+
});
141+
142+
it("should skip JSON lines that do not have a response string field", () => {
143+
const logContent = [
144+
JSON.stringify({ type: "debug", message: "starting" }),
145+
JSON.stringify({ response: "Real response", stats: {} }),
146+
].join("\n");
147+
148+
const result = parseAntigravityLog(logContent);
149+
150+
expect(result.markdown).toContain("Real response");
151+
});
152+
});
153+
});

0 commit comments

Comments
 (0)