fix: expose durable runner admission identity
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { renderSessionSendError, renderSessionSendResult } from "./agentrun/session-send-render";
|
||||
import type { AgentRunOutputMode, AgentRunResourceOptions } from "./agentrun/utils";
|
||||
|
||||
const command = "agentrun send session/sess_admission_fixture --prompt-stdin";
|
||||
const sessionId = "sess_admission_fixture";
|
||||
const runId = "run_admission_fixture";
|
||||
const commandId = "cmd_admission_fixture";
|
||||
const dispatchIntentId = "rdi_admission_fixture";
|
||||
const plannedRunnerJobId = "rjob_planned_fixture";
|
||||
|
||||
const durableAdmissionFixture: Record<string, unknown> = {
|
||||
ok: true,
|
||||
command,
|
||||
data: {
|
||||
action: "session-send",
|
||||
dryRun: false,
|
||||
mutation: true,
|
||||
partialWrite: false,
|
||||
sessionId,
|
||||
decision: "turn",
|
||||
internalCommandType: "turn",
|
||||
run: { id: runId, status: "queued", sessionRef: { sessionId } },
|
||||
command: {
|
||||
id: commandId,
|
||||
state: "pending",
|
||||
type: "turn",
|
||||
dispatchIntent: {
|
||||
id: dispatchIntentId,
|
||||
state: "pending",
|
||||
runnerJobId: plannedRunnerJobId,
|
||||
actualRunnerJobId: null,
|
||||
attemptCount: 0,
|
||||
durable: true,
|
||||
},
|
||||
},
|
||||
runnerJob: {
|
||||
action: "runner-dispatch-admitted",
|
||||
state: "pending",
|
||||
runId,
|
||||
commandId,
|
||||
runnerJobId: plannedRunnerJobId,
|
||||
dispatchIntentId,
|
||||
durable: true,
|
||||
},
|
||||
runnerAdmission: {
|
||||
mode: "durable-dispatch-intent",
|
||||
state: "pending",
|
||||
reason: "session-runner-admission-recovered",
|
||||
disposition: "recovered",
|
||||
failureKind: null,
|
||||
sessionId,
|
||||
runId,
|
||||
commandId,
|
||||
dispatchIntentId,
|
||||
plannedRunnerJobId,
|
||||
actualRunnerJobId: null,
|
||||
attemptCount: 0,
|
||||
durable: true,
|
||||
mutation: true,
|
||||
partialWrite: false,
|
||||
recoveredPriorPartialWrite: true,
|
||||
retryable: true,
|
||||
retryAuthority: "dispatcher",
|
||||
recoveryActions: [
|
||||
{ operation: "describe", resourceKind: "session", resourceName: sessionId },
|
||||
{ operation: "events", resourceKind: "run", resourceName: runId, runId, afterSeq: 0, limit: 100 },
|
||||
],
|
||||
valuesPrinted: false,
|
||||
},
|
||||
pollActions: [
|
||||
{ operation: "logs", resourceKind: "session", resourceName: sessionId, sessionId, limit: 100 },
|
||||
{ operation: "cancel", resourceKind: "session", resourceName: sessionId },
|
||||
],
|
||||
valuesPrinted: false,
|
||||
},
|
||||
};
|
||||
|
||||
const legacySuccessFixture: Record<string, unknown> = {
|
||||
ok: true,
|
||||
command,
|
||||
data: {
|
||||
action: "session-send",
|
||||
dryRun: false,
|
||||
mutation: true,
|
||||
partialWrite: false,
|
||||
sessionId,
|
||||
decision: "turn",
|
||||
internalCommandType: "turn",
|
||||
run: { id: runId, status: "queued", sessionRef: { sessionId } },
|
||||
command: { id: commandId, state: "pending", type: "turn" },
|
||||
runnerJob: { id: "rjob_legacy_fixture", state: "queued", runId, commandId },
|
||||
pollActions: [
|
||||
{ operation: "describe", resourceKind: "session", resourceName: sessionId },
|
||||
{ operation: "events", resourceKind: "run", resourceName: runId, runId, afterSeq: 0, limit: 100 },
|
||||
],
|
||||
valuesPrinted: false,
|
||||
},
|
||||
};
|
||||
|
||||
const typedFailureFixture: Record<string, unknown> = {
|
||||
ok: false,
|
||||
command,
|
||||
failureKind: "validation-failed",
|
||||
message: "durable runner dispatch is not enabled for session turn admission",
|
||||
httpStatus: 503,
|
||||
agentrun: {
|
||||
failureKind: "infra-failed",
|
||||
error: {
|
||||
name: "AgentRunError",
|
||||
failureKind: "infra-failed",
|
||||
details: {
|
||||
code: "runner-admission-failed",
|
||||
reason: "session-runner-dispatcher-disabled",
|
||||
phase: "runner-admission",
|
||||
sessionId,
|
||||
mutation: false,
|
||||
partialWrite: false,
|
||||
retryable: false,
|
||||
retryAuthority: "none",
|
||||
recoveryAction: "restore-runner-dispatcher",
|
||||
recoveryActions: [
|
||||
{ operation: "describe", resourceKind: "session", resourceName: sessionId },
|
||||
],
|
||||
valuesPrinted: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe("AgentRun session send runner admission rendering", () => {
|
||||
test("default output exposes durable admission without claiming a Kubernetes Job exists", () => {
|
||||
const result = renderSessionSendResult(command, durableAdmissionFixture, options("human"));
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.renderedText).toContain("RunnerAdmission:");
|
||||
expect(result.renderedText).toContain("mode=durable-dispatch-intent state=pending disposition=recovered");
|
||||
expect(result.renderedText).toContain(`runId=${runId} commandId=${commandId} dispatchIntentId=${dispatchIntentId}`);
|
||||
expect(result.renderedText).toContain(`plannedRunnerJobId=${plannedRunnerJobId} actualRunnerJobId=-`);
|
||||
expect(result.renderedText).toContain("mutation=true partialWrite=false recoveredPriorPartialWrite=true");
|
||||
expect(result.renderedText).toContain("retryable=true retryAuthority=dispatcher");
|
||||
expect(result.renderedText).toContain(`planned-runnerjob/${plannedRunnerJobId}`);
|
||||
expect(result.renderedText).toContain("它本身不是 Kubernetes Job 已创建的证据");
|
||||
expect(result.renderedText).toContain("durable dispatcher 负责自动重试;这不是重复执行 session send 的授权");
|
||||
expect(result.renderedText).not.toContain(`describe runnerjob/${plannedRunnerJobId}`);
|
||||
expect(result.renderedText).not.toContain("agentrun send session/");
|
||||
expect(Buffer.byteLength(result.renderedText, "utf8")).toBeLessThan(7000);
|
||||
});
|
||||
|
||||
test("JSON and YAML expose the same bounded runner admission contract and read-only Next", () => {
|
||||
const jsonText = renderSessionSendResult(command, durableAdmissionFixture, options("json")).renderedText;
|
||||
const yamlText = renderSessionSendResult(command, durableAdmissionFixture, options("yaml")).renderedText;
|
||||
const json = JSON.parse(jsonText) as Record<string, any>;
|
||||
const yaml = Bun.YAML.parse(yamlText) as Record<string, any>;
|
||||
expect(yaml).toEqual(json);
|
||||
expect(json.runnerAdmission).toMatchObject({
|
||||
mode: "durable-dispatch-intent",
|
||||
state: "pending",
|
||||
reason: "session-runner-admission-recovered",
|
||||
disposition: "recovered",
|
||||
runId,
|
||||
commandId,
|
||||
dispatchIntentId,
|
||||
plannedRunnerJobId,
|
||||
actualRunnerJobId: null,
|
||||
mutation: true,
|
||||
partialWrite: false,
|
||||
recoveredPriorPartialWrite: true,
|
||||
retryable: true,
|
||||
retryAuthority: "dispatcher",
|
||||
});
|
||||
expect(json.resources.runnerJob).toBeNull();
|
||||
expect(json.resources.plannedRunnerJob).toEqual({
|
||||
kind: "planned-runnerjob",
|
||||
id: plannedRunnerJobId,
|
||||
identityRole: "durable-planned",
|
||||
kubernetesJobCreated: null,
|
||||
kubernetesJobCreationEvidence: "not-asserted",
|
||||
state: "pending",
|
||||
durable: true,
|
||||
});
|
||||
expect(json.next.actions.length).toBeGreaterThan(0);
|
||||
for (const action of json.next.actions) {
|
||||
expect(action.mutation).toBe(false);
|
||||
expect(action.command).toMatch(/^bun scripts\/cli\.ts agentrun (?:describe|events|logs|result)\b/u);
|
||||
expect(action.command).not.toContain("agentrun send");
|
||||
expect(action.command).not.toContain(`runnerjob/${plannedRunnerJobId}`);
|
||||
}
|
||||
for (const output of [jsonText, yamlText]) expect(Buffer.byteLength(output, "utf8")).toBeLessThan(7000);
|
||||
});
|
||||
|
||||
test("legacy manager success output keeps runnerJob behavior when runnerAdmission is absent", () => {
|
||||
const human = renderSessionSendResult(command, legacySuccessFixture, options("human")).renderedText;
|
||||
const json = JSON.parse(renderSessionSendResult(command, legacySuccessFixture, options("json")).renderedText) as Record<string, any>;
|
||||
expect(human).not.toContain("RunnerAdmission:");
|
||||
expect(human).toContain("runnerjob/rjob_legacy_fixture state=queued");
|
||||
expect(human).toContain("只使用 manager 返回的资源 identity 做只读下钻");
|
||||
expect(json.runnerAdmission).toBeNull();
|
||||
expect(json.resources.runnerJob).toEqual({ kind: "runnerjob", id: "rjob_legacy_fixture", state: "queued" });
|
||||
expect(json.resources).not.toHaveProperty("plannedRunnerJob");
|
||||
expect(json.next.actions.some((action: Record<string, unknown>) => String(action.command).includes("describe runnerjob/rjob_legacy_fixture"))).toBe(true);
|
||||
});
|
||||
|
||||
test("typed failure remains fail-closed, bounded, and does not suggest a repeated send", () => {
|
||||
const human = renderSessionSendError(command, typedFailureFixture, options("human"));
|
||||
const jsonText = renderSessionSendError(command, typedFailureFixture, options("json")).renderedText;
|
||||
const yamlText = renderSessionSendError(command, typedFailureFixture, options("yaml")).renderedText;
|
||||
const json = JSON.parse(jsonText) as Record<string, any>;
|
||||
const yaml = Bun.YAML.parse(yamlText) as Record<string, any>;
|
||||
expect(human.ok).toBe(false);
|
||||
expect(human.renderedText).toContain("TypedFailure: infra-failed");
|
||||
expect(human.renderedText).toContain("Reason: session-runner-dispatcher-disabled");
|
||||
expect(human.renderedText).toContain("Mutation: false");
|
||||
expect(human.renderedText).toContain("PartialWrite: false");
|
||||
expect(human.renderedText).toContain("Retryable: false");
|
||||
expect(human.renderedText).toContain("RetryAuthority: none");
|
||||
expect(human.renderedText).toContain("RecoveryAction: restore-runner-dispatcher");
|
||||
expect(human.renderedText).not.toContain("agentrun send session/");
|
||||
expect(yaml).toEqual(json);
|
||||
expect(json).toMatchObject({
|
||||
kind: "SessionSendError",
|
||||
mutation: false,
|
||||
partialWrite: false,
|
||||
error: {
|
||||
failureKind: "validation-failed",
|
||||
typedFailureKind: "infra-failed",
|
||||
code: "runner-admission-failed",
|
||||
reason: "session-runner-dispatcher-disabled",
|
||||
retryable: false,
|
||||
retryAuthority: "none",
|
||||
recoveryAction: "restore-runner-dispatcher",
|
||||
},
|
||||
});
|
||||
for (const output of [human.renderedText, jsonText, yamlText]) expect(Buffer.byteLength(output, "utf8")).toBeLessThan(7000);
|
||||
});
|
||||
});
|
||||
|
||||
function options(output: AgentRunOutputMode): AgentRunResourceOptions {
|
||||
return {
|
||||
output,
|
||||
full: false,
|
||||
raw: false,
|
||||
debug: false,
|
||||
taskInput: false,
|
||||
limit: 20,
|
||||
cursor: null,
|
||||
queue: null,
|
||||
state: null,
|
||||
unread: false,
|
||||
readerId: "cli",
|
||||
taskId: null,
|
||||
runId: null,
|
||||
commandId: null,
|
||||
sessionId: null,
|
||||
afterSeq: null,
|
||||
eventDetailSeq: null,
|
||||
tail: null,
|
||||
fullText: false,
|
||||
reason: null,
|
||||
dryRun: false,
|
||||
file: null,
|
||||
aipod: null,
|
||||
idempotencyKey: null,
|
||||
promptStdin: false,
|
||||
node: null,
|
||||
lane: null,
|
||||
passthroughArgs: [],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user