fix(desktop): persist account challenge state and clear it on real execution success

Previously a challenge auth state was lost on client restart (rehydrated
as active/unknown from lastVerifiedAt alone) and never cleared once a
subsequent monitor execution actually succeeded. Persist authState and
authReason so a challenge survives a restart, and add
markTrackedAccountExecutionSucceeded to clear a stale challenge when a
real task run succeeds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-22 23:56:06 +08:00
parent 555b0abc72
commit 184646f9fe
2 changed files with 80 additions and 3 deletions
@@ -1,3 +1,4 @@
import { rmSync } from 'node:fs'
import { tmpdir } from 'node:os' import { tmpdir } from 'node:os'
import { join } from 'node:path' import { join } from 'node:path'
@@ -12,10 +13,11 @@ const silentRefreshPublishAccountSession = vi.fn()
const createSessionHandleForPartition = vi.fn() const createSessionHandleForPartition = vi.fn()
const resolveStoredSessionPartition = vi.fn() const resolveStoredSessionPartition = vi.fn()
const emitRuntimeInvalidated = vi.fn() const emitRuntimeInvalidated = vi.fn()
const accountHealthTestDirectory = join(tmpdir(), 'geo-rankly-account-health-test')
vi.mock('electron/main', () => ({ vi.mock('electron/main', () => ({
app: { app: {
getPath: () => join(tmpdir(), 'geo-rankly-account-health-test'), getPath: () => accountHealthTestDirectory,
}, },
})) }))
@@ -68,6 +70,7 @@ describe('account health challenge recheck', () => {
beforeEach(() => { beforeEach(() => {
vi.resetModules() vi.resetModules()
vi.clearAllMocks() vi.clearAllMocks()
rmSync(join(accountHealthTestDirectory, 'desktop-account-health.json'), { force: true })
resolveStoredSessionPartition.mockReturnValue('persist:test') resolveStoredSessionPartition.mockReturnValue('persist:test')
createSessionHandleForPartition.mockReturnValue({ partition: 'persist:test' }) createSessionHandleForPartition.mockReturnValue({ partition: 'persist:test' })
silentRefreshAIAccountSession.mockResolvedValue(true) silentRefreshAIAccountSession.mockResolvedValue(true)
@@ -124,4 +127,44 @@ describe('account health challenge recheck', () => {
expect(probeAIAccountSession).toHaveBeenCalledWith(account, 'persist:acc-account-1') expect(probeAIAccountSession).toHaveBeenCalledWith(account, 'persist:acc-account-1')
expect(snapshot.authState).toBe('active') expect(snapshot.authState).toBe('active')
}) })
it('clears a stale challenge after a real monitor execution succeeds', async () => {
probeAIAccountSession.mockResolvedValueOnce(challenge()).mockResolvedValueOnce(challenge())
const { getAccountHealthSnapshot, markTrackedAccountExecutionSucceeded, probeTrackedAccount } =
await import('./account-health')
const challenged = await probeTrackedAccount(account, {
trigger: 'manual',
allowSilentRefresh: true,
force: true,
})
expect(challenged.authState).toBe('challenge_required')
const recovered = markTrackedAccountExecutionSucceeded(account)
expect(recovered.authState).toBe('active')
expect(recovered.authReason).toBe('probe_success')
expect(recovered.lastAuthFailureAt).toBeNull()
expect(getAccountHealthSnapshot(account.id)?.authState).toBe('active')
})
it('preserves a challenge across a client restart', async () => {
probeAIAccountSession.mockResolvedValueOnce(challenge()).mockResolvedValueOnce(challenge())
const firstRuntime = await import('./account-health')
const challenged = await firstRuntime.probeTrackedAccount(account, {
trigger: 'manual',
allowSilentRefresh: true,
force: true,
})
expect(challenged.authState).toBe('challenge_required')
vi.resetModules()
const restartedRuntime = await import('./account-health')
expect(restartedRuntime.getAccountHealthSnapshot(account.id)?.authState).toBe(
'challenge_required',
)
expect(restartedRuntime.getAccountHealthSnapshot(account.id)?.lastAuthFailureAt).toBeTruthy()
})
}) })
+36 -2
View File
@@ -46,6 +46,8 @@ type ProbeTrigger = 'scheduled' | 'manual' | 'preflight' | 'confirm'
interface PersistedAccountHealthRecord { interface PersistedAccountHealthRecord {
accountId: string accountId: string
platform: string platform: string
authState?: AccountAuthState
authReason?: AccountAuthReason
lastVerifiedAt: number | null lastVerifiedAt: number | null
lastAuthFailureAt: number | null lastAuthFailureAt: number | null
profile: AccountHealthProfile | null profile: AccountHealthProfile | null
@@ -138,13 +140,26 @@ function nextRegularProbeAt(now = Date.now()): number {
} }
function normalizePersisted(record: PersistedAccountHealthRecord): AccountHealthRecord { function normalizePersisted(record: PersistedAccountHealthRecord): AccountHealthRecord {
const hasNewerAuthFailure = Boolean(
record.lastAuthFailureAt &&
(!record.lastVerifiedAt || record.lastAuthFailureAt >= record.lastVerifiedAt),
)
const authState = record.authState ?? (hasNewerAuthFailure ? 'challenge_required' : null)
const normalizedAuthState = authState ?? (record.lastVerifiedAt ? 'active' : 'unknown')
return { return {
accountId: record.accountId, accountId: record.accountId,
platform: record.platform, platform: record.platform,
authRevision: 0, authRevision: 0,
authState: record.lastVerifiedAt ? 'active' : 'unknown', authState: normalizedAuthState,
probeState: 'idle', probeState: 'idle',
authReason: record.lastVerifiedAt ? 'probe_success' : null, authReason:
record.authReason ??
(normalizedAuthState === 'active'
? 'probe_success'
: normalizedAuthState === 'challenge_required'
? 'captcha_gate'
: null),
lastVerifiedAt: record.lastVerifiedAt ?? null, lastVerifiedAt: record.lastVerifiedAt ?? null,
lastProbeAt: null, lastProbeAt: null,
nextProbeAt: nextInitialProbeAt(), nextProbeAt: nextInitialProbeAt(),
@@ -176,6 +191,8 @@ function persistRecords(): void {
const payload: PersistedAccountHealthRecord[] = [...records.values()].map((record) => ({ const payload: PersistedAccountHealthRecord[] = [...records.values()].map((record) => ({
accountId: record.accountId, accountId: record.accountId,
platform: record.platform, platform: record.platform,
authState: record.authState,
authReason: record.authReason,
lastVerifiedAt: record.lastVerifiedAt, lastVerifiedAt: record.lastVerifiedAt,
lastAuthFailureAt: record.lastAuthFailureAt, lastAuthFailureAt: record.lastAuthFailureAt,
profile: record.profile, profile: record.profile,
@@ -891,6 +908,23 @@ export function markTrackedAccountBound(
return snapshot return snapshot
} }
export function markTrackedAccountExecutionSucceeded(
account: PublishAccountIdentity,
): AccountHealthSnapshot {
const record = ensureRecord(account)
trackedAccounts.set(account.id, account)
return applyActiveResult(
record,
{
verdict: 'active',
reason: 'probe_success',
profile: record.profile,
sessionFingerprint: record.sessionFingerprint,
},
Date.now(),
)
}
export function markTrackedAccountMissingPartition( export function markTrackedAccountMissingPartition(
account: PublishAccountIdentity, account: PublishAccountIdentity,
): AccountHealthSnapshot { ): AccountHealthSnapshot {