feat: 补齐 HWLAB 手动调度能力

This commit is contained in:
Codex
2026-06-01 11:40:08 +08:00
parent eb5e0f57a6
commit 62846f6369
25 changed files with 1159 additions and 70 deletions
+3 -1
View File
@@ -13,11 +13,13 @@ const selfTest: SelfTestCase = async () => {
(error) => error instanceof AgentRunError && error.failureKind === "infra-failed" && error.message.includes("DATABASE_URL is required"),
);
const postgresContract = postgresMigrationContract();
assert.equal(postgresContract.latestMigrationId, "002_v01_backend_profiles");
assert.equal(postgresContract.latestMigrationId, "003_v01_hwlab_manual_dispatch");
assert.ok(Array.isArray(postgresContract.requiredTables));
assert.ok(postgresContract.requiredTables.includes("agentrun_schema_migrations"));
assert.ok(postgresContract.requiredTables.includes("agentrun_runs"));
assert.ok(postgresContract.requiredTables.includes("agentrun_events"));
assert.ok(postgresContract.requiredTables.includes("agentrun_sessions"));
assert.ok(postgresContract.requiredTables.includes("agentrun_runner_jobs"));
return { name: "redaction-postgres", tests: ["redaction", "postgres-store-contract"] };
};
@@ -0,0 +1,141 @@
import assert from "node:assert/strict";
import { execFile as execFileCallback } from "node:child_process";
import { promisify } from "node:util";
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { startManagerServer } from "../../mgr/server.js";
import { ManagerClient } from "../../mgr/client.js";
import { MemoryAgentRunStore } from "../../mgr/store.js";
import { runOnce } from "../../runner/run-once.js";
import type { JsonRecord } from "../../common/types.js";
import { assertNoSecretLeak, type SelfTestCase, type SelfTestContext } from "../harness.js";
const execFile = promisify(execFileCallback);
const selfTest: SelfTestCase = async (context) => {
const fakeKubectl = path.join(context.tmp, "fake-kubectl-hwlab.js");
const createdManifest = path.join(context.tmp, "created-hwlab-runner-job.json");
await writeFile(fakeKubectl, `#!/usr/bin/env bun
const chunks = [];
for await (const chunk of Bun.stdin.stream()) chunks.push(chunk);
const text = Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
await Bun.write(${JSON.stringify(createdManifest)}, text);
const manifest = JSON.parse(text);
console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kind, metadata: { uid: "job-uid-hwlab", resourceVersion: "1", name: manifest.metadata.name, namespace: manifest.metadata.namespace } }));
`);
await chmod(fakeKubectl, 0o755);
const store = new MemoryAgentRunStore();
const server = await startManagerServer({
port: 0,
host: "127.0.0.1",
sourceCommit: "self-test",
store,
runnerJobDefaults: {
namespace: "agentrun-v01",
managerUrl: "http://agentrun-mgr.agentrun-v01.svc.cluster.local:8080",
image: "127.0.0.1:5000/agentrun/agentrun-mgr@sha256:1111111111111111111111111111111111111111111111111111111111111111",
kubectlCommand: fakeKubectl,
},
});
try {
const client = new ManagerClient(server.baseUrl);
const bundle = await createLocalGitBundle(context);
const first = await createHwlabRun(client, context, bundle, "hwlab-session-1", "hello bundle", "hwlab-command-1");
const created = await client.post(`/api/v1/runs/${first.runId}/runner-jobs`, { commandId: first.commandId, idempotencyKey: "hwlab-trace-1" }) as JsonRecord;
const replay = await client.post(`/api/v1/runs/${first.runId}/runner-jobs`, { commandId: first.commandId, idempotencyKey: "hwlab-trace-1" }) as JsonRecord;
assert.equal(replay.idempotentReplay, true);
assert.equal(replay.jobName, created.jobName);
assert.equal(replay.attemptId, created.attemptId);
await assert.rejects(
() => client.post(`/api/v1/runs/${first.runId}/runner-jobs`, { commandId: first.commandId, idempotencyKey: "hwlab-trace-1", runnerId: "runner_changed" }),
(error) => error instanceof Error && error.message.includes("idempotency key reused"),
);
const manifest = JSON.parse(await readFile(createdManifest, "utf8")) as JsonRecord;
assert.ok(JSON.stringify(manifest).includes("AGENTRUN_RESOURCE_BUNDLE_JSON"));
assertNoSecretLeak(created);
const pendingCancel = await createHwlabRun(client, context, bundle, "hwlab-session-cancel-pending", "cancel pending", "hwlab-command-cancel-pending");
const cancelledRun = await client.post(`/api/v1/runs/${pendingCancel.runId}/cancel`, { reason: "self-test pending cancel" }) as { status?: string; terminalStatus?: string; failureKind?: string };
assert.equal(cancelledRun.status, "cancelled");
assert.equal(cancelledRun.terminalStatus, "cancelled");
assert.equal(cancelledRun.failureKind, "cancelled");
const cancelledCommand = await client.get(`/api/v1/runs/${pendingCancel.runId}/commands/${pendingCancel.commandId}`) as { state?: string };
assert.equal(cancelledCommand.state, "cancelled");
await assert.rejects(
() => client.post(`/api/v1/runs/${pendingCancel.runId}/runner-jobs`, { commandId: pendingCancel.commandId, idempotencyKey: "hwlab-cancelled-job" }),
(error) => error instanceof Error && error.message.includes("already terminal"),
);
const sessionRun = await createHwlabRun(client, context, bundle, "hwlab-session-resume", "hello session", "hwlab-command-session");
const runResult = await runOnce({ managerUrl: server.baseUrl, runId: sessionRun.runId, codexCommand: context.fakeCodexCommand, codexArgs: context.fakeCodexArgs, codexHome: context.codexHome, env: { CODEX_HOME: context.codexHome, AGENTRUN_WORKSPACE_ROOT: path.join(context.tmp, "workspaces") } });
assert.equal(runResult.terminalStatus, "completed");
const session = await store.getSession("hwlab-session-resume");
assert.equal(session?.threadId, "thread_selftest_1");
const resultEnvelope = await client.get(`/api/v1/runs/${sessionRun.runId}/commands/${sessionRun.commandId}/result`) as JsonRecord;
assert.equal(resultEnvelope.terminalStatus, "completed");
assert.equal(resultEnvelope.reply, "fake codex stdio reply");
assert.equal(((resultEnvelope.sessionRef as JsonRecord).threadId), "thread_selftest_1");
assert.equal(((resultEnvelope.resourceBundleRef as JsonRecord).commitId), bundle.commitId);
assertNoSecretLeak(resultEnvelope);
const resumed = await createHwlabRun(client, context, bundle, "hwlab-session-resume", "hello resumed", "hwlab-command-session-resumed");
const resumedRun = await client.get(`/api/v1/runs/${resumed.runId}`) as JsonRecord;
assert.equal(((resumedRun.sessionRef as JsonRecord).threadId), "thread_selftest_1");
const runningCancel = await createHwlabRun(client, context, bundle, "hwlab-session-cancel-running", "cancel running", "hwlab-command-cancel-running", 10_000);
const running = runOnce({ managerUrl: server.baseUrl, runId: runningCancel.runId, codexCommand: context.fakeCodexCommand, codexArgs: context.fakeCodexArgs, codexHome: context.codexHome, env: { CODEX_HOME: context.codexHome, AGENTRUN_FAKE_CODEX_MODE: "missing-terminal", AGENTRUN_WORKSPACE_ROOT: path.join(context.tmp, "workspaces-running-cancel") } });
await waitForCommandState(client, runningCancel.runId, runningCancel.commandId, "acknowledged");
await client.post(`/api/v1/commands/${runningCancel.commandId}/cancel`, { reason: "self-test running cancel" });
const runningResult = await running;
assert.equal(runningResult.terminalStatus, "cancelled");
return { name: "hwlab-manual-dispatch", tests: ["runner-job-idempotency", "pending-cancel", "result-envelope", "session-ref-resume", "resource-bundle-materialization", "running-cancel"] };
} finally {
await new Promise<void>((resolve) => server.server.close(() => resolve()));
}
};
async function createLocalGitBundle(context: SelfTestContext): Promise<{ repoUrl: string; commitId: string }> {
const repo = path.join(context.tmp, "bundle-repo");
await mkdir(repo, { recursive: true });
await execFile("git", ["init"], { cwd: repo });
await writeFile(path.join(repo, "README.md"), "HWLAB bundle self-test\n", "utf8");
await execFile("git", ["add", "README.md"], { cwd: repo });
await execFile("git", ["-c", "user.email=selftest@example.invalid", "-c", "user.name=AgentRun SelfTest", "commit", "-m", "bundle selftest"], { cwd: repo });
const { stdout } = await execFile("git", ["rev-parse", "HEAD"], { cwd: repo });
return { repoUrl: repo, commitId: stdout.trim() };
}
async function createHwlabRun(client: ManagerClient, context: SelfTestContext, bundle: { repoUrl: string; commitId: string }, sessionId: string, prompt: string, idempotencyKey: string, timeoutMs = 15_000): Promise<{ runId: string; commandId: string }> {
const run = await client.post("/api/v1/runs", {
tenantId: "hwlab",
projectId: "pikasTech/HWLAB",
workspaceRef: { kind: "opaque", repo: "pikasTech/HWLAB" },
sessionRef: { sessionId, conversationId: sessionId },
resourceBundleRef: { kind: "git", repoUrl: bundle.repoUrl, commitId: bundle.commitId, submodules: false, lfs: false },
providerId: "G14",
backendProfile: "codex",
executionPolicy: {
sandbox: "workspace-write",
approval: "never",
timeoutMs,
network: "default",
secretScope: { allowCredentialEcho: false, providerCredentials: [{ profile: "codex", secretRef: { name: "agentrun-v01-provider-codex", keys: ["auth.json", "config.toml"], mountPath: context.codexHome } }] },
},
traceSink: { kind: "hwlab", traceId: idempotencyKey },
}) as { id: string };
const command = await client.post(`/api/v1/runs/${run.id}/commands`, { type: "turn", payload: { prompt, traceId: idempotencyKey }, idempotencyKey }) as { id: string };
return { runId: run.id, commandId: command.id };
}
async function waitForCommandState(client: ManagerClient, runId: string, commandId: string, state: string): Promise<void> {
const deadline = Date.now() + 5_000;
while (Date.now() < deadline) {
const command = await client.get(`/api/v1/runs/${runId}/commands/${commandId}`) as { state?: string };
if (command.state === state) return;
await new Promise((resolve) => setTimeout(resolve, 100));
}
throw new Error(`command ${commandId} did not reach ${state}`);
}
export default selfTest;