Files
deepseek-harness/apps/desktop/tests/flock-entry.spec.ts
T
winewill 0126d5be20 fix(desktop): load the flock entry lazily and build native/system on demand
`package-target.ts` imported `macos-notarization-proxy.ts`, whose top-level
import of `@deepseek-ai/node-addon-system/flock` resolves through the package
`exports` to the unbuilt `lib/flock.js` on a clean checkout. The step that
emits that file runs inside the same packaging script, so `check:package`,
`package:*`, and the `restore:mac-proxy` recovery command all failed with
`ERR_MODULE_NOT_FOUND` before reaching it; running `pnpm run typecheck` first
left the host addon binary missing instead, which failed at lock time.

The proxy lock now loads the entry at first use through `flock-entry.ts`. When
the import is unresolvable or the host platform package lacks a binary listed
in its `prebuilds.json`, the loader runs `pnpm run build:native-system` and
`pnpm --dir native/system run build:ts` before importing again; other errors
and build failures propagate, and a failed load is retried by the next call.
`check:package` no longer touches the entry at all.

Tests cover the loader steps and load the proxy module from a directory where
the addon package cannot be resolved. The Desktop README records the
behavior in both languages.

(cherry picked from commit 2d74cf50310d4efbbccbbe79a105fcc3a468b19d)
2026-09-23 14:03:10 +08:00

115 lines
4.7 KiB
TypeScript

import { spawn } from 'node:child_process'
import { once } from 'node:events'
import { copyFile, mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { expect, it, vi } from 'vitest'
import { createFlockEntryLoader } from '../scripts/flock-entry.ts'
type Entry = Awaited<ReturnType<ReturnType<typeof createFlockEntryLoader>>>
const entry = { tryLockExclusive: vi.fn() } as Entry
const BUILD_TEXT = 'pnpm run build:native-system && pnpm --dir native/system run build:ts'
function notFound(): NodeJS.ErrnoException {
return Object.assign(new Error("Cannot find module 'lib/flock.js'"), { code: 'ERR_MODULE_NOT_FOUND' })
}
function steps(overrides: Partial<Parameters<typeof createFlockEntryLoader>[0]> = {}) {
return {
importEntry: vi.fn(overrides.importEntry ?? (async () => entry)),
hostAddonBuilt: vi.fn(overrides.hostAddonBuilt ?? (() => true)),
build: vi.fn(overrides.build ?? (async () => {})),
}
}
function quietStderr(): () => void {
const spy = vi.spyOn(process.stderr, 'write').mockReturnValue(true)
return () => { spy.mockRestore() }
}
it('returns the imported entry without building and memoizes it', async () => {
const used = steps()
const load = createFlockEntryLoader(used)
expect(await load()).toBe(entry)
expect(await load()).toBe(entry)
expect(used.importEntry).toHaveBeenCalledTimes(1)
expect(used.hostAddonBuilt).toHaveBeenCalledTimes(1)
expect(used.build).not.toHaveBeenCalled()
})
it('builds once and imports again when the JavaScript entry is missing', async () => {
const order: string[] = []
const used = steps({
importEntry: async () => {
order.push('import')
if (order.length === 1) throw notFound()
return entry
},
build: async () => { order.push('build') },
})
const written: string[] = []
const stderr = vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => { written.push(String(chunk)); return true })
try {
const load = createFlockEntryLoader(used)
expect(await load()).toBe(entry)
expect(await load()).toBe(entry)
} finally { stderr.mockRestore() }
expect(order).toEqual(['import', 'build', 'import'])
expect(written).toEqual([expect.stringContaining(BUILD_TEXT)])
})
it('builds when the entry imports but the host addon binary is missing', async () => {
const used = steps({ hostAddonBuilt: () => false })
const restore = quietStderr()
try {
expect(await createFlockEntryLoader(used)()).toBe(entry)
} finally { restore() }
expect(used.build).toHaveBeenCalledTimes(1)
expect(used.importEntry).toHaveBeenCalledTimes(2)
})
it('propagates other import errors without building', async () => {
const used = steps({ importEntry: async () => { throw new Error('addon binding failed') } })
await expect(createFlockEntryLoader(used)()).rejects.toThrow('addon binding failed')
expect(used.build).not.toHaveBeenCalled()
})
it('propagates a build failure, does not import again, and retries on the next call', async () => {
const used = steps({
importEntry: async () => { throw notFound() },
build: async () => { throw new Error('tsc exited with 2') },
})
const restore = quietStderr()
try {
const load = createFlockEntryLoader(used)
await expect(load()).rejects.toThrow('tsc exited with 2')
expect(used.importEntry).toHaveBeenCalledTimes(1)
used.importEntry.mockResolvedValueOnce(entry)
expect(await load()).toBe(entry)
} finally { restore() }
expect(used.build).toHaveBeenCalledTimes(1)
})
it('loads the proxy module where the addon package cannot be resolved', async () => {
// A directory outside every node_modules tree stands in for a checkout whose native/system is unbuilt.
const root = await mkdtemp(join(tmpdir(), 'flock-entry-test-'))
try {
for (const name of ['macos-notarization-proxy.ts', 'flock-entry.ts']) {
await copyFile(new URL(`../scripts/${name}`, import.meta.url), join(root, name))
}
const child = spawn(process.execPath, ['--input-type=module', '-e', `
const module = await import(${JSON.stringify(pathToFileURL(join(root, 'macos-notarization-proxy.ts')).href)});
process.stdout.write(Object.keys(module).sort().join(','));
`], { stdio: ['ignore', 'pipe', 'pipe'], env: { PATH: process.env.PATH } })
let stdout = ''
let stderr = ''
child.stdout.setEncoding('utf8').on('data', (chunk: string) => { stdout += chunk })
child.stderr.setEncoding('utf8').on('data', (chunk: string) => { stderr = (stderr + chunk).slice(-8192) })
const [code] = await once(child, 'close') as [number | null]
expect(code, stderr).toBe(0)
expect(stdout).toBe('restoreMacOSNotarizationProxy,withMacOSNotarizationProxy')
} finally { await rm(root, { recursive: true, force: true }) }
})