Files
goodbuddy/src/main/agent/dsh-extension-marketplace.test.ts
T
mesalogo cbbe30896e feat: expand multi-runtime workflows for 0.10.0
GoodBuddy previously exposed Runtime capabilities, MCP assignments, plugin controls, context compaction, usage reporting, and task-completion behavior through incomplete or inconsistent paths. This release unifies managed OpenCode, Continue, and DeepSeek Harness controls; adds DSH plugin and image workflows; strengthens MCP and Runtime lifecycle bounds; fixes Windows notification activation; validates cross-architecture packages; and presents the approved bilingual four-section release notes.

The DSH plugin marketplace remains a default-off preview whose trusted third-party code runs with current-user permissions. Ask remains read-only, Execute keeps approval controls, and context compaction may use the selected model without deleting GoodBuddy chat history.

Release note: GoodBuddy 0.10.0 重点完善多 Runtime 工作流,统一 OpenCode、Continue 与 DeepSeek Harness 的能力、MCP、插件和上下文管理,并提升长对话、多会话与任务通知的连贯性。
2026-08-17 12:57:12 +08:00

395 lines
10 KiB
TypeScript

import {
mkdir,
mkdtemp,
rm,
stat,
writeFile
} from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
DshNpmExtensionInstaller,
DshNpmMarketplaceCatalog,
runPackageManager,
type PackageManagerRunner
} from './dsh-extension-marketplace'
const temporaryDirectories: string[] = []
function response(value: unknown): Response {
return new Response(JSON.stringify(value), {
status: 200,
headers: { 'content-type': 'application/json' }
})
}
afterEach(async () => {
await Promise.all(
temporaryDirectories.splice(0).map((directory) =>
rm(directory, { recursive: true, force: true })
)
)
})
describe('DSH npm marketplace', () => {
it('loads every npm search page and keeps only DSH plugin packages', async () => {
const fetcher = vi.fn<typeof fetch>(async (input) => {
const from = Number(new URL(String(input)).searchParams.get('from'))
return response({
total: 251,
objects:
from === 0
? [
{
package: {
name: 'dsh-plugin-greet',
version: '0.1.0',
description: 'A greeting tool.',
keywords: ['dsh-plugin'],
license: 'MIT',
links: {
repository:
'git+https://github.com/example/greet.git'
}
}
},
{
package: {
name: 'not-a-plugin',
version: '1.0.0',
keywords: ['unrelated']
}
}
]
: [
{
package: {
name: 'dsh-second-plugin',
version: '2.0.0',
keywords: ['dsh-plugin']
}
}
]
})
})
const catalog = new DshNpmMarketplaceCatalog({
fetcher,
cacheTtlMs: 60_000
})
const entries = await catalog.list()
expect(entries.map((entry) => entry.package.name)).toEqual([
'dsh-plugin-greet',
'dsh-second-plugin'
])
expect(entries[0]).toMatchObject({
description: 'A greeting tool.',
repository: 'https://github.com/example/greet.git'
})
expect(fetcher).toHaveBeenCalledTimes(2)
await catalog.list()
expect(fetcher).toHaveBeenCalledTimes(2)
})
it('coalesces concurrent catalog loads', async () => {
let resolveFetch: ((response: Response) => void) | undefined
const fetcher = vi.fn<typeof fetch>(
() =>
new Promise<Response>((resolve) => {
resolveFetch = resolve
})
)
const catalog = new DshNpmMarketplaceCatalog({ fetcher })
const first = catalog.list()
const second = catalog.list()
expect(fetcher).toHaveBeenCalledOnce()
resolveFetch?.(
response({
total: 1,
objects: [
{
package: {
name: 'dsh-plugin-greet',
version: '0.1.0',
keywords: ['dsh-plugin']
}
}
]
})
)
await expect(Promise.all([first, second])).resolves.toEqual([
[
expect.objectContaining({
package: expect.objectContaining({
name: 'dsh-plugin-greet'
})
})
],
[
expect.objectContaining({
package: expect.objectContaining({
name: 'dsh-plugin-greet'
})
})
]
])
expect(fetcher).toHaveBeenCalledOnce()
})
it('installs the exact package when an older manifest lacks DSH bundle metadata', async () => {
const destinationDirectory = await mkdtemp(
join(tmpdir(), 'goodbuddy-dsh-npm-installer-')
)
temporaryDirectories.push(destinationDirectory)
const integrity = `sha512-${Buffer.from('verified').toString(
'base64'
)}`
const packageName = 'dsh-plugin-greet'
const version = '0.1.0'
const manifest = {
name: packageName,
version,
main: 'index.js',
dist: { integrity },
dsh: { bundle: { patch: './cordis.patch.yml' } }
}
const npmCliPath = join(destinationDirectory, 'npm-cli.js')
await writeFile(npmCliPath, '// bundled npm fixture\n', 'utf8')
const fetcher = vi.fn<typeof fetch>(async () =>
response({
versions: {
'0.0.1': {
name: packageName,
version: '0.0.1',
dist: { integrity }
},
[version]: manifest
}
})
)
const runner: PackageManagerRunner = vi.fn(
async (_command, _args, options) => {
const installedDirectory = join(
options.cwd,
'node_modules',
packageName
)
await mkdir(installedDirectory, { recursive: true })
await Promise.all([
writeFile(
join(installedDirectory, 'package.json'),
JSON.stringify(manifest),
'utf8'
),
writeFile(
join(installedDirectory, 'index.js'),
'export function apply() {}\n',
'utf8'
),
writeFile(
join(options.cwd, 'package-lock.json'),
JSON.stringify({
packages: {
[`node_modules/${packageName}`]: { integrity }
}
}),
'utf8'
)
])
return { exitCode: 0, stdout: '', stderr: '' }
}
)
const installer = new DshNpmExtensionInstaller({
dshHome: destinationDirectory,
npmCliPath,
fetcher,
runner,
environment: { PATH: 'C:\\Node' }
})
await expect(
installer.install({
entry: {
id: 'greet',
package: { name: packageName, version },
displayName: packageName,
description: 'A greeting tool.'
},
destinationDirectory
})
).resolves.toEqual({
entrypoint: `node_modules/${packageName}/index.js`,
integrity
})
expect(runner).toHaveBeenCalledWith(
process.execPath,
[
npmCliPath,
'install',
'--save-exact',
'--no-audit',
'--no-fund',
'--dangerously-allow-all-scripts',
'--loglevel=error',
`${packageName}@${version}`
],
expect.objectContaining({
cwd: destinationDirectory,
env: expect.objectContaining({
ELECTRON_RUN_AS_NODE: '1',
npm_execpath: npmCliPath,
npm_node_execpath: process.execPath
})
})
)
expect(
(
await stat(
join(
destinationDirectory,
'package-manager-bin',
process.platform === 'win32' ? 'node.cmd' : 'node'
)
)
).isFile()
).toBe(true)
})
it('rejects packages that do not declare a DSH bundle', async () => {
const directory = await mkdtemp(
join(tmpdir(), 'goodbuddy-dsh-not-plugin-')
)
temporaryDirectories.push(directory)
const installer = new DshNpmExtensionInstaller({
dshHome: directory,
fetcher: vi.fn<typeof fetch>(async () =>
response({
versions: {
'1.0.0': {
name: 'not-a-dsh-plugin',
version: '1.0.0',
main: 'index.js',
dist: {
integrity: `sha512-${Buffer.from('verified').toString(
'base64'
)}`
}
}
}
})
),
runner: vi.fn()
})
await expect(
installer.install({
entry: {
id: 'not-plugin',
package: {
name: 'not-a-dsh-plugin',
version: '1.0.0'
},
displayName: 'Not a plugin',
description: 'Missing DSH bundle metadata.'
},
destinationDirectory: directory
})
).rejects.toThrow()
})
it('aborts an active package-manager process and settles the run', async () => {
const directory = await mkdtemp(
join(tmpdir(), 'goodbuddy-dsh-npm-abort-')
)
temporaryDirectories.push(directory)
const controller = new AbortController()
const operation = runPackageManager(
process.execPath,
['-e', 'setInterval(() => {}, 1_000)'],
{
cwd: directory,
env: process.env,
timeoutMs: 60_000,
signal: controller.signal
}
)
await new Promise((resolve) => setTimeout(resolve, 50))
controller.abort(new Error('installer cancellation fixture'))
await expect(operation).rejects.toThrow(
'installer cancellation fixture'
)
})
it('disposes active installs, propagates cancellation, and rejects new work', async () => {
const directory = await mkdtemp(
join(tmpdir(), 'goodbuddy-dsh-installer-dispose-')
)
temporaryDirectories.push(directory)
const integrity = `sha512-${Buffer.from('verified').toString(
'base64'
)}`
const packageName = 'dsh-plugin-cancellable'
const version = '1.0.0'
const runner: PackageManagerRunner = vi.fn(
(_command, _args, options) =>
new Promise<{
exitCode: number
stdout: string
stderr: string
}>((_resolve, reject) => {
const rejectCancellation = (): void => {
reject(options.signal?.reason)
}
options.signal?.addEventListener(
'abort',
rejectCancellation,
{ once: true }
)
})
)
const installer = new DshNpmExtensionInstaller({
dshHome: directory,
fetcher: vi.fn<typeof fetch>(async () =>
response({
versions: {
[version]: {
name: packageName,
version,
main: 'index.js',
dist: { integrity },
dsh: { bundle: { patch: './cordis.patch.yml' } }
}
}
})
),
runner
})
const input = {
entry: {
id: 'cancellable',
package: { name: packageName, version },
displayName: packageName,
description: 'Cancellation fixture.'
},
destinationDirectory: directory
}
const installation = installer.install(input)
await vi.waitFor(() => expect(runner).toHaveBeenCalledOnce())
await installer.dispose()
await expect(installation).rejects.toThrow(
'应用退出,DSH 插件安装已取消'
)
await expect(installer.install(input)).rejects.toThrow(
'DSH 插件安装器正在关闭'
)
})
})