fix(lint): 清零存量 lint 告警并接入 CI / Makefile 门禁
存量清理(首跑 141 errors / 21 warnings → 0 / 0): - 删除孤儿 disable:SideChatView / Sidebar / selection-popup 三处 exhaustive-deps 豁免与 chunk-artifact 的 no-new-func 豁免(多行理由注释把 disable-next-line 顶离了实报行;selection-popup 改为提取 popupOpen 标志真修复) - 死代码:未用导入(Sidebar×7、TextEditor、SideCardSection、config、fs-operations 及 6 个 spec)、sidechat-core 死累加器 total、chunk-loader.spec 死助手 registry、sidechat-seed-validation 死助手 toolResultMessage - 真实小修:agent-opens 三处 throw 补 cause;lazy-chunk any → ComponentType<P>(P extends object);service.ts claimed 初值改定值赋值;chunk-loader task / spec 内 etag / pendingLoad / snapshot 改 const;terminal-links 去掉字符类内多余转义;SubagentView catalogs 空兜底 useMemo 稳定标识;editor-host.spec 组件提出大写 FakeViewer(rules-of-hooks);pty-helpers.spec 内联 require 提升为静态导入;panel-host-css 注释里的零宽空格改为可见写法 - 依赖数组豁免(均带理由):granular scope 字段(EditorHost / PdfView / ChangesTab / GitLens×2)、job.status 粒度(SubagentView×2)、清理期读 ref、mount-only 注册等;ctx(挂载期恒定)直接入 deps(Sidebar×3)、TextEditor previewScroll 补 scope(幂等) - 刻意控制字符正则(conversation-draft / markdown-images)与宿主槽位 any 签名(context-types)以带理由豁免保留 接入: - Makefile:lint 目标(typecheck 旁),check 聚合补 lint 保持「对齐 CI」语义 - ci.yml ci / ci-windows 两 job 与 release.yml 在 Typecheck 后加 Lint 步骤(release 注释同步顺序)
This commit is contained in:
@@ -18,8 +18,7 @@ import { renderToString } from 'react-dom/server'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { act } from 'react-dom/test-utils'
|
||||
import * as primitives from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import { PLUGIN_TOPIC_URL, type PluginEntry } from '../src/client/plugins-shared.ts'
|
||||
import { builtinTabPlugins } from '../src/client/plugins-tabs.ts'
|
||||
import { type PluginEntry } from '../src/client/plugins-shared.ts'
|
||||
import { builtinViewerPlugins } from '../src/client/plugins-viewers.ts'
|
||||
import { AddPluginModal, PluginListBody } from '../src/client/add-plugin-modal.tsx'
|
||||
import { createBetterSidebarService } from '../src/client/service.ts'
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
ALLOWED_SIGNALS,
|
||||
snapshotOf,
|
||||
tryResizePty,
|
||||
type AgentTerminalSnapshot,
|
||||
} from '../src/agent-pty.ts'
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,7 +36,6 @@ describe.skipIf(!chunksBuilt)('built chunk artifacts', () => {
|
||||
if (typeof g.removeEventListener !== 'function') g.removeEventListener = () => {}
|
||||
for (const name of CHUNKS) {
|
||||
const code = readFileSync(`lib/client-${name}.js`, 'utf8')
|
||||
// eslint-disable-next-line no-new-func
|
||||
expect(() => new Function(code)(), name).not.toThrow()
|
||||
const registry = g.__dshChunks__ as Record<string, unknown>
|
||||
expect(typeof registry[name], name).toBe('function')
|
||||
|
||||
@@ -41,11 +41,6 @@ function removeModuleSystem(): void {
|
||||
delete (globalThis as Record<string, unknown>).__DSH_MODULES__
|
||||
}
|
||||
|
||||
/** The global registry the chunk scripts populate (mirror of chunk-loader). */
|
||||
function registry(): Record<string, unknown> {
|
||||
return (globalThis as { __dshChunks__?: Record<string, unknown> }).__dshChunks__ ?? {}
|
||||
}
|
||||
|
||||
/** Simulate a chunk script executing: it assigns its factory to the registry. */
|
||||
function simulateScript(name: string, factory: (require: (spec: string) => unknown) => ChunkExports): void {
|
||||
const g = globalThis as { __dshChunks__?: Record<string, unknown> }
|
||||
@@ -314,8 +309,7 @@ describe('revalidateChunksOnReactivate (HMR re-activation keeps unchanged chunks
|
||||
const revalidating = revalidateChunksOnReactivate()
|
||||
// A lazy open DURING the pending revalidation must NOT get the old cache.
|
||||
let resolved = false
|
||||
let pendingLoad: Promise<ChunkExports> | null = null
|
||||
pendingLoad = loadChunk('editor').then((exports) => { resolved = true; return exports })
|
||||
const pendingLoad = loadChunk('editor').then((exports) => { resolved = true; return exports })
|
||||
await new Promise((resolve) => setTimeout(resolve, 5))
|
||||
expect(resolved, 'loadChunk must await the pending revalidation').toBe(false)
|
||||
// Release the HEAD; the barrier lifts and the load re-injects fresh exports.
|
||||
@@ -369,7 +363,7 @@ describe('revalidateChunksOnReactivate (HMR re-activation keeps unchanged chunks
|
||||
scriptCalls += 1
|
||||
simulateScript('editor', () => ({ TextEditor: `editor-view:${scriptCalls}` }))
|
||||
})
|
||||
let etag = '"v1"'
|
||||
const etag = '"v1"'
|
||||
stubBundleHead(() => etag)
|
||||
await loadChunk('editor')
|
||||
await settleEtag()
|
||||
|
||||
@@ -43,7 +43,7 @@ import type {
|
||||
const tab: TabDescriptor = {
|
||||
id: 'my-plugin:db',
|
||||
title: () => 'Database',
|
||||
icon: (size: number) => null,
|
||||
icon: (_size: number) => null,
|
||||
order: 50,
|
||||
hidden: false,
|
||||
available: (ctx, scope, state) => scope.sessionId !== '' && state.panelOpen && ctx !== null,
|
||||
@@ -116,7 +116,7 @@ service.subscribe(() => {})
|
||||
const seed: OpenTabSeed = { type: 'my-plugin:db', title: 'DB', path: '/p', id: 'x', meta: { a: 1 } }
|
||||
service.openTab(seed)
|
||||
service.openTab(seed, { sessionId: 's1', cwd: '/p' })
|
||||
service.version
|
||||
const _version: string = service.version
|
||||
service.features.includes('badge')
|
||||
const snapshot: SidebarSnapshot | undefined = service.getSnapshot()
|
||||
void snapshot
|
||||
|
||||
+18
-14
@@ -10,12 +10,12 @@
|
||||
*/
|
||||
// @vitest-environment jsdom
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createElement, useEffect } from 'react'
|
||||
import { createElement, useEffect, type ReactNode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { act } from 'react-dom/test-utils'
|
||||
import type { Context } from '../src/context-types.ts'
|
||||
import { EditorHost } from '../src/client/EditorHost.tsx'
|
||||
import { createBetterSidebarService } from '../src/client/service.ts'
|
||||
import { createBetterSidebarService, type FileViewerProps } from '../src/client/service.ts'
|
||||
import { allLeaves, createSidebarStore, type SidebarTab } from '../src/client/state.ts'
|
||||
|
||||
// The act() environment flag (React 18.2 reads it before flushing effects).
|
||||
@@ -258,22 +258,26 @@ describe('EditorHost (files window)', () => {
|
||||
const service = ctx.betterSidebar
|
||||
const calls: string[] = []
|
||||
// A viewer with a hoisted toolbar (the TextEditor contract): register
|
||||
// commands and report the state once on mount.
|
||||
// commands and report the state once on mount. Capitalized so the hooks
|
||||
// rules recognize it as a component.
|
||||
const FakeViewer = (viewerProps: FileViewerProps): ReactNode => {
|
||||
useEffect(() => {
|
||||
viewerProps.onToolbarControls?.({
|
||||
setMode: (next) => { calls.push(`mode:${next}`) },
|
||||
save: () => { calls.push('save') },
|
||||
})
|
||||
viewerProps.onToolbarState?.({ modes: true, mode: 'preview', dirty: true, editable: true, saveState: 'idle' })
|
||||
return () => { viewerProps.onToolbarControls?.(null) }
|
||||
// Mount-only: re-running would re-fire the toolbar registration.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
return null
|
||||
}
|
||||
service.registerFileViewer({
|
||||
id: 'test:fake',
|
||||
exts: ['fake'],
|
||||
fetchStrategy: 'none',
|
||||
component: (viewerProps) => {
|
||||
useEffect(() => {
|
||||
viewerProps.onToolbarControls?.({
|
||||
setMode: (next) => { calls.push(`mode:${next}`) },
|
||||
save: () => { calls.push('save') },
|
||||
})
|
||||
viewerProps.onToolbarState?.({ modes: true, mode: 'preview', dirty: true, editable: true, saveState: 'idle' })
|
||||
return () => { viewerProps.onToolbarControls?.(null) }
|
||||
}, [])
|
||||
return null
|
||||
},
|
||||
component: FakeViewer,
|
||||
})
|
||||
service.openTab({ type: 'editor', title: 'x.fake', path: '/tmp/x.fake', id: 'editor:/tmp/x.fake' })
|
||||
const fileTab = (): SidebarTab =>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { afterAll, describe, expect, it } from 'vitest'
|
||||
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, symlinkSync } from 'node:fs'
|
||||
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, symlinkSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { writeWorkspaceUpload } from '../src/fs-operations.ts'
|
||||
|
||||
@@ -37,6 +37,8 @@ function FakeMarkdownViewer(props: FileViewerProps) {
|
||||
markDirty = undefined
|
||||
props.onToolbarControls?.(null)
|
||||
}
|
||||
// Only the callback identities must re-wire; content is read at render time.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.onToolbarControls, props.onToolbarState])
|
||||
return createElement('div', { 'data-testid': 'markdown-content' }, props.content ?? '')
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ const css = readFileSync('src/client/sidebar.module.css', 'utf8')
|
||||
/** Body of the `:global([data-dsh-panel-host])` rule (up to the un-indented `}`). */
|
||||
const hostRule = css.match(/:global\(\[data-dsh-panel-host\]\) \{([\s\S]*?)\n\}/)?.[1]
|
||||
|
||||
/** Strip /* ... */ comment blocks so prose mentions don't count as declarations. */
|
||||
/** Strip block comments (`/*` to `*`-`/`) so prose mentions don't count as declarations. */
|
||||
const stripComments = (cssText: string): string => cssText.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
|
||||
describe('panel host layer css', () => {
|
||||
|
||||
@@ -12,6 +12,9 @@ vi.mock('node:os', async (importOriginal) => {
|
||||
}
|
||||
})
|
||||
|
||||
import { existsSync, statSync } from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { resolveSidebarConfig } from '../src/config.ts'
|
||||
import {
|
||||
defaultShell,
|
||||
@@ -141,9 +144,6 @@ describe('pty helpers', () => {
|
||||
if (process.platform !== 'darwin') return
|
||||
ensureSpawnHelper()
|
||||
ensureSpawnHelper()
|
||||
const { existsSync, statSync } = require('node:fs') as typeof import('node:fs')
|
||||
const { dirname, join } = require('node:path') as typeof import('node:path')
|
||||
const { createRequire } = require('node:module') as typeof import('node:module')
|
||||
const entry = createRequire(import.meta.url).resolve('node-pty')
|
||||
const root = dirname(dirname(entry))
|
||||
// Prebuilt (tarball) or node-gyp-compiled (build/Release): mirror
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
SIDE_INJECTION_PLUGIN,
|
||||
SIDE_LABEL_PREFIX,
|
||||
type SeedEvent,
|
||||
type SidechatLogEvent,
|
||||
} from '../src/sidechat-core.ts'
|
||||
|
||||
/** One log event fixture (structural, seq === index like the live contract).
|
||||
|
||||
@@ -40,15 +40,6 @@ function assistantMessage(text: string): Record<string, unknown> {
|
||||
}
|
||||
}
|
||||
|
||||
function toolResultMessage(callId: string, text: string): Record<string, unknown> {
|
||||
return {
|
||||
id: `m-${callId}`,
|
||||
role: 'user',
|
||||
content: [{ type: 'tool-result', toolCallId: callId, content: [{ type: 'text', text }] }],
|
||||
source: { kind: 'tool', callId },
|
||||
}
|
||||
}
|
||||
|
||||
/** A parent log with a completed turn, a pending question, and an open
|
||||
* in-progress turn (the exact shape a mid-stream thread creation sees). */
|
||||
function parentLog(): SidebarSessionEvent[] {
|
||||
|
||||
@@ -64,7 +64,7 @@ const EVENTS = [
|
||||
|
||||
/** A subscribable sessions-list snapshot (mirror of the runtime list feed). */
|
||||
function makeStore(initial: SidebarSessionList) {
|
||||
let snapshot = initial
|
||||
const snapshot = initial
|
||||
const listeners = new Set<() => void>()
|
||||
return {
|
||||
getSnapshot: (): SidebarSessionList => snapshot,
|
||||
|
||||
Reference in New Issue
Block a user