CI: fix command-reply payload typing

This commit is contained in:
Peter Steinberger
2025-12-03 00:33:58 +00:00
parent ecac4dd72a
commit f519e22e6d
4 changed files with 23 additions and 14 deletions

View File

@@ -1,6 +1,6 @@
import path from "node:path"; import path from "node:path";
import type { AgentMeta, AgentSpec } from "./types.js"; import type { AgentParseResult, AgentSpec } from "./types.js";
const GEMINI_BIN = "gemini"; const GEMINI_BIN = "gemini";
export const GEMINI_IDENTITY_PREFIX = export const GEMINI_IDENTITY_PREFIX =
@@ -8,10 +8,13 @@ export const GEMINI_IDENTITY_PREFIX =
// Gemini CLI currently prints plain text; --output json is flaky across versions, so we // Gemini CLI currently prints plain text; --output json is flaky across versions, so we
// keep parsing minimal and let MEDIA token stripping happen later in the pipeline. // keep parsing minimal and let MEDIA token stripping happen later in the pipeline.
function parseGeminiOutput(raw: string): { text?: string; meta?: AgentMeta } { function parseGeminiOutput(raw: string): AgentParseResult {
const trimmed = raw.trim(); const trimmed = raw.trim();
const text = trimmed || undefined; const text = trimmed || undefined;
return { texts: text ? [text] : undefined, meta: undefined }; return {
texts: text ? [text] : undefined,
meta: undefined,
} satisfies AgentParseResult;
} }
export const geminiSpec: AgentSpec = { export const geminiSpec: AgentSpec = {

View File

@@ -66,7 +66,7 @@ describe("runCommandReply", () => {
it("injects claude flags and identity prefix", async () => { it("injects claude flags and identity prefix", async () => {
const captures: ReplyPayload[] = []; const captures: ReplyPayload[] = [];
const runner = makeRunner({ stdout: "ok" }, captures); const runner = makeRunner({ stdout: "ok" }, captures);
const { payload } = await runCommandReply({ const { payloads } = await runCommandReply({
reply: { reply: {
mode: "command", mode: "command",
command: ["claude", "{{Body}}"], command: ["claude", "{{Body}}"],
@@ -83,6 +83,7 @@ describe("runCommandReply", () => {
enqueue: enqueueImmediate, enqueue: enqueueImmediate,
}); });
const payload = payloads?.[0];
expect(payload?.text).toBe("ok"); expect(payload?.text).toBe("ok");
const finalArgv = captures[0].argv as string[]; const finalArgv = captures[0].argv as string[];
expect(finalArgv).toContain("--output-format"); expect(finalArgv).toContain("--output-format");
@@ -192,7 +193,7 @@ describe("runCommandReply", () => {
const runner = vi.fn(async () => { const runner = vi.fn(async () => {
throw { stdout: "partial output here", killed: true, signal: "SIGKILL" }; throw { stdout: "partial output here", killed: true, signal: "SIGKILL" };
}); });
const { payload, meta } = await runCommandReply({ const { payloads, meta } = await runCommandReply({
reply: { reply: {
mode: "command", mode: "command",
command: ["echo", "hi"], command: ["echo", "hi"],
@@ -208,6 +209,7 @@ describe("runCommandReply", () => {
commandRunner: runner, commandRunner: runner,
enqueue: enqueueImmediate, enqueue: enqueueImmediate,
}); });
const payload = payloads?.[0];
expect(payload?.text).toContain("Command timed out after 1s"); expect(payload?.text).toContain("Command timed out after 1s");
expect(payload?.text).toContain("partial output"); expect(payload?.text).toContain("partial output");
expect(meta.killed).toBe(true); expect(meta.killed).toBe(true);
@@ -217,7 +219,7 @@ describe("runCommandReply", () => {
const runner = vi.fn(async () => { const runner = vi.fn(async () => {
throw { stdout: "", killed: true, signal: "SIGKILL" }; throw { stdout: "", killed: true, signal: "SIGKILL" };
}); });
const { payload } = await runCommandReply({ const { payloads } = await runCommandReply({
reply: { reply: {
mode: "command", mode: "command",
command: ["echo", "hi"], command: ["echo", "hi"],
@@ -234,6 +236,7 @@ describe("runCommandReply", () => {
commandRunner: runner, commandRunner: runner,
enqueue: enqueueImmediate, enqueue: enqueueImmediate,
}); });
const payload = payloads?.[0];
expect(payload?.text).toContain("(cwd: /tmp/work)"); expect(payload?.text).toContain("(cwd: /tmp/work)");
}); });
@@ -244,7 +247,7 @@ describe("runCommandReply", () => {
const runner = makeRunner({ const runner = makeRunner({
stdout: `hi\nMEDIA:${tmp}\nMEDIA:https://example.com/img.jpg`, stdout: `hi\nMEDIA:${tmp}\nMEDIA:https://example.com/img.jpg`,
}); });
const { payload } = await runCommandReply({ const { payloads } = await runCommandReply({
reply: { reply: {
mode: "command", mode: "command",
command: ["echo", "hi"], command: ["echo", "hi"],
@@ -261,6 +264,7 @@ describe("runCommandReply", () => {
commandRunner: runner, commandRunner: runner,
enqueue: enqueueImmediate, enqueue: enqueueImmediate,
}); });
const payload = payloads?.[0];
expect(payload?.mediaUrls).toEqual(["https://example.com/img.jpg"]); expect(payload?.mediaUrls).toEqual(["https://example.com/img.jpg"]);
await fs.unlink(tmp); await fs.unlink(tmp);
}); });
@@ -318,7 +322,7 @@ describe("runCommandReply", () => {
const runner = makeRunner({ const runner = makeRunner({
stdout: '{"result":"","duration_ms":50,"total_cost_usd":0.001}', stdout: '{"result":"","duration_ms":50,"total_cost_usd":0.001}',
}); });
const { payload } = await runCommandReply({ const { payloads } = await runCommandReply({
reply: { reply: {
mode: "command", mode: "command",
command: ["claude", "{{Body}}"], command: ["claude", "{{Body}}"],
@@ -335,6 +339,7 @@ describe("runCommandReply", () => {
enqueue: enqueueImmediate, enqueue: enqueueImmediate,
}); });
// Should NOT contain raw JSON - empty result should produce fallback message // Should NOT contain raw JSON - empty result should produce fallback message
const payload = payloads?.[0];
expect(payload?.text).not.toContain('{"result"'); expect(payload?.text).not.toContain('{"result"');
expect(payload?.text).toContain("command produced no output"); expect(payload?.text).toContain("command produced no output");
}); });
@@ -343,7 +348,7 @@ describe("runCommandReply", () => {
const runner = makeRunner({ const runner = makeRunner({
stdout: '{"text":"","duration_ms":50}', stdout: '{"text":"","duration_ms":50}',
}); });
const { payload } = await runCommandReply({ const { payloads } = await runCommandReply({
reply: { reply: {
mode: "command", mode: "command",
command: ["claude", "{{Body}}"], command: ["claude", "{{Body}}"],
@@ -360,6 +365,7 @@ describe("runCommandReply", () => {
enqueue: enqueueImmediate, enqueue: enqueueImmediate,
}); });
// Empty text should produce fallback message, not raw JSON // Empty text should produce fallback message, not raw JSON
const payload = payloads?.[0];
expect(payload?.text).not.toContain('{"text"'); expect(payload?.text).not.toContain('{"text"');
expect(payload?.text).toContain("command produced no output"); expect(payload?.text).toContain("command produced no output");
}); });
@@ -368,7 +374,7 @@ describe("runCommandReply", () => {
const runner = makeRunner({ const runner = makeRunner({
stdout: '{"result":"hello world","duration_ms":50}', stdout: '{"result":"hello world","duration_ms":50}',
}); });
const { payload } = await runCommandReply({ const { payloads } = await runCommandReply({
reply: { reply: {
mode: "command", mode: "command",
command: ["claude", "{{Body}}"], command: ["claude", "{{Body}}"],
@@ -384,6 +390,7 @@ describe("runCommandReply", () => {
commandRunner: runner, commandRunner: runner,
enqueue: enqueueImmediate, enqueue: enqueueImmediate,
}); });
const payload = payloads?.[0];
expect(payload?.text).toBe("hello world"); expect(payload?.text).toBe("hello world");
}); });
}); });

View File

@@ -401,7 +401,7 @@ export async function runCommandReply(
} }
verboseLog(`Command auto-reply meta: ${JSON.stringify(meta)}`); verboseLog(`Command auto-reply meta: ${JSON.stringify(meta)}`);
return { payloads, payload: payloads[0], meta }; return { payloads, meta };
} catch (err) { } catch (err) {
const elapsed = Date.now() - started; const elapsed = Date.now() - started;
logger.info( logger.info(
@@ -430,7 +430,7 @@ export async function runCommandReply(
? `${baseMsg}\n\nPartial output before timeout:\n${partialSnippet}` ? `${baseMsg}\n\nPartial output before timeout:\n${partialSnippet}`
: baseMsg; : baseMsg;
return { return {
payload: { text }, payloads: [{ text }],
meta: { meta: {
durationMs: elapsed, durationMs: elapsed,
queuedMs, queuedMs,

View File

@@ -329,8 +329,7 @@ export async function getReplyFromConfig(
timeoutSeconds, timeoutSeconds,
commandRunner, commandRunner,
}); });
const payloadArray = const payloadArray = runResult.payloads ?? [];
runResult.payloads ?? (runResult.payload ? [runResult.payload] : []);
const meta = runResult.meta; const meta = runResult.meta;
const normalizedPayloads = const normalizedPayloads =
payloadArray.length === 1 ? payloadArray[0] : payloadArray; payloadArray.length === 1 ? payloadArray[0] : payloadArray;