fix(agentrun): 添加队列任务重试资源命令

This commit is contained in:
Codex
2026-07-12 02:47:00 +02:00
parent db75577b8f
commit 9276e751f5
6 changed files with 373 additions and 11 deletions
+152 -1
View File
@@ -5,6 +5,10 @@ import { resolveAgentRunLaneTarget } from "./agentrun-lanes";
import { placeholderAgentRunImage, renderAgentRunGitopsFiles } from "./agentrun-manifests";
const agentRunClientYaml = [
"version: 1",
"kind: AgentRunConfig",
"metadata:",
" name: agentrun",
"manager:",
" baseUrl: https://agentrun.127-0-0-1.nip.io/",
" timeoutMs: 15000",
@@ -32,9 +36,27 @@ const agentRunClientYaml = [
"client:",
" role: render-only",
" transport: direct-http",
" sessionPolicy:",
" tenantId: unidesk",
" projectId: test",
" providerId: NC01",
" backendProfile: codex",
" workspaceRef:",
" kind: opaque",
" executionPolicy:",
" sandbox: workspace-write",
" approval: never",
" timeoutMs: 900000",
" network: enabled",
" secretScope:",
" allowCredentialEcho: false",
].join("\n");
const agentRunMinimalClientYaml = [
"version: 1",
"kind: AgentRunConfig",
"metadata:",
" name: agentrun",
"manager:",
" baseUrl: http://agentrun.example.local:8080",
" timeoutMs: 15000",
@@ -46,6 +68,20 @@ const agentRunMinimalClientYaml = [
"client:",
" role: render-only",
" transport: direct-http",
" sessionPolicy:",
" tenantId: unidesk",
" projectId: test",
" providerId: NC01",
" backendProfile: codex",
" workspaceRef:",
" kind: opaque",
" executionPolicy:",
" sandbox: workspace-write",
" approval: never",
" timeoutMs: 900000",
" network: enabled",
" secretScope:",
" allowCredentialEcho: false",
].join("\n");
describe("AgentRun resource bridge argv", () => {
@@ -114,7 +150,7 @@ describe("AgentRun render-only REST client config", () => {
describe("AgentRun default transport contract", () => {
test("server-facing compatibility groups use REST dispatcher, not official SSH CLI wrapper", () => {
const source = readFileSync(new URL("./agentrun.ts", import.meta.url), "utf8");
const source = readFileSync(new URL("./agentrun/entry.ts", import.meta.url), "utf8");
const commandRouter = source.slice(source.indexOf("export async function runAgentRunCommand"), source.indexOf("function isAgentRunRestCompatGroup"));
expect(commandRouter).toContain("runAgentRunRestCompatCommand");
expect(commandRouter).not.toContain("runOfficialAgentRunCli");
@@ -163,6 +199,121 @@ describe("AgentRun default transport contract", () => {
rmSync(tempConfigPath, { force: true });
}
});
test("retry dry-run is server-validated and attempts use the formal resource API", async () => {
const taskId = "qt_failed_1";
const attempt = {
attemptId: "qat_retry_1",
taskId,
retryIndex: 1,
state: "reserved",
idempotencyKey: "retry-key-1",
reason: "dependency repaired",
payloadHash: "sha256:payload",
previousAttemptId: "qat_initial_0",
runId: "run_retry_1",
commandId: "cmd_retry_1",
runnerJobId: "rjob_retry_1",
sessionId: "ses_retry_1",
sessionPath: "/api/v1/sessions/ses_retry_1",
failureKind: "infra-failed",
failureMessage: "kubectl create runner job failed before materialization",
createdAt: "2026-07-12T00:00:00.000Z",
updatedAt: "2026-07-12T00:00:01.000Z",
activatedAt: "2026-07-12T00:00:01.000Z",
terminalAt: null,
};
let retryRequests = 0;
let attemptRequests = 0;
const server = Bun.serve({
port: 0,
async fetch(request) {
const url = new URL(request.url);
expect(request.headers.get("authorization")).toBe("Bearer secret-value");
if (url.pathname === `/api/v1/queue/tasks/${taskId}/retry`) {
retryRequests += 1;
expect(request.method).toBe("POST");
expect(await request.json()).toEqual({
idempotencyKey: "retry-key-1",
reason: "dependency repaired",
dryRun: true,
});
return Response.json({
ok: true,
data: {
action: "queue-retry",
mutation: false,
idempotentReplay: false,
task: { id: taskId, state: "failed", queue: "commander", payloadHash: "sha256:payload" },
attempt: null,
previousAttempt: { ...attempt, attemptId: "qat_initial_0", retryIndex: 0, state: "failed", previousAttemptId: null },
run: null,
command: null,
runnerJob: null,
allowedTransition: { from: "failed", to: "running", retryIndex: 1, previousAttemptId: "qat_initial_0", payloadHash: "sha256:payload", valuesPrinted: false },
pollActions: [],
},
});
}
if (url.pathname === `/api/v1/queue/tasks/${taskId}/attempts`) {
attemptRequests += 1;
expect(request.method).toBe("GET");
expect(url.searchParams.get("cursor")).toBe("1");
expect(url.searchParams.get("limit")).toBe("10");
return Response.json({ ok: true, data: { taskId, items: [attempt], count: 1, cursor: null } });
}
return new Response("not found", { status: 404 });
},
});
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-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 retryResult = await runAgentRunCommand(null, ["retry", `task/${taskId}`, "--idempotency-key", "retry-key-1", "--reason", "dependency repaired", "--dry-run"]);
expect(retryResult.ok).toBe(true);
expect("renderedText" in retryResult).toBe(true);
if ("renderedText" in retryResult) {
expect(retryResult.renderedText).toContain(`Task retry validated: task/${taskId}`);
expect(retryResult.renderedText).toContain("Transition: failed -> running retryIndex=1");
expect(retryResult.renderedText).toContain("Attempt: planned");
}
const attemptsResult = await runAgentRunCommand(null, ["get", "attempts", "--task", taskId, "--cursor", "1", "--limit", "10", "-o", "json"]);
expect(attemptsResult.ok).toBe(true);
expect("renderedText" in attemptsResult).toBe(true);
if ("renderedText" in attemptsResult && typeof attemptsResult.renderedText === "string") {
const payload = JSON.parse(attemptsResult.renderedText) as { kind?: string; taskId?: string; count?: number; items?: Array<Record<string, unknown>> };
expect(payload.kind).toBe("AttemptList");
expect(payload.taskId).toBe(taskId);
expect(payload.count).toBe(1);
expect(payload.items?.[0]?.name).toBe(attempt.attemptId);
expect(payload.items?.[0]?.retryIndex).toBe(1);
expect(payload.items?.[0]?.failureMessage).toBe(attempt.failureMessage);
}
const attemptsHumanResult = await runAgentRunCommand(null, ["get", "attempts", "--task", taskId, "--cursor", "1", "--limit", "10"]);
expect("renderedText" in attemptsHumanResult ? attemptsHumanResult.renderedText : "").toContain("infra-failed: kubectl create runner job failed before materialization");
expect(retryRequests).toBe(1);
expect(attemptRequests).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 });
}
});
test("retry and attempt resource help stays scoped to the controlled commands", async () => {
const retryHelp = await runAgentRunCommand(null, ["retry", "--help"]);
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>");
});
});
describe("AgentRun runner API key SecretRef", () => {