Browse Source

refactor(ui-settings-models): welcome notice reads through the settings scope

Yichen Jiang 2 months ago
parent
commit
bb3128f266

+ 17 - 17
packages/client/ui-settings-models/src/client/index.ts

@@ -22,7 +22,7 @@ import { DeepSeekOnboardingDialog } from './DeepSeekOnboardingDialog.tsx'
 import type { DeepSeekOnboardingInjected } from './DeepSeekOnboardingDialog.tsx'
 import { WelcomeNotice } from './WelcomeNotice.tsx'
 import type { WelcomeNoticeInjected } from './WelcomeNotice.tsx'
-import { refreshWelcomeIfLoaded, WelcomeNoticeStore } from './welcome-store.ts'
+import { decodeWelcomeSection, WelcomeNoticeStore } from './welcome-store.ts'
 import { ModelsSettingsStore } from './store.ts'
 import { en, zh, type ModelsKey } from './locales.ts'
 import { WELCOME_NOTICE_SETTINGS_NAMESPACE } from '../onboarding-copy.ts'
@@ -56,7 +56,7 @@ export function refreshIfLoaded(controller: ModelsSettingsStore): void {
  * ui-settings' apply, whose activation order relative to this one is NOT
  * constrained; registration depends on each slot through `slots.inject()`.
  */
-export const inject = ['slots', 'locale', 'connection', 'remote']
+export const inject = ['slots', 'locale', 'connection', 'remote', 'settingsScope']
 
 /**
  * Register the Models section once the `settings.section` declaration is on
@@ -85,10 +85,12 @@ export function apply(ctx: ClientContext): void {
     api: connection.api,
     t,
   })
-  const welcomeController = new WelcomeNoticeStore(
-    connection.api,
-    connection.isLoopback ? 'host' : 'memory',
-  )
+  // The scope's own memory mode is what keeps a remote browser process-local,
+  // so the store needs no isLoopback branch of its own.
+  const welcomeController = new WelcomeNoticeStore(ctx.settingsScope.bind({
+    namespace: WELCOME_NOTICE_SETTINGS_NAMESPACE,
+    decode: decodeWelcomeSection,
+  }))
   const welcomeInjected = (): WelcomeNoticeInjected => ({
     controller: welcomeController,
     hooks: { welcome: welcomeController.store },
@@ -96,23 +98,21 @@ export function apply(ctx: ClientContext): void {
   })
 
   // Pushed invalidations converge every open surface without polling: any
-  // settings/credentials/topology change refetches once the page loaded.
+  // settings/credentials/topology change refetches once the page loaded. The
+  // welcome notice follows its settings scope, so the shared mirror already
+  // keeps it fresh without a subscription here.
   ctx.effect(() => {
     const refreshModels = (): void => { refreshIfLoaded(controller) }
-    const refreshAll = (): void => {
-      refreshModels()
-      refreshWelcomeIfLoaded(welcomeController)
-    }
     const disposers = [
-      ctx.remote.$on('settings/document-updated', (ns) => {
-        refreshModels()
-        if (ns === WELCOME_NOTICE_SETTINGS_NAMESPACE) refreshWelcomeIfLoaded(welcomeController)
-      }),
+      ctx.remote.$on('settings/document-updated', () => { refreshModels() }),
       ctx.remote.$on('credentials/updated', refreshModels),
       ctx.remote.$on('llm/adapters-updated', refreshModels),
-      ctx.on('connection/reset', refreshAll),
+      ctx.on('connection/reset', refreshModels),
     ]
-    return () => { for (const dispose of disposers) dispose() }
+    return () => {
+      welcomeController.dispose()
+      for (const dispose of disposers) dispose()
+    }
   }, 'ui-settings-models: pushed invalidations')
 
   ctx.slots.inject('settings.section', () => ctx.slots.register({

+ 81 - 79
packages/client/ui-settings-models/src/client/welcome-store.ts

@@ -1,10 +1,14 @@
-/** Welcome-notice state, durable when the browser may use Host settings. */
+/**
+ * Welcome-notice state derived from the welcome settings scope. The scope is
+ * the transport: a loopback browser follows the durable Host section, while a
+ * remote browser's memory-mode scope never answers and the acknowledgement
+ * stays process-local here.
+ */
 
-import type { IApiClient, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client'
-import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
+import type { SettingsScope, SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
 import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
 import {
-  WELCOME_NOTICE_ACK_FIELD, WELCOME_NOTICE_SETTINGS_NAMESPACE, WELCOME_NOTICE_VERSION,
+  WELCOME_NOTICE_ACK_FIELD, WELCOME_NOTICE_VERSION,
 } from '../onboarding-copy.ts'
 
 /** State rendered by the welcome step. */
@@ -14,113 +18,111 @@ export interface WelcomeNoticeState {
   error: string | null
 }
 
-function messageOf(error: unknown): string {
-  return error instanceof Error ? error.message : String(error)
-}
+/** The welcome section as the notice reads it. */
+export type WelcomeSection = Record<string, unknown>
 
-function acknowledgementOf(view: SettingsNamespaceView): string | undefined {
-  if (typeof view.value !== 'object' || view.value === null) return undefined
-  const value = (view.value as Record<string, unknown>)[WELCOME_NOTICE_ACK_FIELD]
-  return typeof value === 'string' ? value : undefined
+/**
+ * Accept any object section verbatim; a malformed durable value reads as an
+ * empty section, so the notice treats it as unacknowledged instead of leaving
+ * the scope stuck on its previous value.
+ * @param section - the wire section value.
+ * @returns the section object, or an empty one for non-object values.
+ */
+export function decodeWelcomeSection(section: unknown): WelcomeSection {
+  return typeof section === 'object' && section !== null && !Array.isArray(section)
+    ? section as WelcomeSection
+    : {}
 }
 
 /** Coordinates durable Host acknowledgement or a process-local remote fallback. */
 export class WelcomeNoticeStore {
   /** uSES-safe state source shared by the registered welcome step. */
   readonly store: SnapshotStore<WelcomeNoticeState> = createSnapshotStore({
-    status: 'idle', acknowledged: false, error: null,
+    status: 'idle' as const, acknowledged: false, error: null,
   })
 
-  private generation = 0
+  private localAcknowledged = false
+  private saving = false
+  private following: (() => void) | undefined
 
   /**
-   * @param api - settings wire face used for durable reads and writes.
-   * @param persistence - remote browsers use memory because settings is loopback-only.
+   * @param scope - the welcome settings namespace scope; its memory mode is
+   * what keeps a remote browser process-local.
    */
-  constructor(
-    private readonly api: Pick<IApiClient, 'settings'>,
-    private readonly persistence: 'host' | 'memory' = 'host',
-  ) {}
+  constructor(private readonly scope: SettingsScope<WelcomeSection>) {}
 
-  /** Load the acknowledgement from Host settings or initialize process-local state. */
+  /** Begin following the bound scope (idempotent) and publish its current answer. */
   async load(): Promise<void> {
-    const generation = ++this.generation
-    if (this.persistence === 'memory') {
-      this.store.update((state) => { state.status = 'ready'; state.error = null })
-      return
+    this.following ??= this.scope.subscribe(() => { this.derive() })
+    this.derive()
+  }
+
+  /**
+   * Persist this copy version, or advance only this process for a remote
+   * browser. Success is judged against the state the write left behind, so a
+   * refused or failed write reports false after its recovery read settles.
+   * @returns true when the selected persistence mode holds the acknowledgement.
+   */
+  async acknowledge(): Promise<boolean> {
+    if (this.scope.getSnapshot().mode === 'memory') {
+      this.localAcknowledged = true
+      this.derive()
+      return true
     }
-    this.store.update((state) => { state.status = 'loading'; state.error = null })
+    this.saving = true
+    this.store.update((state) => { state.status = 'saving'; state.error = null })
     try {
-      const response = await this.api.settings.describe({})
-      if (!response.result.ok) throw new Error(response.result.error.message)
-      const view = response.result.value.namespaces.find(
-        candidate => candidate.ns === WELCOME_NOTICE_SETTINGS_NAMESPACE,
-      )
-      if (view === undefined) throw new Error('welcome acknowledgement settings are unavailable')
-      if (generation !== this.generation) return
-      this.store.update((state) => {
-        state.status = 'ready'
-        state.acknowledged = acknowledgementOf(view) === WELCOME_NOTICE_VERSION
-        state.error = null
-      })
-    } catch (error) {
-      if (generation !== this.generation) return
+      await this.scope.set(WELCOME_NOTICE_ACK_FIELD, WELCOME_NOTICE_VERSION)
+    } finally {
+      this.saving = false
+    }
+    this.derive()
+    const { acknowledged } = this.store.getSnapshot()
+    if (!acknowledged) {
       this.store.update((state) => {
         state.status = 'error'
-        state.acknowledged = false
-        state.error = messageOf(error)
+        state.error = 'the acknowledgement did not persist'
       })
     }
+    return acknowledged
   }
 
-  /**
-   * Persist this copy version, or advance only this process for a remote browser.
-   * @returns true when the selected persistence mode accepted the acknowledgement.
-   */
-  async acknowledge(): Promise<boolean> {
-    const generation = ++this.generation
-    if (this.persistence === 'memory') {
+  /** Stop following the scope. */
+  dispose(): void {
+    this.following?.()
+    this.following = undefined
+  }
+
+  private derive(): void {
+    if (this.saving) return
+    const scope = this.scope.getSnapshot()
+    if (scope.mode === 'memory') {
       this.store.update((state) => {
         state.status = 'ready'
-        state.acknowledged = true
+        state.acknowledged = this.localAcknowledged
         state.error = null
       })
-      return true
+      return
     }
-    this.store.update((state) => { state.status = 'saving'; state.error = null })
-    try {
-      const response = await this.api.settings.mutate({
-        ns: WELCOME_NOTICE_SETTINGS_NAMESPACE,
-        ops: [{ op: 'set', path: [WELCOME_NOTICE_ACK_FIELD], value: WELCOME_NOTICE_VERSION }],
-      })
-      if (!response.result.ok) throw new Error(response.result.error.message)
-      if (generation === this.generation) {
-        this.store.update((state) => {
-          state.status = 'ready'
-          state.acknowledged = true
-          state.error = null
-        })
-      }
-      return true
-    } catch (error) {
-      if (generation === this.generation) {
+    switch (scope.status) {
+      case 'loading':
+        this.store.update((state) => { state.status = 'loading'; state.error = null })
+        return
+      case 'unavailable':
         this.store.update((state) => {
           state.status = 'error'
           state.acknowledged = false
-          state.error = messageOf(error)
+          state.error = 'welcome acknowledgement settings are unavailable'
+        })
+        return
+      case 'ready': {
+        const acknowledged = scope.value?.[WELCOME_NOTICE_ACK_FIELD] === WELCOME_NOTICE_VERSION
+        this.store.update((state) => {
+          state.status = 'ready'
+          state.acknowledged = acknowledged
+          state.error = null
         })
       }
-      return false
     }
   }
 }
-
-/**
- * Refresh only after welcome state has left idle. A memory-mode load retains
- * acknowledgement so reconnect does not reopen a process-local notice.
- * @param controller - welcome state owner whose current status decides whether to load.
- */
-export function refreshWelcomeIfLoaded(controller: WelcomeNoticeStore): void {
-  if (controller.store.getSnapshot().status === 'idle') return
-  void controller.load()
-}

+ 44 - 16
packages/client/ui-settings-models/tests/apply.client.spec.ts

@@ -5,7 +5,11 @@ import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots'
 import { SlotRegistry } from '@deepseek-ai/dsh-client-runtime/client'
 import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
 import { TestRemote, usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime'
+import { apply as settingsApply, inject as settingsInject } from '@deepseek-ai/dsh-client-ui-settings/client'
 import { apply, inject, refreshIfLoaded } from '@deepseek-ai/dsh-client-ui-settings-models/client'
+import {
+  WELCOME_NOTICE_ACK_FIELD, WELCOME_NOTICE_SETTINGS_NAMESPACE, WELCOME_NOTICE_VERSION,
+} from '../src/onboarding-copy.ts'
 import { ModelsSection } from '../src/client/ModelsSection.tsx'
 import { DeepSeekOnboardingDialog } from '../src/client/DeepSeekOnboardingDialog.tsx'
 import { WelcomeNotice } from '../src/client/WelcomeNotice.tsx'
@@ -14,7 +18,7 @@ import { WelcomeNotice } from '../src/client/WelcomeNotice.tsx'
 // the shipped Chinese copy, so they state the browser they assume.
 usePinnedBrowserLanguages('zh-CN')
 
-async function bench(isLoopback = true) {
+async function bench(isLoopback = true, settings?: object) {
   const ctx = new Context()
   await ctx.plugin(SlotRegistry).await()
   const locale = new LocaleRuntime(ctx)
@@ -22,9 +26,10 @@ async function bench(isLoopback = true) {
   // The plugins inject `remote`; forwarded events reach them through the
   // same `$dispatch` handoff the connection sink makes.
   new TestRemote(ctx)
-  // The apply path only captures the wire face; no call leaves this fake
-  // until a section actually loads.
-  ctx.provide('connection', { api: {}, isLoopback } as never)
+  // Without a settings face the mirror's reads fail and stay contained; the
+  // Models join itself never fetches until a section actually loads.
+  ctx.provide('connection', { api: settings === undefined ? {} : { settings }, isLoopback } as never)
+  await ctx.plugin({ inject: [...settingsInject], apply: settingsApply }).await()
   return { ctx, slots: ctx.get('slots') as SlotRegistry, locale }
 }
 
@@ -43,7 +48,7 @@ function declare(slots: SlotRegistry): () => void {
 
 describe('ui-settings-models apply', () => {
   it('declares the services it uses', () => {
-    expect(inject).toEqual(['slots', 'locale', 'connection', 'remote'])
+    expect(inject).toEqual(['slots', 'locale', 'connection', 'remote', 'settingsScope'])
   })
 
   it('registers the models nav entry for declarations before or after apply', async () => {
@@ -204,8 +209,31 @@ describe('pushed invalidations', () => {
     expect(load).toHaveBeenCalledTimes(1)
   })
 
-  it('routes only the onboarding namespace invalidation into welcome state', async () => {
-    const b = await bench()
+  it('welcome state follows the shared mirror across document commits', async () => {
+    // The welcome notice derives from its settings scope: a document commit
+    // reaches it through the mirror's one refresh, with no routing here.
+    const acknowledgement = { current: undefined as string | undefined }
+    const settings = {
+      describe: vi.fn(() => Promise.resolve({
+        rpcId: 'apply-welcome' as never,
+        result: {
+          ok: true as const,
+          value: {
+            writable: true,
+            hasDocument: false,
+            namespaces: [{
+              ns: WELCOME_NOTICE_SETTINGS_NAMESPACE,
+              schema: {},
+              value: acknowledgement.current === undefined ? {} : { [WELCOME_NOTICE_ACK_FIELD]: acknowledgement.current },
+              applies: 'live' as const,
+              secrets: [],
+              revision: 0,
+            }],
+          },
+        },
+      })),
+    }
+    const b = await bench(true, settings)
     declare(b.slots)
     await b.ctx.plugin({ inject: [...inject], apply }).await()
     const entry = b.slots.entries('settings.onboarding')
@@ -214,14 +242,14 @@ describe('pushed invalidations', () => {
       entry.inject as unknown as
       () => import('../src/client/WelcomeNotice.tsx').WelcomeNoticeInjected
     )()
-    injected.hooks.welcome.update((state) => { state.status = 'ready' })
-    const load = vi.spyOn(injected.controller, 'load').mockResolvedValue()
-
-    b.ctx.remote.$dispatch('settings/document-updated', ['llm-deepseek', 1])
-    expect(load).not.toHaveBeenCalled()
-    b.ctx.remote.$dispatch('settings/document-updated', ['ui-onboarding', 2])
-    expect(load).toHaveBeenCalledOnce()
-    b.ctx.emit('connection/reset')
-    expect(load).toHaveBeenCalledTimes(2)
+    await injected.controller.load()
+    await vi.waitFor(() => {
+      expect(injected.hooks.welcome.getSnapshot()).toMatchObject({ status: 'ready', acknowledged: false })
+    })
+    acknowledgement.current = WELCOME_NOTICE_VERSION
+    b.ctx.remote.$dispatch('settings/document-updated', ['ui-onboarding', 1])
+    await vi.waitFor(() => {
+      expect(injected.hooks.welcome.getSnapshot()).toMatchObject({ status: 'ready', acknowledged: true })
+    })
   })
 })

+ 37 - 15
packages/client/ui-settings-models/tests/welcome-notice.client.spec.tsx

@@ -2,9 +2,13 @@
 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 {
+  SettingsDescribeMirror, SettingsScopeController,
+} from '@deepseek-ai/dsh-client-ui-settings/client'
 import { WelcomeNotice } from '../src/client/WelcomeNotice.tsx'
 import type { WelcomeNoticeProps } from '../src/client/WelcomeNotice.tsx'
-import { WelcomeNoticeStore } from '../src/client/welcome-store.ts'
+import { decodeWelcomeSection, WelcomeNoticeStore } from '../src/client/welcome-store.ts'
+import type { WelcomeSection } from '../src/client/welcome-store.ts'
 import { en, zh } from '../src/client/locales.ts'
 import {
   WELCOME_NOTICE_ACK_FIELD, WELCOME_NOTICE_COPY, WELCOME_NOTICE_SETTINGS_NAMESPACE,
@@ -20,7 +24,24 @@ 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({}))) {
+function welcomeView(value: unknown, revision = 0) {
+  return {
+    ns: WELCOME_NOTICE_SETTINGS_NAMESPACE,
+    schema: {},
+    value,
+    base: {},
+    user: {},
+    applies: 'live' as const,
+    secrets: [],
+    revision,
+  }
+}
+
+function mount(
+  version?: string,
+  mutateImpl: () => Promise<unknown> = () =>
+    Promise.resolve(response(welcomeView({ [WELCOME_NOTICE_ACK_FIELD]: WELCOME_NOTICE_VERSION }, 1))),
+) {
   const appRoot = document.createElement('div')
   appRoot.id = 'root'
   document.body.append(appRoot)
@@ -30,21 +51,19 @@ function mount(version?: string, mutateImpl: () => Promise<unknown> = () => Prom
       describe: () => Promise.resolve(response({
         writable: true,
         hasDocument: false,
-        namespaces: [{
-          ns: WELCOME_NOTICE_SETTINGS_NAMESPACE,
-          schema: {},
-          value: version === undefined ? {} : { [WELCOME_NOTICE_ACK_FIELD]: version },
-          base: {},
-          user: {},
-          applies: 'live' as const,
-          secrets: [],
-          revision: 0,
-        }],
+        namespaces: [welcomeView(version === undefined ? {} : { [WELCOME_NOTICE_ACK_FIELD]: version })],
       })),
       mutate,
     },
   }
-  const controller = new WelcomeNoticeStore(api as never)
+  const mirror = new SettingsDescribeMirror(api as never)
+  const scope = new SettingsScopeController<WelcomeSection>(
+    api as never,
+    { namespace: WELCOME_NOTICE_SETTINGS_NAMESPACE, decode: decodeWelcomeSection },
+    mirror,
+  )
+  const controller = new WelcomeNoticeStore(scope)
+  void mirror.load()
   const complete = vi.fn()
   const unusedHook = (() => { throw new Error('unused standard hook') }) as never
   const props: WelcomeNoticeProps = {
@@ -57,7 +76,7 @@ function mount(version?: string, mutateImpl: () => Promise<unknown> = () => Prom
     useWelcome: bindSnapshotSelector(controller.store),
     t: key => zh[key],
   }
-  return { ...render(<WelcomeNotice {...props} />), complete, controller, mutate, appRoot }
+  return { ...render(<WelcomeNotice {...props} />), complete, controller, mirror, mutate, appRoot }
 }
 
 describe('WelcomeNotice', () => {
@@ -100,7 +119,10 @@ describe('WelcomeNotice', () => {
 
   it('skips itself when this exact version was already acknowledged', async () => {
     const h = mount(WELCOME_NOTICE_VERSION)
-    await act(async () => { await h.controller.load() })
+    await act(async () => {
+      await h.mirror.load()
+      await h.controller.load()
+    })
     expect(screen.queryByRole('dialog')).toBeNull()
     expect(h.complete).toHaveBeenCalledOnce()
   })

+ 100 - 133
packages/client/ui-settings-models/tests/welcome-store.client.spec.ts

@@ -1,6 +1,9 @@
 import { describe, expect, it, vi } from 'vitest'
 import type { RpcResponse } from '@deepseek-ai/dsh-api-remotes/client'
-import { refreshWelcomeIfLoaded, WelcomeNoticeStore } from '../src/client/welcome-store.ts'
+import {
+  SettingsDescribeMirror, SettingsScopeController,
+} from '@deepseek-ai/dsh-client-ui-settings/client'
+import { decodeWelcomeSection, WelcomeNoticeStore } from '../src/client/welcome-store.ts'
 import {
   WELCOME_NOTICE_ACK_FIELD, WELCOME_NOTICE_SETTINGS_NAMESPACE, WELCOME_NOTICE_VERSION,
 } from '../src/onboarding-copy.ts'
@@ -10,31 +13,42 @@ function ok<T>(value: T): RpcResponse<T> {
   return { rpcId: `welcome-${rpc++}` as never, result: { ok: true, value } }
 }
 
-function namespace(version?: string) {
+function namespace(value: unknown = {}, revision = 0) {
   return {
     ns: WELCOME_NOTICE_SETTINGS_NAMESPACE,
     schema: {},
-    value: version === undefined ? {} : { [WELCOME_NOTICE_ACK_FIELD]: version },
-    base: {},
-    user: {},
+    value,
     applies: 'live' as const,
     secrets: [],
-    revision: 0,
+    revision,
   }
 }
 
-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 }
+function acknowledgedNamespace(version: string, revision = 1) {
+  return namespace({ [WELCOME_NOTICE_ACK_FIELD]: version }, revision)
+}
+
+/** The welcome store over a real mirror-derived scope and a fake wire. */
+function buildWelcome(
+  api: { describe?: ReturnType<typeof vi.fn>; mutate?: ReturnType<typeof vi.fn> },
+  persistence: 'host' | 'memory' = 'host',
+) {
+  const wire = { settings: api } as never
+  const mirror = new SettingsDescribeMirror(wire, persistence)
+  const scope = new SettingsScopeController(
+    wire,
+    { namespace: WELCOME_NOTICE_SETTINGS_NAMESPACE, decode: decodeWelcomeSection },
+    mirror,
+    persistence,
+  )
+  return { mirror, controller: new WelcomeNoticeStore(scope) }
 }
 
 describe('WelcomeNoticeStore', () => {
   it('acknowledges in memory without calling loopback-only settings APIs', async () => {
-    const describe = vi.fn()
+    const describeCall = vi.fn()
     const mutate = vi.fn()
-    const controller = new WelcomeNoticeStore({ settings: { describe, mutate } } as never, 'memory')
+    const { controller } = buildWelcome({ describe: describeCall, mutate }, 'memory')
 
     await controller.load()
     expect(controller.store.getSnapshot()).toEqual({ status: 'ready', acknowledged: false, error: null })
@@ -42,7 +56,7 @@ describe('WelcomeNoticeStore', () => {
     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(describeCall).not.toHaveBeenCalled()
     expect(mutate).not.toHaveBeenCalled()
   })
 
@@ -52,148 +66,101 @@ describe('WelcomeNoticeStore', () => {
       ['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)
+      const describeCall = vi.fn(() => Promise.resolve(ok({
+        writable: true,
+        hasDocument: false,
+        namespaces: [version === undefined ? namespace() : acknowledgedNamespace(version)],
+      })))
+      const { mirror, controller } = buildWelcome({ describe: describeCall })
+      await mirror.load()
       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)
+  it('persists the owner version through one revision-fenced mutation', async () => {
+    const describeCall = vi.fn(() => Promise.resolve(ok({
+      writable: true, hasDocument: false, namespaces: [namespace({}, 3)],
+    })))
+    const mutate = vi.fn(() => Promise.resolve(ok(acknowledgedNamespace(WELCOME_NOTICE_VERSION, 4))))
+    const { mirror, controller } = buildWelcome({ describe: describeCall, mutate })
+    await mirror.load()
+    await controller.load()
     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 }],
+      expectedRevision: 3,
     })
     expect(controller.store.getSnapshot()).toMatchObject({ status: 'ready', acknowledged: true })
+    // The write answer folded into the mirror; no re-read followed.
+    expect(describeCall).toHaveBeenCalledTimes(1)
   })
 
-  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' })
+  it('keeps the notice pending while the settings read has not answered', async () => {
+    const describeCall = vi.fn(() => Promise.reject(new Error('offline')))
+    const { mirror, controller } = buildWelcome({ describe: describeCall })
+    await mirror.load()
+    await controller.load()
+    // No answer stands, so the step renders nothing and never acknowledges.
+    expect(controller.store.getSnapshot()).toEqual({ status: 'loading', acknowledged: false, error: null })
+  })
 
-    const nonError = new WelcomeNoticeStore({
-      // Durable/wire failures are unknown; exercise containment of a non-Error rejection.
-      settings: { describe: () => Promise.reject(new Error('offline string')) },
-    } as never)
-    await nonError.load()
-    expect(nonError.store.getSnapshot().error).toBe('offline string')
+  it('reports a failed or refused persistence attempt after its recovery read', async () => {
+    const describeCall = vi.fn(() => Promise.resolve(ok({
+      writable: true, hasDocument: false, namespaces: [namespace()],
+    })))
+    const mutate = vi.fn(() => Promise.reject(new Error('disk full')))
+    const { mirror, controller } = buildWelcome({ describe: describeCall, mutate })
+    await mirror.load()
+    await controller.load()
+    await expect(controller.acknowledge()).resolves.toBe(false)
+    expect(controller.store.getSnapshot()).toMatchObject({
+      status: 'error',
+      acknowledged: false,
+      error: 'the acknowledgement did not persist',
+    })
+    // The failed latest write triggered one mirror recovery read.
+    expect(describeCall).toHaveBeenCalledTimes(2)
   })
 
-  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')
-    }
+  it('reports a missing namespace as an error instead of a silent skip', async () => {
+    const describeCall = vi.fn(() => Promise.resolve(ok({
+      writable: true, hasDocument: false, namespaces: [],
+    })))
+    const { mirror, controller } = buildWelcome({ describe: describeCall })
+    await mirror.load()
+    await controller.load()
+    expect(controller.store.getSnapshot()).toMatchObject({
+      status: 'error',
+      error: 'welcome acknowledgement settings are unavailable',
+    })
+  })
 
+  it('reads malformed durable values as unacknowledged', async () => {
     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)
+      const describeCall = vi.fn(() => Promise.resolve(ok({
+        writable: true, hasDocument: false, namespaces: [namespace(value)],
+      })))
+      const { mirror, controller } = buildWelcome({ describe: describeCall })
+      await mirror.load()
       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()
+  it('follows a later document change without an own read', async () => {
+    const describeCall = vi.fn()
+      .mockResolvedValueOnce(ok({ writable: true, hasDocument: false, namespaces: [namespace()] }))
+      .mockResolvedValueOnce(ok({
+        writable: true, hasDocument: false,
+        namespaces: [acknowledgedNamespace(WELCOME_NOTICE_VERSION)],
+      }))
+    const { mirror, controller } = buildWelcome({ describe: describeCall })
+    await mirror.load()
     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')
+    expect(controller.store.getSnapshot()).toMatchObject({ acknowledged: false })
+    await mirror.load()
+    expect(controller.store.getSnapshot()).toMatchObject({ status: 'ready', acknowledged: true })
   })
 })