diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f164e3c4c4..9064a38cea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,8 +39,13 @@ jobs: - name: Typecheck (src + tests + examples) run: pnpm run typecheck + # Type-aware ESLint loads every package tsconfig through the project + # service and peaks at ~3.4GB; the default V8 old-space ceiling (~2GB) + # OOMs it (exit 134). Raise the ceiling well above the peak. - name: Lint run: pnpm run lint + env: + NODE_OPTIONS: --max-old-space-size=8192 # Doc-sync gates (doc-sync-enforcement RFC). doc-typecheck compiles the # fenced ts blocks against the root project-reference graph. The cordis diff --git a/packages/core/session/tests/surface.spec.ts b/packages/core/session/tests/surface.spec.ts index a7f4c13dad..4e6bc1f433 100644 --- a/packages/core/session/tests/surface.spec.ts +++ b/packages/core/session/tests/surface.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import type { SessionEvent, SurfaceEvent, SurfaceEventType } from '@deepseek-ai/dsh-session' -import { Session, SessionId } from '@deepseek-ai/dsh-session' +import { Session, SessionId, isSurfaceEligibleType, isSurfaceEvent } from '@deepseek-ai/dsh-session' import { CallId } from '@deepseek-ai/dsh-llm' /** Build a minimal session with turn boundaries and a single user message. */ @@ -279,3 +279,41 @@ describe('Session.append surface opts', () => { expect(event.surfaceOp).toBe('append') }) }) + +describe('surface type guards', () => { + it('isSurfaceEligibleType is true only for message-producing types', () => { + expect(isSurfaceEligibleType('user/message')).toBe(true) + expect(isSurfaceEligibleType('assistant/message')).toBe(true) + expect(isSurfaceEligibleType('tool/result')).toBe(true) + expect(isSurfaceEligibleType('context/message')).toBe(true) + expect(isSurfaceEligibleType('steering/message')).toBe(true) + expect(isSurfaceEligibleType('turn/start')).toBe(false) + expect(isSurfaceEligibleType('assistant/chunk')).toBe(false) + }) + + it('isSurfaceEvent narrows a fully-formed surface event', () => { + const s = surfaceSession() + const userMessage = s.events.find(e => e.type === 'user/message')! + expect(isSurfaceEvent(userMessage)).toBe(true) + }) + + it('isSurfaceEvent rejects a non-surface-eligible type', () => { + const s = surfaceSession() + const turnStart = s.events.find(e => e.type === 'turn/start')! + expect(isSurfaceEvent(turnStart)).toBe(false) + }) + + it('isSurfaceEvent rejects a surface-eligible type missing its surfaceOp marker', () => { + // A surface-eligible type whose mandatory surfaceOp is absent — the state a + // seed/load log can carry before the marker is validated. surfaceOp is + // optional on SessionEvent, so this is a representable runtime value. + const markerless: SessionEvent = { + type: 'user/message', + seq: 0, + time: 0, + data: { content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' } }, + } + expect(isSurfaceEligibleType(markerless.type)).toBe(true) + expect(isSurfaceEvent(markerless)).toBe(false) + }) +})