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
@@ -0,0 +1,88 @@
|
||||
// Real ComposerControls + Slate in isolated Electron. No model, network or user storage.
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import assert from 'node:assert/strict'
|
||||
import { createRequire } from 'node:module'
|
||||
import { spawn } from 'node:child_process'
|
||||
const require = createRequire(import.meta.url), electron = require('electron')
|
||||
if (typeof electron === 'string') {
|
||||
const directory = fs.mkdtempSync(path.resolve('.tmp/capability-slash-'))
|
||||
const env = { ...process.env }; delete env.ELECTRON_RUN_AS_NODE; delete env.ELECTRON_RENDERER_URL
|
||||
const child = spawn(electron, ['--no-sandbox', path.resolve('scripts/test-capability-slash-ui.mjs'), directory], { env, windowsHide: true, stdio: 'inherit' })
|
||||
const timer = setTimeout(() => child.kill(), 90000)
|
||||
process.exitCode = await new Promise(resolve => child.once('exit', code => { clearTimeout(timer); resolve(code ?? 1) }))
|
||||
} else {
|
||||
void (async () => {
|
||||
const { app, BrowserWindow } = electron, directory = path.resolve(process.argv.at(-1))
|
||||
assert.ok(path.relative(path.resolve('.tmp'), directory).startsWith('capability-slash-'))
|
||||
app.setPath('userData', path.join(directory, 'data')); app.disableHardwareAcceleration()
|
||||
const source = file => JSON.stringify(path.resolve(file).replaceAll('\\', '/'))
|
||||
fs.writeFileSync(path.join(directory, 'fixture.tsx'), `import React,{useState}from'react';import{createRoot}from'react-dom/client';
|
||||
import{ComposerControls}from${source('src/renderer/src/workbuddy/composer/ComposerControls')};
|
||||
import{withCapabilitySelection}from${source('src/renderer/src/workbuddy/composer/capability-selection')};
|
||||
import${source('src/renderer/src/styles/tokens.css')};import${source('src/renderer/src/styles/app.css')};
|
||||
window.ycode={resources:{},chat:{}};
|
||||
function Test(){const[state,set]=useState({text:'',skills:[]}),[version,bump]=useState(0);window.__qa={state,setup:s=>{set(s);bump(v=>v+1)},toggle:()=>set(s=>withCapabilitySelection(s,{id:'archify',name:'archify',kind:'skill'},{source:'menu'}))};return <div className="chat-pane cr-theme" style={{padding:40}}><ComposerControls key={version} value={state.text} userContent={state.userContent} skills={state.skills} attachments={state.attachments??[]} selectedCapabilities={state.skills.map(s=>({id:s.id,kind:'skill'}))} capabilityItems={[{id:'archify',name:'archify',kind:'skill'},{id:'existing',name:'existing',kind:'skill'}]} onChange={text=>set(s=>({...s,text}))} onUserContentChange={userContent=>set(s=>({...s,userContent}))} onSelectCapability={(item,intent)=>set(s=>withCapabilitySelection(s,item,intent))} onRemoveSkill={id=>set(s=>({...s,skills:s.skills.filter(v=>v.id!==id)}))} onSend={()=>{throw Error('Unexpected send')}} /></div>}createRoot(document.getElementById('root')).render(<Test/>);`)
|
||||
await require('esbuild').build({ entryPoints: [path.join(directory, 'fixture.tsx')], bundle: true, platform: 'browser', format: 'esm', jsx: 'automatic', outfile: path.join(directory, 'fixture.js'), loader: { '.svg': 'file', '.png': 'file', '.jpg': 'file', '.woff': 'file', '.woff2': 'file', '.ttf': 'file' }, logLevel: 'warning' })
|
||||
fs.writeFileSync(path.join(directory, 'index.html'), '<html data-theme="light"><link rel="stylesheet" href="fixture.css"><body><div id="root"></div><script type="module" src="fixture.js"></script></body></html>')
|
||||
await app.whenReady()
|
||||
const win = new BrowserWindow({ show: false, width: 1100, height: 850, webPreferences: { offscreen: true, backgroundThrottling: false } }), checks = [], errors = []
|
||||
win.webContents.on('console-message', event => { if (event.level === 'error') errors.push(event.message) })
|
||||
const read = code => win.webContents.executeJavaScript(code), pause = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
const wait = async code => { for (let n = 0; n < 150; n++) { if (await read(code)) return; await pause(30) } throw Error('Timeout ' + code) }
|
||||
const check = (name, actual, expected = true) => { assert.deepEqual(actual, expected, name); checks.push(name) }
|
||||
const key = async (keyCode, modifiers = []) => { win.webContents.sendInputEvent({ type: 'keyDown', keyCode, modifiers }); win.webContents.sendInputEvent({ type: 'keyUp', keyCode, modifiers }); await pause(60) }
|
||||
const setup = async state => { await read(`__qa.setup(${JSON.stringify(state)})`); await pause(160); await read('document.querySelector(".wb-composer__input").focus()') }
|
||||
const choose = async (keyboard = false) => {
|
||||
await wait('!!document.querySelector(".yc-composer-slash-picker")')
|
||||
if (keyboard) { await read('document.querySelector(".yc-capability-picker input").focus()'); await key('Enter') }
|
||||
else await read('[...document.querySelectorAll(".yc-capability-picker button")].find(e=>e.textContent.includes("archify")).click()')
|
||||
await wait('!document.querySelector(".yc-composer-slash-picker")'); await pause(200)
|
||||
}
|
||||
try {
|
||||
await win.loadFile(path.join(directory, 'index.html')); await wait('!!window.__qa')
|
||||
await setup({ text: '', skills: [] }); await win.webContents.insertText('/archify'); await choose()
|
||||
check('mouse selection consumes the textarea slash', await read('__qa.state.text'), '')
|
||||
check('structured content has no stale slash', await read('__qa.state.userContent.filter(v=>v.type==="text").map(v=>v.text).join("")'), '')
|
||||
check('one selected skill survives transition into Slate', await read('__qa.state.skills.map(v=>v.id)'), ['archify'])
|
||||
check('the menu stays closed after editor remount', await read('!!document.querySelector(".yc-composer-slash-picker")'), false)
|
||||
check('the real rich editor is focused', await read('!!document.activeElement.closest(".cr-reference-editor")'))
|
||||
await setup({ text: '', skills: [] }); await win.webContents.insertText('/'); await choose(true)
|
||||
check('Enter consumes a bare slash without sending', await read('__qa.state.text'), '')
|
||||
await setup({ text: 'left RIGHT', skills: [] })
|
||||
await read('document.querySelector("textarea").setSelectionRange(5,5)'); await win.webContents.insertText('/archify'); await choose()
|
||||
check('selection preserves text after the caret', await read('__qa.state.text'), 'left RIGHT')
|
||||
await setup({ text: 'before ', skills: [{ id: 'existing', name: 'existing' }], userContent: [{ type: 'skill', id: 'existing' }, { type: 'text', text: 'before ' }] })
|
||||
await key('End', ['control']); await win.webContents.insertText('/archify'); await choose(true)
|
||||
check('rich text selection preserves the existing skill', await read('__qa.state.skills.map(v=>v.id)'), ['existing', 'archify'])
|
||||
check('rich text selection preserves surrounding text', await read('__qa.state.text'), 'before ')
|
||||
check('rich text references survive token deletion', await read('__qa.state.userContent.filter(v=>v.type==="skill").map(v=>v.id).sort()'), ['archify', 'existing'])
|
||||
await win.webContents.insertText('next'); await wait('__qa.state.text.includes("next")')
|
||||
check('typing continues at the consumed trigger position', await read('__qa.state.text'), 'before next')
|
||||
await setup({ text: '', skills: [{ id: 'archify', name: 'archify' }] })
|
||||
await key('End', ['control']); await win.webContents.insertText('/archify'); await choose()
|
||||
check('slash selection of an already selected skill never toggles it off', await read('__qa.state.skills.map(v=>v.id)'), ['archify'])
|
||||
await read('__qa.toggle()'); await pause(100)
|
||||
check('the add-menu policy still toggles an existing selection', await read('__qa.state.skills.length'), 0)
|
||||
await setup({ text: 'first\nsecond ', skills: [], attachments: [{ id: 'fixture-file', name: 'keep.txt', mimeType: 'text/plain', size: 4 }], userContent: [{ type: 'attachment', id: 'fixture-file' }, { type: 'text', text: 'first\nsecond ' }] })
|
||||
await key('End', ['control']); await win.webContents.insertText('/archify'); await choose()
|
||||
check('multiline rich text survives slash deletion', await read('__qa.state.text'), 'first\nsecond ')
|
||||
check('attachment metadata survives slash selection', await read('__qa.state.attachments.map(v=>v.id)'), ['fixture-file'])
|
||||
check('attachment inline reference survives slash selection', await read('__qa.state.userContent.some(v=>v.type==="attachment"&&v.id==="fixture-file")'))
|
||||
await setup({ text: '', skills: [] }); await win.webContents.insertText('/'); await wait('!!document.querySelector(".yc-composer-slash-picker")'); await read('document.querySelector(".yc-capability-picker input").focus()'); await key('Escape')
|
||||
await pause(250); check('Escape closes the menu until the trigger is edited', await read('!!document.querySelector(".yc-composer-slash-picker")'), false)
|
||||
await win.webContents.insertText('a'); await wait('!!document.querySelector(".yc-composer-slash-picker")')
|
||||
check('editing the dismissed trigger reopens the menu', true)
|
||||
await setup({ text: '', skills: [] })
|
||||
await read('document.querySelector("textarea").dispatchEvent(new CompositionEvent("compositionstart",{bubbles:true}))')
|
||||
await win.webContents.insertText('/'); await pause(150)
|
||||
check('IME composition does not open a slash menu', await read('!!document.querySelector(".yc-composer-slash-picker")'), false)
|
||||
await read('document.querySelector("textarea").dispatchEvent(new CompositionEvent("compositionend",{bubbles:true}))'); await wait('!!document.querySelector(".yc-composer-slash-picker")')
|
||||
check('composition completion allows slash detection', true)
|
||||
check('no renderer errors', errors, [])
|
||||
fs.writeFileSync(path.join(directory, 'report.json'), JSON.stringify({ result: 'PASS', checks, errors }, null, 2)); console.log(JSON.stringify({ result: 'PASS', checks: checks.length, directory })); app.exit(0)
|
||||
} catch (error) {
|
||||
fs.writeFileSync(path.join(directory, 'failure.json'), JSON.stringify({ error: String(error.stack ?? error), checks, errors, state: await read('__qa.state'), focus: await read('({active:document.activeElement.outerHTML,selection:document.getSelection()?.toString(),anchor:document.getSelection()?.anchorNode?.parentElement?.outerHTML})'), html: await read('document.body.innerHTML') }, null, 2)); console.error(error); app.exit(1)
|
||||
}
|
||||
})().catch(error => { console.error(error); electron.app.exit(1) })
|
||||
}
|
||||
Reference in new issue
Block a user