From fc5ee9f786fb15b612474202f97ec83ec2c6fdfd Mon Sep 17 00:00:00 2001 From: imccyu Date: Mon, 3 Aug 2026 18:34:00 +0800 Subject: [PATCH] refactor(client): expose loopback state through connection --- ...versioned-gui-welcome-onboarding.i18n.yaml | 4 ++-- ...-07-30-versioned-gui-welcome-onboarding.md | 2 +- ...-30-versioned-gui-welcome-onboarding.zh.md | 2 +- packages/client/connection/README.i18n.yaml | 4 ++-- packages/client/connection/README.md | 2 +- packages/client/connection/README.zh.md | 2 +- packages/client/connection/package.json | 1 - .../client/connection/src/client/index.ts | 7 ++++++- .../connection/src/loopback-hostname.ts | 7 ++----- .../connection/tests/client-apply.spec.ts | 20 +++++++++++++------ .../client/runtime/tests/client-apply.spec.ts | 1 + .../client/runtime/tests/wire-events.spec.ts | 1 + packages/client/tsdown.client.ts | 2 +- .../ui-settings-general/src/client/index.ts | 7 +------ .../ui-settings-general/tests/apply.spec.ts | 11 ++++------ scripts/client-bundle-purity.spec.ts | 1 - 16 files changed, 38 insertions(+), 36 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.i18n.yaml b/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.i18n.yaml index 6a109260c1..10de05ea42 100644 --- a/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.md -2026-07-30-versioned-gui-welcome-onboarding.md: 1199cec532dd23930c70b236e6fcac80832204ff -2026-07-30-versioned-gui-welcome-onboarding.zh.md: d59e8971689fad1836127fea79cd392d3d5787c3 +2026-07-30-versioned-gui-welcome-onboarding.md: 4707769d4fa9fbf184e09a2e73087dfd326070be +2026-07-30-versioned-gui-welcome-onboarding.zh.md: c9d2e6274c476c59abc077aa3255ffe57a8a48bc diff --git a/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.md b/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.md index 1199cec532..4707769d4f 100644 --- a/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.md +++ b/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.md @@ -14,7 +14,7 @@ The GUI's credential onboarding begins with a DeepSeek-specific readiness check, **Ownerless product onboarding belongs to `ui-settings-general`.** `src/onboarding-copy.ts` is the single editable source for the complete notice, the Continue label, and `WELCOME_NOTICE_VERSION`; both supported GUI locales intentionally render the same Chinese owner copy. Runtime locale dictionaries derive their welcome values from that file, and tests import the same owner instead of repeating paragraph text. The notice is browser UI only: it creates no Session event and contributes no model-visible content. The notice identifies `DSH_TELEMETRY_DISABLED=1` as the telemetry opt-out. -**Loopback acknowledgement is durable per Harness profile.** The Host half registers a `ui-onboarding` section in the user-settings seam, stored under the active `$DSH_HOME/settings.yaml`. A loopback browser shows the notice unless `welcomeNoticeVersion` equals the owner constant exactly. Continue applies one path mutation with the current version and calls `complete()` only after the Host commits it; a failed write leaves the notice open, and closing the page or process writes nothing. Bumping the constant intentionally makes every profile acknowledge the revised copy once. A non-loopback browser must not call the loopback-only settings API. It presents the same notice, but explicit Continue completes the step only in the current browser process; reload or a new process presents it again. +**Loopback acknowledgement is durable per Harness profile.** The Host half registers a `ui-onboarding` section in the user-settings seam, stored under the active `$DSH_HOME/settings.yaml`. The connection plugin publishes whether the current page uses a loopback authority as `ctx.connection.isLoopback`; hostname classification remains internal to the connection package, and other client plugins consume the service state instead of importing its implementation. A loopback browser shows the notice unless `welcomeNoticeVersion` equals the owner constant exactly. Continue applies one path mutation with the current version and calls `complete()` only after the Host commits it; a failed write leaves the notice open, and closing the page or process writes nothing. Bumping the constant intentionally makes every profile acknowledge the revised copy once. A non-loopback browser must not call the loopback-only settings API. It presents the same notice, but explicit Continue completes the step only in the current browser process; reload or a new process presents it again. **Concurrent loopback views converge without stale replacement.** The acknowledgement write omits `expectedRevision` deliberately: every loopback tab writes the same version to one path, so the operation is idempotent and preserves sibling fields instead of rebuilding the section. `settings/document-updated` becomes `host/settings-changed`; an already mounted loopback tab refetches and advances when another tab or an external editor commits the current version. The API proxy exposes this one product namespace through a closed allowlist beside configurable-provider namespaces, without treating its changes as model-catalog invalidations. diff --git a/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.zh.md b/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.zh.md index d59e897168..c9d2e6274c 100644 --- a/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.zh.md +++ b/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.zh.md @@ -14,7 +14,7 @@ GUI 的凭据引导从 DeepSeek 专用的就绪状态检查开始,但内部测 **不属于单一功能的产品引导由 `ui-settings-general` 持有。** `src/onboarding-copy.ts` 是完整通知、「继续」按钮文案和 `WELCOME_NOTICE_VERSION` 的唯一可编辑来源;GUI 支持的两种 locale 都有意渲染同一份中文所有者文案。运行时 locale 字典从该文件派生欢迎文案,测试也导入同一个所有者,而不重复段落文本。该通知只存在于浏览器 UI:它不会创建会话事件,也不会贡献任何模型可见内容。通知明确以 `DSH_TELEMETRY_DISABLED=1` 作为遥测关闭方式。 -**loopback 确认状态按 Harness profile 持久化。** 宿主端在 user-settings seam 中注册 `ui-onboarding` 分节,并存入当前 `$DSH_HOME/settings.yaml`。除非 `welcomeNoticeVersion` 与文案所有者文件中的常量精确相等,否则 loopback 浏览器会显示通知。「继续」会以当前版本执行一次路径变更,并且仅在宿主端提交成功后调用 `complete()`;写入失败时通知保持打开,关闭页面或进程则不会写入任何内容。提升该常量会有意要求每个 profile 对修订后的文案重新确认一次。非 loopback 浏览器不能调用仅限 loopback 的 settings API;它仍显示同一通知,但显式点击「继续」只会在当前浏览器进程中完成该步骤,重新加载或新进程会再次显示通知。 +**loopback 确认状态按 Harness profile 持久化。** 宿主端在 user-settings seam 中注册 `ui-onboarding` 分节,并存入当前 `$DSH_HOME/settings.yaml`。connection 插件通过 `ctx.connection.isLoopback` 统一发布当前页面是否使用 loopback authority;hostname 判定函数留在 connection 包内,其他客户端插件只消费服务状态,不跨插件导入实现函数。除非 `welcomeNoticeVersion` 与文案所有者文件中的常量精确相等,否则 loopback 浏览器会显示通知。「继续」会以当前版本执行一次路径变更,并且仅在宿主端提交成功后调用 `complete()`;写入失败时通知保持打开,关闭页面或进程则不会写入任何内容。提升该常量会有意要求每个 profile 对修订后的文案重新确认一次。非 loopback 浏览器不能调用仅限 loopback 的 settings API;它仍显示同一通知,但显式点击「继续」只会在当前浏览器进程中完成该步骤,重新加载或新进程会再次显示通知。 **并发 loopback 视图无需陈旧的整体替换即可收敛。** 确认写入有意省略 `expectedRevision`:每个 loopback 标签页都向同一路径写入相同版本,因此该操作是幂等的,并会保留同级字段,而不是重建整个分节。`settings/document-updated` 会转为 `host/settings-changed`;另一个标签页或外部编辑器提交当前版本后,已挂载的 loopback 标签页会重新拉取状态并推进。API 网关在可配置提供方 namespace 之外,通过封闭的允许列表暴露这一个产品 namespace,同时不会把它的变更视为模型目录失效事件。 diff --git a/packages/client/connection/README.i18n.yaml b/packages/client/connection/README.i18n.yaml index e159696db3..a636d8bc49 100644 --- a/packages/client/connection/README.i18n.yaml +++ b/packages/client/connection/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/connection/README.md -README.md: 522ae6a14a3b4b07e7f2917133d16a5e83433f69 -README.zh.md: 4eaed862df678997b328d7a4ddcab1f5254c7c60 +README.md: f537fee3273e3b5d2411197cf1a1a6e0d34af5f9 +README.zh.md: a29d2c00e7df3f6290a03ffdad59b70b43702aca diff --git a/packages/client/connection/README.md b/packages/client/connection/README.md index 522ae6a14a..f537fee327 100644 --- a/packages/client/connection/README.md +++ b/packages/client/connection/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -Wire consumer layer: the client plugin's apply mounts `ctx.connection` (shared api client + single-consumer stream-loop starter); the export face carries the wire contract types, the `AbstractApiClient` seam, and the loop's sink/config types. The dedicated `./loopback-hostname` source subpath exposes the zero-dependency predicate shared by the `/api` Host fence and browser welcome-persistence selection; client bundlers inline this source entry, while plain Node cannot load it directly, so it must remain browser-safe and dependency-free. The node half's `/api` route pins the privileged method set (`host.pickDirectory`, `host.openPath`, and the whole configuration plane — `settings.describe`/`update`/`replace`/`mutate` and `credentials.describe`/`set`/`unset`, reads included, since describing returns the exposed configuration and probing an arbitrary reference reports where a credential comes from) to loopback by passing the trust fence with an empty trust list — a declared `trustedHosts` authority reaches every other method, while these stay loopback-local until a real authentication layer exists. The platform subclasses (WebApiClient/FixtureApiClient), the ConnectionController loop, and the fixture data source are package-internal — apply selects and drives them; tests reach them via src. Contract: api-contracts v3 §3. +Wire consumer layer: the client plugin's apply mounts `ctx.connection` (shared api client + current-page loopback state + single-consumer stream-loop starter); the export face carries the wire contract types, the `AbstractApiClient` seam, and the loop's sink/config types. Loopback hostname classification stays package-internal: the `/api` Host fence uses it directly, while other client plugins consume the derived `ctx.connection.isLoopback` state. The node half's `/api` route pins the privileged method set (`host.pickDirectory`, `host.openPath`, and the whole configuration plane — `settings.describe`/`update`/`replace`/`mutate` and `credentials.describe`/`set`/`unset`, reads included, since describing returns the exposed configuration and probing an arbitrary reference reports where a credential comes from) to loopback by passing the trust fence with an empty trust list — a declared `trustedHosts` authority reaches every other method, while these stay loopback-local until a real authentication layer exists. The platform subclasses (WebApiClient/FixtureApiClient), the ConnectionController loop, and the fixture data source are package-internal — apply selects and drives them; tests reach them via src. Contract: api-contracts v3 §3. ## /api browser-trust fence diff --git a/packages/client/connection/README.zh.md b/packages/client/connection/README.zh.md index 4eaed862df..a29d2c00e7 100644 --- a/packages/client/connection/README.zh.md +++ b/packages/client/connection/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -协议消费层:客户端插件的 apply 会挂载 `ctx.connection`(共享 API 客户端 + 单消费方流循环启动器);导出表层携带协议契约类型、`AbstractApiClient` seam,以及循环的 sink/配置类型。专用的 `./loopback-hostname` 源码子路径导出 `/api` Host fence 与浏览器欢迎页持久化选择共用的零依赖判定函数;客户端 bundler 会内联这一源码入口,而 plain Node 无法直接加载它,因此它必须保持浏览器安全且零依赖。node 半侧的 `/api` 路由让特权方法集(`host.pickDirectory`、`host.openPath`,以及整个配置面——`settings.describe`/`update`/`replace`/`mutate` 与 `credentials.describe`/`set`/`unset`,读取也在内,因为 describe 会返回已暴露的配置,而探测任意引用会报出某条凭据来自何处)以空信任表过信任 fence,从而钉在回环——已声明的 `trustedHosts` 授权可达其余全部方法,而这些方法在真正的认证层出现之前仍只限回环本机。平台子类(WebApiClient/FixtureApiClient)、ConnectionController 循环和 fixture 数据源都属于包内部:apply 负责选择并驱动它们,测试则通过 src 访问。契约:api-contracts v3 §3。 +协议消费层:客户端插件的 apply 会挂载 `ctx.connection`(共享 API 客户端 + 当前页面的 loopback 状态 + 单消费方流循环启动器);导出表层携带协议契约类型、`AbstractApiClient` seam,以及循环的 sink/配置类型。Loopback hostname 判定逻辑留在包内部:`/api` Host fence 会直接使用它,其他客户端插件则消费派生的 `ctx.connection.isLoopback` 状态。node 半侧的 `/api` 路由让特权方法集(`host.pickDirectory`、`host.openPath`,以及整个配置面——`settings.describe`/`update`/`replace`/`mutate` 与 `credentials.describe`/`set`/`unset`,读取也在内,因为 describe 会返回已暴露的配置,而探测任意引用会报出某条凭据来自何处)以空信任表过信任 fence,从而钉在回环——已声明的 `trustedHosts` 授权可达其余全部方法,而这些方法在真正的认证层出现之前仍只限回环本机。平台子类(WebApiClient/FixtureApiClient)、ConnectionController 循环和 fixture 数据源都属于包内部:apply 负责选择并驱动它们,测试则通过 src 访问。契约:api-contracts v3 §3。 ## /api 浏览器信任栅栏 diff --git a/packages/client/connection/package.json b/packages/client/connection/package.json index f2842ec061..d86b2bdf2a 100644 --- a/packages/client/connection/package.json +++ b/packages/client/connection/package.json @@ -19,7 +19,6 @@ "types": "./lib/types/client/index.d.ts", "default": "./lib/client.js" }, - "./loopback-hostname": "./src/loopback-hostname.ts", "./src/*": "./src/*", "./package.json": "./package.json" }, diff --git a/packages/client/connection/src/client/index.ts b/packages/client/connection/src/client/index.ts index a7ebfbbd86..9e33e63ce2 100644 --- a/packages/client/connection/src/client/index.ts +++ b/packages/client/connection/src/client/index.ts @@ -8,6 +8,7 @@ import type { IApiClient } from './api.ts' import { ConnectionController, type ConnectionConfig, type ConnectionSinks, type ConnectionState } from './connection.ts' import { FixtureApiClient } from './fixture.ts' import { WebApiClient } from './web-api-client.ts' +import { isLoopbackHostname } from '../loopback-hostname.ts' // ---- Contract re-exports (browser-safe apiproxy channels + core types) ---- export type { @@ -48,6 +49,8 @@ export const inject: string[] = [] export interface ConnectionHandle { /** Shared api client (fixture or real, decided at boot from the page URL). */ readonly api: IApiClient + /** Whether the current page authority is loopback; non-browser contexts default to true. */ + readonly isLoopback: boolean /** * Start the connect/pump/reconnect loop with the consumer's frame sinks. * One consumer owns the streams (the runtime object layer); a second call @@ -64,11 +67,13 @@ export interface ConnectionHandle { * @param ctx - client cordis context. */ export function apply(ctx: Context): void { - const fixture = typeof location !== 'undefined' && new URLSearchParams(location.search).has('fixture') + const pageLocation = typeof location === 'undefined' ? undefined : location + const fixture = pageLocation !== undefined && new URLSearchParams(pageLocation.search).has('fixture') const api: IApiClient = fixture ? new FixtureApiClient() : new WebApiClient() let started = false const handle: ConnectionHandle = { api, + isLoopback: pageLocation === undefined || isLoopbackHostname(pageLocation.hostname), start(sinks, config) { if (started) throw new Error('connection: the stream loop is already owned by another consumer') started = true diff --git a/packages/client/connection/src/loopback-hostname.ts b/packages/client/connection/src/loopback-hostname.ts index 5666f0714d..fe2fe93fc9 100644 --- a/packages/client/connection/src/loopback-hostname.ts +++ b/packages/client/connection/src/loopback-hostname.ts @@ -1,10 +1,7 @@ /** * Browser-safe, zero-dependency loopback classification shared by the `/api` - * Host fence and browser welcome-persistence selection. The dedicated - * `./loopback-hostname` source subpath is inlined into client bundles instead - * of loaded by plain Node, so this module must not add Node-only or runtime - * dependencies. - * @module @deepseek-ai/dsh-client-connection/loopback-hostname + * Host fence and the package's `ctx.connection` state. The predicate stays + * package-internal; client plugins consume the derived state through Cordis. */ /** diff --git a/packages/client/connection/tests/client-apply.spec.ts b/packages/client/connection/tests/client-apply.spec.ts index 6892dc7721..43c71dffb7 100644 --- a/packages/client/connection/tests/client-apply.spec.ts +++ b/packages/client/connection/tests/client-apply.spec.ts @@ -8,7 +8,7 @@ import { apply, type ConnectionHandle } from '../src/client/index.ts' import { FixtureApiClient } from '../src/client/fixture.ts' import { WebApiClient } from '../src/client/web-api-client.ts' -type Win = { location?: { search: string } } +type Win = { location?: { hostname: string; search: string } } afterEach(() => { delete (globalThis as Win).location @@ -24,20 +24,28 @@ async function mount(): Promise { describe('connection client apply', () => { it('mounts ctx.connection with the real client when no ?fixture switch is present', async () => { - ;(globalThis as Win).location = { search: '' } + ;(globalThis as Win).location = { hostname: 'localhost', search: '' } const handle = await mount() expect(handle.api).toBeInstanceOf(WebApiClient) + expect(handle.isLoopback).toBe(true) }) it('selects the fixture client under ?fixture (and with no location at all stays real)', async () => { - ;(globalThis as Win).location = { search: '?fixture' } + ;(globalThis as Win).location = { hostname: '127.0.0.1', search: '?fixture' } expect((await mount()).api).toBeInstanceOf(FixtureApiClient) delete (globalThis as Win).location - expect((await mount()).api).toBeInstanceOf(WebApiClient) + const handle = await mount() + expect(handle.api).toBeInstanceOf(WebApiClient) + expect(handle.isLoopback).toBe(true) + }) + + it('reports non-loopback page authority through the connection handle', async () => { + ;(globalThis as Win).location = { hostname: '192.0.2.20', search: '' } + expect((await mount()).isLoopback).toBe(false) }) it('start() hands out one loop, rejects a second consumer, and stop() aborts the streams', async () => { - ;(globalThis as Win).location = { search: '?fixture' } + ;(globalThis as Win).location = { hostname: 'localhost', search: '?fixture' } const handle = await mount() // config omitted: the `config ?? {}` default arm is part of the surface. const loop = handle.start({}) @@ -46,7 +54,7 @@ describe('connection client apply', () => { }) it('WebApiClient carries requests over globalThis.fetch', async () => { - ;(globalThis as Win).location = { search: '' } + ;(globalThis as Win).location = { hostname: 'localhost', search: '' } const handle = await mount() const original = globalThis.fetch const seen: string[] = [] diff --git a/packages/client/runtime/tests/client-apply.spec.ts b/packages/client/runtime/tests/client-apply.spec.ts index d389efe319..e5691a0619 100644 --- a/packages/client/runtime/tests/client-apply.spec.ts +++ b/packages/client/runtime/tests/client-apply.spec.ts @@ -26,6 +26,7 @@ async function mount(): Promise { const bench: Bench = { ctx, api, sinks: undefined, stopped: 0 } const handle: ConnectionHandle = { api, + isLoopback: true, start: (sinks) => { bench.sinks = sinks return { stop: () => { bench.stopped += 1 } } diff --git a/packages/client/runtime/tests/wire-events.spec.ts b/packages/client/runtime/tests/wire-events.spec.ts index fd7858d60c..21e7f1fc06 100644 --- a/packages/client/runtime/tests/wire-events.spec.ts +++ b/packages/client/runtime/tests/wire-events.spec.ts @@ -20,6 +20,7 @@ async function mount(): Promise { const bench: Bench = { ctx, sinks: undefined } const handle: ConnectionHandle = { api, + isLoopback: true, start: (sinks) => { bench.sinks = sinks return { stop: () => {} } diff --git a/packages/client/tsdown.client.ts b/packages/client/tsdown.client.ts index fad00fe4f0..2ff1856b3d 100644 --- a/packages/client/tsdown.client.ts +++ b/packages/client/tsdown.client.ts @@ -28,7 +28,7 @@ const CSS_VIRTUAL_SUFFIX = '.mjs' * Everything else under @deepseek-ai/* is either a module-table entry * (external) or a leak the purity gate rejects. */ -export const INLINE_SAFE = /^(?:@deepseek-ai\/dsh-(?:host-apiproxy|session|llm|tools|brand)(?:\/|$)|@deepseek-ai\/dsh-client-connection\/loopback-hostname$)/ +export const INLINE_SAFE = /^@deepseek-ai\/dsh-(host-apiproxy|session|llm|tools|brand)(\/|$)/ /** * Documented TEMPORARY exemption, not a platform module (hence not in diff --git a/packages/client/ui-settings-general/src/client/index.ts b/packages/client/ui-settings-general/src/client/index.ts index 6a7510d4ac..87b05e86cb 100644 --- a/packages/client/ui-settings-general/src/client/index.ts +++ b/packages/client/ui-settings-general/src/client/index.ts @@ -8,7 +8,6 @@ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client' import { deferRegistration } from '@deepseek-ai/dsh-client-ui-slots' import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client' -import { isLoopbackHostname } from '@deepseek-ai/dsh-client-connection/loopback-hostname' import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react' // Type-only: pulls the shell's SlotMap merges (trigger/header/section/item). import type {} from '@deepseek-ai/dsh-client-ui-settings/client' @@ -42,10 +41,6 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { /** Dictionary namespace owned by this plugin (shell chrome + General copy). */ const NS = 'settings' -function welcomePersistence(): 'host' | 'memory' { - return typeof location === 'undefined' || isLoopbackHostname(location.hostname) ? 'host' : 'memory' -} - /** * Required services (cordis fiber inject). The target slots are declared by * ui-settings' apply, whose activation order relative to this one is NOT @@ -66,7 +61,7 @@ export function apply(ctx: ClientContext): void { // locale/change re-registration wiring. const t = ctx.locale.bind(NS) const connection = ctx.get('connection') as ConnectionHandle - const welcomeController = new WelcomeNoticeStore(connection.api, welcomePersistence()) + const welcomeController = new WelcomeNoticeStore(connection.api, connection.isLoopback ? 'host' : 'memory') const useWelcomeSnapshot = bindSnapshotSelector(welcomeController.store) const welcomeInjected = (): WelcomeNoticeInjected => ({ controller: welcomeController, diff --git a/packages/client/ui-settings-general/tests/apply.spec.ts b/packages/client/ui-settings-general/tests/apply.spec.ts index 17e4ced2b2..73f6d8207e 100644 --- a/packages/client/ui-settings-general/tests/apply.spec.ts +++ b/packages/client/ui-settings-general/tests/apply.spec.ts @@ -1,6 +1,6 @@ /** Ownerless-copy registrations: the four seats, the dictionaries, thunked labels, and HMR recovery. */ import { Context } from 'cordis' -import { afterEach, describe, expect, it, vi } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots' import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client' import { LocaleService } from '@deepseek-ai/dsh-client-locale/client' @@ -16,8 +16,6 @@ import { WELCOME_NOTICE_SETTINGS_NAMESPACE } from '../src/onboarding-copy.ts' // the shipped Chinese copy, so they state the browser they assume. usePinnedBrowserLanguages('zh-CN') -afterEach(() => { vi.unstubAllGlobals() }) - /** The five seats this plugin fills (slot name → expected component). */ const SEATS = [ ['settings.trigger', TriggerContent], @@ -27,7 +25,7 @@ const SEATS = [ ['settings.onboarding', WelcomeNotice], ] as const -async function bench() { +async function bench(isLoopback = true) { const ctx = new Context() await ctx.plugin(SlotsService).await() const locale = new LocaleService(ctx) @@ -49,7 +47,7 @@ async function bench() { }, }, })) - ctx.provide('connection', { api: { settings: { describe: settingsDescribe } } } as never) + ctx.provide('connection', { api: { settings: { describe: settingsDescribe } }, isLoopback } as never) return { ctx, slots: ctx.get('slots') as SlotsService, locale, settingsDescribe } } @@ -162,8 +160,7 @@ describe('ui-settings-general apply', () => { }) it('keeps remote welcome acknowledgement process-local', async () => { - vi.stubGlobal('location', { hostname: '192.0.2.20' }) - const b = await bench() + const b = await bench(false) declare(b.slots) await b.ctx.plugin({ inject: [...inject], apply }).await() const entry = b.slots.entries('settings.onboarding')[0]! diff --git a/scripts/client-bundle-purity.spec.ts b/scripts/client-bundle-purity.spec.ts index 2eef1ba5ef..d70964bdba 100644 --- a/scripts/client-bundle-purity.spec.ts +++ b/scripts/client-bundle-purity.spec.ts @@ -53,7 +53,6 @@ describe('client bundle purity gate', () => { expect(resolveId('@deepseek-ai/dsh-host-apiproxy/api')).toBeNull() expect(resolveId('@deepseek-ai/dsh-session/surface')).toBeNull() expect(resolveId('@deepseek-ai/dsh-brand')).toBeNull() - expect(resolveId('@deepseek-ai/dsh-client-connection/loopback-hostname')).toBeNull() }) it('throws on any other @deepseek-ai leak', () => {