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:
@@ -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()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user