Explorar el Código

test(session-controller): cover cold host-only projection cache

_Kerman hace 2 semanas
padre
commit
cf06f10229

+ 3 - 0
packages/api/session-controller/package.json

@@ -127,6 +127,9 @@
     "@deepseek-ai/dsh-session-projection-cache": "workspace:^",
     "@deepseek-ai/dsh-session-query": "workspace:^",
     "@deepseek-ai/dsh-session-title": "workspace:^",
+    "@deepseek-ai/dsh-storage": "workspace:^",
+    "@deepseek-ai/dsh-storage-domain": "workspace:^",
+    "@deepseek-ai/dsh-storage-json": "workspace:^",
     "@deepseek-ai/dsh-subagent": "workspace:^",
     "@deepseek-ai/dsh-typert-protocol": "workspace:^",
     "@deepseek-ai/dsh-typert-registry": "workspace:^",

+ 72 - 0
packages/api/session-controller/tests/session-projections.host.spec.ts

@@ -8,6 +8,9 @@
  */
 
 import { describe, expect, it, vi } from 'vitest'
+import { mkdtemp, readFile, rm } from 'node:fs/promises'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
 import { Context } from '@deepseek-ai/cordis'
 import { z } from 'zod'
 import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
@@ -19,6 +22,10 @@ import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
 import type { Session } from '@deepseek-ai/dsh-session'
 import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
+import SessionProjectionCache, { projectionCacheDomainSpec } from '@deepseek-ai/dsh-session-projection-cache'
+import Storage from '@deepseek-ai/dsh-storage'
+import * as StorageDomain from '@deepseek-ai/dsh-storage-domain'
+import * as StorageJson from '@deepseek-ai/dsh-storage-json'
 import { SessionControlController } from '@deepseek-ai/dsh-api-session-controller/src/control.ts'
 import type { SessionControlFrame, SessionFollowFrame } from '@deepseek-ai/dsh-api-session-controller/types'
 import { createSessionTestRemote, type TestSessionRemote } from './test-remote.ts'
@@ -27,6 +34,7 @@ declare module '@deepseek-ai/dsh-session-projection/types' {
   interface SessionProjectionStateMap {
     'test/last-user': LastUserState
     'test/internal-count': number
+    'test/private-prompt': string | null
   }
   interface SessionProjectionMap {
     'test/last-user': { text: string } | null
@@ -91,6 +99,16 @@ const internalCountUnit = () => ({
   stateVersion: 1,
 }) satisfies ProjectionDefinition<'test/internal-count', number>
 
+const privatePromptUnit = () => ({
+  key: 'test/private-prompt',
+  stateSchema: z.string().nullable(),
+  init: () => null,
+  apply: (state, event) => (event.type === 'user/message'
+    ? (event.data.content[0] as { text?: string }).text ?? ''
+    : state),
+  stateVersion: 1,
+}) satisfies ProjectionDefinition<'test/private-prompt', string | null>
+
 async function harness(withRegistry: boolean): Promise<{ ctx: Context; session: Session }> {
   const ctx = new Context()
   await ctx.plugin(SessionStore)
@@ -420,6 +438,60 @@ describe('session.list projections column', () => {
     })
   })
 
+  it('keeps persisted host-only state out of a cold session.list response', async () => {
+    const root = await mkdtemp(join(tmpdir(), 'dsh-api-projcache-'))
+    const ctx = new Context()
+    try {
+      await ctx.plugin(Storage)
+      await ctx.plugin(StorageJson, { root })
+      await ctx.plugin(StorageDomain, { backend: 'json' })
+      await ctx.plugin(SessionStore)
+      await ctx.plugin(AgentRegistry)
+      await ctx.plugin(SessionProjectionRegistry)
+      ctx.sessionProjections.register(privatePromptUnit())
+      await ctx.plugin(SessionProjectionCache, { writeEveryEvents: 100, writeIntervalMs: 60_000 })
+      const gateway = remote(ctx)
+      await new Promise(resolve => setTimeout(resolve, 0))
+
+      const id = SessionId('session-cold-host-state')
+      const secret = 'private prompt text from the cache'
+      let session: Session | undefined
+      const owner = await ctx.plugin(Object.assign((sessionCtx: Context) => {
+        session = sessionCtx.sessions.create(id, { meta: { createdAt: 5, cwd: '/workspace' } })
+      }, { inject: ['sessions'] }))
+      if (session === undefined) throw new Error('session was not created')
+      session.append('turn/start', { turn: 1 })
+      session.append('user/message', createUserMessage({
+        content: [{ type: 'text', text: secret }],
+        source: { kind: 'user' },
+      }), { surfaceOp: 'append' })
+      await ctx.sessionProjectionCache.write(session)
+      const stored = await readFile(
+        join(root, projectionCacheDomainSpec.name, 'sessions', `${id}.json`),
+        'utf8',
+      )
+      expect(stored).toContain(secret)
+
+      const header = session.header
+      await owner.dispose()
+      expect(ctx.sessions.get(id)).toBeUndefined()
+      ctx.provide('sessionPersistence', {
+        list: async () => [header],
+        locate: () => undefined,
+      } as never)
+
+      const response = await gateway.list(request({}))
+      if (!response.ok) throw new Error('unreachable')
+      const row = response.value.items.find(item => item.sessionId === id)
+      expect(row?.projections?.values.sessionListMetadata).toMatchObject({ blank: false })
+      expect('test/private-prompt' in (row?.projections?.values ?? {})).toBe(false)
+      expect(JSON.stringify(row)).not.toContain(secret)
+    } finally {
+      await ctx.fiber.dispose()
+      await rm(root, { recursive: true, force: true })
+    }
+  })
+
   it('cold rows without a cache plugin (or without a stored row) just lack the column', async () => {
     const { ctx } = await harness(true)
     const coldId = SessionId('session-cold-uncached')

+ 9 - 0
pnpm-lock.yaml

@@ -798,6 +798,15 @@ importers:
       '@deepseek-ai/dsh-session-title':
         specifier: workspace:^
         version: link:../../session/session-title
+      '@deepseek-ai/dsh-storage':
+        specifier: workspace:^
+        version: link:../../storage/storage
+      '@deepseek-ai/dsh-storage-domain':
+        specifier: workspace:^
+        version: link:../../storage/storage-domain
+      '@deepseek-ai/dsh-storage-json':
+        specifier: workspace:^
+        version: link:../../storage/storage-json
       '@deepseek-ai/dsh-subagent':
         specifier: workspace:^
         version: link:../../subagent/subagent