Files
deepseek-harness/packages/document/office-kit/tests/engine-package.spec.ts
T
yudshj 50ba635949 feat(office): Office 操作改为常驻的原生 office 工具
- 新包 @deepseek-ai/dsh-tool-office 提供常驻的 office 工具(render / convert / recalculate),由 office-kit 在 Host 中直接调用 Kit 的 JS API;标准模式与 PTC 通过同一个工具调用
- render 的页面图片直接进入工具结果(有数量、像素与字节上限);convert / recalculate 的结果写入 office-kit 按 Session 划分的私有输出目录,由模型用 shell 复制到工作区,fs 与 Host 代码不写用户工作区
- 删除为 shell 执行 Kit CLI 而存在的代码:skill-office 的 node / cli / kitRoot、resolveKitCli、DSH_BUNDLED_OFFICE_CLI / DSH_BUNDLED_OFFICE_NODE / DSH_BUNDLED_RUNTIME_ROOT 与 Electron 下的独立 Node 检查;打包 smoke 改为经 office-kit 真实渲染、转换与重算
- office 由 web-app 的 standard / ptc 预设与 sdk-app 挂载,minimal 不挂载;操作技能正文与目录描述指向 office 工具
- 新增 Agent Note,部分取代 progressive-office-skills 的 shell 执行决定;同步 README、子系统页、catalog 与升级指南(含已保存预设的迁移步骤)

本 PR 的 snapshot 统一在这个提交里重新生成,前 4 个提交中的 snapshot 不一定和那一步的代码对应。
2026-11-04 18:17:58 +08:00

135 lines
6.9 KiB
TypeScript

