fix(agentrun): 添加队列任务重试资源命令
This commit is contained in:
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user