feat: 支持同 run runner 多轮 command
This commit is contained in:
@@ -14,28 +14,29 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
try {
|
||||
const client = new ManagerClient(server.baseUrl);
|
||||
const happy = await createRunWithCommand(client, context, "hello", "selftest-turn", 15_000);
|
||||
const result = await runOnce({ managerUrl: server.baseUrl, runId: happy.runId, codexCommand: context.fakeCodexCommand, codexArgs: context.fakeCodexArgs, codexHome: context.codexHome, env: { CODEX_HOME: context.codexHome } });
|
||||
const result = await runOnce({ managerUrl: server.baseUrl, runId: happy.runId, codexCommand: context.fakeCodexCommand, codexArgs: context.fakeCodexArgs, codexHome: context.codexHome, env: { CODEX_HOME: context.codexHome }, oneShot: true });
|
||||
assert.equal(result.terminalStatus, "completed");
|
||||
assert.equal(typeof (result.runner as { id?: unknown }).id, "string");
|
||||
const events = await client.get(`/api/v1/runs/${happy.runId}/events?afterSeq=0&limit=100`) as { items?: Array<{ type: string; payload: unknown }> };
|
||||
assert.ok(events.items?.some((event) => event.type === "assistant_message"));
|
||||
assert.ok(events.items?.some((event) => event.type === "backend_status" && JSON.stringify(event.payload).includes("run-claimed")));
|
||||
assertNoSecretLeak(events);
|
||||
const finalRun = await client.get(`/api/v1/runs/${happy.runId}`) as { terminalStatus?: string };
|
||||
assert.equal(finalRun.terminalStatus, "completed");
|
||||
const finalRun = await client.get(`/api/v1/runs/${happy.runId}`) as { terminalStatus?: string | null; status?: string };
|
||||
assert.equal(finalRun.terminalStatus, null);
|
||||
assert.equal(finalRun.status, "claimed");
|
||||
const finalCommand = await client.get(`/api/v1/runs/${happy.runId}/commands/${happy.commandId}`) as { state?: string };
|
||||
assert.equal(finalCommand.state, "completed");
|
||||
|
||||
const projectedHome = path.join(context.tmp, "runtime-codex-home");
|
||||
const projected = await createRunWithCommand(client, { workspace: context.workspace, codexHome: projectedHome }, "hello projected", "selftest-projected-codex-home", 15_000);
|
||||
const projectedResult = await runOnce({ managerUrl: server.baseUrl, runId: projected.runId, codexCommand: context.fakeCodexCommand, codexArgs: context.fakeCodexArgs, codexHome: projectedHome, env: { CODEX_HOME: projectedHome, AGENTRUN_CODEX_SECRET_HOME: context.codexHome } });
|
||||
const projectedResult = await runOnce({ managerUrl: server.baseUrl, runId: projected.runId, codexCommand: context.fakeCodexCommand, codexArgs: context.fakeCodexArgs, codexHome: projectedHome, env: { CODEX_HOME: projectedHome, AGENTRUN_CODEX_SECRET_HOME: context.codexHome }, oneShot: true });
|
||||
assert.equal(projectedResult.terminalStatus, "completed");
|
||||
await access(path.join(projectedHome, "auth.json"));
|
||||
await access(path.join(projectedHome, "config.toml"));
|
||||
|
||||
const deepseekHome = path.join(context.tmp, "runtime-deepseek-home");
|
||||
const deepseek = await createRunWithCommand(client, { ...context, backendProfile: "deepseek" }, "hello deepseek", "selftest-deepseek-turn", 15_000);
|
||||
const deepseekResult = await runOnce({ managerUrl: server.baseUrl, runId: deepseek.runId, backendProfile: "deepseek", codexCommand: context.fakeCodexCommand, codexArgs: context.fakeCodexArgs, codexHome: deepseekHome, env: { CODEX_HOME: deepseekHome, AGENTRUN_CODEX_SECRET_HOME: context.deepseekHome } });
|
||||
const deepseekResult = await runOnce({ managerUrl: server.baseUrl, runId: deepseek.runId, backendProfile: "deepseek", codexCommand: context.fakeCodexCommand, codexArgs: context.fakeCodexArgs, codexHome: deepseekHome, env: { CODEX_HOME: deepseekHome, AGENTRUN_CODEX_SECRET_HOME: context.deepseekHome }, oneShot: true });
|
||||
assert.equal(deepseekResult.terminalStatus, "completed");
|
||||
await access(path.join(deepseekHome, "auth.json"));
|
||||
await access(path.join(deepseekHome, "config.toml"));
|
||||
@@ -49,12 +50,12 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
);
|
||||
|
||||
const configModel = await createRunWithCommand(client, context, "hello config model", "selftest-config-model", 15_000);
|
||||
const configModelResult = await runOnce({ managerUrl: server.baseUrl, runId: configModel.runId, codexCommand: context.fakeCodexCommand, codexArgs: context.fakeCodexArgs, codexHome: context.codexHome, env: { CODEX_HOME: context.codexHome, AGENTRUN_FAKE_CODEX_MODE: "reject-unexpected-model" } });
|
||||
const configModelResult = await runOnce({ managerUrl: server.baseUrl, runId: configModel.runId, codexCommand: context.fakeCodexCommand, codexArgs: context.fakeCodexArgs, codexHome: context.codexHome, env: { CODEX_HOME: context.codexHome, AGENTRUN_FAKE_CODEX_MODE: "reject-unexpected-model" }, oneShot: true });
|
||||
assert.equal(configModelResult.terminalStatus, "completed", "unspecified model should be omitted so Codex config.toml remains authoritative");
|
||||
|
||||
const explicitModel = await createRunWithCommand(client, context, "hello explicit model placeholder", "selftest-explicit-model-placeholder", 15_000);
|
||||
const explicitCommand = await client.post(`/api/v1/runs/${explicitModel.runId}/commands`, { type: "turn", payload: { prompt: "hello explicit model", model: "gpt-5.5" }, idempotencyKey: "selftest-explicit-model-command" }) as { id: string };
|
||||
const explicitModelResult = await runOnce({ managerUrl: server.baseUrl, runId: explicitModel.runId, commandId: explicitCommand.id, codexCommand: context.fakeCodexCommand, codexArgs: context.fakeCodexArgs, codexHome: context.codexHome, env: { CODEX_HOME: context.codexHome, AGENTRUN_FAKE_CODEX_MODE: "require-explicit-model" } });
|
||||
const explicitModelResult = await runOnce({ managerUrl: server.baseUrl, runId: explicitModel.runId, commandId: explicitCommand.id, codexCommand: context.fakeCodexCommand, codexArgs: context.fakeCodexArgs, codexHome: context.codexHome, env: { CODEX_HOME: context.codexHome, AGENTRUN_FAKE_CODEX_MODE: "require-explicit-model" }, oneShot: true });
|
||||
assert.equal(explicitModelResult.terminalStatus, "completed", "explicit command payload model should still be forwarded");
|
||||
|
||||
await runFailureCase({ client, managerUrl: server.baseUrl, context, mode: "missing-turn-result", expectedStatus: "failed", expectedFailureKind: "backend-response-invalid" });
|
||||
@@ -83,6 +84,7 @@ async function runFailureCase(options: { client: ManagerClient; managerUrl: stri
|
||||
codexArgs: options.context.fakeCodexArgs,
|
||||
codexHome: options.context.codexHome,
|
||||
env: { CODEX_HOME: options.context.codexHome, AGENTRUN_FAKE_CODEX_MODE: options.mode },
|
||||
oneShot: true,
|
||||
}) as JsonRecord;
|
||||
assert.equal(result.terminalStatus, options.expectedStatus, options.mode);
|
||||
assert.equal(result.failureKind, options.expectedFailureKind, options.mode);
|
||||
@@ -113,12 +115,15 @@ async function runSecretFailureCase(options: { client: ManagerClient; managerUrl
|
||||
codexArgs: options.context.fakeCodexArgs,
|
||||
codexHome: path.join(options.context.tmp, "missing-codex-home"),
|
||||
env: { CODEX_HOME: path.join(options.context.tmp, "missing-codex-home") },
|
||||
oneShot: true,
|
||||
}) as JsonRecord;
|
||||
assert.equal(result.terminalStatus, "blocked", "secret unavailable");
|
||||
assert.equal(result.failureKind, "secret-unavailable", "secret unavailable");
|
||||
const run = await options.client.get(`/api/v1/runs/${item.runId}`) as { status?: string; failureKind?: string };
|
||||
assert.equal(run.status, "blocked", "secret unavailable");
|
||||
assert.equal(run.failureKind, "secret-unavailable", "secret unavailable");
|
||||
const command = await options.client.get(`/api/v1/runs/${item.runId}/commands/${item.commandId}`) as { state?: string };
|
||||
assert.equal(command.state, "failed", "secret unavailable command state");
|
||||
const envelope = await options.client.get(`/api/v1/runs/${item.runId}/commands/${item.commandId}/result`) as JsonRecord;
|
||||
assert.equal(envelope.terminalStatus, "failed", "secret unavailable result terminal");
|
||||
assert.equal(envelope.failureKind, "secret-unavailable", "secret unavailable result kind");
|
||||
}
|
||||
|
||||
async function runSpawnFailureCase(options: { client: ManagerClient; managerUrl: string; context: SelfTestContext }): Promise<void> {
|
||||
@@ -130,6 +135,7 @@ async function runSpawnFailureCase(options: { client: ManagerClient; managerUrl:
|
||||
codexArgs: [],
|
||||
codexHome: options.context.codexHome,
|
||||
env: { CODEX_HOME: options.context.codexHome },
|
||||
oneShot: true,
|
||||
}) as JsonRecord;
|
||||
assert.equal(result.terminalStatus, "failed", "spawn failure");
|
||||
assert.equal(result.failureKind, "backend-spawn-failed", "spawn failure");
|
||||
|
||||
@@ -69,7 +69,7 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin
|
||||
);
|
||||
|
||||
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") } });
|
||||
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") }, oneShot: true });
|
||||
assert.equal(runResult.terminalStatus, "completed");
|
||||
const session = await store.getSession("hwlab-session-resume");
|
||||
assert.equal(session?.threadId, "thread_selftest_1");
|
||||
@@ -84,14 +84,29 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin
|
||||
const resumedRun = await client.get(`/api/v1/runs/${resumed.runId}`) as JsonRecord;
|
||||
assert.equal(((resumedRun.sessionRef as JsonRecord).threadId), "thread_selftest_1");
|
||||
|
||||
const multiTurn = await createHwlabRun(client, context, bundle, "hwlab-session-multiturn", "hello first turn", "hwlab-command-multiturn-1");
|
||||
const multiturnRunner = runOnce({ managerUrl: server.baseUrl, runId: multiTurn.runId, commandId: multiTurn.commandId, codexCommand: context.fakeCodexCommand, codexArgs: context.fakeCodexArgs, codexHome: context.codexHome, env: { CODEX_HOME: context.codexHome, AGENTRUN_WORKSPACE_ROOT: path.join(context.tmp, "workspaces-multiturn") }, idleTimeoutMs: 500, pollIntervalMs: 50 });
|
||||
await waitForCommandState(client, multiTurn.runId, multiTurn.commandId, "completed");
|
||||
const secondCommand = await client.post(`/api/v1/runs/${multiTurn.runId}/commands`, { type: "turn", payload: { prompt: "hello second turn", traceId: "hwlab-command-multiturn-2" }, idempotencyKey: "hwlab-command-multiturn-2" }) as { id: string };
|
||||
await waitForCommandState(client, multiTurn.runId, secondCommand.id, "completed");
|
||||
const multiturnResult = await multiturnRunner as JsonRecord;
|
||||
assert.equal(multiturnResult.commandsProcessed, 2);
|
||||
const multiEventsResponse = await client.get(`/api/v1/runs/${multiTurn.runId}/events?afterSeq=0&limit=200`) as { items?: Array<{ type?: string; payload?: JsonRecord }> };
|
||||
const multiEvents = multiEventsResponse.items ?? [];
|
||||
assert.equal(multiEvents.filter((event) => event.type === "backend_status" && event.payload?.phase === "resource-bundle-materialized").length, 1);
|
||||
assert.equal(multiEvents.filter((event) => event.type === "backend_status" && event.payload?.phase === "command-terminal").length, 2);
|
||||
const secondEnvelope = await client.get(`/api/v1/runs/${multiTurn.runId}/commands/${secondCommand.id}/result`) as JsonRecord;
|
||||
assert.equal(secondEnvelope.terminalStatus, "completed");
|
||||
assert.equal(secondEnvelope.reply, "fake codex stdio reply");
|
||||
|
||||
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") } });
|
||||
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") }, oneShot: true });
|
||||
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"] };
|
||||
return { name: "hwlab-manual-dispatch", tests: ["runner-job-idempotency", "pending-cancel", "result-envelope", "session-ref-resume", "resource-bundle-materialization", "same-run-runner-multiturn", "running-cancel"] };
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.server.close(() => resolve()));
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ async function assertEventContractAndCompletedSemantics(client: ManagerClient, c
|
||||
const happy = await createRunWithCommand(client, context, "hello event contract", "selftest-event-contract", 15_000);
|
||||
await client.post(`/api/v1/runs/${happy.runId}/events`, { type: "tool_call", payload: { method: "selftest/tool", item: { command: "echo ok" } } });
|
||||
await client.post(`/api/v1/runs/${happy.runId}/events`, { type: "diff", payload: { filesChanged: 1, summary: "selftest diff" } });
|
||||
const result = await runOnce({ managerUrl, runId: happy.runId, codexCommand: context.fakeCodexCommand, codexArgs: context.fakeCodexArgs, codexHome: context.codexHome, env: { CODEX_HOME: context.codexHome } });
|
||||
const result = await runOnce({ managerUrl, runId: happy.runId, codexCommand: context.fakeCodexCommand, codexArgs: context.fakeCodexArgs, codexHome: context.codexHome, env: { CODEX_HOME: context.codexHome }, oneShot: true });
|
||||
assert.equal(result.terminalStatus, "completed");
|
||||
const eventsResponse = await client.get(`/api/v1/runs/${happy.runId}/events?afterSeq=0&limit=100`) as { items?: RunEvent[] };
|
||||
const events = eventsResponse.items ?? [];
|
||||
@@ -81,7 +81,7 @@ async function assertEventContractAndCompletedSemantics(client: ManagerClient, c
|
||||
const envelope = await client.get(`/api/v1/runs/${happy.runId}/commands/${happy.commandId}/result`) as JsonRecord;
|
||||
assert.equal(envelope.completed, true);
|
||||
assert.equal(envelope.terminalStatus, "completed");
|
||||
assert.equal(envelope.terminalSource, "terminal_status-event");
|
||||
assert.equal(envelope.terminalSource, "command-record");
|
||||
assertNoSecretLeak({ eventsResponse, envelope });
|
||||
|
||||
const partial = await createRunWithCommand(client, context, "partial should not complete", "selftest-partial-not-completed", 15_000);
|
||||
@@ -132,7 +132,7 @@ async function assertResourceBundleFailure(client: ManagerClient, context: SelfT
|
||||
resourceBundleRef: { kind: "git", repoUrl: repo.repoUrl, commitId: "0000000000000000000000000000000000000000", submodules: false, lfs: false },
|
||||
}) as { id: string };
|
||||
const command = await client.post(`/api/v1/runs/${run.id}/commands`, { type: "turn", payload: { prompt: "bad bundle" }, idempotencyKey: "selftest-bad-bundle" }) as { id: string };
|
||||
const result = await runOnce({ managerUrl, runId: run.id, commandId: command.id, codexCommand: context.fakeCodexCommand, codexArgs: context.fakeCodexArgs, codexHome: context.codexHome, env: { CODEX_HOME: context.codexHome, AGENTRUN_WORKSPACE_ROOT: path.join(context.tmp, "bad-bundle-workspaces") } }) as JsonRecord;
|
||||
const result = await runOnce({ managerUrl, runId: run.id, commandId: command.id, codexCommand: context.fakeCodexCommand, codexArgs: context.fakeCodexArgs, codexHome: context.codexHome, env: { CODEX_HOME: context.codexHome, AGENTRUN_WORKSPACE_ROOT: path.join(context.tmp, "bad-bundle-workspaces") }, oneShot: true }) as JsonRecord;
|
||||
assert.equal(result.terminalStatus, "failed");
|
||||
assert.equal(result.failureKind, "infra-failed");
|
||||
const envelope = await client.get(`/api/v1/runs/${run.id}/commands/${command.id}/result`) as JsonRecord;
|
||||
|
||||
Reference in New Issue
Block a user