refactor(desktop): drop link-era profile cleanup
Remove cleanProfileCorePackages and migrateDesktopProfileLinks with their specs; applyRelease keeps runtime validation, settings migration, and profile creation. A regression pins that installed packages, declarations, legacy state, and the lockfile survive a launch.
This commit is contained in:
1 parent
a311386983
commit
938b62c0aa
7 files changed
+30
-388
No files matched your search
@@ -326,7 +326,7 @@ async function main(): Promise<void> {
|
||||
startup ??= (async () => {
|
||||
await navigateMain(applicationUrl)
|
||||
await backend.start(async () => {
|
||||
await manager.applyRelease(app.isPackaged)
|
||||
await manager.applyRelease()
|
||||
})
|
||||
if (backend.host !== undefined) updateJournal?.action('workspace-ready')
|
||||
// The existing Web document resumes through the boot IPC response.
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
/** Remove application-owned packages left in external plugin profiles before production boot. */
|
||||
|
||||
import { existsSync, lstatSync, readFileSync, rmSync, unlinkSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { dump, load } from 'js-yaml'
|
||||
import { DESKTOP_PACKAGE_SET_FILE, readDesktopCorePackageSet } from './core-package-set.ts'
|
||||
|
||||
/** Enables the temporary production-profile cleanup; remove this module and its caller together. */
|
||||
const CLEAN_PROFILE_CORE_PACKAGES = true
|
||||
|
||||
function object(value: unknown): Record<string, unknown> {
|
||||
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
||||
throw new Error('desktop profile cleanup: expected an object')
|
||||
}
|
||||
return value as Record<string, unknown>
|
||||
}
|
||||
|
||||
function stat(path: string): ReturnType<typeof lstatSync> | undefined {
|
||||
try { return lstatSync(path) } catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
function remove(path: string): boolean {
|
||||
const entry = stat(path)
|
||||
if (entry === undefined) return false
|
||||
if (entry.isSymbolicLink()) unlinkSync(path)
|
||||
else rmSync(path, { recursive: entry.isDirectory() })
|
||||
return true
|
||||
}
|
||||
|
||||
function requireDirectory(path: string): void {
|
||||
const entry = stat(path)
|
||||
if (entry !== undefined && (!entry.isDirectory() || entry.isSymbolicLink())) {
|
||||
throw new Error(`desktop profile cleanup: package parent is not a real directory: ${path}`)
|
||||
}
|
||||
}
|
||||
|
||||
function prune(value: Record<string, unknown>, field: string, names: ReadonlySet<string>): boolean {
|
||||
if (value[field] === undefined) return false
|
||||
const entries = object(value[field])
|
||||
let changed = false
|
||||
for (const name of names) {
|
||||
if (!Object.hasOwn(entries, name)) continue
|
||||
Reflect.deleteProperty(entries, name)
|
||||
changed = true
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete current and recorded Desktop core packages, their declarations, and stale lockfile resolutions.
|
||||
* Managed links are unlinked without touching their targets; unrelated plugins and profile configuration survive.
|
||||
* The caller holds the profile lock and must not have started the Host. Development launches do nothing.
|
||||
* @param profile - Absolute Desktop profile directory.
|
||||
* @param packageNames - Package names from the verified application runtime descriptor.
|
||||
* @param production - Whether this launch uses the packaged application.
|
||||
*/
|
||||
export function cleanProfileCorePackages(profile: string, packageNames: readonly string[], production: boolean): void {
|
||||
// oxlint-disable-next-line typescript/no-unnecessary-condition -- release cleanup has an explicit source switch.
|
||||
if (!CLEAN_PROFILE_CORE_PACKAGES || !production || !existsSync(profile)) return
|
||||
const names = new Set(packageNames)
|
||||
const recordPath = join(profile, DESKTOP_PACKAGE_SET_FILE)
|
||||
if (existsSync(recordPath)) {
|
||||
for (const entry of readDesktopCorePackageSet(profile).packages) names.add(entry.name)
|
||||
}
|
||||
const roots = [join(profile, 'node_modules')]
|
||||
for (const root of roots) {
|
||||
requireDirectory(root)
|
||||
for (const name of names) requireDirectory(dirname(join(root, name)))
|
||||
}
|
||||
const manifestPath = join(profile, 'package.json')
|
||||
const manifest = existsSync(manifestPath) ? object(JSON.parse(readFileSync(manifestPath, 'utf8'))) : undefined
|
||||
const workspacePath = join(profile, 'pnpm-workspace.yaml')
|
||||
const workspace = existsSync(workspacePath) ? object(load(readFileSync(workspacePath, 'utf8'))) : undefined
|
||||
let manifestChanged = false
|
||||
if (manifest !== undefined) {
|
||||
for (const field of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
|
||||
manifestChanged = prune(manifest, field, names) || manifestChanged
|
||||
}
|
||||
if (manifest.pnpm !== undefined) manifestChanged = prune(object(manifest.pnpm), 'overrides', names) || manifestChanged
|
||||
}
|
||||
const workspaceChanged = workspace !== undefined && prune(workspace, 'overrides', names)
|
||||
const packageResidue = roots.some(root => [...names].some(name => stat(join(root, name)) !== undefined))
|
||||
if (manifestChanged || workspaceChanged || packageResidue) remove(join(profile, 'pnpm-lock.yaml'))
|
||||
if (manifestChanged) writeFileSync(manifestPath, `${JSON.stringify(manifest, undefined, 2)}\n`)
|
||||
if (workspaceChanged) writeFileSync(workspacePath, dump(workspace))
|
||||
for (const root of roots) for (const name of names) remove(join(root, name))
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
/** One-time removal of package links recorded by Desktop profile management. */
|
||||
|
||||
import { lstatSync, readFileSync, readlinkSync, unlinkSync } from 'node:fs'
|
||||
import { dirname, isAbsolute, join, resolve } from 'node:path'
|
||||
|
||||
/** Legacy Desktop package-link ownership file. */
|
||||
export const DESKTOP_PROFILE_STATE = 'desktop-runtime-state.json'
|
||||
|
||||
interface DesktopPackageLink {
|
||||
readonly name: string
|
||||
readonly target: string
|
||||
}
|
||||
|
||||
const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._~-]*\/[a-z0-9][a-z0-9._~-]*|[a-z0-9][a-z0-9._~-]*)$/u
|
||||
|
||||
function record(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function stat(path: string): ReturnType<typeof lstatSync> | undefined {
|
||||
try { return lstatSync(path) } catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove recorded links still owned by Desktop and delete their legacy state file.
|
||||
* Installed directories, changed links, and links beneath redirected package directories are retained.
|
||||
* @param profile - Desktop profile directory.
|
||||
*/
|
||||
export function migrateDesktopProfileLinks(profile: string): void {
|
||||
const statePath = join(profile, DESKTOP_PROFILE_STATE)
|
||||
if (stat(statePath) === undefined) return
|
||||
const value: unknown = JSON.parse(readFileSync(statePath, 'utf8'))
|
||||
if (!record(value) || !Array.isArray(value.links)) throw new Error('desktop profile: invalid legacy package links')
|
||||
const links = value.links.map((link: unknown): DesktopPackageLink => {
|
||||
if (!record(link) || typeof link.name !== 'string' || !PACKAGE_NAME.test(link.name)
|
||||
|| typeof link.target !== 'string' || !isAbsolute(link.target)) {
|
||||
throw new Error('desktop profile: invalid legacy package link')
|
||||
}
|
||||
return { name: link.name, target: link.target }
|
||||
})
|
||||
const modules = join(profile, 'node_modules')
|
||||
if (stat(modules)?.isDirectory() === true) {
|
||||
for (const link of links) {
|
||||
const path = join(modules, link.name)
|
||||
if (dirname(path) !== modules && stat(dirname(path))?.isDirectory() !== true) continue
|
||||
if (stat(path)?.isSymbolicLink() === true
|
||||
&& resolve(dirname(path), readlinkSync(path)) === resolve(link.target)) {
|
||||
unlinkSync(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
unlinkSync(statePath)
|
||||
}
|
||||
@@ -25,8 +25,6 @@ import { readDesktopRuntime } from './runtime-tree.ts'
|
||||
import {
|
||||
initProfile, PROFILE_TEMPLATES, sanitizeProfile, type ProfileTemplate,
|
||||
} from '@deepseek-ai/dsh-app-boot'
|
||||
import { migrateDesktopProfileLinks } from './profile-packages.ts'
|
||||
import { cleanProfileCorePackages } from './profile-core-cleanup.ts'
|
||||
|
||||
const PROJECT_NAME = '@deepseek-ai/dsh-desktop-runtime'
|
||||
const DSH_PACKAGE = '@deepseek-ai/dsh'
|
||||
@@ -81,14 +79,11 @@ export class DesktopProjectManager {
|
||||
|
||||
/**
|
||||
* Load application metadata and prepare the external plugin profile without installing packages.
|
||||
* @param production - Remove application-owned profile packages before packaged Host startup.
|
||||
*/
|
||||
async applyRelease(production = false): Promise<void> {
|
||||
async applyRelease(): Promise<void> {
|
||||
await this.withLock(() => {
|
||||
const descriptor = readDesktopRuntime(this.runtime.dsh)
|
||||
cleanProfileCorePackages(this.paths.profile, descriptor.sharedPackages.map(entry => entry.name), production)
|
||||
readDesktopRuntime(this.runtime.dsh)
|
||||
migrateProfileSettings(this.paths.profile)
|
||||
migrateDesktopProfileLinks(this.paths.profile)
|
||||
createPluginProfile(this.paths.profile)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, expect, it } from 'vitest'
|
||||
import { load } from 'js-yaml'
|
||||
import { cleanProfileCorePackages } from '../src/profile-core-cleanup.ts'
|
||||
|
||||
const roots: string[] = []
|
||||
const core = '@deepseek-ai/dsh-web-app'
|
||||
const extra = '@deepseek-ai/optional-plugin'
|
||||
|
||||
function fixture(): string {
|
||||
const root = mkdtempSync(join(tmpdir(), 'desktop-core-cleanup-'))
|
||||
roots.push(root)
|
||||
mkdirSync(join(root, 'node_modules', core), { recursive: true })
|
||||
mkdirSync(join(root, 'node_modules', extra), { recursive: true })
|
||||
writeFileSync(join(root, 'package.json'), JSON.stringify({
|
||||
dependencies: { [core]: 'file:./desktop-packages/web.tgz', [extra]: '1.0.0' },
|
||||
optionalDependencies: { [core]: '1.0.0' },
|
||||
pnpm: { overrides: { [core]: '1.0.0', third: '2.0.0' } },
|
||||
dsh: { profile: { bundles: [core, extra] } },
|
||||
}))
|
||||
writeFileSync(join(root, 'pnpm-workspace.yaml'), `nodeLinker: hoisted\noverrides:\n '${core}': file:./desktop-packages/web.tgz\n third: 2.0.0\n`)
|
||||
writeFileSync(join(root, 'pnpm-lock.yaml'), 'old resolutions\n')
|
||||
writeFileSync(join(root, 'cordis.patch.yml'), '[]\n')
|
||||
return root
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('removes core packages and reinstall declarations while preserving optional plugins and configuration', () => {
|
||||
const root = fixture()
|
||||
cleanProfileCorePackages(root, [core], true)
|
||||
expect(existsSync(join(root, 'node_modules', core))).toBe(false)
|
||||
expect(existsSync(join(root, 'node_modules', extra))).toBe(true)
|
||||
expect(JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))).toEqual({
|
||||
dependencies: { [extra]: '1.0.0' }, optionalDependencies: {},
|
||||
pnpm: { overrides: { third: '2.0.0' } }, dsh: { profile: { bundles: [core, extra] } },
|
||||
})
|
||||
expect(load(readFileSync(join(root, 'pnpm-workspace.yaml'), 'utf8'))).toEqual({ nodeLinker: 'hoisted', overrides: { third: '2.0.0' } })
|
||||
expect(readFileSync(join(root, 'cordis.patch.yml'), 'utf8')).toBe('[]\n')
|
||||
expect(existsSync(join(root, 'pnpm-lock.yaml'))).toBe(false)
|
||||
writeFileSync(join(root, 'pnpm-lock.yaml'), 'new plugin resolutions\n')
|
||||
cleanProfileCorePackages(root, [core], true)
|
||||
expect(readFileSync(join(root, 'pnpm-lock.yaml'), 'utf8')).toBe('new plugin resolutions\n')
|
||||
})
|
||||
|
||||
it('does not clean development profiles', () => {
|
||||
const root = fixture()
|
||||
const before = readFileSync(join(root, 'package.json'), 'utf8')
|
||||
cleanProfileCorePackages(root, [core], false)
|
||||
expect(existsSync(join(root, 'node_modules', core))).toBe(true)
|
||||
expect(readFileSync(join(root, 'package.json'), 'utf8')).toBe(before)
|
||||
expect(existsSync(join(root, 'pnpm-lock.yaml'))).toBe(true)
|
||||
})
|
||||
|
||||
it('unlinks core packages without deleting their target, including dangling links', () => {
|
||||
const root = fixture()
|
||||
const target = join(root, 'development-package')
|
||||
mkdirSync(target)
|
||||
writeFileSync(join(target, 'sentinel'), 'keep')
|
||||
rmSync(join(root, 'node_modules', core), { recursive: true })
|
||||
symlinkSync(target, join(root, 'node_modules', core), 'junction')
|
||||
cleanProfileCorePackages(root, [core], true)
|
||||
expect(readFileSync(join(target, 'sentinel'), 'utf8')).toBe('keep')
|
||||
symlinkSync(target, join(root, 'node_modules', core), 'junction')
|
||||
rmSync(target, { recursive: true })
|
||||
cleanProfileCorePackages(root, [core], true)
|
||||
expect(() => lstatSync(join(root, 'node_modules', core))).toThrow()
|
||||
})
|
||||
|
||||
it('uses the old package inventory to remove retired core names', () => {
|
||||
const root = fixture()
|
||||
const names = ['@deepseek-ai/dsh', '@deepseek-ai/dsh-desktop-host', core].sort()
|
||||
writeFileSync(join(root, 'desktop-packages.json'), JSON.stringify({ schemaVersion: 1, packages: names.map((name, index) => ({
|
||||
name, version: '0.1.2', file: `${index}.tgz`, bytes: 1, integrity: 'sha512-YQ==',
|
||||
})) }))
|
||||
cleanProfileCorePackages(root, [], true)
|
||||
expect(existsSync(join(root, 'node_modules', core))).toBe(false)
|
||||
expect(existsSync(join(root, 'node_modules', extra))).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects invalid metadata before deleting packages or declarations', () => {
|
||||
const root = fixture()
|
||||
writeFileSync(join(root, 'desktop-packages.json'), '{"schemaVersion":1,"packages":[{"name":"../../outside"}]}')
|
||||
const before = readFileSync(join(root, 'package.json'), 'utf8')
|
||||
expect(() => { cleanProfileCorePackages(root, [core], true) }).toThrow('invalid package record')
|
||||
expect(readFileSync(join(root, 'package.json'), 'utf8')).toBe(before)
|
||||
expect(existsSync(join(root, 'node_modules', core))).toBe(true)
|
||||
})
|
||||
|
||||
it('refuses redirected package parents without deleting their contents', () => {
|
||||
const root = fixture()
|
||||
const target = join(root, 'external-scope')
|
||||
mkdirSync(join(target, 'dsh-web-app'), { recursive: true })
|
||||
rmSync(join(root, 'node_modules', '@deepseek-ai'), { recursive: true })
|
||||
symlinkSync(target, join(root, 'node_modules', '@deepseek-ai'), 'junction')
|
||||
expect(() => { cleanProfileCorePackages(root, [core], true) }).toThrow('not a real directory')
|
||||
expect(existsSync(join(target, 'dsh-web-app'))).toBe(true)
|
||||
})
|
||||
@@ -1,126 +0,0 @@
|
||||
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, rmdirSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { afterEach, expect, it } from 'vitest'
|
||||
import { DESKTOP_PROFILE_STATE, migrateDesktopProfileLinks } from '../src/profile-packages.ts'
|
||||
|
||||
const roots: string[] = []
|
||||
const symlinks: string[] = []
|
||||
|
||||
function fixture() {
|
||||
const root = mkdtempSync(join(tmpdir(), 'desktop-profile-migration-'))
|
||||
roots.push(root)
|
||||
const profile = join(root, 'profile')
|
||||
const target = join(root, 'runtime-package')
|
||||
mkdirSync(join(profile, 'node_modules'), { recursive: true })
|
||||
mkdirSync(target)
|
||||
return { root, profile, target }
|
||||
}
|
||||
|
||||
function link(target: string, path: string): void {
|
||||
mkdirSync(dirname(path), { recursive: true })
|
||||
symlinkSync(target, path, process.platform === 'win32' ? 'junction' : 'dir')
|
||||
symlinks.push(path)
|
||||
}
|
||||
|
||||
function writeState(profile: string, links: unknown): void {
|
||||
writeFileSync(join(profile, DESKTOP_PROFILE_STATE), JSON.stringify({
|
||||
schemaVersion: 'obsolete', runtimeId: null, version: 8, lockHash: false, links,
|
||||
}))
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const path of symlinks.splice(0).reverse()) {
|
||||
try {
|
||||
if (lstatSync(path).isSymbolicLink()) unlinkSync(path)
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
||||
}
|
||||
}
|
||||
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('removes recorded links and state without interpreting obsolete runtime fields', () => {
|
||||
const { profile, target } = fixture()
|
||||
const packagePath = join(profile, 'node_modules', '@deepseek-ai/cordis')
|
||||
link(target, packagePath)
|
||||
writeFileSync(join(target, 'package.json'), '{"name":"@deepseek-ai/cordis"}')
|
||||
writeState(profile, [{ name: '@deepseek-ai/cordis', target }])
|
||||
migrateDesktopProfileLinks(profile)
|
||||
expect(existsSync(packagePath)).toBe(false)
|
||||
expect(existsSync(join(profile, DESKTOP_PROFILE_STATE))).toBe(false)
|
||||
expect(readFileSync(join(target, 'package.json'), 'utf8')).toContain('@deepseek-ai/cordis')
|
||||
expect(() => { migrateDesktopProfileLinks(profile) }).not.toThrow()
|
||||
})
|
||||
|
||||
it('removes an owned broken link without following its target', () => {
|
||||
const { profile, target } = fixture()
|
||||
const packagePath = join(profile, 'node_modules', 'plugin')
|
||||
link(target, packagePath)
|
||||
writeState(profile, [{ name: 'plugin', target }])
|
||||
rmdirSync(target)
|
||||
migrateDesktopProfileLinks(profile)
|
||||
expect(() => lstatSync(packagePath)).toThrow()
|
||||
expect(existsSync(join(profile, DESKTOP_PROFILE_STATE))).toBe(false)
|
||||
})
|
||||
|
||||
it('preserves a pnpm-installed directory replacing a recorded link', () => {
|
||||
const { profile, target } = fixture()
|
||||
const packagePath = join(profile, 'node_modules', 'plugin')
|
||||
mkdirSync(packagePath)
|
||||
writeFileSync(join(packagePath, 'package.json'), '{"name":"plugin","version":"2.0.0"}')
|
||||
writeState(profile, [{ name: 'plugin', target }])
|
||||
migrateDesktopProfileLinks(profile)
|
||||
expect(readFileSync(join(packagePath, 'package.json'), 'utf8')).toContain('2.0.0')
|
||||
expect(existsSync(join(profile, DESKTOP_PROFILE_STATE))).toBe(false)
|
||||
})
|
||||
|
||||
it('preserves changed and unrecorded package links', () => {
|
||||
const { root, profile, target } = fixture()
|
||||
const replacement = join(root, 'user-package')
|
||||
mkdirSync(replacement)
|
||||
const changed = join(profile, 'node_modules', 'changed')
|
||||
const unrecorded = join(profile, 'node_modules', 'unrecorded')
|
||||
link(replacement, changed)
|
||||
link(target, unrecorded)
|
||||
const changedTarget = readlinkSync(changed)
|
||||
const unrecordedTarget = readlinkSync(unrecorded)
|
||||
writeState(profile, [{ name: 'changed', target }])
|
||||
migrateDesktopProfileLinks(profile)
|
||||
expect(readlinkSync(changed)).toBe(changedTarget)
|
||||
expect(readlinkSync(unrecorded)).toBe(unrecordedTarget)
|
||||
})
|
||||
|
||||
it('leaves an uninitialized profile untouched', () => {
|
||||
const { root } = fixture()
|
||||
const missingProfile = join(root, 'absent-profile')
|
||||
migrateDesktopProfileLinks(missingProfile)
|
||||
expect(existsSync(missingProfile)).toBe(false)
|
||||
})
|
||||
|
||||
it.each(['../../outside', '../outside', '@scope/../../outside', '..\\outside'])('rejects escaping package name %s before removing any links', (name) => {
|
||||
const { root, profile, target } = fixture()
|
||||
const packagePath = join(profile, 'node_modules', 'owned')
|
||||
const outside = join(root, 'outside')
|
||||
link(target, packagePath)
|
||||
link(target, outside)
|
||||
writeState(profile, [{ name: 'owned', target }, { name, target }])
|
||||
expect(() => { migrateDesktopProfileLinks(profile) }).toThrow('invalid legacy package link')
|
||||
expect(lstatSync(packagePath).isSymbolicLink()).toBe(true)
|
||||
expect(lstatSync(outside).isSymbolicLink()).toBe(true)
|
||||
expect(existsSync(join(profile, DESKTOP_PROFILE_STATE))).toBe(true)
|
||||
})
|
||||
|
||||
it.each(['node_modules', 'node_modules/@scope'])('preserves package links beneath redirected %s', (parent) => {
|
||||
const { root, profile, target } = fixture()
|
||||
const external = join(root, 'external')
|
||||
mkdirSync(external)
|
||||
const packagePath = join(external, 'plugin')
|
||||
link(target, packagePath)
|
||||
if (parent === 'node_modules') rmdirSync(join(profile, 'node_modules'))
|
||||
link(external, join(profile, parent))
|
||||
writeState(profile, [{ name: parent === 'node_modules' ? 'plugin' : '@scope/plugin', target }])
|
||||
migrateDesktopProfileLinks(profile)
|
||||
expect(lstatSync(packagePath).isSymbolicLink()).toBe(true)
|
||||
expect(existsSync(join(profile, DESKTOP_PROFILE_STATE))).toBe(false)
|
||||
})
|
||||
@@ -43,16 +43,37 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('desktop external plugin profile', () => {
|
||||
it('cleans application packages only when preparing a production launch', async () => {
|
||||
it('preserves installed packages, profile state, and the lockfile when preparing a launch', async () => {
|
||||
const { manager } = setup()
|
||||
await manager.applyRelease()
|
||||
seedPlugin(manager)
|
||||
const profile = manager.paths.profile
|
||||
const name = '@deepseek-ai/dsh-web-app'
|
||||
const path = join(manager.paths.profile, 'node_modules', name)
|
||||
const path = join(profile, 'node_modules', name)
|
||||
mkdirSync(path, { recursive: true })
|
||||
await manager.applyRelease()
|
||||
expect(existsSync(path)).toBe(true)
|
||||
await manager.applyRelease(true)
|
||||
expect(existsSync(path)).toBe(false)
|
||||
writeFileSync(join(path, 'package.json'), JSON.stringify({ name, version: '1.0.0' }))
|
||||
const manifestPath = join(profile, 'package.json')
|
||||
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as { dependencies: Record<string, string> }
|
||||
manifest.dependencies[name] = '1.0.0'
|
||||
writeFileSync(manifestPath, JSON.stringify(manifest))
|
||||
writeFileSync(join(profile, 'desktop-runtime-state.json'), JSON.stringify({ links: [] }))
|
||||
writeFileSync(join(profile, 'pnpm-lock.yaml'), 'lockfileVersion: 9.0\n')
|
||||
const files = [
|
||||
'package.json',
|
||||
'pnpm-workspace.yaml',
|
||||
'desktop-runtime-state.json',
|
||||
'pnpm-lock.yaml',
|
||||
`node_modules/${name}/package.json`,
|
||||
'node_modules/plugin/package.json',
|
||||
'node_modules/plugin/bundle.yml',
|
||||
]
|
||||
const before = files.map(file => readFileSync(join(profile, file), 'utf8'))
|
||||
|
||||
await expect(manager.applyRelease()).resolves.toBeUndefined()
|
||||
|
||||
expect(files.map(file => readFileSync(join(profile, file), 'utf8'))).toEqual(before)
|
||||
expect(lstatSync(path).isDirectory()).toBe(true)
|
||||
expect(lstatSync(join(profile, 'node_modules/plugin')).isDirectory()).toBe(true)
|
||||
})
|
||||
it('reuses plugin files without scanning manifests and can disable them', async () => {
|
||||
const { manager } = setup()
|
||||
|
||||
Reference in new issue
Block a user