Files
goodbuddy/src/main/goodbuddy-config-service.test.ts
T
mesalogo bf1d34e897 feat: configure canvas pages sent for note analysis
Canvas analysis previously sent every page. Add a Magic Notes setting for
1-8 canvas pages, defaulting to 1 for new and historical settings. Select
the first N pages in current canvas order before capture and use the same
text/image range for saved entries, drafts and canvas-source todos.

Keep full 50-page saving/export, expose the same setting through config
tools, and use the shared settings select with an inline help icon.
Include pagination, persistence, production IPC and isolated Electron
regressions, including text-only input and scaled page boundaries.

Validation: focused feature tests and isolated Electron regressions passed;
the final compact UI passed 14 tests. Typecheck and lint were reported
passing by the coordinating agent. No full-suite pass is claimed because
the completed full test command exposed no summary. No live model calls
were made for this commit preparation.

Release note: 魔法笔记新增“发送画布页数”设置,可选 1~8 页,默认 1 页,按当前画布顺序发送前 N 页的文字和图片;标题旁问号可查看说明,保存和导出完整画布不受影响。
2026-09-20 00:07:07 +08:00

582 lines
16 KiB
TypeScript

import {
access,
mkdtemp,
mkdir,
rm,
writeFile
} from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { ApplicationSettingsStore } from './application-settings-store'
import {
BrowserProfileService,
MemoryBrowserProfileStore
} from './capabilities/browser-profile-service'
import {
CapabilityService,
type CapabilityCipher
} from './capabilities/capability-service'
import { GoodBuddyConfigService } from './goodbuddy-config-service'
const temporaryDirectories: string[] = []
const cipher: CapabilityCipher = {
isAvailable: () => true,
encrypt: (value) => Buffer.from(value),
decrypt: (value) => value.toString()
}
async function writeSkill(
root: string,
id: string,
body = 'Follow the user request.'
): Promise<string> {
const directory = join(root, id)
await mkdir(directory, { recursive: true })
await writeFile(
join(directory, 'SKILL.md'),
[
'---',
`id: ${id}`,
`name: ${id}`,
`description: ${id} test skill`,
'---',
'',
body
].join('\n'),
'utf8'
)
return directory
}
async function createHarness(now = 1_000) {
const directory = await mkdtemp(join(tmpdir(), 'goodbuddy-config-'))
temporaryDirectories.push(directory)
const builtinRoot = join(directory, 'builtin')
const importedRoot = join(directory, 'imported')
const workspace = join(directory, 'workspace')
await mkdir(workspace, { recursive: true })
await writeSkill(builtinRoot, 'built-in')
const capabilities = new CapabilityService(
join(directory, 'capabilities.json'),
builtinRoot,
importedRoot,
cipher,
{
browserProfiles: new BrowserProfileService(
new MemoryBrowserProfileStore()
)
}
)
const application = new ApplicationSettingsStore(
join(directory, 'application.json')
)
const service = new GoodBuddyConfigService(application, capabilities, {
now: () => now,
planTtlMs: 100
})
return {
directory,
workspace,
capabilities,
application,
service,
setNow(value: number) {
now = value
}
}
}
afterEach(async () => {
await Promise.all(
temporaryDirectories
.splice(0)
.map((directory) => rm(directory, { recursive: true, force: true }))
)
})
describe('GoodBuddyConfigService', () => {
it('publishes persisted application changes from config apply through the settings event', async () => {
const { application, service, workspace } = await createHarness()
const changed = vi.fn()
const unsubscribe = application.onChanged(changed)
const current = await application.get()
const applicationNavigation = { ...current.applicationNavigation, order: [...current.applicationNavigation.order].reverse() }
const plan = await service.plan('settings-sync', workspace, {
operations: [{ operation: 'application.update', updates: { applicationNavigation, magicNoteCanvasPageCount: 8 } }],
})
expect(changed).not.toHaveBeenCalled()
await service.apply('settings-sync', { planId: plan.planId }, new AbortController().signal, async () => true)
expect(changed).toHaveBeenCalledExactlyOnceWith(await application.get())
expect(changed.mock.calls[0]![0].applicationNavigation).toEqual(applicationNavigation)
expect((await service.getSnapshot()).application.magicNoteCanvasPageCount).toBe(8)
unsubscribe()
await application.update({ localInferenceEnabled: false })
expect(changed).toHaveBeenCalledOnce()
})
it('returns only sanitized configuration and publishes common examples', async () => {
const { service, capabilities } = await createHarness()
await capabilities.saveMcpServer(undefined, {
name: 'Secret MCP',
description: '',
enabled: false,
allowDynamicTools: false,
assignments: ['model'],
secret: { action: 'replace', value: 'never-return-this' },
transport: 'http',
url: 'https://mcp.example.com/tools'
})
expect(service.getCapabilities()).toMatchObject({
server: 'goodbuddy_config',
applyRequiresApproval: true,
operations: expect.arrayContaining([
expect.objectContaining({
operation: 'skill.import',
exampleRequest: expect.stringContaining('导入')
})
])
})
const snapshot = await service.getSnapshot()
expect(snapshot.mcpServers[0]).toMatchObject({
name: 'Secret MCP',
secretConfigured: true,
transport: 'http'
})
expect(JSON.stringify(snapshot)).not.toContain('never-return-this')
expect(JSON.stringify(snapshot)).not.toContain(
'https://mcp.example.com/tools'
)
})
it('scopes one-shot plans to a request and rejects expired plans', async () => {
const harness = await createHarness()
const plan = await harness.service.plan(
'request-one',
harness.workspace,
{
operations: [
{
operation: 'application.update',
updates: { checkUpdatesOnStartup: false }
}
]
}
)
const authorize = vi.fn(async () => true)
await expect(
harness.service.apply(
'request-two',
{ planId: plan.planId },
new AbortController().signal,
authorize
)
).rejects.toThrow('不属于当前请求')
expect(authorize).not.toHaveBeenCalled()
const expiring = await harness.service.plan(
'request-one',
harness.workspace,
{
operations: [
{
operation: 'application.update',
updates: { checkUpdatesOnStartup: false }
}
]
}
)
harness.setNow(1_101)
await expect(
harness.service.apply(
'request-one',
{ planId: expiring.planId },
new AbortController().signal,
authorize
)
).rejects.toThrow('已过期')
expect(authorize).not.toHaveBeenCalled()
})
it('rejects a plan that expires while native approval is open', async () => {
const harness = await createHarness()
const plan = await harness.service.plan(
'request-one',
harness.workspace,
{
operations: [
{
operation: 'application.update',
updates: { checkUpdatesOnStartup: false }
}
]
}
)
await expect(
harness.service.apply(
'request-one',
{ planId: plan.planId },
new AbortController().signal,
async () => {
harness.setNow(1_101)
return true
}
)
).rejects.toThrow('确认期间已过期')
await expect(harness.application.get()).resolves.toMatchObject({
checkUpdatesOnStartup: true
})
})
it('requires approval and applies a plan only once', async () => {
const harness = await createHarness()
const deniedPlan = await harness.service.plan(
'request-one',
harness.workspace,
{
operations: [
{
operation: 'application.update',
updates: { checkUpdatesOnStartup: false }
}
]
}
)
await expect(
harness.service.apply(
'request-one',
{ planId: deniedPlan.planId },
new AbortController().signal,
async () => false
)
).rejects.toThrow('用户拒绝')
await expect(harness.application.get()).resolves.toMatchObject({
checkUpdatesOnStartup: true
})
const approvedPlan = await harness.service.plan(
'request-one',
harness.workspace,
{
operations: [
{
operation: 'application.update',
updates: { checkUpdatesOnStartup: false }
}
]
}
)
await expect(
harness.service.apply(
'request-one',
{ planId: approvedPlan.planId },
new AbortController().signal,
async () => true
)
).resolves.toMatchObject({
status: 'applied',
appliedOperations: 1,
reload: 'none'
})
await expect(harness.application.get()).resolves.toMatchObject({
checkUpdatesOnStartup: false
})
expect(harness.service.takePendingReload('request-one')).toBe(
'after-current-request'
)
await expect(
harness.service.apply(
'request-one',
{ planId: approvedPlan.planId },
new AbortController().signal,
async () => true
)
).rejects.toThrow('不存在')
})
it('imports the exact inspected Skill and requests a deferred reload', async () => {
const harness = await createHarness()
await writeSkill(harness.workspace, 'meeting-helper')
const plan = await harness.service.plan(
'request-one',
harness.workspace,
{
operations: [
{
operation: 'skill.import',
sourcePath: './meeting-helper',
enabled: false,
assignments: ['model']
}
]
}
)
expect(plan.operations[0]).toMatchObject({
sourcePath: './meeting-helper'
})
await harness.service.apply(
'request-one',
{ planId: plan.planId },
new AbortController().signal,
async () => true
)
await expect(harness.capabilities.getSnapshot()).resolves.toMatchObject({
skills: expect.arrayContaining([
expect.objectContaining({
id: 'meeting-helper',
source: 'imported',
enabled: false,
assignments: ['model']
})
])
})
expect(harness.service.takePendingReload('request-one')).toBe(
'after-current-request'
)
expect(harness.service.takePendingReload('request-one')).toBe('none')
const secondSource = await writeSkill(
harness.workspace,
'changing-skill'
)
const changingPlan = await harness.service.plan(
'request-two',
harness.workspace,
{
operations: [
{
operation: 'skill.import',
sourcePath: secondSource,
enabled: true,
assignments: ['model']
}
]
}
)
await writeFile(
join(secondSource, 'extra.js'),
'console.log("changed")',
'utf8'
)
await expect(
harness.service.apply(
'request-two',
{ planId: changingPlan.planId },
new AbortController().signal,
async () => true
)
).rejects.toThrow('确认后已发生变化')
})
it('reports partial application and still requests reload after a later failure', async () => {
const harness = await createHarness()
await writeSkill(harness.workspace, 'partial-skill')
const plan = await harness.service.plan(
'request-partial',
harness.workspace,
{
operations: [
{
operation: 'skill.import',
sourcePath: './partial-skill',
enabled: true,
assignments: ['model']
},
{
operation: 'application.update',
updates: { checkUpdatesOnStartup: false }
}
]
}
)
await writeFile(
join(harness.workspace, 'partial-skill', 'changed.txt'),
'changed',
'utf8'
)
await expect(
harness.service.apply(
'request-partial',
{ planId: plan.planId },
new AbortController().signal,
async () => true
)
).rejects.toThrow('已发生变化')
const secondPlan = await harness.service.plan(
'request-partial-two',
harness.workspace,
{
operations: [
{
operation: 'skill.import',
sourcePath: './partial-skill',
enabled: true,
assignments: ['model']
},
{
operation: 'skill.setEnabled',
skillId: 'built-in',
enabled: false
}
]
}
)
const originalSetSkillEnabled = harness.capabilities.setSkillEnabled.bind(
harness.capabilities
)
vi.spyOn(harness.capabilities, 'setSkillEnabled').mockImplementationOnce(
async () => {
throw new Error('later operation failed')
}
)
const result = await harness.service.apply(
'request-partial-two',
{ planId: secondPlan.planId },
new AbortController().signal,
async () => true
)
expect(result).toMatchObject({
status: 'partially-applied',
appliedOperations: 1,
reload: 'after-current-request',
error: 'later operation failed'
})
expect(harness.service.takePendingReload('request-partial-two')).toBe(
'after-current-request'
)
vi.mocked(harness.capabilities.setSkillEnabled).mockImplementation(
originalSetSkillEnabled
)
})
it('rejects Skill paths outside the workspace', async () => {
const harness = await createHarness()
const outside = await writeSkill(harness.directory, 'outside-skill')
await expect(
harness.service.plan('request-one', harness.workspace, {
operations: [
{
operation: 'skill.import',
sourcePath: outside,
enabled: true,
assignments: ['model']
}
]
})
).rejects.toThrow('必须位于当前工作区')
})
it('shows executable MCP details and invalidates hidden connection changes', async () => {
const harness = await createHarness()
const existing = await harness.capabilities.saveMcpServer(undefined, {
name: 'Existing MCP',
description: '',
enabled: false,
allowDynamicTools: false,
assignments: ['model'],
secret: { action: 'keep' },
transport: 'http',
url: 'https://old.example.com/mcp'
})
const serverId = existing.mcpServers[0]!.id
const addPlan = await harness.service.plan(
'request-add',
harness.workspace,
{
operations: [
{
operation: 'mcp.add',
connection: {
name: 'Local MCP',
description: '',
allowDynamicTools: false,
transport: 'stdio',
command: 'node',
args: ['server.js', '--safe']
},
enabled: false,
assignments: ['model']
}
]
}
)
expect(addPlan.steps[0]?.summary).toContain(
'命令 "node" "server.js" "--safe"'
)
const enablePlan = await harness.service.plan(
'request-enable',
harness.workspace,
{
operations: [
{
operation: 'mcp.setEnabled',
serverId,
enabled: true
}
]
}
)
await harness.capabilities.saveMcpServer(serverId, {
name: 'Existing MCP',
description: '',
enabled: false,
allowDynamicTools: false,
assignments: ['model'],
secret: { action: 'keep' },
transport: 'http',
url: 'https://new.example.com/mcp'
})
await expect(
harness.service.apply(
'request-enable',
{ planId: enablePlan.planId },
new AbortController().signal,
async () => true
)
).rejects.toThrow('确认前已发生变化')
})
it('does not create persistent inspection artifacts while planning a ZIP Skill', async () => {
const harness = await createHarness()
const archivePath = join(harness.workspace, 'skill.zip')
const { strToU8, zipSync } = await import('fflate')
await writeFile(
archivePath,
Buffer.from(
zipSync({
'zip-helper/SKILL.md': strToU8(
[
'---',
'id: zip-helper',
'name: zip-helper',
'description: ZIP helper',
'---',
'',
'Help with ZIP files.'
].join('\n')
)
})
)
)
await harness.service.plan('request-zip', harness.workspace, {
operations: [
{
operation: 'skill.import',
sourcePath: './skill.zip',
enabled: false,
assignments: []
}
]
})
await expect(
access(join(harness.directory, 'imported'))
).rejects.toThrow()
})
})