352 lines
26 KiB
JavaScript
352 lines
26 KiB
JavaScript
// Production Electron integration with isolated data and a localhost-only model.
|
|
const assert = require('node:assert/strict')
|
|
const fs = require('node:fs/promises')
|
|
const path = require('node:path')
|
|
|
|
if (!process.versions.electron) {
|
|
;(async () => {
|
|
await fs.mkdir(path.resolve('.tmp'), { recursive: true })
|
|
const directory = await fs.mkdtemp(path.resolve('.tmp/three-modules-'))
|
|
const env = { ...process.env, YCODE_QA_DIRECTORY: directory,
|
|
YCODE_USER_DATA_DIR: path.join(directory, 'data'), YCODE_OMP_HOME: path.join(directory, 'home'),
|
|
YCODE_DISABLE_GPU: '1', YCODE_QA_OFFSCREEN: '1', YCODE_DISABLE_SYSTEM_INTEGRATION: '1' }
|
|
await Promise.all(['data', 'home', 'workspace'].map(name => fs.mkdir(path.join(directory, name))))
|
|
delete env.ELECTRON_RUN_AS_NODE
|
|
delete env.ELECTRON_RENDERER_URL
|
|
const child = require('node:child_process').spawn(require('electron'),
|
|
['--no-sandbox', '--disable-gpu-sandbox', __filename],
|
|
{ env, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
let log = ''
|
|
child.stdout.on('data', bytes => { log += bytes; process.stdout.write(bytes) })
|
|
child.stderr.on('data', bytes => { log += bytes })
|
|
const timer = setTimeout(() => child.kill(), 120_000)
|
|
child.once('exit', async code => {
|
|
clearTimeout(timer)
|
|
await fs.writeFile(path.join(directory, 'process.log'), log)
|
|
console.log(JSON.stringify({ directory, exitCode: code }))
|
|
if (code) console.error(log.slice(-12000))
|
|
process.exitCode = code ?? 1
|
|
})
|
|
})().catch(error => { console.error(error); process.exitCode = 1 })
|
|
} else {
|
|
const { app, BrowserWindow } = require('electron')
|
|
const { pathToFileURL } = require('node:url')
|
|
const { createServer } = require('node:http')
|
|
const directory = process.env.YCODE_QA_DIRECTORY
|
|
const relative = path.relative(path.resolve('.tmp'), directory || '')
|
|
assert.ok(relative.startsWith('three-modules-') && !relative.includes(path.sep) && !path.isAbsolute(relative))
|
|
app.setPath('home', path.join(directory, 'home'))
|
|
const checks = []
|
|
const requests = []
|
|
const errors = []
|
|
let win, server, releaseManual
|
|
const manualGate = new Promise(resolve => { releaseManual = resolve })
|
|
const read = source => win.webContents.executeJavaScript(source)
|
|
const pause = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
const wait = async (operation, label) => {
|
|
for (let attempt = 0; attempt < 650; attempt++) {
|
|
const result = await operation()
|
|
if (result) return result
|
|
await pause(40)
|
|
}
|
|
throw Error(`Timeout: ${label}`)
|
|
}
|
|
const waitUi = source => wait(() => read(source), source)
|
|
const check = label => { checks.push(label); console.log(`PASS ${label}`) }
|
|
const invoke = (namespace, method, ...args) => read(`window.ycode[${JSON.stringify(namespace)}][${JSON.stringify(method)}](...${JSON.stringify(args)})`)
|
|
const fill = (selector, value, tag = 'HTMLInputElement', event = 'input') => read(`(() => {
|
|
const input = document.querySelector(${JSON.stringify(selector)});
|
|
Object.getOwnPropertyDescriptor(${tag}.prototype, 'value').set.call(input, ${JSON.stringify(value)});
|
|
input.dispatchEvent(new Event(${JSON.stringify(event)}, {bubbles:true}));
|
|
})()`)
|
|
const sse = (body, delta, finishReason = null) => `data: ${JSON.stringify({ id: 'three-modules-response', object: 'chat.completion.chunk', created: 1,
|
|
model: body.model, choices: [{ index: 0, delta, finish_reason: finishReason }] })}\n\n`
|
|
const navigate = async label => {
|
|
await read(`Array.from(document.querySelectorAll('.conversation-list-tab-button')).find(el => el.textContent.trim() === ${JSON.stringify(label)}).click()`)
|
|
}
|
|
|
|
;(async () => {
|
|
server = createServer(async (req, res) => {
|
|
let raw = ''
|
|
for await (const chunk of req) raw += chunk
|
|
const body = JSON.parse(raw || '{}')
|
|
const prompt = [...(body.messages || [])].reverse().find(message => message.role === 'user')
|
|
const text = JSON.stringify(prompt?.content ?? '')
|
|
requests.push({ model: body.model, text, tools: body.tools?.map(tool => tool.function?.name) })
|
|
if (text.includes('FORCED_FAILURE')) {
|
|
res.writeHead(400, { 'content-type': 'application/json' })
|
|
res.end(JSON.stringify({ error: { message: 'CONTROLLED_UPSTREAM_FAILURE' } }))
|
|
return
|
|
}
|
|
res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache' })
|
|
if (text.includes('PROJECT_FULL_ACCESS_COMMAND') && !body.messages.some(message => message.role === 'tool')) {
|
|
const tool = body.tools?.find(item => item.function?.name === 'execute_command')
|
|
assert.ok(tool, 'actual model tool schema must expose command execution')
|
|
const args = { command: `node -e "require('fs').writeFileSync('project-full-access.txt','FULL_ACCESS_OK')"` }
|
|
res.write(sse(body, { role: 'assistant', tool_calls: [{ index: 0, id: 'three-modules-command', type: 'function',
|
|
function: { name: tool.function.name, arguments: JSON.stringify(args) } }] }))
|
|
res.write(sse(body, {}, 'tool_calls'))
|
|
} else if (text.includes('SCHEDULE_WRITE_ARTIFACT') && !body.messages.some(message => message.role === 'tool')) {
|
|
const tool = body.tools?.find(item => /^(write|write_file|write_to_file)$/i.test(item.function?.name))
|
|
assert.ok(tool, 'actual model tool schema must expose file writing')
|
|
const args = { path: 'scheduled-report.md', file_path: 'scheduled-report.md', content: '# Scheduled artifact\n\nGenerated through the real tool loop.\n' }
|
|
res.write(sse(body, { role: 'assistant', tool_calls: [{ index: 0, id: 'three-modules-write', type: 'function',
|
|
function: { name: tool.function.name, arguments: JSON.stringify(args) } }] }))
|
|
res.write(sse(body, {}, 'tool_calls'))
|
|
} else {
|
|
res.write(sse(body, { role: 'assistant', content: text.includes('MANUAL_HELD') ? 'MANUAL_PARTIAL' : 'SCHEDULE_SUCCEEDED' }))
|
|
if (text.includes('MANUAL_HELD')) await manualGate
|
|
if (res.destroyed) return
|
|
res.write(sse(body, { content: '\nFINAL_PERSISTED_RESULT' }))
|
|
res.write(sse(body, {}, 'stop'))
|
|
}
|
|
res.end('data: [DONE]\n\n')
|
|
})
|
|
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
|
|
await import(pathToFileURL(path.resolve('out/main/index.mjs')).href)
|
|
await app.whenReady()
|
|
await wait(async () => {
|
|
win = BrowserWindow.getAllWindows().find(item => item.getTitle() === 'YCode')
|
|
return win && !win.webContents.isLoading() && await read('!!window.ycode?.automation && !!document.querySelector(".chat-pane")')
|
|
}, 'production renderer')
|
|
win.setSize(1336, 902)
|
|
win.webContents.on('console-message', (...args) => {
|
|
const details = args[0]
|
|
const level = typeof details === 'object' ? details.level : args[1]
|
|
const message = typeof details === 'object' ? details.message : args[2]
|
|
if (level === 'error' || Number(level) >= 3) errors.push(message)
|
|
})
|
|
const registry = await invoke('workspace', 'attach', path.join(directory, 'workspace'))
|
|
const workspaceId = registry.registry?.selectedWorkspaceId ?? registry.selectedWorkspaceId
|
|
assert.ok(workspaceId, JSON.stringify(registry))
|
|
await invoke('models', 'save', { id: 'three-modules-model', name: 'Three modules QA', providerId: 'custom',
|
|
model: 'three-modules-upstream', baseUrl: `http://127.0.0.1:${server.address().port}/v1`,
|
|
authMode: 'none', supportsToolCall: true, maxToolRounds: 5,
|
|
supportsReasoning: true, defaultEffort: 'medium', supportedEfforts: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
canDisableThinking: true })
|
|
await invoke('models', 'select', 'three-modules-model')
|
|
const project = await invoke('projects', 'create', { name: '三模块验收', instructions: 'QA_PROJECT_INSTRUCTION',
|
|
workspaceId, experts: [], skills: [], connectors: [] })
|
|
const definition = await invoke('automation', 'create', { name: '自动产物验收', prompt: 'SCHEDULE_WRITE_ARTIFACT',
|
|
schedule: { type: 'once', scheduledAt: new Date(Date.now() + 2500).toISOString(), timeZone: 'Asia/Shanghai' },
|
|
execution: { projectId: project.projectId, workspaceId, modelId: 'three-modules-model', permissionMode: 'full-access' } })
|
|
const completed = await wait(async () => (await invoke('automation', 'listRuns', { automationId: definition.id }))
|
|
.find(run => ['succeeded', 'failed', 'cancelled'].includes(run.status)), 'scheduled execution terminal')
|
|
assert.equal(completed.status, 'succeeded', JSON.stringify(completed))
|
|
assert.ok(completed.sessionId && completed.runId)
|
|
const messages = await invoke('session', 'messages', completed.sessionId)
|
|
assert.equal(messages.at(-1).status, 'done')
|
|
assert.ok(messages.at(-1).content.includes('FINAL_PERSISTED_RESULT'))
|
|
const session = (await invoke('session', 'list')).find(item => item.id === completed.sessionId)
|
|
assert.equal(session.projectId, project.projectId)
|
|
assert.equal(session.workspaceId, workspaceId)
|
|
assert.equal(session.modelId, 'three-modules-model')
|
|
await waitUi(`!!document.querySelector('[data-session-id="${completed.sessionId}"]')`)
|
|
assert.equal(await fs.readFile(path.join(directory, 'workspace', 'scheduled-report.md'), 'utf8'), '# Scheduled artifact\n\nGenerated through the real tool loop.\n')
|
|
check('scheduled task executes a real model/tool turn with frozen project, workspace and model; terminal follows persistence')
|
|
|
|
const manualDefinition = await invoke('automation', 'create', { name: '运行中验收', prompt: 'MANUAL_HELD',
|
|
schedule: { type: 'once', scheduledAt: new Date(Date.now() + 3600_000).toISOString(), timeZone: 'Asia/Shanghai' },
|
|
execution: { workspaceId, modelId: 'three-modules-model' } })
|
|
const beforeManual = Date.now()
|
|
const acknowledged = await invoke('automation', 'runNow', manualDefinition.id)
|
|
assert.equal(acknowledged.status, 'running')
|
|
assert.ok(Date.now() - beforeManual < 5000, 'runNow must acknowledge before the held response completes')
|
|
const running = await wait(async () => (await invoke('automation', 'listRuns', { automationId: manualDefinition.id })).find(run => run.sessionId), 'real session linked while running')
|
|
assert.equal(running.status, 'running')
|
|
assert.equal(await invoke('automation', 'runNow', manualDefinition.id).then(() => false, () => true), true)
|
|
check('manual execution acknowledges immediately, publishes real session while streaming and rejects overlapping execution')
|
|
releaseManual()
|
|
await wait(async () => (await invoke('automation', 'listRuns', { automationId: manualDefinition.id })).some(run => run.status === 'succeeded'), 'manual terminal')
|
|
await invoke('automation', 'setEnabled', manualDefinition.id, false)
|
|
assert.equal(await invoke('automation', 'runNow', manualDefinition.id).then(() => false, () => true), true)
|
|
|
|
const failing = await invoke('automation', 'create', { name: '失败验收', prompt: 'FORCED_FAILURE',
|
|
schedule: { type: 'once', scheduledAt: new Date(Date.now() + 3600_000).toISOString(), timeZone: 'UTC' },
|
|
execution: { workspaceId, modelId: 'three-modules-model' } })
|
|
await invoke('automation', 'runNow', failing.id)
|
|
const failure = await wait(async () => (await invoke('automation', 'listRuns', { automationId: failing.id })).find(run => run.status === 'failed'), 'actual upstream failure')
|
|
assert.ok(failure.error)
|
|
assert.equal((await invoke('session', 'messages', failure.sessionId)).at(-1).status, 'error')
|
|
check('upstream error is persisted as a failed automation and assistant error; paused task cannot run')
|
|
|
|
const snapshot = await invoke('library', 'getSnapshot')
|
|
const artifact = snapshot.artifacts.find(entry => entry.sessionId === completed.sessionId && entry.name === 'scheduled-report.md')
|
|
assert.ok(artifact, JSON.stringify(snapshot))
|
|
assert.equal(artifact.availability, 'available')
|
|
assert.equal(artifact.projectId, project.projectId)
|
|
assert.equal(snapshot.artifacts.length, 1, 'read-only source and unrelated workspace files must not be indexed')
|
|
await navigate('资料库')
|
|
await waitUi('!!document.querySelector(".library-page")')
|
|
await read(`Array.from(document.querySelectorAll('.library-nav-item')).find(el => el.textContent.includes('本地产物')).click()`)
|
|
await waitUi('!!document.querySelector(".library-file-row")')
|
|
await read('document.querySelector(".library-file-row button[aria-label^=更多操作]").click()')
|
|
await waitUi('!!document.querySelector(".library-file-menu .library-favorite")')
|
|
await read('document.querySelector(".library-favorite").click()')
|
|
await wait(async () => (await invoke('library', 'getSnapshot')).artifacts.find(entry => entry.id === artifact.id)?.favorite,
|
|
'favorite saved through visible more menu')
|
|
await fs.writeFile(path.join(directory, 'library-light.png'), (await win.webContents.capturePage()).toPNG())
|
|
await read('document.querySelector(".library-file-button").click()')
|
|
await waitUi('!!document.querySelector(".chat-pane") && document.body.innerText.includes("Scheduled artifact")')
|
|
const recent = await invoke('library', 'getSnapshot')
|
|
assert.ok(recent.artifacts[0].recentAt)
|
|
assert.equal(recent.artifacts[0].favorite, true)
|
|
check('library lists only the actual generated artifact and opens it through its source session; favorite/recent persist')
|
|
|
|
const loaded = new Promise(resolve => win.webContents.once('did-finish-load', resolve))
|
|
win.webContents.reload()
|
|
await loaded
|
|
await waitUi('!!window.ycode?.automation && !!document.querySelector(".conversation-list-tab-button")')
|
|
assert.equal((await invoke('library', 'getSnapshot')).artifacts[0].favorite, true)
|
|
assert.ok((await invoke('automation', 'list')).some(item => item.id === definition.id))
|
|
await navigate('定时任务')
|
|
await waitUi('!!document.querySelector(".automation-page")')
|
|
await waitUi('document.body.innerText.includes("自动产物验收")')
|
|
await fs.writeFile(path.join(directory, 'automations-light.png'), (await win.webContents.capturePage()).toPNG())
|
|
await read('localStorage.setItem("ycode:theme", "dark"); window.dispatchEvent(new StorageEvent("storage", {key:"ycode:theme",newValue:"dark"}))')
|
|
await waitUi('document.documentElement.dataset.theme === "dark"')
|
|
await pause(350)
|
|
await fs.writeFile(path.join(directory, 'automations-dark.png'), (await win.webContents.capturePage()).toPNG())
|
|
check('definitions, run records and library preferences survive renderer reload; real pages render in light/dark')
|
|
|
|
await read('document.querySelector(".automation-create-main").click()')
|
|
await waitUi('!!document.querySelector(".automation-editor-modal")')
|
|
await read('document.querySelector("button[aria-label=重复频率]").click()')
|
|
await waitUi('document.querySelectorAll(".atm-freq-menu-popover [role=option]").length === 7')
|
|
await read('Array.from(document.querySelectorAll(".atm-freq-menu-popover [role=option]")).find(el => el.textContent.trim() === "双周").click()')
|
|
await waitUi('!document.querySelector(".atm-freq-menu-popover")')
|
|
await fill('.automation-name-input', '表单验收')
|
|
await fill('.automation-prompt-input', 'FORM_DAILY_TASK', 'HTMLTextAreaElement')
|
|
await fs.writeFile(path.join(directory, 'automation-editor-dark.png'), (await win.webContents.capturePage()).toPNG())
|
|
await read('document.querySelector(".automation-editor-footer .automation-button--primary").click()')
|
|
await waitUi('!!document.querySelector(".automation-full-access-confirm")')
|
|
assert.equal(await read('document.querySelector("[data-testid=automation-full-access-confirm-ok]").disabled'), true)
|
|
await read('document.querySelector(".automation-full-access-confirm__checkbox input").click()')
|
|
await read('document.querySelector("[data-testid=automation-full-access-confirm-ok]").click()')
|
|
await waitUi(`!document.querySelector('.automation-editor-modal') && !!document.querySelector('button[aria-label="编辑 表单验收"]')`)
|
|
const formCreated = (await invoke('automation', 'list')).find(item => item.name === '表单验收')
|
|
assert.equal(formCreated.schedule.frequency, 'biweekly')
|
|
assert.ok(formCreated.schedule.rrule.includes('INTERVAL=2'))
|
|
assert.equal(formCreated.execution.modelId, 'three-modules-model')
|
|
await read(`document.querySelector('button[aria-label="编辑 表单验收"]').click()`)
|
|
await waitUi('!!document.querySelector(".automation-prompt-input")')
|
|
await fill('.automation-prompt-input', 'FORM_EDITED_TASK', 'HTMLTextAreaElement')
|
|
await read('document.querySelector(".automation-editor-footer .automation-button--primary").click()')
|
|
await waitUi('!document.querySelector(".automation-editor-modal")')
|
|
const formEdited = (await invoke('automation', 'list')).find(item => item.id === formCreated.id)
|
|
assert.equal(formEdited.prompt, 'FORM_EDITED_TASK')
|
|
assert.equal(formEdited.schedule.anchorAt, formCreated.schedule.anchorAt)
|
|
await read(`document.querySelector('button[aria-label="暂停 表单验收"]').click()`)
|
|
await waitUi(`!!document.querySelector('button[aria-label="启用 表单验收"]')`)
|
|
assert.equal((await invoke('automation', 'list')).find(item => item.id === formCreated.id).status, 'PAUSED')
|
|
await read(`document.querySelector('button[aria-label="更多操作 表单验收"]').click()`)
|
|
await waitUi('!!document.querySelector(".automation-task-menu")')
|
|
await read('Array.from(document.querySelectorAll(".automation-task-menu button")).find(el=>el.textContent.trim()==="删除").click()')
|
|
await waitUi('!!document.querySelector(".automation-confirm-dialog")')
|
|
await read('document.querySelector(".automation-confirm-actions .automation-button--danger").click()')
|
|
await waitUi(`!document.querySelector('.automation-confirm-dialog') && !document.querySelector('button[aria-label="编辑 表单验收"]')`)
|
|
assert.ok(!(await invoke('automation', 'list')).some(item => item.id === formCreated.id))
|
|
check('visible editor creates/edits a biweekly rule without moving its anchor, then pauses/deletes through real IPC')
|
|
|
|
await navigate('资料库')
|
|
await waitUi('!!document.querySelector(".library-page")')
|
|
await read(`Array.from(document.querySelectorAll('.library-nav-item')).find(el => el.textContent.includes('本地产物')).click()`)
|
|
await waitUi('!!document.querySelector(".library-file-row")')
|
|
await pause(350)
|
|
await fs.writeFile(path.join(directory, 'library-dark.png'), (await win.webContents.capturePage()).toPNG())
|
|
await read('document.querySelector(".conversation-list-tab-button-more").click()')
|
|
await waitUi('!!document.querySelector(".yc-project-more button")')
|
|
await read('document.querySelector(".yc-project-more button").click()')
|
|
await waitUi('!!document.querySelector(".project-grid__card[role=button]")')
|
|
await read('new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))')
|
|
await read(`document.querySelector('.project-grid__card[role=button]')
|
|
.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))`)
|
|
await waitUi('!!document.querySelector("textarea[aria-label=项目任务输入]")')
|
|
await read('document.querySelector(".project-detail-view__project-composer .cr-model-selector__trigger").click()')
|
|
await waitUi('!!document.querySelector(".cr-model-selector__menu")')
|
|
await waitUi('!!document.querySelector(".cr-model-selector__menu .cr-model-selector__item[aria-selected=true]")')
|
|
await pause(220)
|
|
const modelPoint = await read(`(() => {
|
|
const item = document.querySelector('.cr-model-selector__menu .cr-model-selector__item[aria-selected="true"]')
|
|
if (!item) return null
|
|
const rect = item.getBoundingClientRect()
|
|
return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 }
|
|
})()`)
|
|
assert.ok(modelPoint)
|
|
win.webContents.sendInputEvent({ type: 'mouseMove', x: Math.round(modelPoint.x), y: Math.round(modelPoint.y) })
|
|
await waitUi('!!document.querySelector(".cr-model-selector__sub-menu-action")')
|
|
await pause(220)
|
|
const effortPoint = await read(`(() => {
|
|
const item = document.querySelector('.cr-model-selector__sub-menu-action')
|
|
const rect = item.getBoundingClientRect()
|
|
return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 }
|
|
})()`)
|
|
win.webContents.sendInputEvent({ type: 'mouseMove', x: Math.round(effortPoint.x), y: Math.round(effortPoint.y) })
|
|
await waitUi('!!document.querySelector(".cr-model-selector__sub-panel")')
|
|
await read(`Array.from(document.querySelectorAll('.cr-model-selector__sub-panel-item'))
|
|
.find(el => el.textContent.trim() === '极致').click()`)
|
|
await waitUi(`Array.from(document.querySelectorAll('.cr-model-selector__sub-panel-item'))
|
|
.some(el => el.textContent.trim() === '极致' && el.classList.contains('cr-model-selector__sub-panel-item--active'))`)
|
|
win.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Escape' })
|
|
win.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'Escape' })
|
|
await waitUi('!document.querySelector(".cr-model-selector__menu")')
|
|
await read('document.querySelector(".project-detail-view__project-composer .cr-permission-setting").click()')
|
|
await waitUi('!!document.querySelector(".cr-permission-panel input[role=switch]")')
|
|
await read('document.querySelector(".cr-permission-panel input[role=switch]").click()')
|
|
await waitUi('!!document.querySelector(".permission-confirm-dialog")')
|
|
assert.equal(await read('document.querySelector(".permission-confirm-dialog .wb-composer__danger-button").disabled'), true)
|
|
await read('document.querySelector(".permission-confirm-acknowledgement").click()')
|
|
await read('document.querySelector(".permission-confirm-dialog .wb-composer__danger-button").click()')
|
|
await waitUi('document.querySelector(".project-detail-view__project-composer .cr-permission-setting")?.textContent.includes("允许完全访问")')
|
|
const projectSessionIdsBefore = new Set((await invoke('session', 'list')).map(item => item.id))
|
|
await fill('textarea[aria-label=项目任务输入]', 'PROJECT_FULL_ACCESS_COMMAND', 'HTMLTextAreaElement')
|
|
await read('document.querySelector(".project-detail-view__project-composer .cr-send-button").click()')
|
|
await waitUi('!document.querySelector(".project-detail-view") && document.body.innerText.includes("PROJECT_FULL_ACCESS_COMMAND") && document.body.innerText.includes("FINAL_PERSISTED_RESULT")')
|
|
const projectTask = (await invoke('session', 'list')).find(item => item.projectId === project.projectId && !projectSessionIdsBefore.has(item.id))
|
|
assert.ok(projectTask)
|
|
assert.equal((await invoke('session', 'messages', projectTask.id)).find(message => message.role === 'user').content, 'PROJECT_FULL_ACCESS_COMMAND')
|
|
assert.equal(projectTask.projectId, project.projectId)
|
|
assert.equal(projectTask.thinkingLevel, 'max')
|
|
assert.equal(await fs.readFile(path.join(directory, 'workspace', 'project-full-access.txt'), 'utf8'), 'FULL_ACCESS_OK')
|
|
check('project composer forwards max thinking and full-access, switches immediately, and executes a real privileged tool turn')
|
|
|
|
// A separate, unused directory avoids Windows locking an OMP process cwd.
|
|
const failingWorkspace = path.resolve(directory, 'first-send-failure-workspace')
|
|
assert.ok(failingWorkspace.startsWith(path.resolve(directory) + path.sep))
|
|
await fs.mkdir(failingWorkspace)
|
|
const failingRegistry = await invoke('workspace', 'attach', failingWorkspace)
|
|
await invoke('projects', 'create', { name: '失败恢复项目', instructions: '',
|
|
workspaceId: failingRegistry.selectedWorkspaceId, experts: [], skills: [], connectors: [] })
|
|
await read('document.querySelector(".conversation-list-tab-button-more").click()')
|
|
await waitUi('!!document.querySelector(".yc-project-more button")')
|
|
await read('document.querySelector(".yc-project-more button").click()')
|
|
await waitUi(`Array.from(document.querySelectorAll('.project-grid__card[role=button]')).some(el => el.textContent.includes('失败恢复项目'))`)
|
|
await read(`Array.from(document.querySelectorAll('.project-grid__card[role=button]')).find(el => el.textContent.includes('失败恢复项目')).click()`)
|
|
await waitUi('!!document.querySelector("textarea[aria-label=项目任务输入]")')
|
|
const beforeFailedSend = (await invoke('session', 'list')).length
|
|
await fs.rmdir(failingWorkspace)
|
|
try {
|
|
await fill('textarea[aria-label=项目任务输入]', 'RESTORE_PROJECT_DRAFT', 'HTMLTextAreaElement')
|
|
await read('document.querySelector(".project-detail-view__project-composer .cr-send-button").click()')
|
|
await waitUi('!document.querySelector(".project-detail-view") && document.querySelector(".wb-composer__input")?.value === "RESTORE_PROJECT_DRAFT"')
|
|
assert.equal((await invoke('session', 'list')).length, beforeFailedSend)
|
|
} finally { await fs.mkdir(failingWorkspace) }
|
|
check('project first-send failure restores the composer draft and does not leave an empty session')
|
|
assert.deepEqual(errors, [])
|
|
assert.ok(requests.every(request => request.model === 'three-modules-upstream'))
|
|
await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify({ result: 'PASS', checks, completed, failure, artifact, requests, rendererErrors: errors }, null, 2))
|
|
console.log(`THREE_MODULES_PASS ${directory}`)
|
|
server.closeAllConnections()
|
|
server.close()
|
|
app.exit(0)
|
|
})().catch(async error => {
|
|
console.error(error)
|
|
releaseManual?.()
|
|
if (win && !win.isDestroyed()) await fs.writeFile(path.join(directory, 'failure.png'), (await win.webContents.capturePage()).toPNG()).catch(() => {})
|
|
await fs.writeFile(path.join(directory, 'failure.json'), JSON.stringify({ error: String(error.stack || error), checks, requests, rendererErrors: errors }, null, 2))
|
|
server?.closeAllConnections()
|
|
server?.close()
|
|
app.exit(1)
|
|
})
|
|
}
|