From 0a91d2cff0b1084603c8d95b920e4e2093dc75c8 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Wed, 22 Jul 2026 17:23:22 +0800 Subject: [PATCH] ci: coverage --- .../connection/tests/client-apply.spec.ts | 9 +++++---- packages/client/connection/tests/fake-api.ts | 20 ++++++++++++------- .../client/connection/tests/fixture.spec.ts | 12 +++++++++++ .../runtime/tests/client-loader.spec.ts | 6 +++--- packages/client/runtime/tests/fake-api.ts | 20 +++++++++++-------- packages/client/ui-layout/tests/apply.spec.ts | 4 +++- tsconfig.base.json | 3 +++ 7 files changed, 51 insertions(+), 23 deletions(-) diff --git a/packages/client/connection/tests/client-apply.spec.ts b/packages/client/connection/tests/client-apply.spec.ts index 89b854de5b..6892dc7721 100644 --- a/packages/client/connection/tests/client-apply.spec.ts +++ b/packages/client/connection/tests/client-apply.spec.ts @@ -50,12 +50,13 @@ describe('connection client apply', () => { const handle = await mount() const original = globalThis.fetch const seen: string[] = [] - globalThis.fetch = ((input: URL | RequestInfo) => { - seen.push(String(input)) + globalThis.fetch = (input: URL | RequestInfo) => { + seen.push(typeof input === 'string' ? input : input instanceof URL ? input.href : input.url) return Promise.resolve(new Response('{}', { status: 200 })) - }) as typeof fetch + } try { - await (handle.api as WebApiClient).host.describe({}).catch(() => undefined) // schema rejection is fine — the transport hop is the assertion + // Schema rejection is fine — the transport hop is the assertion. + await (handle.api as WebApiClient).host.describe({}).catch(() => undefined) } finally { globalThis.fetch = original } diff --git a/packages/client/connection/tests/fake-api.ts b/packages/client/connection/tests/fake-api.ts index 20cdf291e0..af5743bf9a 100644 --- a/packages/client/connection/tests/fake-api.ts +++ b/packages/client/connection/tests/fake-api.ts @@ -55,12 +55,16 @@ export class FakeApiClient implements IApiClient { private readonly muxConns: StreamConn[] = [] private readonly hostConns: StreamConn[] = [] + // Parameter annotations below are local structural types on purpose: the CI + // lint lane runs without built artifacts, where IApiClient's wire types + // (apiproxy subpath) resolve to any and inferred params trip no-unsafe-argument. readonly sessions: IApiClient['sessions'] = { - list: payload => this.record('session.list', payload, this.onList(payload)), - create: payload => this.record('session.create', payload, this.onCreate(payload)), - history: payload => this.record('session.history', payload, this.onHistory(payload)), - prompt: payload => this.record('session.prompt', payload, this.onPrompt(payload)), - cancel: payload => this.record('session.cancel', payload, this.onCancel(payload)), + list: (payload: unknown) => this.record('session.list', payload, this.onList(payload)), + create: (payload: unknown) => this.record('session.create', payload, this.onCreate(payload)), + history: (payload: { sessionId: SessionId; beforeSeq?: number; maxMessages?: number }) => + this.record('session.history', payload, this.onHistory(payload)), + prompt: (payload: unknown) => this.record('session.prompt', payload, this.onPrompt(payload)), + cancel: (payload: unknown) => this.record('session.cancel', payload, this.onCancel(payload)), } readonly host: IApiClient['host'] = { @@ -82,8 +86,10 @@ export class FakeApiClient implements IApiClient { } readonly events: IApiClient['events'] = { - mux: (_payload, signal, onOpen) => this.openStream(this.muxConns, signal, onOpen), - host: (_payload, signal, onOpen) => this.openStream(this.hostConns, signal, onOpen), + mux: (_payload: unknown, signal: AbortSignal, onOpen?: () => void) => + this.openStream(this.muxConns, signal, onOpen), + host: (_payload: unknown, signal: AbortSignal, onOpen?: () => void) => + this.openStream(this.hostConns, signal, onOpen), } respond(): Promise<{ accepted: false; reason: 'not-pending' }> { diff --git a/packages/client/connection/tests/fixture.spec.ts b/packages/client/connection/tests/fixture.spec.ts index f67a8f14f0..dee5eda0f4 100644 --- a/packages/client/connection/tests/fixture.spec.ts +++ b/packages/client/connection/tests/fixture.spec.ts @@ -4,6 +4,18 @@ * baseline replay, timing hooks) — this is the vitest-side drift detector for * the hand-written fixture/host parallel implementations. */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment, + @typescript-eslint/no-unsafe-member-access, + @typescript-eslint/no-unsafe-call, + @typescript-eslint/no-unsafe-argument, + @typescript-eslint/no-unsafe-return -- + * CI's no-build lint lane cannot resolve the wire types this suite drives + * (they arrive through apiproxy's lib/types exports, absent without a build), + * so every contract-typed expression collapses to `any` there while the same + * code lints clean locally (hence the locally-unused directive). The suite is + * exactly a traversal of that cross-package contract face (unary table, + * stream replay, envelope tap); typecheck runs with project references and + * keeps the real type safety. */ import { afterEach, describe, expect, it, vi } from 'vitest' import type { SessionId } from '../src/client/api.ts' diff --git a/packages/client/runtime/tests/client-loader.spec.ts b/packages/client/runtime/tests/client-loader.spec.ts index 383098d0fe..457b1fc13c 100644 --- a/packages/client/runtime/tests/client-loader.spec.ts +++ b/packages/client/runtime/tests/client-loader.spec.ts @@ -238,11 +238,11 @@ describe('DOM default seams (stubbed globals)', () => { } const g = globalThis as { document?: unknown; fetch: typeof fetch } g.document = fakeDoc - g.fetch = ((url: URL | RequestInfo) => Promise.resolve( - String(url).includes('bad') + g.fetch = (url: URL | RequestInfo) => Promise.resolve( + (typeof url === 'string' ? url : url instanceof URL ? url.href : url.url).includes('bad') ? new Response('x', { status: 500 }) : new Response('window.DSHClientProxy.loadPlugin(globalThis.__seamHandoff)', { status: 200 }), - )) as typeof fetch + ) try { delete win.DSHClientProxy const ctx = new Context() diff --git a/packages/client/runtime/tests/fake-api.ts b/packages/client/runtime/tests/fake-api.ts index cf6645391a..c13ef09fcb 100644 --- a/packages/client/runtime/tests/fake-api.ts +++ b/packages/client/runtime/tests/fake-api.ts @@ -58,16 +58,20 @@ export class FakeApiClient implements IApiClient { private readonly muxConns: StreamConn[] = [] private readonly hostConns: StreamConn[] = [] + // Parameters carry local structural annotations: the CI lint lane runs + // without built lib/, so IApiClient's indexed-access types collapse to any + // and inferred parameters would trip no-unsafe-argument. readonly sessions: IApiClient['sessions'] = { - list: payload => this.record('session.list', payload, this.onList(payload)), - create: payload => this.record('session.create', payload, this.onCreate(payload)), - history: payload => this.record('session.history', payload, this.onHistory(payload)), - prompt: payload => this.record('session.prompt', payload, this.onPrompt(payload)), - cancel: payload => this.record('session.cancel', payload, this.onCancel(payload)), + list: (payload: unknown) => this.record('session.list', payload, this.onList(payload)), + create: (payload: unknown) => this.record('session.create', payload, this.onCreate(payload)), + history: (payload: { sessionId: SessionId; beforeSeq?: number; maxMessages?: number }) => + this.record('session.history', payload, this.onHistory(payload)), + prompt: (payload: unknown) => this.record('session.prompt', payload, this.onPrompt(payload)), + cancel: (payload: unknown) => this.record('session.cancel', payload, this.onCancel(payload)), } readonly host: IApiClient['host'] = { - describe: payload => this.record('host.describe', payload, this.onDescribe(payload)), + describe: (payload: unknown) => this.record('host.describe', payload, this.onDescribe(payload)), } /** When true, streams never fire onOpen (misbehaving-carrier material for the handshake timeout guard). */ @@ -85,8 +89,8 @@ export class FakeApiClient implements IApiClient { } readonly events: IApiClient['events'] = { - mux: (_payload, signal, onOpen) => this.openStream(this.muxConns, signal, onOpen), - host: (_payload, signal, onOpen) => this.openStream(this.hostConns, signal, onOpen), + mux: (_payload: unknown, signal: AbortSignal, onOpen?: () => void) => this.openStream(this.muxConns, signal, onOpen), + host: (_payload: unknown, signal: AbortSignal, onOpen?: () => void) => this.openStream(this.hostConns, signal, onOpen), } respond(): Promise<{ accepted: false; reason: 'not-pending' }> { diff --git a/packages/client/ui-layout/tests/apply.spec.ts b/packages/client/ui-layout/tests/apply.spec.ts index da49028b34..a5bfbb7d48 100644 --- a/packages/client/ui-layout/tests/apply.spec.ts +++ b/packages/client/ui-layout/tests/apply.spec.ts @@ -62,7 +62,9 @@ describe('node half + invariant companion', () => { it('invariant companion registers under the package name', async () => { const register = vi.fn().mockReturnValue(() => {}) const ctx = { invariants: { register } } as never - const dispose = await invariant.apply(ctx) + // The /invariant subpath types live in lib/types (build product); assert + // the surface so the call stays typed where lint runs without a build. + const dispose = await (invariant as { apply: (ctx: never) => Promise<() => void> }).apply(ctx) expect(register).toHaveBeenCalledWith('@deepseek-ai/dsh-client-ui-layout', expect.any(Function)) // The installer is the declared no-op — calling it must not throw. expect(() => { (register.mock.calls[0]![1] as (c: never) => void)(undefined as never) }).not.toThrow() diff --git a/tsconfig.base.json b/tsconfig.base.json index ee6ec2f993..8218c0c02e 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -37,6 +37,7 @@ "@deepseek-ai/dsh-invariants": ["./packages/support/invariants/src/index.ts"], "@deepseek-ai/dsh-session/invariant": ["./packages/core/session/src/invariant.ts"], "@deepseek-ai/dsh-session/types": ["./packages/core/session/src/types.ts"], + "@deepseek-ai/dsh-session/surface": ["./packages/core/session/src/surface.ts"], "@deepseek-ai/dsh-llm/types": ["./packages/llm/llm/src/types.ts"], "@deepseek-ai/dsh-llm/brand": ["./packages/llm/llm/src/brand.ts"], "@deepseek-ai/dsh-tools/presentation": ["./packages/core/tools/src/presentation.ts"], @@ -82,6 +83,7 @@ // here. The build graph's project references (tsconfig.build.json) stay // explicit — TS project references have no wildcard form. "@deepseek-ai/dsh-host-apiproxy": ["./packages/host/apiproxy/src"], + "@deepseek-ai/dsh-host-apiproxy/client": ["./packages/host/apiproxy/src/fetch/client.ts"], "@deepseek-ai/dsh-host-apiproxy/*": ["./packages/host/apiproxy/src/*"], "@deepseek-ai/dsh-host-runtime": ["./packages/host/runtime/src"], "@deepseek-ai/dsh-host-webserver": ["./packages/host/webserver/src"], @@ -105,6 +107,7 @@ "@deepseek-ai/dsh-client-ui-theme": ["./packages/client/ui-theme/src"], "@deepseek-ai/dsh-client-ui-theme/client": ["./packages/client/ui-theme/src/client"], "@deepseek-ai/dsh-client-i18n": ["./packages/client/i18n/src"], + "@deepseek-ai/dsh-client-i18n/invariant": ["./packages/client/i18n/src/invariant"], "@deepseek-ai/dsh-client-i18n/client": ["./packages/client/i18n/src/client"], "@deepseek-ai/dsh-client-web": ["./packages/client/web/src"], "@deepseek-ai/dsh-*": [