/** Engine resolution distinguishes missing installations from incomplete ones and preserves ASAR asset paths. */
import { lstatSync, mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import { pathToFileURL } from 'node:url'
import { tmpdir } from 'node:os'
import { basename, dirname, join } from 'node:path'
import { afterEach, expect, it, onTestFinished, vi } from 'vitest'
import { createEnginePackageResolver } from '../src/locate.ts'
vi.mock('node:module', async (original) => {
const actual = await original<typeof import('node:module')>()
return { createRequire: vi.fn(actual.createRequire) }
})
vi.mock('node:fs', async (original) => {
const actual = await original<typeof import('node:fs')>()
return { ...actual, lstatSync: vi.fn(actual.lstatSync) }
})
const { createRequire: nodeCreateRequire } = await vi.importActual<typeof import('node:module')>('node:module')
afterEach(() => { vi.resetAllMocks() })
function engineTree() {
const root = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-engine-package-')))
onTestFinished(() => { rmSync(root, { recursive: true, force: true, maxRetries: 3, retryDelay: 10 }) })
const kitDirectory = join(root, 'node_modules', '@deepseek-ai', 'libreoffice-kit')
const kitSource = join(kitDirectory, 'lib', 'index.js')
mkdirSync(dirname(kitSource), { recursive: true })
writeFileSync(join(kitDirectory, 'package.json'), JSON.stringify({ main: 'lib/index.js' }))
writeFileSync(kitSource, '')
const host = nodeCreateRequire(join(root, 'caller.cjs'))
const kitEntry = host.resolve('@deepseek-ai/libreoffice-kit')
const kit = nodeCreateRequire(kitEntry)
const resolve = vi.fn(kit.resolve)
kit.resolve = Object.assign(resolve, { paths: kit.resolve.paths.bind(kit.resolve) })
vi.mocked(createRequire).mockImplementation((from) => {
if (from === kitEntry || from === pathToFileURL(kitEntry).href) return kit
return nodeCreateRequire(from)
})
// A private name prevents globally installed packages from satisfying an absent fixture.
const name = `@deepseek-ai/libreoffice-kit-${basename(root)}`
const directory = join(kitDirectory, 'node_modules', name)
return { root, name, directory, resolve, kit, kitEntry, resolveEnginePackage: createEnginePackageResolver(kitEntry) }
}
function thrownBy(action: () => void): unknown {
try { action() } catch (error) { return error }
throw new Error('Expected the operation to throw')
}
it.each([
['/App.app/Contents/Resources/app.asar/dsh/node_modules/@deepseek-ai/libreoffice-kit-darwin-arm64/package.json',
'/App.app/Contents/Resources/app.asar.unpacked/dsh/node_modules/@deepseek-ai/libreoffice-kit-darwin-arm64/package.json'],
['C:\\DSH\\resources\\app.asar\\dsh\\node_modules\\@deepseek-ai\\libreoffice-kit-win32-x64\\package.json',
'C:\\DSH\\resources\\app.asar.unpacked\\dsh\\node_modules\\@deepseek-ai\\libreoffice-kit-win32-x64\\package.json'],
['/App/resources/app.asar.unpacked/dsh/node_modules/x/package.json', '/App/resources/app.asar.unpacked/dsh/node_modules/x/package.json'],
['/workspace/node_modules/x/package.json', '/workspace/node_modules/x/package.json'],
['/workspace/notes.asar.txt/package.json', '/workspace/notes.asar.txt/package.json'],
['/app.asar/nested.asar/package.json', '/app.asar.unpacked/nested.asar/package.json'],
])('maps %s', (path, expected) => {
const fixture = engineTree()
fixture.resolve.mockReturnValue(path)
expect(fixture.resolveEnginePackage(fixture.name)).toBe(expected)
})
it('uses physical native paths and preserves the WASM logical path in an archive', () => {
const root = '/App/resources/app.asar/dsh/node_modules'
const kitEntry = `${root}/@deepseek-ai/libreoffice-kit/lib/index.js`
const kit = nodeCreateRequire(import.meta.url)
kit.resolve = Object.assign((id: string) => `${root}/${id}`, { paths: kit.resolve.paths.bind(kit.resolve) })
vi.mocked(createRequire).mockImplementation((from) => {
if (from === kitEntry || from === pathToFileURL(kitEntry).href) return kit
return nodeCreateRequire(from)
})
const resolveEnginePackage = createEnginePackageResolver(kitEntry)
expect(resolveEnginePackage('@deepseek-ai/libreoffice-kit-darwin-arm64'))
.toBe('/App/resources/app.asar.unpacked/dsh/node_modules/@deepseek-ai/libreoffice-kit-darwin-arm64/package.json')
expect(resolveEnginePackage('@deepseek-ai/libreoffice-kit-wasm'))
.toBe('/App/resources/app.asar/dsh/node_modules/@deepseek-ai/libreoffice-kit-wasm/package.json')
})
it.each(['missing manifest', 'dangling symlink', 'absent'] as const)(
'distinguishes an engine package with %s on the kit resolution path', (state) => {
const fixture = engineTree()
if (state === 'missing manifest') mkdirSync(fixture.directory, { recursive: true })
if (state === 'dangling symlink') {
mkdirSync(dirname(fixture.directory), { recursive: true })
symlinkSync(join(fixture.root, 'missing-target'), fixture.directory, 'junction')
}
const error = thrownBy(() => fixture.resolveEnginePackage(fixture.name))
const nodeError: unknown = fixture.resolve.mock.results[0]?.value
expect(nodeError).toMatchObject({ code: 'MODULE_NOT_FOUND' })
if (state === 'absent') {
expect(error).toBe(nodeError)
} else {
expect(error).toBeInstanceOf(Error)
expect(error).not.toHaveProperty('code', 'MODULE_NOT_FOUND')
expect(error).toHaveProperty('cause')
if (!(error instanceof Error)) throw error
expect(error.cause).toBe(nodeError)
expect(error.message).toContain(fixture.name)
}
},
)
it('keeps a Node resolution error other than MODULE_NOT_FOUND unchanged', () => {
const fixture = engineTree()
mkdirSync(fixture.directory, { recursive: true })
writeFileSync(join(fixture.directory, 'package.json'), '{invalid')
const error = thrownBy(() => fixture.resolveEnginePackage(fixture.name))
expect(error).toMatchObject({ code: 'ERR_INVALID_PACKAGE_CONFIG' })
expect(error).toBe(fixture.resolve.mock.results[0]?.value)
expect(lstatSync).not.toHaveBeenCalled()
})
it('preserves Node missing errors when the name has no package search paths', () => {
const fixture = engineTree()
const error = thrownBy(() => fixture.resolveEnginePackage('node:fs'))
expect(error).toMatchObject({ code: 'MODULE_NOT_FOUND' })
expect(error).toBe(fixture.resolve.mock.results[0]?.value)
expect(lstatSync).not.toHaveBeenCalled()
})
it('propagates permission errors while checking the kit installation', () => {
const fixture = engineTree()
const denied = Object.assign(new Error('Cannot inspect engine installation'), { code: 'EACCES' })
const paths = fixture.kit.resolve.paths(fixture.name)
if (!paths?.[0]) throw new Error('Fixture package must have Node resolution paths')
const firstDirectory = join(paths[0], fixture.name)
vi.mocked(lstatSync).mockImplementationOnce((path) => {
expect(path).toBe(firstDirectory)
throw denied
})
expect(thrownBy(() => fixture.resolveEnginePackage(fixture.name))).toBe(denied)
})