Merge pull request #6169 from deepseek-harness/fix/account-failure-stderr

fix(account): 账号接口仅失败时输出 stderr 日志
This commit is contained in:
lsdsjy authored and GitHub committed 2026-10-14 14:24:37 +08:00
commit b2ea1d1b71
13 files changed
+290 -46

No files matched your search

@@ -36,5 +36,5 @@
en: 7afbd3f7afc20bd8
zh: f54c57e3d6738c7c
/deepseek-ai-dsh-client-ui-settings-account/known-limitations-and-deferred-work/dev-note:
en: 2facb0fc2e6c1a9c
zh: bd04803f5c9e132c
en: ebc93259d9f494e2
zh: dcc679e43cdbd594
@@ -114,6 +114,6 @@ The [desktop login decision](../../../.agents/notes/implemented/architecture/202
Usage and top-up show a centered 24px loading indicator without visible loading text until the native document loads; the return action remains available. The loading SVG is embedded locally from Figma node 2957:72553.
Default-model initialization runs after publishing and accepting the sign-in frame, without delaying subsequent account frames. Failures are recorded in diagnostics and do not mark the signed-in account as failed.
Default-model initialization runs after publishing and accepting the sign-in frame, without delaying subsequent account frames. Failures are recorded with `console.error` in the renderer Console and do not mark the signed-in account as failed. Profile and balance reads also report only failures, without account data or raw exceptions; successful refreshes and account frames are silent.
Read-only onboarding settings and a failed initial account stream leave the workspace accessible. A failed initial settings read exposes Retry, which reissues the shared settings read. Overlapping onboarding and Platform overlays release background interaction only after their last owner unmounts.
@@ -114,6 +114,6 @@ Desktop preload 标记在账号凭证已存储后启用引导。进度属于本
用量和充值在原生页面加载完成前显示居中的 24px 加载图标,不显示加载文字;返回操作始终可用。加载 SVG 从 Figma 节点 2957:72553 内嵌到本地样式。
默认模型初始化在登录帧发布并确认接收后运行,不延迟后续账号帧。初始化失败会记录诊断日志,不会将已登录账号标记为失败。
默认模型初始化在登录帧发布并确认接收后运行,不延迟后续账号帧。初始化失败通过 `console.error` 记录在渲染进程 Console 中,不会将已登录账号标记为失败。资料和余额读取也仅在失败时记录日志,不包含账号数据或原始异常;成功刷新和账号状态帧不输出日志。
引导设置只读或初始账号流失败时,工作区保持可访问。首次设置读取失败会显示重试入口,重新发起共享设置读取。引导与 Platform 覆盖层重叠时,最后一个持有者卸载后才恢复背景交互。
@@ -93,6 +93,7 @@ export function apply(ctx: Context): void {
let value: AccountDetails[K] | null
try { value = await query() }
catch { value = { status: 'failed' } }
if (value?.status === 'failed') console.error('[deepseek-account] account read failed', { field })
if (generation === revision && value !== null) publish({ ...snapshot, details: { ...snapshot.details, [field]: value } })
}
const request = Promise.all([
@@ -133,9 +134,9 @@ export function apply(ctx: Context): void {
if (initialize) void (async () => {
try {
const initialized = await ctx.remote.session.initializeDefaultModel()
if (!initialized.ok) console.info('[deepseek-account] default model initialization failed', { reason: 'refused' })
if (!initialized.ok) console.error('[deepseek-account] default model initialization failed', { reason: 'refused' })
} catch (_error) {
console.info('[deepseek-account] default model initialization failed', { reason: 'disconnected' })
console.error('[deepseek-account] default model initialization failed', { reason: 'disconnected' })
}
})()
void refresh()
@@ -207,6 +207,40 @@ it('reports the account sampled by the click when a native read outlasts a sign-
expect(new URL(String(open.mock.calls.at(-1)![0])).searchParams.get('prefill_uid')).toBe('account-user')
}, 60_000)
it('keeps successful account refreshes silent and reports only failed reads', async ({ start }) => {
vi.stubGlobal('dshDesktop', {})
const output = vi.spyOn(console, 'error').mockImplementation(() => undefined)
const info = vi.spyOn(console, 'info').mockImplementation(() => undefined)
const log = vi.spyOn(console, 'log').mockImplementation(() => undefined)
const c = await start()
const actions = operations(c)
await actions.refreshAccount()
c.mock.remote.account.getProfile.mockResolvedValue(ok(profile))
c.mock.remote.account.getBalance.mockResolvedValue(ok({ status: 'ready', value: [], bonusWallets: [] }))
c.mock.streams.push('account/watch', stored)
await vi.waitFor(() => { expect(actions.hooks.account.getSnapshot().details?.profile).toEqual(profile) })
await actions.refreshAccount()
for (const spy of [output, info, log]) {
expect(spy.mock.calls.filter(call => String(call[0]).startsWith('[deepseek-account]'))).toEqual([])
}
c.mock.remote.account.getProfile.mockResolvedValueOnce(ok({ status: 'failed' }))
c.mock.remote.account.getBalance.mockRejectedValueOnce(new Error('private-transport-detail'))
await actions.refreshAccount()
expect(output).toHaveBeenCalledWith('[deepseek-account] account read failed', expect.objectContaining({
field: 'profile',
}))
expect(output).toHaveBeenCalledWith('[deepseek-account] account read failed', expect.objectContaining({
field: 'balance',
}))
expect(JSON.stringify(output.mock.calls)).not.toContain('private-transport-detail')
c.mock.remote.account.getBalance.mockResolvedValueOnce({ ok: false, error: new RemoteError('gateway/internal', 'private-rpc-detail', {}) })
await actions.refreshAccount()
expect(output).toHaveBeenCalledWith('[deepseek-account] account read failed', expect.objectContaining({
field: 'balance',
}))
expect(JSON.stringify(output.mock.calls)).not.toContain('private-rpc-detail')
}, 60_000)
it('coalesces refreshes, publishes independent failures, and rejects stale responses after sign-out or unload', async ({ start }) => {
vi.stubGlobal('dshDesktop', {})
const c = await start()
@@ -4,11 +4,12 @@ import { expect, it, vi, afterEach } from 'vitest'
import type { AccountView, SignInAttemptId } from '@deepseek-ai/dsh-deepseek-account/types'
import { apply, type AccountSectionInjected } from '../src/client/index.ts'
afterEach(() => vi.unstubAllGlobals())
afterEach(() => { vi.unstubAllGlobals(); vi.unstubAllEnvs() })
it.each(['accepted', 'refused', 'disconnected', 'pending'] as const)('initializes account models and keeps reading account state: %s', async (outcome) => {
vi.stubGlobal('dshDesktop', {})
const log = vi.spyOn(console, 'info').mockImplementation(() => {})
vi.stubEnv('DSH_CLIENT_VERSION', '0.0.0-test')
const log = vi.spyOn(console, 'error').mockImplementation(() => {})
const observed: { status: string | undefined; failed: boolean | undefined }[] = []
const abort = new AbortController()
const done = Promise.withResolvers<undefined>()
@@ -28,7 +29,7 @@ it.each(['accepted', 'refused', 'disconnected', 'pending'] as const)('initialize
let operations: AccountSectionInjected | undefined
const ctx = {
effect: (install: () => (() => void) | undefined) => { const dispose = install(); if (dispose) disposers.push(dispose) },
locale: { register: () => () => {}, bind: () => (key: string) => key },
locale: { register: () => () => {}, bind: () => (key: string) => key, getSnapshot: () => ({ active: 'en' }) },
configForms: {
get: () => ({ subscribe: () => () => {}, getSnapshot: () => ({ status: 'loading', value: undefined }) }),
describe: () => ({ subscribe: () => () => {}, getSnapshot: () => ({ error: null }) }),
@@ -15,8 +15,8 @@
en: af856ba79fe31b8e
zh: 15b7d1a5d1856de2
/deepseek-ai-dsh-deepseek-account-platform/use-this-package:
en: ec9bb416eec2dcae
zh: 15b9c498388be972
en: 03ce750274b355f6
zh: 0bfb86570c686e17
/deepseek-ai-dsh-deepseek-account-platform/understand-the-implementation:
en: 76d2da2dab100ac3
zh: 2c9404f27915daab
@@ -59,7 +59,7 @@ A private patch can read environment variables through Cordis expressions:
The authenticated client supplies its browser-accessible callback origin, including the local SSH forwarding port, and its UI kind. The provider fixes the callback path and binds the resulting redirect_uri to both initialization and exchange. Callback requests use state and PKCE validation rather than RPC authentication. Completion, cancellation and disposal remove only the attempt route. Exchange failures publish failed state without retrying: Web closes its authorization tab and keeps the failure dialog in the original tab; the callback page attempts to close itself and offers a manual-close message. Desktop receives HTTP 204 and focuses its existing login UI through the account stream.
Host diagnostics use the `[deepseek-account]` prefix on standard output and in the Host inspector Console. They include endpoint paths, HTTP status, numeric response codes, failure stages, invalid field paths, received types and validation codes, and browser URL rejection rules; headers, request and response bodies, authorization URLs, and raw exceptions are excluded.
Host diagnostics use `console.error` with the `[deepseek-account]` prefix on standard error and in the Host inspector Console, only for request, response-validation, or sign-in failures. Successful requests, caller cancellation, and normal account state changes are silent; request timeouts remain failures. Failure diagnostics include endpoint paths, HTTP status, numeric response codes, failure stages, invalid field paths, received types and validation codes, and browser URL rejection rules; headers, request and response bodies, authorization URLs, and raw exceptions are excluded.
`rewriteBrowserOrigin` defaults to `false`, requiring same-origin browser URLs. A private development patch may set it to `true` to map authorization and completion URLs to `platformOrigin`, preserving their fixed paths and query strings. Source URLs must use HTTPS or already match the configured origin; user information, fragments and unexpected paths remain rejected. Shipped profiles retain strict same-origin validation.
@@ -59,7 +59,7 @@ getUnnotifiedBonuses 读取同一来源上的 GET /api/v0/users/get_unnotified_b
已鉴权客户端提供浏览器可访问的回调来源(含 SSH 本地转发端口)和 UI 类型。提供者固定回调路径,并在初始化与兑换时使用同一个 redirect_uri。回调请求使用 state 和 PKCE 校验,不要求 RPC 鉴权。完成、取消和卸载仅移除本次尝试的路由。兑换失败发布 failed 状态,不自动重试:Web 关闭授权标签页,原标签页显示失败弹窗;回调页尝试自行关闭,并提供手动关闭提示。Desktop 收到 HTTP 204,并通过账号状态流聚焦现有登录界面。
Host 诊断通过标准输出及 Host 调试器 Console 输出,前缀为 `[deepseek-account]`。日志包含接口路径、HTTP 状态、数值响应码、失败阶段、校验失败的字段路径、实际类型和校验码,以及浏览器 URL 拒绝规则,不包含请求头、请求和响应正文、授权 URL 或原始异常。
Host 诊断仅在请求、响应校验或登录失败时,通过 `console.error` 写入标准错误及 Host 调试器 Console,前缀为 `[deepseek-account]`。成功请求、调用方主动取消和正常账号状态变化不输出日志;请求超时仍记录为失败。失败诊断包含接口路径、HTTP 状态、数值响应码、失败阶段、校验失败的字段路径、实际类型和校验码,以及浏览器 URL 拒绝规则,不包含请求头、请求和响应正文、授权 URL 或原始异常。
`rewriteBrowserOrigin` 默认为 `false`,要求浏览器地址同源。私有开发 patch 可设为 `true`,将授权页和完成页地址映射到 `platformOrigin`,保留固定路径和查询字符串。原地址必须使用 HTTPS 或已匹配配置来源;仍拒绝用户名密码、片段和非预期路径。发布的 profile 保持严格同源校验。
@@ -29,23 +29,39 @@ const unnotified = z.array(bonus)
export function profile(value: unknown): AccountProfile {
const parsed = user.safeParse(value)
if (!parsed.success) throw new PlatformAuthError('protocol')
const { email, mobile, mobile_number: mobileNumber, id_profile: identity } = parsed.data
return projectProfile(parsed.data)
}
function projectProfile(value: z.infer<typeof user>): AccountProfile {
const { email, mobile, mobile_number: mobileNumber, id_profile: identity } = value
return {
id: parsed.data.id == null ? null : parsed.data.id as AccountUserId,
id: value.id == null ? null : value.id as AccountUserId,
avatarUrl: identity?.picture || null,
name: identity?.name || null, contact: mobile || mobileNumber || email || null,
}
}
function accountPayload<T>(schema: z.ZodType<T>, value: unknown, field: 'profile' | 'balance' | 'bonus'): T {
const parsed = schema.safeParse(value)
if (!parsed.success) {
console.error(`[deepseek-account] ${field} schema rejected`, {
issues: parsed.error.issues.map(issue => ({ path: issue.path, code: issue.code })),
})
throw new PlatformAuthError('protocol')
}
return parsed.data
}
const queries: { [K in keyof AccountDetails]: {
path: '/auth-api/v0/users/current' | '/api/v0/users/get_user_summary'
parse: (value: unknown) => AccountDetails[K]
} } = {
profile: { path: '/auth-api/v0/users/current', parse: value => ({ status: 'ready', value: profile(value) }) },
profile: { path: '/auth-api/v0/users/current', parse: value => ({
status: 'ready', value: projectProfile(accountPayload(user, value, 'profile')),
}) },
balance: { path: '/api/v0/users/get_user_summary', parse: (value) => {
const parsed = summary.safeParse(value)
if (!parsed.success) throw new PlatformAuthError('protocol')
return { status: 'ready', value: parsed.data.normal_wallets, bonusWallets: parsed.data.bonus_wallets }
const parsed = accountPayload(summary, value, 'balance')
return { status: 'ready', value: parsed.normal_wallets, bonusWallets: parsed.bonus_wallets }
} },
}
@@ -79,9 +95,8 @@ export async function readAccountDetail<K extends keyof AccountDetails>(field: K
*/
export async function readUnnotifiedBonuses(origin: string, token: string,
signal: AbortSignal, headers: Record<string, string>): Promise<readonly AccountBonusNotification[]> {
const parsed = unnotified.safeParse(await requestUnnotifiedBonuses(origin, token, signal, headers))
if (!parsed.success) throw new PlatformAuthError('protocol')
return parsed.data.map(row => ({
const parsed = accountPayload(unnotified, await requestUnnotifiedBonuses(origin, token, signal, headers), 'bonus')
return parsed.map(row => ({
orderId: row.order_id as AccountBonusOrderId, campaign: row.campaign, amount: row.amount,
currency: row.currency, grantedAt: row.granted_at, expiresAt: row.expires_at, message: row.msg,
}))
@@ -172,7 +172,6 @@ export class PlatformAccount extends DeepSeekAccount {
if (!parsed.success) throw new PlatformAuthError('storage')
if (parsed.data.issuer === this.origin) return
await this.ctx.credentials.deleteRecord(KEY)
console.info('[deepseek-account] stored grant discarded', { reason: 'issuer-mismatch' })
}
override async getState(): Promise<AccountView> {
@@ -433,7 +432,7 @@ export class PlatformAccount extends DeepSeekAccount {
} else attempt.callback?.writeHead(204, { 'cache-control': 'no-store' }).end()
}).catch((error: unknown) => {
const code = error instanceof PlatformAuthError ? error.code : 'protocol'
console.info('[deepseek-account] sign-in failed', { errorCode: code })
console.error('[deepseek-account] sign-in failed', { errorCode: code })
this.update(attempt, { phase: code === 'expired' ? 'expired' : 'failed', errorCode: code })
this.finishFailedCallback(attempt)
}).then(async () => {
@@ -613,7 +612,7 @@ export class PlatformAccount extends DeepSeekAccount {
}
private rejectPayload(stage: string, error: z.ZodError): never {
console.info('[deepseek-account] payload rejected', {
console.error('[deepseek-account] payload rejected', {
stage, issues: error.issues.map(issue => ({
path: issue.path, code: issue.code,
receivedType: issue.input === null ? 'null' : Array.isArray(issue.input) ? 'array' : typeof issue.input,
@@ -40,12 +40,12 @@ export function platformOrigin(value: string, allowLoopbackHttp: boolean): strin
export function browserUrl(value: string, origin: string, path: string, rewriteOrigin = false): string {
let url: URL
try { url = new URL(value) } catch {
console.info('[deepseek-account] browser URL rejected', { path, reason: 'invalid-url' })
console.error('[deepseek-account] browser URL rejected', { path, reason: 'invalid-url' })
throw new PlatformAuthError('protocol')
}
const allowedOrigin = url.origin === origin || (rewriteOrigin && url.protocol === 'https:')
if (!allowedOrigin || url.pathname !== path || url.username || url.password || url.hash) {
console.info('[deepseek-account] browser URL rejected', {
console.error('[deepseek-account] browser URL rejected', {
path, originMismatch: !allowedOrigin, pathMismatch: url.pathname !== path,
hasCredentials: Boolean(url.username || url.password), hasFragment: Boolean(url.hash),
})
@@ -82,7 +82,7 @@ export const initialization = z.object({
export const exchange = z.object({ token: z.string().regex(/^[\x21-\x7e]+$/), authorized_url: z.url(), user: z.unknown().optional() })
/**
* Read one bounded platform response with stable, non-secret diagnostics.
* Read one bounded platform response; non-cancellation failures emit non-secret diagnostics to stderr.
* @param origin - validated platform origin.
* @param method - platform endpoint suffix.
* @param body - protocol request, never logged.
@@ -163,22 +163,28 @@ export async function logoutAccount(origin: string, token: string,
}, signal)
}
/** Caller cancellation stays silent; standard TimeoutError aborts remain failures. */
function callerCancelled(signal: AbortSignal): boolean {
const reason: unknown = signal.reason
return signal.aborted && !(reason instanceof Error && reason.name === 'TimeoutError')
}
async function platformRequest(url: string, init: RequestInit, signal: AbortSignal): Promise<unknown> {
const path = new URL(url).pathname
console.info('[deepseek-account] request', { path, method: init.method })
let response: Response
try {
response = await fetch(url, { ...init, redirect: 'error', signal })
} catch {
console.info('[deepseek-account] request failed', { path, errorCode: 'no-response', aborted: signal.aborted })
if (!callerCancelled(signal)) console.error('[deepseek-account] request failed', { path, errorCode: 'no-response', aborted: signal.aborted })
throw new PlatformAuthError('no-response')
}
console.info('[deepseek-account] response', { path, status: response.status })
if (response.status === 401 && new Headers(init.headers).has('x-dsh-auth-token')) {
if (!callerCancelled(signal)) console.error('[deepseek-account] request failed', { path, status: response.status, errorCode: 'expired' })
await response.body?.cancel()
throw new AccountUnauthorizedError()
}
if (!response.ok || response.body === null) {
if (!callerCancelled(signal)) console.error('[deepseek-account] request failed', { path, status: response.status, errorCode: 'network' })
await response.body?.cancel()
throw new PlatformAuthError('network')
}
@@ -186,6 +192,8 @@ async function platformRequest(url: string, init: RequestInit, signal: AbortSign
const chunks: Uint8Array[] = []
let size = 0
let stage = 'read-body'
let codes: { code: number; data?: { biz_code: number } | undefined } | undefined
let validationError: z.ZodError | undefined
try {
while (true) {
const next = await reader.read()
@@ -199,20 +207,16 @@ async function platformRequest(url: string, init: RequestInit, signal: AbortSign
}
stage = 'parse-json'
const payload: unknown = JSON.parse(Buffer.concat(chunks).toString('utf8'))
const responseCodes = z.object({ code: z.number().int(), data: z.object({ biz_code: z.number().int() }).optional() }).safeParse(payload)
if (responseCodes.success) codes = responseCodes.data
const authorization = z.object({ code: z.literal(40003) }).safeParse(payload)
if (authorization.success && new Headers(init.headers).has('x-dsh-auth-token')) {
throw new AccountUnauthorizedError()
}
const codes = z.object({ code: z.number().int(), data: z.object({ biz_code: z.number().int() }).optional() }).safeParse(payload)
if (codes.success) console.info('[deepseek-account] response codes', {
path, code: codes.data.code, bizCode: codes.data.data?.biz_code,
})
stage = 'envelope'
const parsed = envelope.safeParse(payload)
if (!parsed.success) {
console.info('[deepseek-account] envelope rejected', {
path, issues: parsed.error.issues.map(issue => ({ path: issue.path, code: issue.code })),
})
validationError = parsed.error
throw new PlatformAuthError('protocol')
}
stage = 'business-code'
@@ -223,8 +227,11 @@ async function platformRequest(url: string, init: RequestInit, signal: AbortSign
}
return parsed.data.data.biz_data
} catch (error) {
console.info('[deepseek-account] response rejected', { path, stage,
errorCode: error instanceof PlatformAuthError ? error.code : 'protocol' })
if (!callerCancelled(signal)) console.error('[deepseek-account] response rejected', {
path, status: response.status, stage, code: codes?.code, bizCode: codes?.data?.biz_code,
issues: validationError?.issues.map(issue => ({ path: issue.path, code: issue.code })),
errorCode: error instanceof PlatformAuthError ? error.code : 'protocol',
})
if (error instanceof PlatformAuthError) throw error
throw new PlatformAuthError('protocol')
} finally {
@@ -1,5 +1,7 @@
import { expect, it, vi } from 'vitest'
import { browserUrl, requestPlatform } from '../src/protocol.ts'
import { browserUrl, logoutAccount, requestAccount, requestBonusNotified, requestPlatform, requestUnnotifiedBonuses } from '../src/protocol.ts'
import type { AccountBonusOrderId } from '@deepseek-ai/dsh-deepseek-account/types'
import { readAccountDetail, readUnnotifiedBonuses } from '../src/details.ts'
it.each(['/dsh/authorize', '/dsh/authorized'])('maps %s to the configured development origin and preserves query bytes', (path) => {
const origin = 'http://localhost:8081'
@@ -18,7 +20,7 @@ it.each(['/dsh/authorize', '/dsh/authorized'])('maps %s to the configured develo
})
it('reports business failure codes without credentials or response messages', async () => {
const output = vi.spyOn(console, 'info').mockImplementation(() => undefined)
const output = vi.spyOn(console, 'error').mockImplementation(() => undefined)
const fetcher = vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(JSON.stringify({
code: 0, data: { biz_code: 40123, biz_data: { token: 'response-secret' } }, message: 'private-message',
})))
@@ -26,6 +28,7 @@ it('reports business failure codes without credentials or response messages', as
await expect(requestPlatform('https://platform.deepseek.com', 'auth_init',
{ code_verifier: 'request-secret' }, new AbortController().signal,
{ Cookie: 'cookie-secret' })).rejects.toThrow('account: protocol')
expect(output).toHaveBeenCalledOnce()
const logged = JSON.stringify(output.mock.calls)
expect(logged).toContain('/auth-api/v0/dsh/auth_init')
expect(logged).toContain('40123')
@@ -40,7 +43,7 @@ it('reports business failure codes without credentials or response messages', as
})
it('reports transport failure without exposing the thrown error', async () => {
const output = vi.spyOn(console, 'info').mockImplementation(() => undefined)
const output = vi.spyOn(console, 'error').mockImplementation(() => undefined)
const fetcher = vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('private-network-detail'))
try {
await expect(requestPlatform('https://platform.deepseek.com', 'auth_init', {},
@@ -56,7 +59,7 @@ it('reports transport failure without exposing the thrown error', async () => {
it('identifies invalid envelope fields without logging response values', async () => {
const output = vi.spyOn(console, 'info').mockImplementation(() => undefined)
const output = vi.spyOn(console, 'error').mockImplementation(() => undefined)
const fetcher = vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(JSON.stringify({
code: 0, data: { biz_code: 'private-code', biz_data: { token: 'private-token' } },
})))
@@ -76,7 +79,7 @@ it('identifies invalid envelope fields without logging response values', async (
})
it('reports browser URL rejection rules without exposing the destination', () => {
const output = vi.spyOn(console, 'info').mockImplementation(() => undefined)
const output = vi.spyOn(console, 'error').mockImplementation(() => undefined)
try {
expect(() => browserUrl('https://private.example/wrong?code=private-code',
'https://platform.deepseek.com', '/dsh/authorize')).toThrow('account: protocol')
@@ -102,14 +105,198 @@ it.each([
start(controller) { controller.error(new Error('private-stream-error')) },
})), code: 'protocol' },
])('rejects $name without exposing response data', async ({ response, code }) => {
const output = vi.spyOn(console, 'info').mockImplementation(() => undefined)
const output = vi.spyOn(console, 'error').mockImplementation(() => undefined)
const fetcher = vi.spyOn(globalThis, 'fetch').mockResolvedValue(response())
try {
await expect(requestPlatform('https://platform.deepseek.com', 'auth_init', {},
new AbortController().signal, {})).rejects.toThrow(`account: ${code}`)
expect(output).toHaveBeenCalled()
expect(JSON.stringify(output.mock.calls)).not.toContain('private-')
} finally {
fetcher.mockRestore()
output.mockRestore()
}
})
it('keeps successful authorization, account, bonus and logout requests silent', async () => {
const output = ['log', 'info', 'error'] as const
const spies = output.map(method => vi.spyOn(console, method).mockImplementation(() => undefined))
const fetcher = vi.spyOn(globalThis, 'fetch').mockImplementation(async () => new Response(JSON.stringify({
code: 0, data: { biz_code: 0, biz_data: null },
})))
try {
const origin = 'https://platform.deepseek.com'
const signal = new AbortController().signal
for (const method of ['auth_init', 'auth_exchange', 'auth_cancel']) {
await requestPlatform(origin, method, {}, signal, {})
}
for (const path of ['/auth-api/v0/users/current', '/api/v0/users/get_user_summary'] as const) {
await requestAccount(origin, path, 'private-token', signal, {})
}
await requestUnnotifiedBonuses(origin, 'private-token', signal, {})
await requestBonusNotified(origin, 'private-token', 'fixture-order' as AccountBonusOrderId, signal, {})
await logoutAccount(origin, 'private-token', signal, {})
fetcher.mockResolvedValueOnce(new Response(JSON.stringify({
code: 0, data: { biz_code: 0, biz_data: { email: 'private-email' } },
})))
expect(await readAccountDetail('profile', origin, 'private-token', signal, {})).toMatchObject({ status: 'ready' })
fetcher.mockResolvedValueOnce(new Response(JSON.stringify({
code: 0, data: { biz_code: 0, biz_data: { normal_wallets: [], bonus_wallets: [] } },
})))
expect(await readAccountDetail('balance', origin, 'private-token', signal, {})).toMatchObject({ status: 'ready' })
for (const spy of spies) expect(spy).not.toHaveBeenCalled()
} finally {
fetcher.mockRestore()
for (const spy of spies) spy.mockRestore()
}
})
it.each([
{ name: 'HTTP rejection', status: 401, payload: 'private-body' },
{ name: 'authorization code rejection', status: 200, payload: JSON.stringify({ code: 40003 }) },
])('reports $name on the error console without the credential or body', async ({ status, payload }) => {
const output = vi.spyOn(console, 'error').mockImplementation(() => undefined)
const info = vi.spyOn(console, 'info').mockImplementation(() => undefined)
const fetcher = vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(payload, { status }))
try {
await expect(requestAccount('https://platform.deepseek.com', '/auth-api/v0/users/current',
'private-token', new AbortController().signal, {})).rejects.toThrow('account: expired')
expect(output).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
path: '/auth-api/v0/users/current', status,
}))
expect(JSON.stringify(output.mock.calls)).not.toContain('private-')
expect(info).not.toHaveBeenCalled()
} finally {
fetcher.mockRestore()
info.mockRestore()
output.mockRestore()
}
})
it('reports invalid bonus fields without logging bonus data', async () => {
const output = vi.spyOn(console, 'error').mockImplementation(() => undefined)
const fetcher = vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(JSON.stringify({
code: 0, data: { biz_code: 0, biz_data: [{ order_id: 'private-order', msg: 'private-message' }] },
})))
try {
await expect(readUnnotifiedBonuses('https://platform.deepseek.com', 'private-token',
new AbortController().signal, {})).rejects.toThrow('account: protocol')
expect(output.mock.calls[0]?.[0]).toBe('[deepseek-account] bonus schema rejected')
expect(JSON.stringify(output.mock.calls)).toContain('order_id')
expect(JSON.stringify(output.mock.calls)).not.toContain('private-')
} finally {
fetcher.mockRestore()
output.mockRestore()
}
})
it.each([
{ stage: 'fetch', code: 'no-response' },
{ stage: 'unauthorized', code: 'expired' },
{ stage: 'http', code: 'network' },
{ stage: 'body', code: 'protocol' },
])('keeps caller cancellation silent during $stage', async ({ stage, code }) => {
const output = vi.spyOn(console, 'error').mockImplementation(() => undefined)
const fetcher = vi.spyOn(globalThis, 'fetch')
const cancellation = new AbortController()
const reading = Promise.withResolvers<undefined>()
let body: ReadableStreamDefaultController<Uint8Array> | undefined
try {
if (stage === 'body') {
fetcher.mockResolvedValue(new Response(new ReadableStream<Uint8Array>({
start(controller) { body = controller },
pull() { reading.resolve(undefined) },
})))
} else if (stage === 'fetch') {
cancellation.abort()
fetcher.mockRejectedValue(cancellation.signal.reason)
} else {
fetcher.mockImplementation(async () => {
cancellation.abort()
return new Response('private-response', { status: stage === 'unauthorized' ? 401 : 503 })
})
}
const request = requestAccount('https://platform.deepseek.com', '/auth-api/v0/users/current',
'private-token', cancellation.signal, {})
if (stage === 'body') {
await reading.promise
cancellation.abort()
body!.error(cancellation.signal.reason)
}
await expect(request).rejects.toThrow(`account: ${code}`)
expect(output).not.toHaveBeenCalled()
} finally {
body?.error()
fetcher.mockRestore()
output.mockRestore()
}
})
it('keeps cancellation with a non-error reason silent', async () => {
const output = vi.spyOn(console, 'error').mockImplementation(() => undefined)
const fetcher = vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('private-network-error'))
const cancellation = new AbortController()
cancellation.abort('private-cancel-reason')
try {
expect(await readAccountDetail('profile', 'https://platform.deepseek.com', 'private-token',
cancellation.signal, {})).toEqual({ status: 'failed' })
expect(output).not.toHaveBeenCalled()
} finally {
fetcher.mockRestore()
output.mockRestore()
}
})
it.each(['fetch', 'body'] as const)('reports request timeouts during %s once without raw exceptions', async (stage) => {
const output = vi.spyOn(console, 'error').mockImplementation(() => undefined)
const fetcher = vi.spyOn(globalThis, 'fetch')
const timeout = new AbortController()
const reading = Promise.withResolvers<undefined>()
let body: ReadableStreamDefaultController<Uint8Array> | undefined
try {
if (stage === 'body') {
fetcher.mockResolvedValue(new Response(new ReadableStream<Uint8Array>({
start(controller) { body = controller },
pull() { reading.resolve(undefined) },
})))
} else {
timeout.abort(new DOMException('private-timeout-message', 'TimeoutError'))
fetcher.mockRejectedValue(timeout.signal.reason)
}
const request = readAccountDetail('profile', 'https://platform.deepseek.com', 'private-token', timeout.signal, {})
if (stage === 'body') {
await reading.promise
timeout.abort(new DOMException('private-timeout-message', 'TimeoutError'))
body!.error(timeout.signal.reason)
}
expect(await request).toEqual({ status: 'failed' })
expect(output).toHaveBeenCalledOnce()
expect(JSON.stringify(output.mock.calls)).not.toContain('private-')
} finally {
body?.error()
fetcher.mockRestore()
output.mockRestore()
}
})
it('reports invalid profile fields once without logging profile values', async () => {
const output = vi.spyOn(console, 'error').mockImplementation(() => undefined)
const fetcher = vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(JSON.stringify({
code: 0, data: { biz_code: 0, biz_data: { email: 42, id: 'private-id' } },
})))
try {
expect(await readAccountDetail('profile', 'https://platform.deepseek.com', 'private-token',
new AbortController().signal, {})).toEqual({ status: 'failed' })
expect(output).toHaveBeenCalledOnce()
expect(output.mock.calls[0]?.[0]).toBe('[deepseek-account] profile schema rejected')
const logged = JSON.stringify(output.mock.calls)
expect(logged).toContain('email')
expect(logged).toContain('invalid_type')
expect(logged).not.toContain('private-')
} finally {
fetcher.mockRestore()
output.mockRestore()
}
})