feat: ship skill and MCP workflows with signed updater for v0.0.7

This commit is contained in:
xiaohan.yuan committed 2026-10-21 00:04:17 +08:00
1 parent 1b52df0603
commit a01085c656
136 files changed
+18646 -217

No files matched your search

+62 -1
View File
@@ -6,6 +6,8 @@ import { buildComposerSendPayload, MAX_ATTACHMENT_BYTES, oversizedAttachmentName
import { MessageActionToolbar } from '../src/renderer/src/workbuddy/message-actions/MessageActionToolbar'
import { UserMessageToolbar } from '../src/renderer/src/workbuddy/message-actions/UserMessageToolbar'
import { UserMessageEditor } from '../src/renderer/src/workbuddy/conversation/UserMessageEditor'
import { messageCapabilityRefs } from '../src/renderer/src/workbuddy/conversation/message-capabilities'
import { draftFingerprint, draftIsEmpty, draftPlainText, mergeFailedDraft } from '../src/renderer/src/workbuddy/composer/draft-fingerprint'
import { buildCopyMessagePayload, buildUserMessageClipboard, getSortedRequestMessages, projectSafeMessage, safeStringify, USER_CONTENT_BLOCKS_MIME } from '../src/renderer/src/workbuddy/message-actions/project-safe-message'
import type { ChatMessage } from '../src/shared/types'
@@ -41,6 +43,61 @@ const legacy: ChatMessage = {
createdAt: 1
}
const userMessage: ChatMessage = { id: 'user-copy', role: 'user', content: 'plain user text', createdAt: 1 }
assert.deepEqual(messageCapabilityRefs({ ...userMessage, skills: [{ id: 'skill-a', name: 'Skill A', uri: '/skill-a' }],
mcpServerIds: ['mcp-a'], command: 'compact' }), { skillIds: ['skill-a'], mcpServerIds: ['mcp-a'], command: 'compact' },
'editing and regenerating must preserve message-scoped capability references')
const compactDraft = { text: '', command: 'compact' as const }
assert.notEqual(draftFingerprint(compactDraft), draftFingerprint({ text: '/compact', command: 'compact' }),
'compact draft identity must use the real empty editor snapshot, not the synthetic wire text')
assert.equal(draftIsEmpty({ text: '', userContent: [{ type: 'resource', id: 'C:/work/a.ts' }] }), false,
'rich-only drafts are not empty')
assert.equal(draftIsEmpty({ text: '', userContent: [{ type: 'text', text: ' ' }] }), true,
'empty rich text does not keep a draft alive')
assert.equal(draftPlainText({ text: '', userContent: [{ type: 'text', text: 'rich-only text' }] }), 'rich-only text',
'rich-only textual content reaches the send payload')
const failedDraft = {
text: 'old request',
attachments: [{ id: 'attachment-a', name: 'a.txt', mimeType: 'text/plain', size: 1 }],
resources: ['C:/work/a.ts', 'C:/work/shared.ts'],
quotes: [{ id: 'quote-a', kind: 'table' as const, sourceId: 'source-a', markdown: 'a', preview: 'a' }],
skills: [{ id: 'skill-a', name: 'Skill A' }],
mcpServerIds: ['mcp-a']
}
const currentDraft = {
text: 'new request',
attachments: [
{ id: 'attachment-b', name: 'b.txt', mimeType: 'text/plain', size: 1 },
{ id: 'attachment-a', name: 'duplicate.txt', mimeType: 'text/plain', size: 1 }
],
resources: ['C:/work/b.ts', 'C:/work/shared.ts'],
quotes: [{ id: 'quote-b', kind: 'table' as const, sourceId: 'source-b', markdown: 'b', preview: 'b' }],
skills: [{ id: 'skill-b', name: 'Skill B' }, { id: 'skill-a', name: 'Duplicate' }],
mcpServerIds: ['mcp-b', 'mcp-a']
}
const mergedDraft = mergeFailedDraft(currentDraft, failedDraft)
assert.equal(mergedDraft.draft.text, 'old request\nnew request', 'failed content must stay before subsequent input')
assert.deepEqual(mergedDraft.draft.attachments?.map(item => item.id), ['attachment-a', 'attachment-b'])
assert.deepEqual(mergedDraft.draft.resources, ['C:/work/a.ts', 'C:/work/shared.ts', 'C:/work/b.ts'])
assert.deepEqual(mergedDraft.draft.quotes?.map(item => item.id), ['quote-a', 'quote-b'])
assert.deepEqual(mergedDraft.draft.skills?.map(item => item.id), ['skill-a', 'skill-b'])
assert.deepEqual(mergedDraft.draft.mcpServerIds, ['mcp-a', 'mcp-b'])
assert.equal(mergedDraft.commandConflict, false)
const orderedFailure = mergeFailedDraft({ text: 'later' }, {
text: 'beforeafter', resources: ['C:/work/inline.ts'],
userContent: [{ type: 'text', text: 'before' }, { type: 'resource', id: 'C:/work/inline.ts' }, { type: 'text', text: 'after' }]
})
assert.deepEqual(orderedFailure.draft.userContent, [
{ type: 'text', text: 'before' }, { type: 'resource', id: 'C:/work/inline.ts' }, { type: 'text', text: 'after\nlater' }
], 'failed draft keeps inline reference order before subsequent input')
const commandFailure = mergeFailedDraft({ text: 'next ordinary draft' }, { text: '', command: 'compact' })
assert.equal(commandFailure.draft.command, 'compact', 'failed command tag remains removable instead of being discarded')
assert.equal(commandFailure.commandConflict, true, 'mixed command and ordinary content must be surfaced as a conflict')
assert.equal(mergeFailedDraft({ text: 'new' }, { text: '', userContent: [{ type: 'text', text: 'old rich text' }] }).draft.text,
'old rich text\nnew', 'failed rich-only text is not lost when subsequent input exists')
const failedQueueEdit = mergeFailedDraft({ text: 'typed after submit' }, { text: 'queued edit', queueEditId: 'queue-item-a' })
assert.equal(failedQueueEdit.draft.queueEditId, 'queue-item-a', 'failed queue-edit submissions retain the durable handoff identity')
assert.equal(draftFingerprint({ text: 'queued edit', queueEditId: 'queue-item-a' }), draftFingerprint({ text: 'queued edit' }),
'queue handoff identity never changes the submitted-content fingerprint')
const userClipboard = buildUserMessageClipboard({ ...userMessage, content: '<plain>&\u200B' })
assert.equal(userClipboard.plainText, '<plain>&', 'user text/plain omits the source zero-width marker')
assert.equal(userClipboard.html, '&lt;plain&gt;&amp;\u200B')
@@ -175,7 +232,11 @@ assert.equal(composerMarkup.indexOf('Model slot') < composerMarkup.indexOf('语
assert.ok(composerMarkup.indexOf('cr-input-toolbar__left') < composerMarkup.indexOf('cr-input-toolbar__right'))
assert.ok(composerMarkup.indexOf('cr-input-toolbar__right') < composerMarkup.indexOf('wb-composer__workspace-footer'), 'workspace footer remains outside the input toolbar')
const activeRunMarkup = renderToStaticMarkup(<ComposerControls value="next draft" onChange={() => undefined} onSend={() => undefined} onStop={() => undefined} />)
assert.match(activeRunMarkup, /aria-label="停止"/)
assert.match(activeRunMarkup, /aria-label="停止当前运行"/)
assert.match(activeRunMarkup, /aria-label="发送"/)
const emptyActiveRunMarkup = renderToStaticMarkup(<ComposerControls value="" onChange={() => undefined} onSend={() => undefined} onStop={() => undefined} />)
assert.match(emptyActiveRunMarkup, /aria-label="停止"/)
assert.doesNotMatch(emptyActiveRunMarkup, /aria-label="发送"/)
assert.doesNotMatch(activeRunMarkup, /<textarea[^>]*disabled/)
console.log('Composer/message action regression cases passed')