fix: 修复整合包失败后的完整重试流程
Axolotl desktop CI / guardrails (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
Sync LobeHub models / sync (push) Has been cancelled

This commit is contained in:
Disy committed 2026-09-19 00:13:43 +08:00
1 parent 392f01ea4b
commit a47d309103
8 files changed
+266 -32

No files matched your search

@@ -0,0 +1,124 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import {
hostedRetryRoute,
hostedRuntimeRetryInstanceId,
retryInstallJob,
} from './hosted-install-retry.ts'
import type { InstallJobSnapshot } from './install.ts'
function job(overrides: Partial<InstallJobSnapshot> = {}): InstallJobSnapshot {
return {
job_id: 'job',
instance_id: 'local:instance',
source_instance_id: null,
instance_deleted: false,
kind: 'install_existing_instance',
status: 'failed',
execution_mode: 'normal',
provider: 'minecraft',
target: { type: 'existing_instance', instance_id: 'local:instance' },
phase: 'downloading_minecraft',
progress: null,
details: { type: 'empty' },
parallel: null,
display: null,
error: null,
rollback_error: null,
pause_reason: null,
upgrade_result: null,
created: '2026-09-18T00:00:00Z',
modified: '2026-09-18T00:00:00Z',
finished: '2026-09-18T00:00:01Z',
summary: {
files_completed: 0,
files_total: null,
bytes_downloaded: 0,
bytes_total: null,
speed_bytes_per_second: null,
eta_seconds: null,
source: null,
fallback_count: 0,
},
items: [],
...overrides,
}
}
test('StarLight runtime retries keep the existing instance identity', () => {
assert.equal(hostedRuntimeRetryInstanceId(job()), 'local:instance')
assert.equal(
hostedRuntimeRetryInstanceId(
job({ instance_id: null, target: { type: 'existing_instance', instance_id: 'target' } }),
),
'target',
)
})
test('deleted and unrelated install jobs do not enter the StarLight runtime retry path', () => {
assert.equal(hostedRuntimeRetryInstanceId(job({ instance_deleted: true })), null)
assert.equal(hostedRuntimeRetryInstanceId(job({ kind: 'create_instance' })), null)
})
test('the hosted retry link targets the current instance content route', () => {
assert.equal(
hostedRetryRoute('local:instance/with space'),
'/instance/local%3Ainstance%2Fwith%20space',
)
assert.doesNotMatch(hostedRetryRoute('local:instance'), /\/mods$/)
})
test('a StarLight runtime failure retries the complete hosted transaction only', async () => {
const calls: string[] = []
const result = await retryInstallJob(job(), {
getInstanceMode: async (instanceId) => {
calls.push(`mode:${instanceId}`)
return 'starlight'
},
retryHosted: async (instanceId, sourceJobId) => {
calls.push(`hosted:${instanceId}:${sourceJobId}`)
},
retryGeneric: async (jobId) => {
calls.push(`generic:${jobId}`)
return job()
},
})
assert.equal(result, null)
assert.deepEqual(calls, ['mode:local:instance', 'hosted:local:instance:job'])
})
test('a Local runtime failure keeps the generic retry path', async () => {
const replacement = job({ job_id: 'replacement', status: 'queued' })
const calls: string[] = []
const result = await retryInstallJob(job(), {
getInstanceMode: async () => 'local',
retryHosted: async () => {
calls.push('hosted')
},
retryGeneric: async (jobId) => {
calls.push(`generic:${jobId}`)
return replacement
},
})
assert.equal(result, replacement)
assert.deepEqual(calls, ['generic:job'])
})
test('mode lookup failures never fall back to an incomplete generic retry', async () => {
let genericCalls = 0
await assert.rejects(
retryInstallJob(job(), {
getInstanceMode: async () => {
throw new Error('instance unavailable')
},
retryHosted: async () => {},
retryGeneric: async () => {
genericCalls++
return job()
},
}),
/instance unavailable/,
)
assert.equal(genericCalls, 0)
})
@@ -0,0 +1,34 @@
import type { InstallJobSnapshot } from './install.ts'
interface InstallRetryDependencies {
getInstanceMode: (instanceId: string) => Promise<'starlight' | 'local'>
retryHosted: (instanceId: string, sourceJobId: string) => Promise<void>
retryGeneric: (jobId: string) => Promise<InstallJobSnapshot>
}
/**
* Minecraft installation jobs started by StarLight synchronization are only
* one stage of the full operation. Retrying that stage alone leaves the
* pending pack journal unapplied, so these jobs must resume through
* `hostedSync` instead of the generic install-job retry command.
*/
export function hostedRuntimeRetryInstanceId(job: InstallJobSnapshot): string | null {
if (job.kind !== 'install_existing_instance' || job.instance_deleted) return null
return job.instance_id ?? job.target.instance_id ?? null
}
export function hostedRetryRoute(instanceId: string): string {
return `/instance/${encodeURIComponent(instanceId)}`
}
export async function retryInstallJob(
job: InstallJobSnapshot,
dependencies: InstallRetryDependencies,
): Promise<InstallJobSnapshot | null> {
const instanceId = hostedRuntimeRetryInstanceId(job)
if (instanceId && (await dependencies.getInstanceMode(instanceId)) === 'starlight') {
await dependencies.retryHosted(instanceId, job.job_id)
return null
}
return dependencies.retryGeneric(job.job_id)
}