Procházet zdrojové kódy

test(schedule): close coverage gaps

pku-xht před 2 měsíci
rodič
revize
75d9676a77

+ 62 - 7
packages/client/ui-schedule/tests/reminder-row.spec.tsx

@@ -9,6 +9,66 @@ import { zh } from '../src/client/locales.ts'
 
 const t: ReminderRowProps['t'] = makeTranslate(zh)
 
+const invalidSidecars: ReadonlyArray<{ name: string; view: unknown }> = [
+  { name: 'non-object', view: undefined },
+  { name: 'null', view: null },
+  { name: 'array', view: [] },
+  {
+    name: 'missing schedule id',
+    view: {
+      scheduleId: null,
+      prompt: 'not trusted',
+      occurrenceAt: '2026-08-05T08:00:00.000Z',
+      deliveryMode: 'session-local',
+    },
+  },
+  {
+    name: 'empty schedule id',
+    view: {
+      scheduleId: '',
+      prompt: 'not trusted',
+      occurrenceAt: '2026-08-05T08:00:00.000Z',
+      deliveryMode: 'session-local',
+    },
+  },
+  {
+    name: 'non-string prompt',
+    view: {
+      scheduleId: 'schedule-7',
+      prompt: 7,
+      occurrenceAt: '2026-08-05T08:00:00.000Z',
+      deliveryMode: 'session-local',
+    },
+  },
+  {
+    name: 'non-string occurrence',
+    view: {
+      scheduleId: 'schedule-7',
+      prompt: 'not trusted',
+      occurrenceAt: 7,
+      deliveryMode: 'session-local',
+    },
+  },
+  {
+    name: 'empty occurrence',
+    view: {
+      scheduleId: 'schedule-7',
+      prompt: 'not trusted',
+      occurrenceAt: '',
+      deliveryMode: 'session-local',
+    },
+  },
+  {
+    name: 'unsupported delivery mode',
+    view: {
+      scheduleId: 'schedule-7',
+      prompt: 'not trusted',
+      occurrenceAt: '2026-08-05T08:00:00.000Z',
+      deliveryMode: 'external',
+    },
+  },
+]
+
 afterEach(cleanup)
 
 function props(view: unknown): ReminderRowProps {
@@ -40,13 +100,8 @@ describe('ReminderRow', () => {
     expect(time.getAttribute('datetime')).toBe('2026-08-05T08:00:00.000Z')
   })
 
-  it('contains an incompatible sidecar as a visible unavailable receipt', () => {
-    render(<ReminderRow {...props({
-      scheduleId: '',
-      prompt: 'not trusted',
-      occurrenceAt: 123,
-      deliveryMode: 'external',
-    })} />)
+  it.each(invalidSidecars)('contains an incompatible $name sidecar as an unavailable receipt', ({ view }) => {
+    render(<ReminderRow {...props(view)} />)
 
     expect(screen.getByText('提醒回执不可用 · schedule/change')).toBeTruthy()
     expect(screen.queryByText('not trusted')).toBeNull()

+ 2 - 2
packages/session/session-persistence/src/coordinator.ts

@@ -1143,7 +1143,7 @@ export class PersistenceCoordinator<TornMarker = unknown> {
       const seed = session.events.slice(0, live.seedEnd)
       await this.onCreated(session, seed)
     }).catch((error: unknown) => {
-      if (live.init === init) live.init = undefined
+      live.init = undefined
       throw error
     })
     live.init = init
@@ -1254,7 +1254,7 @@ export class PersistenceCoordinator<TornMarker = unknown> {
       if (tracked.materialized || tracked.cursor !== 0) {
         throw new Error(`session "${session.header.id}" lost its persisted artifact during live initialization`)
       }
-      if (seed.length > 0) await this.appendCore(session.header.id, seed)
+      await this.appendCore(session.header.id, seed)
       return
     }
     await this.adoptLivePrefix(session, seed, stored, tracked)

+ 81 - 0
packages/session/session-persistence/tests/persistence.spec.ts

@@ -467,6 +467,87 @@ describe('PersistenceCoordinator retryable live initialization', () => {
     }
   })
 
+  it('retries a fork seed when initialization rejects before materialization', async () => {
+    const ctx = new Context()
+    await ctx.plugin(SessionStore)
+    const backend = new ControlledBackend()
+    const appendGate = Promise.withResolvers<undefined>()
+    backend.beforeAppend = async (attempt) => {
+      if (attempt === 1) {
+        await appendGate.promise
+        throw new Error('pre-commit init write failure')
+      }
+    }
+    const fiber = await ctx.plugin(Object.assign((inner: Context) => {
+      new PersistenceCoordinator(inner, backend)
+    }, { inject: ['sessions'] }))
+
+    try {
+      const seed = oneTurnLog()
+      const session = ctx.sessions.create(SessionId('retry-unmaterialized-fork-seed'), {
+        seed,
+        meta: { cwd: '/w', seedLength: seed.length },
+      })
+      await vi.waitFor(() => { expect(backend.appendAttempts).toBe(1) })
+      const first = ctx.sessions.flush(session)
+      appendGate.resolve(undefined)
+      await expect(first).rejects.toThrow('pre-commit init write failure')
+      await expect(ctx.sessions.flush(session)).resolves.toBe(true)
+
+      expect(backend.appendAttempts).toBe(2)
+      expect(backend.store.get(session.id)?.events.map(event => event.seq))
+        .toEqual([0, 1, 2, 3, 4, 5, 6])
+    } finally {
+      appendGate.resolve(undefined)
+      await fiber.dispose()
+      await ctx.fiber.dispose()
+    }
+  })
+
+  it('rejects a retry when its adopted durable prefix disappears', async () => {
+    const ctx = new Context()
+    await ctx.plugin(SessionStore)
+    const backend = new ControlledBackend()
+    const id = SessionId('retry-missing-adopted-prefix')
+    const seed = oneTurnLog()
+    const stored = seed.slice(0, 1)
+    const storedMeta = meta(id, '/w')
+    backend.store.set(id, { meta: storedMeta, events: structuredClone(stored) })
+    const appendGate = Promise.withResolvers<undefined>()
+    backend.beforeAppend = async (attempt) => {
+      if (attempt === 1) {
+        await appendGate.promise
+        throw new Error('pre-commit adoption write failure')
+      }
+    }
+    const fiber = await ctx.plugin(Object.assign((inner: Context) => {
+      new PersistenceCoordinator(inner, backend)
+    }, { inject: ['sessions'] }))
+
+    try {
+      const session = ctx.sessions.create(id, {
+        seed,
+        meta: { cwd: '/w', seedLength: seed.length },
+      })
+      await vi.waitFor(() => { expect(backend.appendAttempts).toBe(1) })
+      const first = ctx.sessions.flush(session)
+      appendGate.resolve(undefined)
+      await expect(first).rejects.toThrow('pre-commit adoption write failure')
+
+      backend.store.delete(id)
+      await expect(ctx.sessions.flush(session))
+        .rejects.toThrow('lost its persisted artifact during live initialization')
+      expect(backend.appendAttempts).toBe(1)
+    } finally {
+      appendGate.resolve(undefined)
+      if (!backend.store.has(id)) {
+        backend.store.set(id, { meta: storedMeta, events: structuredClone(stored) })
+      }
+      await fiber.dispose()
+      await ctx.fiber.dispose()
+    }
+  })
+
   it('retries only a missing suffix after stored-session adoption rejects', async () => {
     const ctx = new Context()
     await ctx.plugin(SessionStore)