Merge branch 'master' into worktree/locale-browser-default

This commit is contained in:
creatixchu
2026-07-31 16:34:06 +08:00
parent e5563ae433
commit c7f706acba
29 changed files with 67648 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
import { Context } from 'cordis'
import { describe, expect, it } from 'vitest'
import { Settings, settingsNamespace, type SettingsNamespace } from '@deepseek-ai/dsh-settings'
import { apply } from '../src/index.ts'
import { WELCOME_NOTICE_SETTINGS_NAMESPACE } from '../src/onboarding-copy.ts'
class MemorySettings extends Settings {
readonly writable = true
protected load(): Promise<Record<string, unknown>> { return Promise.resolve({}) }
protected persist(_ns: SettingsNamespace, _section: Record<string, unknown>): Promise<void> {
return Promise.resolve()
}
}
describe('ui-settings-general host', () => {
it('registers and disposes the durable onboarding namespace with its fiber', async () => {
const ctx = new Context()
await ctx.plugin(MemorySettings).await()
const fiber = ctx.plugin({ apply })
await fiber.await()
expect(ctx.settings.describe().map(row => row.ns)).toContain(
settingsNamespace(WELCOME_NOTICE_SETTINGS_NAMESPACE),
)
await fiber.dispose()
expect(ctx.settings.describe().map(row => row.ns)).not.toContain(
settingsNamespace(WELCOME_NOTICE_SETTINGS_NAMESPACE),
)
})
})

View File

