Let users retain useful assistant replies or complete conversations without leaving their chat workflow. Preview and edit captured text, choose an existing note or atomically create a note with its first entry, and return to the source conversation or message from saved records. Add an application-scoped Notes workbar tab with title/body search and quick text appends. Preserve drafts across conversation switches and guard closing, replacement and disabling. Schema 48 stores optional source metadata without rewriting file-backed bodies; source deletion retains the saved note. Older clients cannot reopen an upgraded database without a pre-upgrade backup. Release note: 可将助手回答或完整会话预览、编辑后加入魔法笔记,并从笔记返回原会话或消息。右侧快速笔记支持搜索和追加文字,切换会话保留草稿;采集本身不调用模型,保存后自动评论仍遵循已有设置。 Validation: isolated HEAD-based candidate passed 22 files / 930 focused tests, plus 20 gateway tests after including its create-result fixture adjustment. Real Electron App/preload/IPC/SQLite acceptance passed for all 502 historical messages, exact-entry/source navigation, native keyboard input in a 214px panel, reopen and source-deletion retention, with zero capture model calls. Repository lint and web TypeScript passed. Cached whitespace and secret-pattern checks passed; native-client changes and unrelated IPC extraction are excluded. Validation limits: the isolated source tree reuses installed node_modules, where a parallel task upgraded DSH from the committed 0.1.2-rc.1 to 0.1.7-rc.2. Full typecheck reports 14 DSH errors in three unchanged files; the full-suite attempt with --bail=1 also fails in deepseek-harness-host.test.ts. These are not all-green repository or release-package results. Earlier execution-time full suite failures and fixture repairs remain recorded in the feature progress doc.
290 lines
10 KiB
TypeScript
290 lines
10 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
WORKBAR_APP_DEFINITIONS,
|
|
WORKBAR_LIMITS,
|
|
normalizeWorkbarLayoutPreferences,
|
|
workbarAppDefinitionSchema,
|
|
workbarLayoutPreferencesSchema,
|
|
workbarTabInstanceSchema
|
|
} from './workbar-contracts'
|
|
|
|
const taskId = '00000000-0000-4000-8000-000000000101'
|
|
const terminalId = '00000000-0000-4000-8000-000000000102'
|
|
const projectId = '00000000-0000-4000-8000-000000000201'
|
|
|
|
describe('workbar contracts', () => {
|
|
it('keeps notes as a context-independent single instance through layout normalization', () => {
|
|
const instance = { id: taskId, appId: 'notes', title: 'Notes' } as const
|
|
const layout = { instances: [instance, { ...instance, id: terminalId }], activeInstanceId: terminalId, expanded: true, dock: 'right', widthRatio: 0.3 }
|
|
expect(workbarLayoutPreferencesSchema.safeParse(layout).success).toBe(false)
|
|
expect(normalizeWorkbarLayoutPreferences(layout, [])?.instances).toEqual([instance])
|
|
expect(workbarTabInstanceSchema.safeParse({ ...instance, targetRef: { type: 'conversation', conversationId: 'A' } }).success).toBe(false)
|
|
expect(workbarTabInstanceSchema.safeParse({ ...instance, targetRef: { type: 'project', projectId } }).success).toBe(false)
|
|
})
|
|
it('persists supervision conversation and task targets without allowing task terminal bindings', () => {
|
|
for (const targetRef of [{ type: 'conversation', conversationId: 'A' }, { type: 'task', taskId: 'task-A' }]) {
|
|
const instance = { id: taskId, appId: 'tasks', title: 'Tasks', targetRef }
|
|
expect(workbarTabInstanceSchema.parse(instance).targetRef).toEqual(targetRef)
|
|
expect(workbarTabInstanceSchema.safeParse({ ...instance, appId: 'terminal' }).success).toBe(false)
|
|
}
|
|
})
|
|
it('freezes the first application catalog and instance policies', () => {
|
|
expect(
|
|
WORKBAR_APP_DEFINITIONS.map(({ id, instancePolicy, defaultContext, defaultOpen, required, closable, reorderable }) => ({
|
|
id,
|
|
instancePolicy,
|
|
defaultContext,
|
|
defaultOpen,
|
|
required,
|
|
closable,
|
|
reorderable
|
|
}))
|
|
).toEqual([
|
|
{ id: 'tasks', instancePolicy: 'single', defaultContext: 'current-project', defaultOpen: true, required: true, closable: false, reorderable: false },
|
|
{ id: 'workspace', instancePolicy: 'single', defaultContext: 'current-project', defaultOpen: true, required: true, closable: false, reorderable: false },
|
|
{ id: 'browser', instancePolicy: 'multiple', defaultContext: 'current-conversation', defaultOpen: true, required: false, closable: true, reorderable: true },
|
|
{ id: 'results', instancePolicy: 'single', defaultContext: 'current-project', defaultOpen: true, required: false, closable: true, reorderable: true },
|
|
{ id: 'notes', instancePolicy: 'single', defaultContext: 'application', defaultOpen: false, required: false, closable: true, reorderable: true },
|
|
{
|
|
id: 'terminal',
|
|
instancePolicy: 'multiple',
|
|
defaultContext: 'current-project',
|
|
defaultOpen: false,
|
|
required: false,
|
|
closable: true,
|
|
reorderable: true
|
|
}
|
|
])
|
|
for (const definition of WORKBAR_APP_DEFINITIONS) {
|
|
expect(workbarAppDefinitionSchema.parse(definition)).toEqual(
|
|
definition
|
|
)
|
|
}
|
|
})
|
|
|
|
it('accepts an ordered layout with public target references', () => {
|
|
const layout = {
|
|
instances: [
|
|
{ id: taskId, appId: 'tasks', title: '任务中心' },
|
|
{
|
|
id: terminalId,
|
|
appId: 'terminal',
|
|
title: '终端 · 项目',
|
|
targetRef: { type: 'project', projectId }
|
|
}
|
|
],
|
|
activeInstanceId: terminalId,
|
|
expanded: true,
|
|
dock: 'right',
|
|
widthRatio: 0.35,
|
|
taskScope: 'current-project'
|
|
} as const
|
|
|
|
expect(workbarLayoutPreferencesSchema.parse(layout)).toEqual(layout)
|
|
})
|
|
|
|
it('persists a browser conversation binding without a runtime tab identifier', () => {
|
|
expect(
|
|
workbarTabInstanceSchema.parse({
|
|
id: '00000000-0000-4000-8000-000000000103',
|
|
appId: 'browser',
|
|
title: '浏览器 · 会话 A',
|
|
targetRef: { type: 'conversation', conversationId: 'conversation-a' }
|
|
})
|
|
).toEqual({
|
|
id: '00000000-0000-4000-8000-000000000103',
|
|
appId: 'browser',
|
|
title: '浏览器 · 会话 A',
|
|
targetRef: { type: 'conversation', conversationId: 'conversation-a' }
|
|
})
|
|
expect(
|
|
workbarTabInstanceSchema.safeParse({
|
|
id: '00000000-0000-4000-8000-000000000103',
|
|
appId: 'browser',
|
|
title: '浏览器',
|
|
targetRef: { type: 'conversation', conversationId: 'conversation-a' },
|
|
tabId: '00000000-0000-4000-8000-000000000999'
|
|
}).success
|
|
).toBe(false)
|
|
})
|
|
|
|
it('explicitly keeps every existing app visible across context switches', () => {
|
|
for (const definition of WORKBAR_APP_DEFINITIONS) {
|
|
expect(definition.visibleAcrossContextSwitches).toBe(true)
|
|
const { visibleAcrossContextSwitches, ...withoutVisibility } = definition
|
|
expect(workbarAppDefinitionSchema.safeParse(withoutVisibility).success).toBe(false)
|
|
expect(
|
|
workbarAppDefinitionSchema.parse({
|
|
...withoutVisibility,
|
|
visibleAcrossContextSwitches
|
|
}).visibleAcrossContextSwitches
|
|
).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('rejects unknown and sensitive target parameters', () => {
|
|
for (const targetRef of [
|
|
{ type: 'local', cwd: 'C:\\secret' },
|
|
{ type: 'ssh-host', hostId: projectId, password: 'secret' },
|
|
{
|
|
type: 'project',
|
|
projectId,
|
|
environment: { MODEL_API_KEY: 'secret' }
|
|
}
|
|
]) {
|
|
expect(
|
|
workbarTabInstanceSchema.safeParse({
|
|
id: terminalId,
|
|
appId: 'terminal',
|
|
title: '终端',
|
|
targetRef
|
|
}).success
|
|
).toBe(false)
|
|
}
|
|
expect(
|
|
workbarTabInstanceSchema.safeParse({
|
|
id: taskId,
|
|
appId: 'tasks',
|
|
title: '任务中心',
|
|
privateState: {}
|
|
}).success
|
|
).toBe(false)
|
|
})
|
|
|
|
it('enforces title, count, uniqueness, and active-instance bounds', () => {
|
|
expect(
|
|
workbarTabInstanceSchema.safeParse({
|
|
id: terminalId,
|
|
appId: 'terminal',
|
|
title: 'x'.repeat(WORKBAR_LIMITS.maximumTitleBytes + 1),
|
|
targetRef: { type: 'local' }
|
|
}).success
|
|
).toBe(false)
|
|
|
|
const task = { id: taskId, appId: 'tasks', title: '任务中心' }
|
|
const base = {
|
|
instances: [task],
|
|
activeInstanceId: taskId,
|
|
expanded: true,
|
|
dock: 'right',
|
|
widthRatio: 0.4,
|
|
taskScope: 'current-project'
|
|
}
|
|
expect(
|
|
workbarLayoutPreferencesSchema.safeParse({
|
|
...base,
|
|
instances: [task, { ...task, id: terminalId }]
|
|
}).success
|
|
).toBe(false)
|
|
expect(
|
|
workbarLayoutPreferencesSchema.safeParse({
|
|
...base,
|
|
activeInstanceId: terminalId
|
|
}).success
|
|
).toBe(false)
|
|
expect(
|
|
workbarLayoutPreferencesSchema.safeParse({
|
|
...base,
|
|
instances: Array.from(
|
|
{ length: WORKBAR_LIMITS.maximumOpenInstances + 1 },
|
|
(_, index) => ({
|
|
id: `00000000-0000-4000-8000-${String(index).padStart(12, '0')}`,
|
|
appId: 'terminal',
|
|
title: `终端 ${index}`,
|
|
targetRef: { type: 'local' }
|
|
})
|
|
)
|
|
}).success
|
|
).toBe(false)
|
|
})
|
|
|
|
it('normalizes duplicate policies, fixed defaults, and stale active state', () => {
|
|
const browserId = '00000000-0000-4000-8000-000000000103'
|
|
const secondBrowserId = '00000000-0000-4000-8000-000000000104'
|
|
const workspaceId = '00000000-0000-4000-8000-000000000105'
|
|
const normalized = normalizeWorkbarLayoutPreferences(
|
|
{
|
|
instances: [
|
|
{ id: browserId, appId: 'browser', title: '浏览器' },
|
|
{ id: secondBrowserId, appId: 'browser', title: '浏览器 2' },
|
|
{ id: workspaceId, appId: 'workspace', title: '工作区' },
|
|
{ id: terminalId, appId: 'workspace', title: '重复工作区' }
|
|
],
|
|
activeInstanceId: terminalId,
|
|
expanded: true,
|
|
dock: 'right',
|
|
widthRatio: 0.4,
|
|
taskScope: 'all-projects'
|
|
},
|
|
[
|
|
{ id: taskId, appId: 'tasks', title: '任务中心' },
|
|
{ id: workspaceId, appId: 'workspace', title: '工作区' }
|
|
]
|
|
)
|
|
|
|
expect(normalized?.instances.map(({ id, appId }) => ({ id, appId })))
|
|
.toEqual([
|
|
{ id: taskId, appId: 'tasks' },
|
|
{ id: workspaceId, appId: 'workspace' },
|
|
{ id: browserId, appId: 'browser' },
|
|
{ id: secondBrowserId, appId: 'browser' }
|
|
])
|
|
expect(normalized?.activeInstanceId).toBe(taskId)
|
|
expect(normalized?.taskScope).toBe('all-projects')
|
|
})
|
|
|
|
it('pins restored tasks and workspace without changing other order or selection', () => {
|
|
const workspace = {
|
|
id: '00000000-0000-4000-8000-000000000105',
|
|
appId: 'workspace', title: 'Workspace'
|
|
} as const
|
|
const tasks = { id: taskId, appId: 'tasks', title: 'Tasks' } as const
|
|
const terminal = {
|
|
id: terminalId, appId: 'terminal', title: 'Terminal',
|
|
targetRef: { type: 'local' }
|
|
} as const
|
|
const browser = {
|
|
id: '00000000-0000-4000-8000-000000000103',
|
|
appId: 'browser', title: 'Browser',
|
|
targetRef: { type: 'conversation', conversationId: 'conversation-a' }
|
|
} as const
|
|
const layout = {
|
|
instances: [terminal, workspace, browser, tasks],
|
|
activeInstanceId: browser.id,
|
|
expanded: true, dock: 'right', widthRatio: 0.4,
|
|
taskScope: 'all-projects'
|
|
} as const
|
|
|
|
const normalized = normalizeWorkbarLayoutPreferences(layout, [tasks, workspace])
|
|
expect(normalized).toEqual({
|
|
...layout,
|
|
instances: [tasks, workspace, terminal, browser]
|
|
})
|
|
expect(layout.instances).toEqual([terminal, workspace, browser, tasks])
|
|
expect(normalizeWorkbarLayoutPreferences(normalized, [tasks, workspace]))
|
|
.toEqual(normalized)
|
|
})
|
|
|
|
it('preserves a valid empty layout with a null active instance', () => {
|
|
expect(
|
|
normalizeWorkbarLayoutPreferences(
|
|
{
|
|
instances: [],
|
|
activeInstanceId: null,
|
|
expanded: false,
|
|
dock: 'right',
|
|
widthRatio: 0.4
|
|
},
|
|
[]
|
|
)
|
|
).toEqual({
|
|
instances: [],
|
|
activeInstanceId: null,
|
|
expanded: false,
|
|
dock: 'right',
|
|
widthRatio: 0.4,
|
|
taskScope: 'current-project'
|
|
})
|
|
})
|
|
})
|