From 08f0cb8bc9c578467d66a56930f5e1c3b62ffd80 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 12 Jul 2026 18:42:18 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=B0=86=E8=B0=83=E5=BA=A6=E9=87=8D?= =?UTF-8?q?=E8=AF=95=E5=BD=92=E4=B8=BA=E5=9F=BA=E7=A1=80=E8=AE=BE=E6=96=BD?= =?UTF-8?q?=E8=BF=9B=E5=B1=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mgr/runner-retention.ts | 2 +- .../22-runner-retention-stale-pending.ts | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/mgr/runner-retention.ts b/src/mgr/runner-retention.ts index 4ac3a33..aedc634 100644 --- a/src/mgr/runner-retention.ts +++ b/src/mgr/runner-retention.ts @@ -976,7 +976,7 @@ function hasNoBusinessProgress(run: RunRecord, command: CommandRecord, commands: if (isoMs(run.createdAt) > cutoff || isoMs(command.createdAt) > cutoff) return false; 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 allowedPhases = new Set(["run-created", "command-created", "runner-dispatch-retry", "runner-job-created", "runner-dispatch-completed"]); const userMessages = events.filter((event) => event.type === "user_message"); if (userMessages.length > 1) return false; const phases = events.flatMap((event) => { diff --git a/src/selftest/cases/22-runner-retention-stale-pending.ts b/src/selftest/cases/22-runner-retention-stale-pending.ts index e2c559f..317df26 100644 --- a/src/selftest/cases/22-runner-retention-stale-pending.ts +++ b/src/selftest/cases/22-runner-retention-stale-pending.ts @@ -120,6 +120,7 @@ const selfTest: SelfTestCase = async (context) => { try { await assertTwentyStalePendingSelectsOnlyOldest({ fixturePath, statePath, kubectlCommand }); await assertLegacyNoUserMessageCandidate({ fixturePath, statePath, kubectlCommand }); + await assertDispatchRetryIsInfrastructureProgress({ fixturePath, statePath, kubectlCommand }); await assertEventOnlyFailedMountCandidate({ fixturePath, statePath, kubectlCommand }); await assertTerminalClaimDriftCandidate({ fixturePath, statePath, kubectlCommand }); await assertTerminalCommandClaimDriftCandidate({ fixturePath, statePath, kubectlCommand }); @@ -155,6 +156,7 @@ const selfTest: SelfTestCase = async (context) => { "stale-pending-typed-candidate", "formal-user-message-is-not-runner-progress", "legacy-no-user-message-candidate", + "dispatch-retry-is-not-business-progress", "user-message-command-binding-protected", "incoming-selects-only-oldest", "no-incoming-does-not-delete", @@ -320,6 +322,26 @@ async function assertLegacyNoUserMessageCandidate(input: { fixturePath: string; assert.equal(firstSummary(summary, "selected").candidateKind, "stale-pending"); } +async function assertDispatchRetryIsInfrastructureProgress(input: { fixturePath: string; statePath: string; kubectlCommand: string }): Promise { + const fact = pendingFact(108, new Date(Date.now() - 60 * 60_000).toISOString()); + fact.events.splice(3, 0, runEvent(fact.run.id, 4, fact.run.createdAt, "backend_status", { + phase: "runner-dispatch-retry", + commandId: fact.command.id, + reason: "runner retention retry self-test", + valuesPrinted: false, + })); + fact.events = fact.events.map((event, index) => ({ ...event, id: `evt-${fact.run.id}-${index + 1}`, seq: index + 1 })); + await writeFixture(input, { + jobs: [runnerJob(fact, 108)], + pods: [runnerPod(fact, 108, "failed-config")], + }); + const summary = await enforceRunnerRetentionBeforeCreate({ store: new RetentionFactStore([fact]), options: retentionOptions(input.kubectlCommand, 1), incomingRunnerCount: 0 }); + assert.equal(summary.inactiveCandidateCount, 1); + const candidate = firstSummary(summary, "inactiveCandidates"); + assert.equal(candidate.candidateKind, "stale-pending"); + assert.equal(candidate.classificationReason, "stale-pending-no-business-progress"); +} + async function assertEventOnlyFailedMountCandidate(input: { fixturePath: string; statePath: string; kubectlCommand: string }): Promise { const fact = pendingFact(90, new Date(Date.now() - 60 * 60_000).toISOString()); const fixture = {