A business package imported the Remote assembly for `ctx.remote` and the Connection plugin for the wire types it passes around. The assembly now re-exports the carrier's Client-facing types, so a business package names one package. The re-export is type-only: the carrier's runtime values keep their own module edge, since inlining them here would duplicate the carrier inside the assembly bundle. Four surfaces that had no Remote assembly dependency declare one now.
185 lines
7.8 KiB
TypeScript
185 lines
7.8 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import type { RpcResponse } from '@deepseek-ai/dsh-api-remotes/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 in memory without calling loopback-only settings APIs', async () => {
|
|
const describe = vi.fn()
|
|
const mutate = vi.fn()
|
|
const controller = new WelcomeNoticeStore({ settings: { describe, mutate } } as never, 'memory')
|
|
|
|
await controller.load()
|
|
expect(controller.store.getSnapshot()).toEqual({ status: 'ready', acknowledged: false, error: null })
|
|
await expect(controller.acknowledge()).resolves.toBe(true)
|
|
expect(controller.store.getSnapshot()).toEqual({ status: 'ready', acknowledged: true, error: null })
|
|
await controller.load()
|
|
expect(controller.store.getSnapshot()).toEqual({ status: 'ready', acknowledged: true, error: null })
|
|
expect(describe).not.toHaveBeenCalled()
|
|
expect(mutate).not.toHaveBeenCalled()
|
|
})
|
|
|
|
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, hasDocument: false, 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, hasDocument: false, 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,
|
|
hasDocument: false,
|
|
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, hasDocument: false, namespaces: [namespace()] })))
|
|
const controller = new WelcomeNoticeStore({ settings: { describe } } as never)
|
|
const stale = controller.load()
|
|
await controller.load()
|
|
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, hasDocument: false, 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, hasDocument: false, 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')
|
|
})
|
|
})
|