Files
deepseek-harness/apps/desktop/tests/register-dsh.spec.ts
T
Pine 604a817546 refactor(desktop): shell polish — register the dsh launcher, use vendored pnpm, docs
Register the dsh CLI launcher from the desktop shell, prefer the vendored
pnpm in the CLI plugin command, and update the desktop and CLI reference docs.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-15 17:04:36 +08:00

114 lines
4.8 KiB
TypeScript

import { lstatSync, mkdirSync, mkdtempSync, readlinkSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { appendToUserPath, buildDshCmd, registerDshLauncher } from '../src/register-dsh.ts'
const dirs: string[] = []
/** A fake harness dir (with or without a `dsh` launcher) and a bin dir to register into. */
function fixtures(): { harnessDir: string; binDir: string } {
const root = mkdtempSync(join(tmpdir(), 'dsh-register-'))
dirs.push(root)
return { harnessDir: join(root, 'harness'), binDir: join(root, 'bin') }
}
afterEach(() => {
for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true })
})
describe('registerDshLauncher', () => {
it('symlinks the harness launcher into the bin dir when absent', () => {
const { harnessDir, binDir } = fixtures()
mkdirSync(join(harnessDir, 'bin'), { recursive: true })
writeFileSync(join(harnessDir, 'dsh'), '#!/bin/sh\n')
expect(registerDshLauncher(harnessDir, binDir)).toEqual({
status: 'registered', path: join(binDir, 'dsh'), created: true,
})
expect(readlinkSync(join(binDir, 'dsh'))).toBe(join(harnessDir, 'dsh'))
})
it('is idempotent: confirms an existing link that already points at the launcher', () => {
const { harnessDir, binDir } = fixtures()
mkdirSync(harnessDir, { recursive: true })
mkdirSync(binDir, { recursive: true })
symlinkSync(join(harnessDir, 'dsh'), join(binDir, 'dsh'))
expect(registerDshLauncher(harnessDir, binDir)).toEqual({
status: 'registered', path: join(binDir, 'dsh'), created: false,
})
})
it('falls back to a user-writable dir when the primary is unwritable', () => {
const { harnessDir, binDir } = fixtures()
mkdirSync(harnessDir, { recursive: true })
writeFileSync(join(harnessDir, 'dsh'), '#!/bin/sh\n')
// A file occupies the primary's parent path, so creating the link there
// fails (ENOTDIR) and the fallback dir is used instead — mirroring a
// root-owned, non-writable /usr/local/bin.
mkdirSync(binDir, { recursive: true })
const fileBlocker = join(binDir, 'blocker')
writeFileSync(fileBlocker, '')
const result = registerDshLauncher(harnessDir, join(fileBlocker, 'nested'), join(binDir, 'fallback'), false)
if (result.status !== 'registered') {
throw new Error(`expected a registered link, got ${JSON.stringify(result)}`)
}
expect(result.created).toBe(true)
expect(readlinkSync(join(binDir, 'fallback', 'dsh'))).toBe(join(harnessDir, 'dsh'))
})
it('leaves an existing non-dsh entry untouched', () => {
const { harnessDir, binDir } = fixtures()
mkdirSync(harnessDir, { recursive: true })
mkdirSync(binDir, { recursive: true })
writeFileSync(join(binDir, 'dsh'), 'a real file, not ours\n')
expect(registerDshLauncher(harnessDir, binDir)).toEqual({
status: 'already-present', path: join(binDir, 'dsh'),
})
// The file is not replaced by a symlink and its content is untouched.
expect(lstatSync(join(binDir, 'dsh')).isSymbolicLink()).toBe(false)
expect(readFileSync(join(binDir, 'dsh'), 'utf8')).toBe('a real file, not ours\n')
})
it('leaves a symlink pointing elsewhere untouched', () => {
const { harnessDir, binDir } = fixtures()
mkdirSync(harnessDir, { recursive: true })
mkdirSync(binDir, { recursive: true })
symlinkSync('/elsewhere/dsh', join(binDir, 'dsh'))
expect(registerDshLauncher(harnessDir, binDir)).toEqual({
status: 'already-present', path: join(binDir, 'dsh'),
})
expect(readlinkSync(join(binDir, 'dsh'))).toBe('/elsewhere/dsh')
})
it('skips when the harness has no launcher (a development checkout)', () => {
const { harnessDir, binDir } = fixtures()
mkdirSync(harnessDir, { recursive: true })
expect(registerDshLauncher(harnessDir, binDir)).toEqual({ status: 'skipped' })
})
})
describe('buildDshCmd', () => {
it('runs the bundled node against the CLI entry with the caller arguments', () => {
expect(buildDshCmd()).toBe('@echo off\r\n"%~dp0bin\\node.exe" "%~dp0apps\\cli\\lib\\bin.js" %*\r\n')
})
})
describe('appendToUserPath', () => {
it('appends a missing directory', () => {
expect(appendToUserPath('C:\\a;C:\\b', 'C:\\harness'))
.toBe('C:\\a;C:\\b;C:\\harness')
})
it('is case-insensitive and idempotent for a present directory', () => {
expect(appendToUserPath('C:\\a;C:\\Harness', 'c:\\harness')).toBe('C:\\a;C:\\Harness')
})
it('appends to a trailing semicolon without doubling it', () => {
expect(appendToUserPath('C:\\a;', 'C:\\harness')).toBe('C:\\a;C:\\harness')
})
it('appends to an empty PATH', () => {
expect(appendToUserPath('', 'C:\\harness')).toBe(';C:\\harness')
})
})