fix(web): address settings document review

This commit is contained in:
Yichen Jiang
2026-08-04 17:31:36 +08:00
parent e31b7221e7
commit 4f717f2da7
42 changed files with 219 additions and 115 deletions

View File

@@ -39,7 +39,7 @@ async function bench(isLoopback = true) {
ok: true as const,
value: {
writable: true,
documentPath: '/tmp/test-settings.yaml',
hasDocument: true,
namespaces: [{
ns: WELCOME_NOTICE_SETTINGS_NAMESPACE,
schema: {},
@@ -175,10 +175,25 @@ describe('ui-settings-general apply', () => {
await vi.waitFor(() => { expect(b.settingsDescribe).toHaveBeenCalledTimes(3) })
})
it('refreshes loaded document availability on reconnect without reading it eagerly', async () => {
const b = await bench()
declare(b.slots)
await b.ctx.plugin({ inject: [...inject], apply }).await()
const entry = b.slots.entries('settings.action')[0]!
const { controller } = (entry.inject as unknown as () => SettingsDocumentActionInjected)()
b.ctx.emit('connection/reset')
expect(b.settingsDescribe).not.toHaveBeenCalled()
await controller.load()
expect(b.settingsDescribe).toHaveBeenCalledOnce()
b.ctx.emit('connection/reset')
await vi.waitFor(() => { expect(b.settingsDescribe).toHaveBeenCalledTimes(2) })
})
it('keeps remote welcome acknowledgement process-local', async () => {
const b = await bench(false)
declare(b.slots)
await b.ctx.plugin({ inject: [...inject], apply }).await()
const fiber = b.ctx.plugin({ inject: [...inject], apply })
await fiber.await()
const entry = b.slots.entries('settings.onboarding')[0]!
const { controller } = (entry.inject as unknown as () => WelcomeNoticeInjected)()
@@ -187,6 +202,8 @@ describe('ui-settings-general apply', () => {
expect(controller.store.getSnapshot()).toMatchObject({ status: 'ready', acknowledged: true })
expect(b.settingsDescribe).not.toHaveBeenCalled()
expect(b.slots.entries('settings.action')).toEqual([])
await fiber.dispose()
for (const [name] of SEATS) expect(b.slots.entries(name)).toEqual([])
})
it('re-registers after an HMR collapse of the declaring chain (stale disposers must not block)', async () => {

View File

@@ -70,7 +70,7 @@ describe('SettingsDocumentAction', () => {
rpcId: 'document-action' as never,
result: {
ok: true as const,
value: { writable: true, documentPath: '/tmp/custom.yaml', namespaces: [] },
value: { writable: true, hasDocument: true, namespaces: [] },
},
})),
openDocument,
@@ -87,17 +87,23 @@ describe('SettingsDocumentAction', () => {
await waitFor(() => { expect(openDocument).toHaveBeenCalledWith({}) })
})
it('stays absent when the provider has no local document', async () => {
it('stays absent without a document and retries availability after remount', async () => {
const describe = vi.fn()
.mockResolvedValueOnce({
rpcId: 'document-action-absent' as never,
result: { ok: true as const, value: { writable: true, hasDocument: false, namespaces: [] } },
})
.mockResolvedValueOnce({
rpcId: 'document-action-ready' as never,
result: { ok: true as const, value: { writable: true, hasDocument: true, namespaces: [] } },
})
const controller = new SettingsDocumentStore({
settings: {
describe: vi.fn(() => Promise.resolve({
rpcId: 'document-action' as never,
result: { ok: true as const, value: { writable: true, namespaces: [] } },
})),
describe,
openDocument: vi.fn(),
},
} as never)
render(<SettingsDocumentAction
const first = render(<SettingsDocumentAction
{...kit}
t={t}
controller={controller}
@@ -105,6 +111,15 @@ describe('SettingsDocumentAction', () => {
/>)
await waitFor(() => { expect(controller.store.getSnapshot().status).toBe('unavailable') })
expect(screen.queryByRole('button', { name: 'Open configuration file' })).toBeNull()
first.unmount()
render(<SettingsDocumentAction
{...kit}
t={t}
controller={controller}
useSnapshot={bindSnapshotSelector(controller.store)}
/>)
expect(await screen.findByRole('button', { name: 'Open configuration file' })).toBeTruthy()
expect(describe).toHaveBeenCalledTimes(2)
})
it('keeps the action available and reports a native-open failure', async () => {
@@ -114,7 +129,7 @@ describe('SettingsDocumentAction', () => {
rpcId: 'document-action' as never,
result: {
ok: true as const,
value: { writable: true, documentPath: '/tmp/settings.yaml', namespaces: [] },
value: { writable: true, hasDocument: true, namespaces: [] },
},
})),
openDocument: vi.fn(() => Promise.resolve({

View File

@@ -2,16 +2,16 @@ import { describe, expect, it, vi } from 'vitest'
import type { RpcResponse } from '@deepseek-ai/dsh-client-connection/client'
import { SettingsDocumentStore } from '../src/client/settings-document-store.ts'
function response(documentPath?: string): RpcResponse<{
function response(hasDocument = false): RpcResponse<{
writable: boolean
documentPath?: string
hasDocument: boolean
namespaces: []
}> {
return {
rpcId: 'settings-document' as never,
result: {
ok: true,
value: { writable: true, ...documentPath === undefined ? {} : { documentPath }, namespaces: [] },
value: { writable: true, hasDocument, namespaces: [] },
},
}
}
@@ -32,7 +32,7 @@ function describeFailed(message: string): RpcResponse<never> {
describe('SettingsDocumentStore', () => {
it('loads provider metadata and asks the settings domain to open its document', async () => {
const describe = vi.fn(() => Promise.resolve(response('/home/test/settings.yaml')))
const describe = vi.fn(() => Promise.resolve(response(true)))
const openDocument = vi.fn(() => Promise.resolve(opened()))
const controller = new SettingsDocumentStore({ settings: { describe, openDocument } } as never)
await controller.load()
@@ -72,7 +72,7 @@ describe('SettingsDocumentStore', () => {
let resolveOpen!: (response: RpcResponse<{ opened: true }>) => void
const openDocument = vi.fn(() => new Promise<RpcResponse<{ opened: true }>>((resolve) => { resolveOpen = resolve }))
const controller = new SettingsDocumentStore({
settings: { describe: () => Promise.resolve(response('/tmp/settings.yaml')), openDocument },
settings: { describe: () => Promise.resolve(response(true)), openDocument },
} as never)
await controller.load()
const first = controller.open()
@@ -93,7 +93,7 @@ describe('SettingsDocumentStore', () => {
const first = new Promise<ReturnType<typeof response>>((resolve) => { resolveFirst = resolve })
const describe = vi.fn()
.mockReturnValueOnce(first)
.mockResolvedValueOnce(response('/tmp/current.yaml'))
.mockResolvedValueOnce(response(true))
let rejectOpen!: (reason?: unknown) => void
const controller = new SettingsDocumentStore({
settings: {
@@ -119,7 +119,7 @@ describe('SettingsDocumentStore', () => {
settings: {
describe: vi.fn()
.mockReturnValueOnce(rejectedFirst)
.mockResolvedValueOnce(response('/tmp/current.yaml')),
.mockResolvedValueOnce(response(true)),
openDocument: vi.fn(),
},
} as never)

View File

@@ -23,6 +23,7 @@ function mount(version?: string, mutateImpl: () => Promise<unknown> = () => Prom
settings: {
describe: () => Promise.resolve(response({
writable: true,
hasDocument: false,
namespaces: [{
ns: WELCOME_NOTICE_SETTINGS_NAMESPACE,
schema: {},

View File

@@ -53,7 +53,7 @@ describe('WelcomeNoticeStore', () => {
] as const) {
const api = {
settings: {
describe: vi.fn(() => Promise.resolve(ok({ writable: true, namespaces: [namespace(version)] }))),
describe: vi.fn(() => Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: [namespace(version)] }))),
},
}
const controller = new WelcomeNoticeStore(api as never)
@@ -101,7 +101,7 @@ describe('WelcomeNoticeStore', () => {
rpcId: 'failed' as never,
result: { ok: false as const, error: { code: 'internal' as const, message: 'denied', details: {} } },
}),
() => Promise.resolve(ok({ writable: true, namespaces: [] })),
() => Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: [] })),
]) {
const controller = new WelcomeNoticeStore({ settings: { describe } } as never)
await controller.load()
@@ -112,6 +112,7 @@ describe('WelcomeNoticeStore', () => {
const controller = new WelcomeNoticeStore({
settings: { describe: () => Promise.resolve(ok({
writable: true,
hasDocument: false,
namespaces: [{ ...namespace(), value }],
})) },
} as never)
@@ -133,18 +134,20 @@ describe('WelcomeNoticeStore', () => {
const first = deferred<ReturnType<typeof ok>>()
const describe = vi.fn()
.mockImplementationOnce(() => first.promise)
.mockImplementationOnce(() => Promise.resolve(ok({ writable: true, namespaces: [namespace()] })))
.mockImplementationOnce(() => Promise.resolve(ok({ writable: true, hasDocument: false, 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)] }))
first.resolve(ok({ writable: true, hasDocument: false, 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)] })))
.mockImplementationOnce(() => Promise.resolve(ok({
writable: true, hasDocument: false, namespaces: [namespace(WELCOME_NOTICE_VERSION)],
})))
const staleFailure = controller.load()
await controller.load()
failed.reject('stale failure')
@@ -154,7 +157,7 @@ describe('WelcomeNoticeStore', () => {
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 describe = vi.fn(() => Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: [namespace()] })))
const controller = new WelcomeNoticeStore({
settings: { mutate: () => write.promise, describe },
} as never)