fix: 兼容用户输入事件的陈旧 runner 判定
This commit is contained in:
@@ -750,8 +750,21 @@ function hasNoBusinessProgress(run: RunRecord, command: CommandRecord, commands:
|
||||
if (commands.length !== 1 || commands[0]?.id !== command.id) return false;
|
||||
if (events.length === 0 || events.length >= 500) return false;
|
||||
const allowedPhases = new Set(["run-created", "command-created", "runner-job-created", "runner-dispatch-completed"]);
|
||||
const phases = events.map((event) => typeof event.payload.phase === "string" ? event.payload.phase : null);
|
||||
if (events.some((event, index) => event.type !== "backend_status" || phases[index] === null || !allowedPhases.has(phases[index] as string))) return false;
|
||||
const userMessages = events.filter((event) => event.type === "user_message");
|
||||
if (userMessages.length > 1) return false;
|
||||
const phases = events.flatMap((event) => {
|
||||
if (event.type === "user_message") {
|
||||
const payload = event.payload;
|
||||
const valid = payload.commandId === command.id
|
||||
&& typeof payload.userMessageId === "string" && payload.userMessageId.length > 0
|
||||
&& typeof payload.text === "string" && payload.text.length > 0
|
||||
&& typeof payload.source === "string" && payload.source.length > 0;
|
||||
return valid ? [] : [null];
|
||||
}
|
||||
const phase = typeof event.payload.phase === "string" ? event.payload.phase : null;
|
||||
return event.type === "backend_status" && phase !== null && allowedPhases.has(phase) ? [phase] : [null];
|
||||
});
|
||||
if (phases.includes(null)) return false;
|
||||
if (!phases.includes("run-created") || !phases.includes("command-created") || !phases.includes("runner-job-created")) return false;
|
||||
return events.every((event) => event.payload.commandId === undefined || event.payload.commandId === command.id);
|
||||
}
|
||||
|
||||
@@ -110,6 +110,7 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
|
||||
try {
|
||||
await assertTwentyStalePendingSelectsOnlyOldest({ fixturePath, statePath, kubectlCommand });
|
||||
await assertLegacyNoUserMessageCandidate({ fixturePath, statePath, kubectlCommand });
|
||||
await assertEventOnlyFailedMountCandidate({ fixturePath, statePath, kubectlCommand });
|
||||
await assertProtectedMatrix({ fixturePath, statePath, kubectlCommand });
|
||||
await assertPendingToRunningRaceFailsClosed({ fixturePath, statePath, kubectlCommand });
|
||||
@@ -128,6 +129,9 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
name: "runner-retention-stale-pending",
|
||||
tests: [
|
||||
"stale-pending-typed-candidate",
|
||||
"formal-user-message-is-not-runner-progress",
|
||||
"legacy-no-user-message-candidate",
|
||||
"user-message-command-binding-protected",
|
||||
"incoming-selects-only-oldest",
|
||||
"no-incoming-does-not-delete",
|
||||
"fresh-pending-protected",
|
||||
@@ -236,6 +240,25 @@ async function assertProtectedMatrix(input: { fixturePath: string; statePath: st
|
||||
const identityMismatchPod = runnerPod(base, 100, "pending");
|
||||
((identityMismatchPod.metadata as JsonRecord).annotations as JsonRecord)["agentrun.pikastech.local/command-id"] = "cmd-mismatched";
|
||||
await assertProtected(input, [base], [identityMismatchPod], "pod-identity-mismatch");
|
||||
const mismatchedUserMessage = pendingFact(106, oldAt);
|
||||
const userMessage = mismatchedUserMessage.events.find((event) => event.type === "user_message");
|
||||
assert.ok(userMessage);
|
||||
userMessage.payload.commandId = "cmd-mismatched";
|
||||
await assertProtected(input, [mismatchedUserMessage], [runnerPod(mismatchedUserMessage, 106, "failed-config")], "business-progress-observed-or-unverifiable");
|
||||
}
|
||||
|
||||
async function assertLegacyNoUserMessageCandidate(input: { fixturePath: string; statePath: string; kubectlCommand: string }): Promise<void> {
|
||||
const fact = pendingFact(89, new Date(Date.now() - 60 * 60_000).toISOString());
|
||||
fact.events = fact.events
|
||||
.filter((event) => event.type !== "user_message")
|
||||
.map((event, index) => ({ ...event, id: `evt-${fact.run.id}-${index + 1}`, seq: index + 1 }));
|
||||
await writeFixture(input, {
|
||||
jobs: [runnerJob(fact, 89)],
|
||||
pods: [runnerPod(fact, 89, "failed-config")],
|
||||
});
|
||||
const summary = await enforceRunnerRetentionBeforeCreate({ store: new RetentionFactStore([fact]), options: retentionOptions(input.kubectlCommand, 1), incomingRunnerCount: 1 });
|
||||
assert.equal(summary.selectedRunnerCount, 1);
|
||||
assert.equal(firstSummary(summary, "selected").candidateKind, "stale-pending");
|
||||
}
|
||||
|
||||
async function assertEventOnlyFailedMountCandidate(input: { fixturePath: string; statePath: string; kubectlCommand: string }): Promise<void> {
|
||||
@@ -495,11 +518,12 @@ function pendingFact(index: number, createdAt: string, variant: "pending" | "cla
|
||||
};
|
||||
const events: RunEvent[] = [
|
||||
runEvent(runId, 1, createdAt, "backend_status", { phase: "run-created" }),
|
||||
runEvent(runId, 2, createdAt, "backend_status", { phase: "command-created", commandId }),
|
||||
runEvent(runId, 3, createdAt, "backend_status", { phase: "runner-job-created", commandId }),
|
||||
runEvent(runId, 4, createdAt, "backend_status", { phase: "runner-dispatch-completed", commandId }),
|
||||
...(claimed ? [runEvent(runId, 5, progressedAt, "backend_status", { phase: "run-claimed", commandId })] : []),
|
||||
...(toolExecuting ? [runEvent(runId, 5, progressedAt, "tool_call", { commandId, tool: "shell", phase: "started" })] : []),
|
||||
runEvent(runId, 2, createdAt, "user_message", { commandId, traceId: `trc-retention-${suffix}`, userMessageId: `msg-retention-${suffix}`, text: "retention self-test", source: "agentrun-turn-command-payload", valuesPrinted: false }),
|
||||
runEvent(runId, 3, createdAt, "backend_status", { phase: "command-created", commandId }),
|
||||
runEvent(runId, 4, createdAt, "backend_status", { phase: "runner-job-created", commandId }),
|
||||
runEvent(runId, 5, createdAt, "backend_status", { phase: "runner-dispatch-completed", commandId }),
|
||||
...(claimed ? [runEvent(runId, 6, progressedAt, "backend_status", { phase: "run-claimed", commandId })] : []),
|
||||
...(toolExecuting ? [runEvent(runId, 6, progressedAt, "tool_call", { commandId, tool: "shell", phase: "started" })] : []),
|
||||
];
|
||||
return { run, command, events };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user