fix(agentrun): 完善重试命令发现与错误输出

This commit is contained in:
Codex
2026-07-12 02:55:57 +02:00
parent 9276e751f5
commit c4f3724430
4 changed files with 219 additions and 17 deletions
+145
View File
@@ -4,6 +4,20 @@ import { parseAgentRunClientConfigYaml, resolveAgentRunAuth, runAgentRunCommand,
import { resolveAgentRunLaneTarget } from "./agentrun-lanes";
import { placeholderAgentRunImage, renderAgentRunGitopsFiles } from "./agentrun-manifests";
function renderedTextOf(value: unknown): string {
if (typeof value !== "object" || value === null) throw new Error("expected rendered CLI result");
const text = (value as { renderedText?: unknown }).renderedText;
if (typeof text !== "string") throw new Error("expected renderedText string");
return text;
}
function renderedContentTypeOf(value: unknown): string {
if (typeof value !== "object" || value === null) throw new Error("expected rendered CLI result");
const contentType = (value as { contentType?: unknown }).contentType;
if (typeof contentType !== "string") throw new Error("expected contentType string");
return contentType;
}
const agentRunClientYaml = [
"version: 1",
"kind: AgentRunConfig",
@@ -313,6 +327,137 @@ describe("AgentRun default transport contract", () => {
const attemptHelp = await runAgentRunCommand(null, ["get", "attempts", "--help"]);
expect("renderedText" in retryHelp ? retryHelp.renderedText : "").toContain("agentrun retry task/<taskId> --idempotency-key <key> --reason <text>");
expect("renderedText" in attemptHelp ? attemptHelp.renderedText : "").toContain("agentrun get attempts --task <taskId>");
const rootHelp = Bun.spawnSync(["bun", "scripts/cli.ts", "agentrun", "--help"], {
cwd: new URL("../..", import.meta.url).pathname,
stdout: "pipe",
stderr: "pipe",
});
const rootHelpText = rootHelp.stdout.toString();
expect(rootHelp.exitCode).toBe(0);
expect(rootHelpText).toContain("Verbs: get, describe, events, logs, result, ack, cancel, retry,");
expect(rootHelpText).toContain("Resources: task/qt, attempt, run,");
expect(rootHelpText).toContain("agentrun retry task/<taskId> --idempotency-key <key> --reason <text> --dry-run");
expect(rootHelpText).toContain("agentrun get attempts --task <taskId> --limit 20");
expect(rootHelpText.split("\n").length).toBeLessThan(40);
});
test("local retry and attempt validation preserves bounded human and machine errors", async () => {
const cases = [
{
args: ["retry", "task/qt_failed_1", "--reason", "dependency repaired"],
message: "retry requires --idempotency-key <key>",
},
{
args: ["retry", "task/qt_failed_1", "--idempotency-key", "retry-key-1"],
message: "retry requires --reason <text>",
},
{
args: ["get", "attempts"],
message: "get attempts requires --task <taskId>",
},
{
args: ["retry", "run/run_invalid", "--idempotency-key", "retry-key-1", "--reason", "dependency repaired"],
message: "retry supports task/<taskId>",
},
{
args: ["retry"],
message: "retry requires task/<taskId>",
},
];
for (const item of cases) {
const human = await runAgentRunCommand(null, item.args);
const humanText = renderedTextOf(human);
expect(human.ok).toBe(false);
expect(humanText).toContain("Error: validation-failed");
expect(humanText).toContain(item.message);
expect(humanText.length).toBeLessThan(1600);
for (const outputArgs of [["-o", "json"], ["--raw"]]) {
const machine = await runAgentRunCommand(null, [...item.args, ...outputArgs]);
const machineText = renderedTextOf(machine);
const payload = JSON.parse(machineText) as { ok?: boolean; failureKind?: string; message?: string };
expect(machine.ok).toBe(false);
expect(renderedContentTypeOf(machine)).toBe("application/json");
expect(payload.ok).toBe(false);
expect(payload.failureKind).toBe("validation-failed");
expect(payload.message).toBe(item.message);
expect(machineText.length).toBeLessThan(2400);
}
}
for (const cliArgs of [
["retry", "run/run_invalid", "--idempotency-key", "retry-key-1", "--reason", "dependency repaired", "-o", "json"],
["get", "attempts", "--raw"],
]) {
const cli = Bun.spawnSync(["bun", "scripts/cli.ts", "agentrun", ...cliArgs], {
cwd: new URL("../..", import.meta.url).pathname,
stdout: "pipe",
stderr: "pipe",
});
const payload = JSON.parse(cli.stdout.toString()) as { ok?: boolean; failureKind?: string };
expect(cli.exitCode).toBe(1);
expect(payload.ok).toBe(false);
expect(payload.failureKind).toBe("validation-failed");
expect(cli.stdout.toString().length).toBeLessThan(2400);
}
});
test("server retry validation errors stay bounded in human and JSON output", async () => {
const taskId = "qt_completed_1";
const longMessage = `queue task ${taskId} cannot be retried from state completed: ${"x".repeat(6000)}`;
const longDiagnostic = `server diagnostic: ${"y".repeat(9000)}`;
let requests = 0;
const server = Bun.serve({
port: 0,
async fetch(request) {
const url = new URL(request.url);
expect(url.pathname).toBe(`/api/v1/queue/tasks/${taskId}/retry`);
expect(request.method).toBe("POST");
expect(await request.json()).toEqual({ idempotencyKey: "retry-key-terminal", reason: "operator requested retry", dryRun: true });
requests += 1;
return Response.json({
ok: false,
failureKind: "schema-invalid",
message: longMessage,
details: { allowedStates: ["failed", "blocked"], diagnostic: longDiagnostic, valuesPrinted: false },
valuesPrinted: false,
}, { status: 409 });
},
});
const previousConfig = process.env.AGENTRUN_CLIENT_CONFIG;
const previousKey = process.env.HWLAB_API_KEY;
process.env.HWLAB_API_KEY = "secret-value";
const tempConfigPath = `/tmp/unidesk-agentrun-retry-error-test-${process.pid}-${Date.now()}.yaml`;
try {
process.env.AGENTRUN_CLIENT_CONFIG = tempConfigPath;
await Bun.write(tempConfigPath, agentRunMinimalClientYaml.replace("http://agentrun.example.local:8080", server.url.href.replace(/\/$/u, "")));
const baseArgs = ["retry", `task/${taskId}`, "--idempotency-key", "retry-key-terminal", "--reason", "operator requested retry", "--dry-run"];
const human = await runAgentRunCommand(null, baseArgs);
const humanText = renderedTextOf(human);
expect(human.ok).toBe(false);
expect(humanText).toContain(`queue task ${taskId} cannot be retried from state completed`);
expect(humanText).not.toContain("x".repeat(500));
expect(humanText.length).toBeLessThan(1800);
const json = await runAgentRunCommand(null, [...baseArgs, "-o", "json"]);
const jsonText = renderedTextOf(json);
const payload = JSON.parse(jsonText) as { ok?: boolean; failureKind?: string; message?: string; agentrun?: { details?: { diagnostic?: string } } };
expect(json.ok).toBe(false);
expect(renderedContentTypeOf(json)).toBe("application/json");
expect(payload.failureKind).toBe("validation-failed");
expect(payload.message?.length).toBeLessThanOrEqual(400);
expect(payload.agentrun?.details?.diagnostic?.length).toBeLessThanOrEqual(400);
expect(jsonText.length).toBeLessThan(5000);
expect(requests).toBe(2);
} finally {
server.stop(true);
if (previousConfig === undefined) delete process.env.AGENTRUN_CLIENT_CONFIG;
else process.env.AGENTRUN_CLIENT_CONFIG = previousConfig;
if (previousKey === undefined) delete process.env.HWLAB_API_KEY;
else process.env.HWLAB_API_KEY = previousKey;
rmSync(tempConfigPath, { force: true });
}
});
});