存量清理(首跑 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 注释同步顺序)
160 lines
7.6 KiB
TypeScript
160 lines
7.6 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
|
|
// Pin the passwd login shell to a non-bash value for the pty helpers tests:
|
|
// the CI runner's own login shell is /bin/bash, which the old hardcoded
|
|
// fallback would have satisfied by coincidence, so the fallback chain needs
|
|
// a mock passwd to be genuinely discriminating. No other test reads os.userInfo.
|
|
vi.mock('node:os', async (importOriginal) => {
|
|
const os = await importOriginal<typeof import('node:os')>()
|
|
return {
|
|
...os,
|
|
userInfo: () => ({ ...os.userInfo(), shell: '/usr/bin/zsh' }),
|
|
}
|
|
})
|
|
|
|
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,
|
|
ensureSpawnHelper,
|
|
resolveShellExecutable,
|
|
shellDisplayName,
|
|
shellSpawnArgs,
|
|
} from '../src/pty-manager.ts'
|
|
|
|
describe('pty helpers', () => {
|
|
it('prefers an explicit shell, then SHELL, then the account login shell on POSIX', () => {
|
|
// The platform is injected so this chain also runs (and is asserted) on
|
|
// non-POSIX developer machines. The userInfo mock at the top of the file
|
|
// pins the passwd login shell to /usr/bin/zsh.
|
|
expect(defaultShell({ platform: 'linux', env: { SHELL: '/explicit/zsh' } })).toBe('/explicit/zsh')
|
|
// Surrounding whitespace must not leak into the spawned executable path.
|
|
expect(defaultShell({ platform: 'linux', env: { SHELL: ' /explicit/zsh ' } })).toBe('/explicit/zsh')
|
|
expect(defaultShell({ platform: 'linux', env: { SHELL: ' ' } })).toBe('/usr/bin/zsh')
|
|
expect(defaultShell({ platform: 'linux', env: {} })).toBe('/usr/bin/zsh')
|
|
})
|
|
|
|
it('Windows: explicit shell wins, then DSH_SIDEBAR_SHELL, then a probed pwsh.exe, then powershell.exe 5.1', () => {
|
|
// The explicit shell (the `shell` config field) beats every automatic
|
|
// source, and whitespace-only values count as unset.
|
|
expect(defaultShell({ platform: 'win32', explicit: 'C:\\Tools\\pwsh.exe', env: { DSH_SIDEBAR_SHELL: 'pwsh.exe' } }))
|
|
.toBe('C:\\Tools\\pwsh.exe')
|
|
expect(defaultShell({ platform: 'win32', explicit: ' ', env: { DSH_SIDEBAR_SHELL: 'pwsh.exe' } }))
|
|
.toBe('pwsh.exe')
|
|
|
|
// PATH is probed entry by entry; the first directory containing pwsh.exe
|
|
// wins and the returned value is the full resolved path. The candidate is
|
|
// normalized to forward slashes before comparing so the assertion holds
|
|
// under BOTH join() implementations: on the ubuntu runners node:path is
|
|
// POSIX and joins 'C:\\other' + 'pwsh.exe' as 'C:\\other/pwsh.exe'.
|
|
const fromPath = defaultShell({
|
|
platform: 'win32',
|
|
env: { PATH: 'C:\\tools;C:\\other' },
|
|
exists: path => path.replaceAll('\\', '/') === 'C:/other/pwsh.exe',
|
|
})
|
|
expect(fromPath.replaceAll('\\', '/')).toBe('C:/other/pwsh.exe')
|
|
|
|
// PATH misses fall through to the known install directories. On 32-bit
|
|
// Node, ProgramW6432 is the real 64-bit Program Files and is preferred.
|
|
const fromProgramW6432 = defaultShell({
|
|
platform: 'win32',
|
|
env: { ProgramW6432: 'C:\\PF64' },
|
|
exists: path => path.replaceAll('\\', '/') === 'C:/PF64/PowerShell/7/pwsh.exe',
|
|
})
|
|
expect(fromProgramW6432.replaceAll('\\', '/')).toBe('C:/PF64/PowerShell/7/pwsh.exe')
|
|
const fromProgramFiles = defaultShell({
|
|
platform: 'win32',
|
|
env: { ProgramFiles: 'C:\\PF', LOCALAPPDATA: 'C:\\Users\\x\\AppData\\Local' },
|
|
exists: path => path.replaceAll('\\', '/') === 'C:/PF/PowerShell/7/pwsh.exe',
|
|
})
|
|
expect(fromProgramFiles.replaceAll('\\', '/')).toBe('C:/PF/PowerShell/7/pwsh.exe')
|
|
|
|
// Nothing installed: keep the inbox 5.1 fallback instead of breaking.
|
|
expect(defaultShell({ platform: 'win32', env: {}, exists: () => false })).toBe('powershell.exe')
|
|
})
|
|
|
|
it('Windows: resolves bare custom shells through PATH/PATHEXT before node-pty spawn', () => {
|
|
const files = new Set([
|
|
'C:/Tools/PowerShell/pwsh.exe',
|
|
'C:/Windows/System32/cmd.exe',
|
|
])
|
|
const options = {
|
|
platform: 'win32' as const,
|
|
// Mixed-case Path/PATHEXT pins the case-insensitive environment lookup
|
|
// that real Windows process.env provides but plain test objects do not.
|
|
env: {
|
|
Path: 'C:\\Tools\\PowerShell',
|
|
PathExt: '.COM;.EXE;.BAT;.CMD',
|
|
SystemRoot: 'C:\\Windows',
|
|
},
|
|
exists: (path: string) => files.has(path.replaceAll('\\', '/')),
|
|
}
|
|
expect(resolveShellExecutable('pwsh', options).replaceAll('\\', '/'))
|
|
.toBe('C:/Tools/PowerShell/pwsh.exe')
|
|
expect(resolveShellExecutable('cmd', options).replaceAll('\\', '/'))
|
|
.toBe('C:/Windows/System32/cmd.exe')
|
|
})
|
|
|
|
it('Windows: accepts .exe names and absolute paths, and names a missing configured shell', () => {
|
|
const options = {
|
|
platform: 'win32' as const,
|
|
env: { PATH: 'C:\\Tools' },
|
|
exists: (path: string) => path.replaceAll('\\', '/') === 'C:/Tools/pwsh.exe'
|
|
|| path.replaceAll('\\', '/') === 'D:/Portable Shell/custom.exe',
|
|
}
|
|
expect(resolveShellExecutable('pwsh.exe', options).replaceAll('\\', '/'))
|
|
.toBe('C:/Tools/pwsh.exe')
|
|
expect(resolveShellExecutable('D:\\Portable Shell\\custom.exe', options).replaceAll('\\', '/'))
|
|
.toBe('D:/Portable Shell/custom.exe')
|
|
expect(() => resolveShellExecutable('missing-shell', options))
|
|
.toThrow('shell executable not found: "missing-shell"')
|
|
})
|
|
|
|
it('keeps POSIX bare shell resolution delegated to execvp', () => {
|
|
expect(resolveShellExecutable(' zsh ', { platform: 'linux', env: {}, exists: () => false })).toBe('zsh')
|
|
})
|
|
|
|
it('trims the configured shell and defaults it to auto for old documents', () => {
|
|
expect(resolveSidebarConfig(undefined).shell).toBe('')
|
|
expect(resolveSidebarConfig({ shell: ' pwsh.exe ' }).shell).toBe('pwsh.exe')
|
|
expect(resolveSidebarConfig({ shell: '/bin/zsh', shellArgs: ['--noprofile'] }).shellArgs).toEqual(['--noprofile'])
|
|
expect(resolveSidebarConfig(undefined).shellArgs).toEqual([])
|
|
})
|
|
|
|
it('derives a short display name for terminal tab titles', () => {
|
|
expect(shellDisplayName('/bin/zsh')).toBe('zsh')
|
|
expect(shellDisplayName('/usr/bin/bash')).toBe('bash')
|
|
expect(shellDisplayName('C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe')).toBe('powershell')
|
|
expect(shellDisplayName('pwsh.exe')).toBe('pwsh')
|
|
expect(shellDisplayName('/weird')).toBe('weird')
|
|
expect(shellDisplayName('/')).toBe('/')
|
|
})
|
|
|
|
it('uses explicit shell args verbatim and keeps platform defaults when none are configured', () => {
|
|
expect(shellSpawnArgs(['--noprofile', '--no-rc'])).toEqual(['--noprofile', '--no-rc'])
|
|
expect(shellSpawnArgs([])).toEqual(process.platform === 'win32' ? [] : ['-l'])
|
|
})
|
|
|
|
it('restores the spawn-helper executable bit idempotently', () => {
|
|
// node-pty's spawn-helper is a macOS-only artifact: binding.gyp builds
|
|
// the executable only for OS=="mac", and no other platform ships one to
|
|
// restore (linux uses forkpty directly). Skip elsewhere.
|
|
if (process.platform !== 'darwin') return
|
|
ensureSpawnHelper()
|
|
ensureSpawnHelper()
|
|
const entry = createRequire(import.meta.url).resolve('node-pty')
|
|
const root = dirname(dirname(entry))
|
|
// Prebuilt (tarball) or node-gyp-compiled (build/Release): mirror
|
|
// ensureSpawnHelper's candidate list — either location is acceptable.
|
|
const candidates = [
|
|
join(root, 'prebuilds', `${process.platform}-${process.arch}`, 'spawn-helper'),
|
|
join(root, 'build', 'Release', 'spawn-helper'),
|
|
]
|
|
const helper = candidates.find(existsSync)
|
|
expect(helper).toBeTruthy()
|
|
expect((statSync(helper!).mode & 0o111) !== 0).toBe(true)
|
|
})
|
|
})
|