feat: ship skill and MCP workflows with signed updater for v0.0.7
This commit is contained in:
1 parent
1b52df0603
commit
a01085c656
136 files changed
+18646
-217
No files matched your search
@@ -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, '<plain>&\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')
|
||||
Reference in new issue
Block a user