|
|
@@ -1,11 +1,9 @@
|
|
|
/** Persistent manager behavior through a real profile Include and Loader. */
|
|
|
-import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
|
+import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
|
+import { realpath } from 'node:fs/promises'
|
|
|
import { join } from 'node:path'
|
|
|
import { tmpdir } from 'node:os'
|
|
|
import type { Context } from '@deepseek-ai/cordis'
|
|
|
-import type AgentRegistry from '@deepseek-ai/dsh-agent'
|
|
|
-import { Session, SessionId } from '@deepseek-ai/dsh-session'
|
|
|
-import type { UserMessage } from '@deepseek-ai/dsh-llm'
|
|
|
import { expect, it, onTestFinished, vi } from 'vitest'
|
|
|
import {
|
|
|
boot, composeEntries, initProfile, readProfilePatches, readProfileManifest, reconcileProfilePatches,
|
|
|
@@ -17,9 +15,11 @@ import Timer from '@deepseek-ai/cordis-plugin-timer'
|
|
|
import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
|
|
|
import { Group } from '@deepseek-ai/cordis-plugin-loader'
|
|
|
import * as operations from '../src/operations.ts'
|
|
|
+import { parse, parseDocument } from 'yaml'
|
|
|
|
|
|
async function fixture(reload: 'live' | 'startup' = 'live', overlay = false, prepare?: (ctx: Context) => void, config: Config = {}) {
|
|
|
- const home = mkdtempSync(join(tmpdir(), 'plugin-manager-'))
|
|
|
+ // pnpm resolves workspace roots through native realpath, including Windows 8.3 aliases.
|
|
|
+ const home = await realpath(mkdtempSync(join(tmpdir(), 'plugin-manager-')))
|
|
|
const dir = join(home, 'profiles', 'test')
|
|
|
const anchor = join(home, 'package.json')
|
|
|
writeFileSync(anchor, '{"name":"installation","dependencies":{}}\n')
|
|
|
@@ -139,6 +139,75 @@ it('installs only valid bundle declarations and honors installation without acti
|
|
|
expect((await manager.listBundles()).find(row => row.name === 'another-bundle')?.enabled).toBe(true)
|
|
|
})
|
|
|
|
|
|
+it('cleans a blocked installation and retries only after explicit profile build approval', async () => {
|
|
|
+ const { manager, dir, bundle } = await fixture()
|
|
|
+ const policy = join(dir, 'pnpm-workspace.yaml')
|
|
|
+ const run = vi.spyOn(operations, 'runProfilePnpm').mockImplementation(async (_context, args) => {
|
|
|
+ const manifest = readProfileManifest('test', dir)
|
|
|
+ const completion = { exitCode: 0, output: '', truncated: false, logPath: join(dir, 'pnpm.log') }
|
|
|
+ if (args[0] === 'remove') {
|
|
|
+ delete manifest.dependencies?.addon
|
|
|
+ } else {
|
|
|
+ manifest.dependencies = { ...manifest.dependencies, addon: '1.0.0' }
|
|
|
+ if (run.mock.calls.length === 1) {
|
|
|
+ writeFileSync(policy, 'allowBuilds:\n native: set this to true or false\n denied: false\n')
|
|
|
+ completion.exitCode = 1
|
|
|
+ completion.output = 'ERR_PNPM_IGNORED_BUILDS'
|
|
|
+ } else {
|
|
|
+ expect(parse(readFileSync(policy, 'utf8'))).toEqual({ allowBuilds: { native: true, denied: false } })
|
|
|
+ bundle('addon', [])
|
|
|
+ }
|
|
|
+ }
|
|
|
+ writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest))
|
|
|
+ return completion
|
|
|
+ })
|
|
|
+ onTestFinished(() => { run.mockRestore() })
|
|
|
+ expect(await manager.installBundle('addon')).toMatchObject({
|
|
|
+ application: 'failed', pendingBuilds: ['native'], cleanup: { name: 'addon' },
|
|
|
+ })
|
|
|
+ expect(readProfileManifest('test', dir).dependencies).not.toHaveProperty('addon')
|
|
|
+ expect(parse(readFileSync(policy, 'utf8'))).toMatchObject({ allowBuilds: { native: 'set this to true or false' } })
|
|
|
+ expect(await manager.installBundle('addon', { approvedBuilds: ['denied'] })).toMatchObject({ application: 'failed', changed: false, error: { code: 'stale-approval' } })
|
|
|
+ expect(run).toHaveBeenCalledTimes(2)
|
|
|
+ expect(await manager.installBundle('addon', { approvedBuilds: ['native'], enabled: false })).toMatchObject({ application: 'applied', changed: true })
|
|
|
+ expect(readProfileManifest('test', dir).dsh?.profile?.bundles).not.toContain('addon')
|
|
|
+})
|
|
|
+
|
|
|
+it('retains approved policy and reports it as changed when the registry fails before adding a dependency', async () => {
|
|
|
+ const { manager, dir } = await fixture()
|
|
|
+ writeFileSync(join(dir, 'pnpm-workspace.yaml'), 'allowBuilds:\n native: set this to true or false\n')
|
|
|
+ const run = vi.spyOn(operations, 'runProfilePnpm').mockResolvedValue({ exitCode: 1, output: 'registry unavailable', truncated: false, logPath: '/log' })
|
|
|
+ onTestFinished(() => { run.mockRestore() })
|
|
|
+ expect(await manager.installBundle('addon', { approvedBuilds: ['native'] })).toMatchObject({
|
|
|
+ changed: true, application: 'failed', pendingBuilds: [], approvedBuilds: ['native'], error: { diagnostic: 'registry unavailable' },
|
|
|
+ })
|
|
|
+ expect(parse(readFileSync(join(dir, 'pnpm-workspace.yaml'), 'utf8'))).toEqual({ allowBuilds: { native: true } })
|
|
|
+})
|
|
|
+
|
|
|
+it('runs a real pnpm dependency script only after approval and cleanup retry', async () => {
|
|
|
+ const { manager, dir, profile } = await fixture('startup')
|
|
|
+ const addon = join(profile.cwd, 'addon')
|
|
|
+ mkdirSync(addon)
|
|
|
+ writeFileSync(join(addon, 'package.json'), JSON.stringify({ name: 'approval-fixture-addon', version: '1.0.0',
|
|
|
+ scripts: { install: 'node build.cjs' }, dsh: { bundle: { patch: './cordis.patch.yml' } } }))
|
|
|
+ writeFileSync(join(addon, 'build.cjs'), 'require("node:fs").writeFileSync("built.txt", "built")\n')
|
|
|
+ writeFileSync(join(addon, 'cordis.patch.yml'), '[]\n')
|
|
|
+ writeFileSync(join(dir, 'package.json'), '{"name":"approval-fixture","private":true}\n')
|
|
|
+ const policy = parseDocument(readFileSync(join(dir, 'pnpm-workspace.yaml'), 'utf8'))
|
|
|
+ policy.set('offline', true)
|
|
|
+ policy.set('storeDir', join(profile.cwd, 'store'))
|
|
|
+ writeFileSync(join(dir, 'pnpm-workspace.yaml'), String(policy))
|
|
|
+ const blocked = await manager.installBundle('file:./addon', { enabled: false })
|
|
|
+ expect(blocked, JSON.stringify(blocked)).toMatchObject({ application: 'failed', cleanup: { name: 'approval-fixture-addon' } })
|
|
|
+ expect(blocked.pendingBuilds).toHaveLength(1)
|
|
|
+ const built = join(dir, 'node_modules', 'approval-fixture-addon', 'built.txt')
|
|
|
+ expect(existsSync(built)).toBe(false)
|
|
|
+ expect(readProfileManifest('test', dir).dependencies?.['approval-fixture-addon']).toBeUndefined()
|
|
|
+ const allowed = await manager.installBundle('file:./addon', { enabled: false, approvedBuilds: blocked.pendingBuilds! })
|
|
|
+ expect(allowed, JSON.stringify(allowed)).toMatchObject({ application: 'restart-required', packageResult: { exitCode: 0 } })
|
|
|
+ expect(readFileSync(built, 'utf8')).toBe('built')
|
|
|
+})
|
|
|
+
|
|
|
it('unloads before removing packages and retries inactive dependencies whose files are missing', async () => {
|
|
|
const { manager, dir, ctx } = await fixture()
|
|
|
const remove = vi.spyOn(operations, 'runProfilePnpm').mockImplementation(async () => {
|
|
|
@@ -188,29 +257,6 @@ it('keeps saved changes after activation failure and allows a corrected configur
|
|
|
})
|
|
|
|
|
|
|
|
|
-it('combines concurrent changes into durable notices without waking Agents', async () => {
|
|
|
- const session = Session.create(SessionId('manager-notices'))
|
|
|
- const wake = vi.fn()
|
|
|
- const notices: UserMessage[] = []
|
|
|
- const liveAgents = [{
|
|
|
- inject(message: UserMessage) {
|
|
|
- notices.push(message)
|
|
|
- session.append('user/message', message, { surfaceOp: 'append' })
|
|
|
- }, followup: wake, steer: wake,
|
|
|
- }, { inject() { throw new Error('already disposed') } }]
|
|
|
- const agents = { list: () => liveAgents }
|
|
|
- const { manager } = await fixture('live', false, (ctx) => { ctx.provide('agents', agents as unknown as AgentRegistry) })
|
|
|
- const id = (await manager.listPlugins()).find(row => row.patchId === 'managed')!.entryId
|
|
|
- await Promise.all([manager.setPluginEnabled(id, false), manager.setPluginEnabled(id, true)])
|
|
|
- expect(notices).toHaveLength(1)
|
|
|
- const noticeText = notices[0]?.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('')
|
|
|
- expect(noticeText).toContain('"enabled":false')
|
|
|
- expect(noticeText).toContain('"enabled":true')
|
|
|
- expect(wake).not.toHaveBeenCalled()
|
|
|
- expect(session.snapshotEvents().filter(row => row.type === 'user/message')).toHaveLength(1)
|
|
|
-})
|
|
|
-
|
|
|
-
|
|
|
it('reports plain dependencies, missing versions and invalid selected bundles distinctly', async () => {
|
|
|
const { manager, dir, profile } = await fixture()
|
|
|
writeFileSync(profile.installAnchor, '{}')
|
|
|
@@ -396,15 +442,6 @@ it('handles missing patch files and retains non-Error package diagnostics', asyn
|
|
|
await expect(manager.setPluginEnabled(id, true)).rejects.toThrow()
|
|
|
})
|
|
|
|
|
|
-it('bounds batched notices and discloses omitted operation results', async () => {
|
|
|
- const messages: UserMessage[] = []
|
|
|
- const { manager } = await fixture('live', false, (ctx) => {
|
|
|
- ctx.provide('agents', { list: () => [{ inject: (message: UserMessage) => { messages.push(message) } }] } as unknown as AgentRegistry)
|
|
|
- }, { outputBytes: 1, notificationDelayMs: 0 })
|
|
|
- await manager.setBundleEnabled('extra', false)
|
|
|
- expect(messages[0]?.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('')).toContain('"omitted":1')
|
|
|
-})
|
|
|
-
|
|
|
it('applies a manager change through the active HMR service', async () => {
|
|
|
const { manager } = await fixture()
|
|
|
const id = (await manager.listPlugins()).find(row => row.patchId === 'managed')!.entryId
|
|
|
@@ -513,3 +550,11 @@ it('applies watched configuration while pnpm installation is still running', asy
|
|
|
expect(readProfileManifest('test', dir).dsh?.profile?.bundles).toEqual(['core', 'extra', 'new-bundle'])
|
|
|
expect(ctx.get('managedProbe')).toBeUndefined()
|
|
|
})
|
|
|
+
|
|
|
+it.each(['[', 'allowBuilds: false\n'])('preserves pnpm diagnostics when pending approvals cannot be read: %s', async (policy) => {
|
|
|
+ const { manager, dir } = await fixture()
|
|
|
+ writeFileSync(join(dir, 'pnpm-workspace.yaml'), policy)
|
|
|
+ const run = vi.spyOn(operations, 'runProfilePnpm').mockResolvedValue({ exitCode: 1, output: 'original pnpm failure', truncated: false, logPath: '/log' })
|
|
|
+ onTestFinished(() => { run.mockRestore() })
|
|
|
+ expect(await manager.installBundle('addon')).toMatchObject({ application: 'failed', error: { diagnostic: 'original pnpm failure' } })
|
|
|
+})
|