feat: add persistent desktop assistant workspace

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
lofyer
2026-07-31 22:33:03 +08:00
co-authored by factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
parent 698a15ad14
commit 6ef1795b81
101 changed files with 31866 additions and 1176 deletions
+232
View File
@@ -0,0 +1,232 @@
import { mkdtemp, readFile, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import type { AgentEvent } from '../../shared/contracts'
import { ContinueAgentRuntime } from './continue-runtime'
import { ModelAgentRuntime } from './model-runtime'
import { OpenCodeRuntime } from './opencode-runtime'
import { AgentRuntimeController } from './runtime-controller'
const enabled = process.env.GOODBUDDY_RUN_RUNTIME_E2E === '1'
const apiKey = process.env.ANTHROPIC_API_KEY ?? ''
const configuredBaseUrl =
process.env.ANTHROPIC_BASE_URL ?? 'https://api.anthropic.com'
const baseUrl = new URL(configuredBaseUrl).origin
const modelName =
process.env.GOODBUDDY_E2E_MODEL ?? 'claude-sonnet-5'
const portableRoot = join(
process.cwd(),
'dist',
'GoodBuddy-0.1.0-win-x64-portable'
)
async function collectText(
events: AsyncGenerator<AgentEvent, void, void>
): Promise<string> {
let output = ''
for await (const event of events) {
if (event.type === 'text') {
output += event.delta
}
}
return output
}
describe.runIf(enabled)('runtime end-to-end', () => {
let workspace = ''
beforeAll(async () => {
if (!apiKey) {
throw new Error('ANTHROPIC_API_KEY is required for Runtime E2E')
}
workspace = await mkdtemp(join(tmpdir(), 'goodbuddy-runtime-e2e-'))
})
afterAll(async () => {
if (workspace) {
await new Promise((resolve) => setTimeout(resolve, 500))
await rm(workspace, { recursive: true, force: true })
}
})
it(
'streams a complete response through the direct model runtime',
async () => {
const runtime = new ModelAgentRuntime({
apiKey,
baseUrl,
model: modelName
})
try {
const output = await collectText(
runtime.run(
{
requestId: crypto.randomUUID(),
conversationId: crypto.randomUUID(),
workMode: 'ask',
prompt:
'Return exactly this text and nothing else: MODEL_E2E_OK'
},
new AbortController().signal
)
)
expect(output).toContain('MODEL_E2E_OK')
} finally {
await runtime.dispose()
}
},
120_000
)
it(
'cancels an in-flight direct model task',
async () => {
const runtime = new ModelAgentRuntime({
apiKey,
baseUrl,
model: modelName
})
const abortController = new AbortController()
try {
const result = collectText(
runtime.run(
{
requestId: crypto.randomUUID(),
conversationId: crypto.randomUUID(),
workMode: 'ask',
prompt:
'Write a detailed technical essay of at least 3000 words.'
},
abortController.signal
)
)
setTimeout(() => abortController.abort(), 50)
await expect(result).rejects.toMatchObject({
name: 'AbortError'
})
} finally {
await runtime.dispose()
}
},
120_000
)
it(
'completes an approved file task through bundled OpenCode',
async () => {
const runtime = new AgentRuntimeController(
new OpenCodeRuntime({
embedded: true,
binaryPath: '',
bundledBinaryPath: join(
portableRoot,
'resources',
'runtimes',
'opencode',
'opencode.exe'
),
configPath: '',
defaultWorkspace: workspace,
modelProfile: {
id: crypto.randomUUID(),
name: 'E2E model',
baseUrl,
modelName,
apiKey
}
})
)
const approvals: string[] = []
try {
await collectText(
runtime.run(
{
requestId: crypto.randomUUID(),
conversationId: crypto.randomUUID(),
workMode: 'execute',
prompt:
'Create opencode-output.txt in the current workspace with exactly OPENCODE_E2E_OK. Use the file tools and finish only after verifying the file.'
},
new AbortController().signal,
async (request) => {
approvals.push(request.scopeKey)
return 'once'
}
)
)
expect(approvals).toContain('runtime:whole-run')
await expect(
readFile(join(workspace, 'opencode-output.txt'), 'utf8')
).resolves.toBe('OPENCODE_E2E_OK')
} finally {
await runtime.dispose()
}
},
180_000
)
it(
'completes an approved file task through bundled Continue',
async () => {
const runtime = new AgentRuntimeController(
new ContinueAgentRuntime({
binaryPath: '',
bundledBinaryPath: join(
portableRoot,
'resources',
'runtimes',
'continue',
'dist',
'cn.js'
),
configPath: '',
mode: 'agent',
defaultWorkspace: workspace,
hostCacheRoot: join(workspace, '.continue-host'),
modelProfile: {
id: crypto.randomUUID(),
name: 'E2E model',
baseUrl,
modelName,
apiKey
}
})
)
const approvals: string[] = []
try {
const output = await collectText(
runtime.run(
{
requestId: crypto.randomUUID(),
conversationId: crypto.randomUUID(),
workMode: 'execute',
prompt:
'Create continue-output.txt in the current workspace with exactly CONTINUE_E2E_OK. Use tools and finish only after verifying the file.'
},
new AbortController().signal,
async (request) => {
approvals.push(request.scopeKey)
return 'once'
}
)
)
if (approvals.length === 0) {
throw new Error(
`Continue did not request tool approval: ${output.slice(0, 500)}`
)
}
await expect(
readFile(join(workspace, 'continue-output.txt'), 'utf8')
).resolves.toBe('CONTINUE_E2E_OK')
} finally {
await runtime.dispose()
}
},
180_000
)
})