@@ -0,0 +1,101 @@
// @vitest-environment jsdom
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
import { WelcomeNotice } from '../src/client/WelcomeNotice.tsx'
import type { WelcomeNoticeProps } from '../src/client/WelcomeNotice.tsx'
import { WelcomeNoticeStore } from '../src/client/welcome-store.ts'
import { zh } from '../src/client/locales.ts'
import {
WELCOME_NOTICE_ACK_FIELD, WELCOME_NOTICE_COPY, WELCOME_NOTICE_SETTINGS_NAMESPACE,
WELCOME_NOTICE_VERSION,
} from '../src/onboarding-copy.ts'
afterEach(cleanup)
function response<T>(value: T) {
return { rpcId: 'welcome-rpc' as never, result: { ok: true as const, value } }
}
function mount(version?: string, mutateImpl: () => Promise<unknown> = () => Promise.resolve(response({}))) {
const mutate = vi.fn(mutateImpl)
const api = {
settings: {
describe: () => Promise.resolve(response({
writable: true,
namespaces: [{
ns: WELCOME_NOTICE_SETTINGS_NAMESPACE,
schema: {},
value: version === undefined ? {} : { [WELCOME_NOTICE_ACK_FIELD]: version },
applies: 'live' as const,
secrets: [],
revision: 0,
}],
})),
mutate,
},
}
const controller = new WelcomeNoticeStore(api as never)
const complete = vi.fn()
const unusedHook = (() => { throw new Error('unused standard hook') }) as never
const props: WelcomeNoticeProps = {
stepId: 'welcome-notice',
complete,
openSection: vi.fn(),
useSessions: unusedHook,
useWorkspaces: unusedHook,
controller,
useSnapshot: bindSnapshotSelector(controller.store),
t: key => key in zh ? zh[key as keyof typeof zh] : key,
}
return { ...render(<WelcomeNotice {...props} />), complete, controller, mutate }
}
describe('WelcomeNotice', () => {
it('renders the owner copy with one primary action and no dismissal control', async () => {
const h = mount()
const page = await screen.findByRole('region', { name: WELCOME_NOTICE_COPY.zh.title })
expect(screen.getByText(WELCOME_NOTICE_COPY.zh.title)).toBeTruthy()
for (const text of WELCOME_NOTICE_COPY.zh.paragraphs) expect(page.textContent).toContain(text)
expect(page.textContent?.match(/感谢您愿意拨冗试用 DeepSeek Harness/g) ?? []).toHaveLength(1)
const buttons = page.querySelectorAll('button')
expect(buttons).toHaveLength(1)
expect(screen.getByRole('button', { name: WELCOME_NOTICE_COPY.zh.continueLabel })).toBeTruthy()
expect(document.activeElement).toBe(screen.getByRole('heading', { name: WELCOME_NOTICE_COPY.zh.title }))
fireEvent.keyDown(document, { key: 'Escape' })
expect(h.complete).not.toHaveBeenCalled()
expect(screen.getByRole('region')).toBeTruthy()
})
it('completes only after the acknowledgement write commits', async () => {
const h = mount()
await screen.findByRole('region')
fireEvent.click(screen.getByRole('button', { name: WELCOME_NOTICE_COPY.zh.continueLabel }))
await act(async () => { await Promise.resolve() })
expect(h.mutate).toHaveBeenCalledOnce()
expect(h.complete).toHaveBeenCalledOnce()
})
it('skips itself when this exact version was already acknowledged', async () => {
const h = mount(WELCOME_NOTICE_VERSION)
await act(async () => { await h.controller.load() })
expect(screen.queryByRole('region')).toBeNull()
expect(h.complete).toHaveBeenCalledOnce()
})
it('keeps the sole action disabled while saving and reports a refused write', async () => {
let resolveWrite!: (value: unknown) => void
const write = new Promise<unknown>((resolve) => { resolveWrite = resolve })
const h = mount(undefined, () => write)
await screen.findByRole('region')
const action = screen.getByRole<HTMLButtonElement>('button', { name: WELCOME_NOTICE_COPY.zh.continueLabel })
fireEvent.click(action)
expect(action.disabled).toBe(true)
resolveWrite({
rpcId: 'welcome-refused' as never,
result: { ok: false, error: { code: 'settings-rejected', message: 'read only', details: { ns: WELCOME_NOTICE_SETTINGS_NAMESPACE } } },
})
expect((await screen.findByRole('alert')).textContent).toBe('暂时无法保存确认状态,请重试。')
expect(h.complete).not.toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,166 @@
import { describe, expect, it, vi } from 'vitest'
import type { RpcResponse } from '@deepseek-ai/dsh-client-connection/client'
import { WelcomeNoticeStore } from '../src/client/welcome-store.ts'
import { refreshWelcomeIfLoaded } from '../src/client/welcome-store.ts'
import {
WELCOME_NOTICE_ACK_FIELD, WELCOME_NOTICE_SETTINGS_NAMESPACE, WELCOME_NOTICE_VERSION,
} from '../src/onboarding-copy.ts'
let rpc = 0
function ok<T>(value: T): RpcResponse<T> {
return { rpcId: `welcome-${rpc++}` as never, result: { ok: true, value } }
}
function namespace(version?: string) {
return {
ns: WELCOME_NOTICE_SETTINGS_NAMESPACE,
schema: {},
value: version === undefined ? {} : { [WELCOME_NOTICE_ACK_FIELD]: version },
applies: 'live' as const,
secrets: [],
revision: 0,
}
}
function deferred<T>() {
let resolve!: (value: T) => void
let reject!: (reason: unknown) => void
const promise = new Promise<T>((res, rej) => { resolve = res; reject = rej })
return { promise, resolve, reject }
}
describe('WelcomeNoticeStore', () => {
it('acknowledges only the exact current copy version', async () => {
for (const [version, acknowledged] of [
[undefined, false],
['older-copy', false],
[WELCOME_NOTICE_VERSION, true],
] as const) {
const api = {
settings: {
describe: vi.fn(() => Promise.resolve(ok({ writable: true, namespaces: [namespace(version)] }))),
},
}
const controller = new WelcomeNoticeStore(api as never)
await controller.load()
expect(controller.store.getSnapshot()).toMatchObject({ status: 'ready', acknowledged })
}
})
it('persists the owner version through one idempotent path mutation', async () => {
const mutate = vi.fn(() => Promise.resolve(ok(namespace(WELCOME_NOTICE_VERSION))))
const controller = new WelcomeNoticeStore({ settings: { mutate } } as never)
await expect(controller.acknowledge()).resolves.toBe(true)
expect(mutate).toHaveBeenCalledWith({
ns: WELCOME_NOTICE_SETTINGS_NAMESPACE,
ops: [{ op: 'set', path: [WELCOME_NOTICE_ACK_FIELD], value: WELCOME_NOTICE_VERSION }],
})
expect(controller.store.getSnapshot()).toMatchObject({ status: 'ready', acknowledged: true })
})
it('keeps the notice pending when loading or persistence fails', async () => {
const load = new WelcomeNoticeStore({
settings: { describe: () => Promise.reject(new Error('offline')) },
} as never)
await load.load()
expect(load.store.getSnapshot()).toEqual({ status: 'error', acknowledged: false, error: 'offline' })
const save = new WelcomeNoticeStore({
settings: { mutate: () => Promise.reject(new Error('disk full')) },
} as never)
await expect(save.acknowledge()).resolves.toBe(false)
expect(save.store.getSnapshot()).toEqual({ status: 'error', acknowledged: false, error: 'disk full' })
const nonError = new WelcomeNoticeStore({
// Durable/wire failures are unknown; exercise containment of a non-Error rejection.
// oxlint-disable-next-line typescript/prefer-promise-reject-errors
settings: { describe: () => Promise.reject('offline string') },
} as never)
await nonError.load()
expect(nonError.store.getSnapshot().error).toBe('offline string')
})
it('reports business failures, missing namespaces, and malformed durable values', async () => {
for (const describe of [
() => Promise.resolve({
rpcId: 'failed' as never,
result: { ok: false as const, error: { code: 'internal' as const, message: 'denied', details: {} } },
}),
() => Promise.resolve(ok({ writable: true, namespaces: [] })),
]) {
const controller = new WelcomeNoticeStore({ settings: { describe } } as never)
await controller.load()
expect(controller.store.getSnapshot().status).toBe('error')
}
for (const value of [null, 42, { [WELCOME_NOTICE_ACK_FIELD]: 42 }]) {
const controller = new WelcomeNoticeStore({
settings: { describe: () => Promise.resolve(ok({
writable: true,
namespaces: [{ ...namespace(), value }],
})) },
} as never)
await controller.load()
expect(controller.store.getSnapshot()).toMatchObject({ status: 'ready', acknowledged: false })
}
const save = new WelcomeNoticeStore({
settings: { mutate: () => Promise.resolve({
rpcId: 'failed-save' as never,
result: { ok: false, error: { code: 'settings-rejected', message: 'denied', details: { ns: WELCOME_NOTICE_SETTINGS_NAMESPACE } } },
}) },
} as never)
await expect(save.acknowledge()).resolves.toBe(false)
expect(save.store.getSnapshot().error).toBe('denied')
})
it('lets the latest load win over stale success and failure', async () => {
const first = deferred<ReturnType<typeof ok>>()
const describe = vi.fn()
.mockImplementationOnce(() => first.promise)
.mockImplementationOnce(() => Promise.resolve(ok({ writable: true, namespaces: [namespace()] })))
const controller = new WelcomeNoticeStore({ settings: { describe } } as never)
const stale = controller.load()
await controller.load()
first.resolve(ok({ writable: true, namespaces: [namespace(WELCOME_NOTICE_VERSION)] }))
await stale
expect(controller.store.getSnapshot().acknowledged).toBe(false)
const failed = deferred<ReturnType<typeof ok>>()
describe
.mockImplementationOnce(() => failed.promise)
.mockImplementationOnce(() => Promise.resolve(ok({ writable: true, namespaces: [namespace(WELCOME_NOTICE_VERSION)] })))
const staleFailure = controller.load()
await controller.load()
failed.reject('stale failure')
await staleFailure
expect(controller.store.getSnapshot()).toMatchObject({ status: 'ready', acknowledged: true, error: null })
})
it('contains stale acknowledgement settlements and refreshes only a loaded store', async () => {
const write = deferred<ReturnType<typeof ok>>()
const describe = vi.fn(() => Promise.resolve(ok({ writable: true, namespaces: [namespace()] })))
const controller = new WelcomeNoticeStore({
settings: { mutate: () => write.promise, describe },
} as never)
refreshWelcomeIfLoaded(controller)
expect(describe).not.toHaveBeenCalled()
const staleWrite = controller.acknowledge()
await controller.load()
write.resolve(ok(namespace(WELCOME_NOTICE_VERSION)))
await expect(staleWrite).resolves.toBe(true)
expect(controller.store.getSnapshot().acknowledged).toBe(false)
refreshWelcomeIfLoaded(controller)
await vi.waitFor(() => { expect(describe).toHaveBeenCalledTimes(2) })
const failedWrite = deferred<ReturnType<typeof ok>>()
const staleFailure = new WelcomeNoticeStore({
settings: { mutate: () => failedWrite.promise, describe },
} as never)
const pending = staleFailure.acknowledge()
await staleFailure.load()
failedWrite.reject('late failure')
await expect(pending).resolves.toBe(false)
expect(staleFailure.store.getSnapshot().status).toBe('ready')
})
})