mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
feat(source-control-ai): support OMP generation (#20624)
* feat(source-control-ai): support OMP text generation Read prompts on stdin, retain OMP configured model by default, and reuse JSON model discovery. Co-authored-by: unknown <1784931579@qq.com> * test(source-control-ai): cover OMP large input and model overrides * fix(omp): keep configured model default out of discovered catalog * fix(omp): hide config default from model discovery catalog * fix(omp): separate terminal discovery from generation defaults * test(omp): keep model probe import compatible with CLI typecheck * test: align Source Control AI registry contracts with OMP --------- Co-authored-by: unknown <1784931579@qq.com>
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
import { expect, it } from 'vitest'
|
||||
import { getAgentModelProbeSpec } from '../../shared/agent-model-probe-spec'
|
||||
import { finalizeModelDiscoveryOutput } from './commit-message-model-discovery-policy'
|
||||
|
||||
const output = JSON.stringify({
|
||||
models: [
|
||||
{
|
||||
provider: 'provider',
|
||||
id: 'exact-model',
|
||||
selector: 'provider/exact-model',
|
||||
name: 'Exact model'
|
||||
}
|
||||
]
|
||||
})
|
||||
it('keeps the OMP configured default out of generic terminal discovery results', () => {
|
||||
const spec = getAgentModelProbeSpec('omp')
|
||||
if (!spec) {
|
||||
throw new Error('Missing OMP probe spec')
|
||||
}
|
||||
expect(finalizeModelDiscoveryOutput(spec, output, '', 0)).toMatchObject({
|
||||
success: true,
|
||||
defaultModelId: 'provider/exact-model',
|
||||
models: [{ id: 'provider/exact-model' }]
|
||||
})
|
||||
})
|
||||
@@ -62,7 +62,8 @@ const AGENT_ARGS_PLACEHOLDER_OVERRIDES: Partial<Record<TuiAgent, string>> = {
|
||||
// Why: Source Control AI action prompts are short, reviewable tasks; the
|
||||
// mini Codex model is a better default hint than the frontier model.
|
||||
codex: '--model gpt-5.4-mini',
|
||||
copilot: '--model gpt-5.4-mini'
|
||||
copilot: '--model gpt-5.4-mini',
|
||||
omp: '--model <provider/model>'
|
||||
}
|
||||
|
||||
const MODEL_FLAG_BY_AGENT: Partial<Record<TuiAgent, string>> = {
|
||||
|
||||
@@ -31,14 +31,23 @@ describe('getAgentModelProbeSpec', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('aliases commit-message agents by identity rather than copying them', () => {
|
||||
it('aliases agents without a generation-only default by identity', () => {
|
||||
// A lossy adapter here would silently drop fields like
|
||||
// `modelDiscovery.stdinPayload` and break Claude discovery.
|
||||
for (const id of listCommitMessageAgentIds()) {
|
||||
for (const id of listCommitMessageAgentIds().filter((agentId) => agentId !== 'omp')) {
|
||||
expect(getAgentModelProbeSpec(id)).toBe(getCommitMessageAgentSpec(id))
|
||||
}
|
||||
})
|
||||
|
||||
it('removes only the OMP generation sentinel while preserving discovery', () => {
|
||||
const generation = getCommitMessageAgentSpec('omp')!
|
||||
const probe = getAgentModelProbeSpec('omp')!
|
||||
expect(generation.defaultModelId).toBe('default')
|
||||
expect(probe.defaultModelId).toBe('')
|
||||
expect(probe.models).toEqual([])
|
||||
expect(probe.modelDiscovery).toBe(generation.modelDiscovery)
|
||||
})
|
||||
|
||||
it('keeps grok out of the commit-message registry', () => {
|
||||
expect(getCommitMessageAgentSpec('grok')).toBeUndefined()
|
||||
expect(listCommitMessageAgentIds()).not.toContain('grok')
|
||||
|
||||
@@ -26,5 +26,17 @@ const MODEL_DISCOVERY_ONLY_SPECS: Partial<Record<TuiAgent, AgentModelProbeSpec>>
|
||||
}
|
||||
|
||||
export function getAgentModelProbeSpec(agentId: TuiAgent): AgentModelProbeSpec | undefined {
|
||||
return getCommitMessageAgentSpec(agentId) ?? MODEL_DISCOVERY_ONLY_SPECS[agentId]
|
||||
const spec = getCommitMessageAgentSpec(agentId) ?? MODEL_DISCOVERY_ONLY_SPECS[agentId]
|
||||
if (!spec) {
|
||||
return undefined
|
||||
}
|
||||
// OMP's `default` means the provider configured in its own settings, not a selectable model.
|
||||
if (agentId !== 'omp') {
|
||||
return spec
|
||||
}
|
||||
return {
|
||||
...spec,
|
||||
models: spec.models.filter((model) => model.id !== 'default'),
|
||||
defaultModelId: spec.defaultModelId === 'default' ? '' : spec.defaultModelId
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ describe('COMMIT_MESSAGE_AGENT_SPECS', () => {
|
||||
'copilot',
|
||||
'cursor',
|
||||
'kimi',
|
||||
'omp',
|
||||
'opencode',
|
||||
'pi'
|
||||
])
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { OMP_MODEL_LIST_ARGS, parseOmpModelList } from './omp-model-list-probe'
|
||||
import type { TuiAgent } from './tui-agent'
|
||||
import { isTuiAgentEnabled } from './tui-agent-selection'
|
||||
import { labelFromModelId } from './model-id-label'
|
||||
@@ -88,6 +89,29 @@ export type CommitMessageAgentCapability = {
|
||||
}
|
||||
|
||||
export const COMMIT_MESSAGE_AGENT_SPECS: Partial<Record<TuiAgent, CommitMessageAgentSpec>> = {
|
||||
omp: {
|
||||
id: 'omp',
|
||||
label: 'OMP',
|
||||
binary: 'omp',
|
||||
promptDelivery: 'stdin',
|
||||
buildArgs: ({ model, thinkingLevel }) => [
|
||||
'--print',
|
||||
'--no-session',
|
||||
'--no-tools',
|
||||
'--no-extensions',
|
||||
'--no-skills',
|
||||
'--no-rules',
|
||||
'--mode',
|
||||
'text',
|
||||
...(model && model !== 'default' ? ['--model', model] : []),
|
||||
...(thinkingLevel ? ['--thinking', thinkingLevel] : [])
|
||||
],
|
||||
singletonOptions: [['--model'], ['--thinking']],
|
||||
modelSource: 'dynamic',
|
||||
modelDiscovery: { binary: 'omp', args: OMP_MODEL_LIST_ARGS, parse: parseOmpModelList },
|
||||
models: [{ id: 'default', label: 'Config default' }],
|
||||
defaultModelId: 'default'
|
||||
},
|
||||
...buildPrimaryCommitMessageAgentSpecs({
|
||||
CLAUDE_THINKING_LEVELS,
|
||||
OPENAI_THINKING_LEVELS,
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { planCommitMessageGeneration } from './commit-message-plan'
|
||||
import { getAgentModelProbeSpec } from './agent-model-probe-spec'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getDefaultSettings } from './constants'
|
||||
import { getCommitMessageAgentSpec } from './commit-message-agent-spec'
|
||||
import { resolveSourceControlAiForOperation } from './source-control-ai'
|
||||
|
||||
describe('OMP Source Control AI', () => {
|
||||
it.each(['commitMessage', 'pullRequest', 'branchName'] as const)(
|
||||
'uses the configured OMP default for %s without requiring a model override',
|
||||
(operation) => {
|
||||
const settings = getDefaultSettings('/tmp')
|
||||
settings.defaultTuiAgent = 'omp'
|
||||
const result = resolveSourceControlAiForOperation({ settings, repo: null, operation })
|
||||
expect(result).toMatchObject({
|
||||
ok: true,
|
||||
value: { params: { agentId: 'omp', model: 'default' } }
|
||||
})
|
||||
}
|
||||
)
|
||||
it('delivers large diffs on stdin and keeps generation isolated from tools and extensions', () => {
|
||||
const spec = getCommitMessageAgentSpec('omp')
|
||||
expect(spec?.promptDelivery).toBe('stdin')
|
||||
if (!spec) {
|
||||
throw new Error('Missing OMP spec')
|
||||
}
|
||||
const args = spec.buildArgs({ prompt: 'large diff', model: 'default' })
|
||||
expect(args).toEqual([
|
||||
'--print',
|
||||
'--no-session',
|
||||
'--no-tools',
|
||||
'--no-extensions',
|
||||
'--no-skills',
|
||||
'--no-rules',
|
||||
'--mode',
|
||||
'text'
|
||||
])
|
||||
expect(
|
||||
spec?.buildArgs({ prompt: '', model: 'provider/exact-model', thinkingLevel: 'low' })
|
||||
).toEqual([...args, '--model', 'provider/exact-model', '--thinking', 'low'])
|
||||
})
|
||||
it('discovers provider-qualified models through the existing OMP JSON parser', () => {
|
||||
const discovery = getCommitMessageAgentSpec('omp')?.modelDiscovery
|
||||
expect(discovery?.args).toEqual(['models', '--json'])
|
||||
expect(
|
||||
discovery?.parse(
|
||||
JSON.stringify({
|
||||
models: [{ provider: 'provider', id: 'm', selector: 'provider/m', name: 'Model' }]
|
||||
})
|
||||
)
|
||||
).toEqual([{ id: 'provider/m', label: 'Model', description: 'provider' }])
|
||||
})
|
||||
it.each(['escape', 'literal'] as const)(
|
||||
'plans recipe model overrides with %s path parsing',
|
||||
(backslash) => {
|
||||
const prompt = `diff --git a/a b/a\n${'large patch\n'.repeat(10000)}`
|
||||
const result = planCommitMessageGeneration(
|
||||
{
|
||||
agentId: 'omp',
|
||||
model: 'provider/model',
|
||||
backslash,
|
||||
agentArgs: '--model provider/override'
|
||||
},
|
||||
prompt
|
||||
)
|
||||
expect(result.ok).toBe(true)
|
||||
if (!result.ok) {
|
||||
throw new Error(result.error)
|
||||
}
|
||||
expect(result.plan.stdinPayload).toBe(prompt)
|
||||
expect(result.plan.args).not.toContain(prompt)
|
||||
expect(result.plan.args.filter((arg) => arg === '--model')).toHaveLength(1)
|
||||
expect(result.plan.args).toContain('provider/override')
|
||||
expect(result.plan.args).not.toContain('provider/model')
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('does not expose OMP config default as a terminal discovery model', () => {
|
||||
const spec = getAgentModelProbeSpec('omp')
|
||||
expect(spec?.models.some((model) => model.id === 'default')).toBe(false)
|
||||
expect(spec?.defaultModelId).toBe('')
|
||||
})
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { CommitMessageModel } from './commit-message-agent-spec'
|
||||
import { labelFromModelId } from './model-id-label'
|
||||
|
||||
// Why: `omp models --json` is the one machine-readable listing OMP offers; the
|
||||
// default table output groups rows per provider and would need a brittle parser.
|
||||
export const OMP_MODEL_LIST_ARGS = ['models', '--json']
|
||||
|
||||
/** The outermost JSON value on stdout, or null when none parses. */
|
||||
function parseJsonObject(stdout: string): unknown {
|
||||
const trimmed = stdout.trim()
|
||||
try {
|
||||
return JSON.parse(trimmed)
|
||||
} catch {
|
||||
// Why: an update notice or extension warning can precede the JSON on stdout;
|
||||
// the listing itself is the outermost object.
|
||||
const start = trimmed.indexOf('{')
|
||||
const end = trimmed.lastIndexOf('}')
|
||||
if (start === -1 || end <= start) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
return JSON.parse(trimmed.slice(start, end + 1))
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Parses `omp models --json`. Ids are OMP's `provider/model` selector — the form
|
||||
* `--model` and `/model` resolve exactly, unlike a bare model id that several
|
||||
* providers can share. */
|
||||
export function parseOmpModelList(stdout: string): CommitMessageModel[] {
|
||||
const parsed = parseJsonObject(stdout)
|
||||
if (!parsed || typeof parsed !== 'object' || !('models' in parsed)) {
|
||||
return []
|
||||
}
|
||||
const rows: unknown = parsed.models
|
||||
if (!Array.isArray(rows)) {
|
||||
return []
|
||||
}
|
||||
const byId = new Map<string, CommitMessageModel>()
|
||||
for (const row of rows) {
|
||||
const value: unknown = row
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
continue
|
||||
}
|
||||
const provider =
|
||||
'provider' in value && typeof value.provider === 'string' ? value.provider.trim() : ''
|
||||
const bareId = 'id' in value && typeof value.id === 'string' ? value.id.trim() : ''
|
||||
const selector =
|
||||
'selector' in value && typeof value.selector === 'string' ? value.selector.trim() : ''
|
||||
const id = selector || (provider && bareId ? `${provider}/${bareId}` : '')
|
||||
if (!id || byId.has(id)) {
|
||||
continue
|
||||
}
|
||||
const name = 'name' in value && typeof value.name === 'string' ? value.name.trim() : ''
|
||||
byId.set(id, {
|
||||
id,
|
||||
label: name || labelFromModelId(id),
|
||||
// Why: the same model name ships under several providers; the provider is
|
||||
// what tells two "DeepSeek V4 Pro" rows apart in the picker.
|
||||
...(provider ? { description: provider } : {})
|
||||
})
|
||||
}
|
||||
return [...byId.values()]
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getDefaultSettings } from './constants'
|
||||
import { getCommitMessageAgentSpec } from './commit-message-agent-spec'
|
||||
import {
|
||||
resolveSourceControlAiForOperation,
|
||||
normalizeSourceControlAiSettings
|
||||
} from './source-control-ai'
|
||||
|
||||
for (const hostKey of ['local', 'ssh:omp-host']) {
|
||||
describe(`OMP discovery preserves generation choices on ${hostKey}`, () => {
|
||||
it.each([undefined, 'default', 'provider/exact-model'])(
|
||||
'retains selected model %s when a discovered catalog is cached',
|
||||
(selectedModel) => {
|
||||
const spec = getCommitMessageAgentSpec('omp')
|
||||
if (!spec) {
|
||||
throw new Error('Missing OMP spec')
|
||||
}
|
||||
const models =
|
||||
spec.modelDiscovery?.parse(
|
||||
JSON.stringify({
|
||||
models: [
|
||||
{
|
||||
provider: 'provider',
|
||||
id: 'other-model',
|
||||
selector: 'provider/other-model',
|
||||
name: 'Other model'
|
||||
},
|
||||
{
|
||||
provider: 'provider',
|
||||
id: 'exact-model',
|
||||
selector: 'provider/exact-model',
|
||||
name: 'Exact model'
|
||||
}
|
||||
]
|
||||
})
|
||||
) ?? []
|
||||
expect(models.map((model) => model.id)).toEqual([
|
||||
'provider/other-model',
|
||||
'provider/exact-model'
|
||||
])
|
||||
const settings = getDefaultSettings('/disposable-omp-home')
|
||||
settings.defaultTuiAgent = 'omp'
|
||||
const config = normalizeSourceControlAiSettings(
|
||||
settings.sourceControlAi,
|
||||
settings.commitMessageAi
|
||||
)
|
||||
config.selectedModelByAgentByHost = selectedModel
|
||||
? { [hostKey]: { omp: selectedModel } }
|
||||
: {}
|
||||
settings.sourceControlAi = {
|
||||
...config,
|
||||
discoveredModelsByAgentByHost: { [hostKey]: { omp: models } }
|
||||
}
|
||||
for (const operation of ['commitMessage', 'pullRequest', 'branchName'] as const) {
|
||||
const resolved = resolveSourceControlAiForOperation({
|
||||
settings,
|
||||
operation,
|
||||
discoveryHostKey: hostKey
|
||||
})
|
||||
expect(resolved.ok).toBe(true)
|
||||
if (!resolved.ok) {
|
||||
throw new Error(resolved.error)
|
||||
}
|
||||
expect(resolved.value.params.model).toBe(selectedModel ?? 'default')
|
||||
const args = spec.buildArgs({ prompt: '', model: resolved.value.params.model })
|
||||
if (selectedModel === 'provider/exact-model') {
|
||||
expect(args.slice(-2)).toEqual(['--model', 'provider/exact-model'])
|
||||
} else {
|
||||
expect(args).not.toContain('--model')
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -427,7 +427,7 @@ describe('source-control AI action recipes', () => {
|
||||
).toEqual({
|
||||
ok: false,
|
||||
error:
|
||||
'Agent "aider" does not support Source Control AI commit messages. Supported agents: Claude, Codex, OpenCode, Pi, Amp, Cursor, Kimi, GitHub Copilot, Antigravity, or Custom command.'
|
||||
'Agent "aider" does not support Source Control AI commit messages. Supported agents: OMP, Claude, Codex, OpenCode, Pi, Amp, Cursor, Kimi, GitHub Copilot, Antigravity, or Custom command.'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { getCommitMessageAgentSpec } from '../../src/shared/commit-message-agent-spec.ts'
|
||||
const reference = process.argv[2]
|
||||
assert.ok(reference, 'Pass the read-only source checkout path')
|
||||
const { parseArgs } = await import(pathToFileURL(join(resolve(reference), 'packages/coding-agent/src/cli/args.ts')).href)
|
||||
const spec = getCommitMessageAgentSpec('omp')
|
||||
assert.ok(spec)
|
||||
for (const model of ['default', 'provider/model']) {
|
||||
const args = spec.buildArgs({ prompt: 'Generated entirely from stdin', model })
|
||||
const parsed = parseArgs(args)
|
||||
assert.equal(parsed.print, true)
|
||||
assert.equal(parsed.noSession, true)
|
||||
assert.equal(parsed.noTools, true)
|
||||
assert.equal(parsed.noExtensions, true)
|
||||
assert.equal(parsed.noSkills, true)
|
||||
assert.equal(parsed.noRules, true)
|
||||
assert.equal(parsed.mode, 'text')
|
||||
assert.equal(parsed.model, model === 'default' ? undefined : model)
|
||||
assert.deepEqual(parsed.messages, [])
|
||||
}
|
||||
console.log(JSON.stringify({ actualArgumentParser: true, promptDelivery: spec.promptDelivery, configDefault: true, explicitModel: true, modelCalls: 0 }))
|
||||
@@ -0,0 +1,28 @@
|
||||
# OMP Source Control AI settings proof
|
||||
|
||||
Run `ORCA_BACKGROUND_LAUNCH=1 node tests/tools/omp-source-control-ui/run.mjs`.
|
||||
The runner rebuilds the existing background-safe Electron fixture and the production
|
||||
`CommitMessageAiPane` with its real styles. It uses disposable HOME/ZDOTDIR/userData,
|
||||
never reveals a window, and closes its own app after recording evidence under
|
||||
`.bench-fixtures/omp-source-control-*/`.
|
||||
|
||||
DOM assertions and screenshots cover:
|
||||
|
||||
- OMP appears in the commit-message agent menu and can be selected.
|
||||
- Selecting OMP keeps CLI arguments empty so OMP uses its configured provider/model.
|
||||
- An explicit `--model provider/exact-model` argument can be saved and remains in the
|
||||
input after the save completes.
|
||||
|
||||
The settings persistence adapter is in-memory. This is a rendered production
|
||||
component check, not a full app IPC, restart-persistence, or generation test. No
|
||||
generator is called or mocked. Current Source Control AI settings expose action
|
||||
recipes with CLI arguments; they do not have a Discover Models button. The separate
|
||||
`omp-model-discovery-policy.test.ts` and `omp-source-control-discovery.test.ts`
|
||||
regressions cover real discovery-result policy and production generation selection
|
||||
with cached models, including SSH host keys. The configured-default sentinel stays
|
||||
out of generic terminal catalogs while generation without a model override still
|
||||
uses OMP configuration.
|
||||
|
||||
`tests/tools/omp-source-control-runtime-smoke.mjs` separately validates argv against
|
||||
the actual OMP argument parser in a read-only reference checkout. It makes no model
|
||||
call and must not be described as real generated output.
|
||||
@@ -0,0 +1,5 @@
|
||||
@import '../../../src/renderer/src/assets/main.css';
|
||||
@source './fixture.tsx';
|
||||
@source '../../../src/renderer/src/components/settings';
|
||||
@source '../../../src/renderer/src/components/source-control';
|
||||
@source '../../../src/renderer/src/components/ui';
|
||||
@@ -0,0 +1,46 @@
|
||||
import React from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { getDefaultSettings } from '../../../src/shared/constants'
|
||||
import type { GlobalSettings } from '../../../src/shared/global-settings-types'
|
||||
import { CommitMessageAiPane } from '../../../src/renderer/src/components/settings/CommitMessageAiPane'
|
||||
import { TooltipProvider } from '../../../src/renderer/src/components/ui/tooltip'
|
||||
import { useAppStore } from '../../../src/renderer/src/store'
|
||||
import './fixture.css'
|
||||
|
||||
const settings = getDefaultSettings('/disposable-omp-ui-home')
|
||||
settings.sourceControlAi = {
|
||||
enabled: true,
|
||||
agentId: null,
|
||||
selectedModelByAgent: {},
|
||||
selectedThinkingByModel: {},
|
||||
instructionsByOperation: {},
|
||||
actions: {},
|
||||
customAgentCommand: ''
|
||||
}
|
||||
const updateSettings = async (patch: Partial<GlobalSettings>): Promise<void> => {
|
||||
const current = useAppStore.getState().settings
|
||||
if (!current) {
|
||||
throw new Error('Settings fixture not initialized')
|
||||
}
|
||||
useAppStore.setState({ settings: { ...current, ...patch } })
|
||||
}
|
||||
useAppStore.setState({ settings, repos: [], settingsSearchQuery: '', updateSettings })
|
||||
|
||||
function Fixture(): React.JSX.Element {
|
||||
const current = useAppStore((state) => state.settings)
|
||||
if (!current) {
|
||||
throw new Error('Missing fixture settings')
|
||||
}
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<main className="mx-auto max-w-4xl p-6">
|
||||
<CommitMessageAiPane settings={current} updateSettings={updateSettings} />
|
||||
</main>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
const root = document.getElementById('root')
|
||||
if (!root) {
|
||||
throw new Error('Missing fixture root')
|
||||
}
|
||||
createRoot(root).render(<Fixture />)
|
||||
@@ -0,0 +1,11 @@
|
||||
<!doctype html>
|
||||
<html class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>OMP Source Control AI settings proof</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./fixture.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,74 @@
|
||||
import { _electron as electron, expect } from '@stablyai/playwright-test'
|
||||
import { build as buildMain } from 'esbuild'
|
||||
import { build as buildRenderer } from 'vite'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
|
||||
const root = fileURLToPath(new URL('../../../', import.meta.url))
|
||||
const parent = path.join(root, '.bench-fixtures')
|
||||
mkdirSync(parent, { recursive: true })
|
||||
const output = mkdtempSync(path.join(parent, 'omp-source-control-'))
|
||||
const home = path.join(output, 'home')
|
||||
mkdirSync(home)
|
||||
const main = path.join(output, 'main.cjs')
|
||||
await buildMain({
|
||||
entryPoints: [path.join(root, 'tests/tools/benchmarks/spinner-rendering/main.ts')],
|
||||
outfile: main,
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
external: ['electron']
|
||||
})
|
||||
await buildRenderer({
|
||||
configFile: false,
|
||||
root: import.meta.dirname,
|
||||
base: './',
|
||||
logLevel: 'silent',
|
||||
plugins: [tailwindcss()],
|
||||
resolve: { alias: { '@': path.join(root, 'src/renderer/src') } },
|
||||
build: { outDir: path.join(output, 'renderer'), emptyOutDir: true }
|
||||
})
|
||||
const { ELECTRON_RUN_AS_NODE: _node, ...env } = process.env
|
||||
const app = await electron.launch({
|
||||
args: [main],
|
||||
env: { ...env, HOME: home, ZDOTDIR: home, ORCA_BACKGROUND_LAUNCH: '1' }
|
||||
})
|
||||
const report = {
|
||||
scope:
|
||||
'Production CommitMessageAiPane in hidden Electron; in-memory settings persistence; no generator invoked',
|
||||
errors: []
|
||||
}
|
||||
try {
|
||||
const page = await app.firstWindow()
|
||||
page.on('pageerror', (error) => report.errors.push(error.message))
|
||||
await page.goto(pathToFileURL(path.join(output, 'renderer/index.html')).href)
|
||||
const firstAgent = page.getByRole('combobox').first()
|
||||
await expect(firstAgent).toBeVisible()
|
||||
await firstAgent.click()
|
||||
const omp = page.getByRole('option', { name: 'OMP', exact: true })
|
||||
await expect(omp).toBeVisible()
|
||||
await page.screenshot({ path: path.join(output, 'omp-selectable.png') })
|
||||
await omp.click()
|
||||
await expect(firstAgent).toContainText('OMP')
|
||||
const firstArgs = page.locator('input').first()
|
||||
await expect(firstArgs).toHaveValue('')
|
||||
await page.screenshot({ path: path.join(output, 'omp-configured-default.png') })
|
||||
await firstArgs.fill('--model provider/exact-model')
|
||||
const firstSave = page.getByRole('button', { name: 'Save', exact: true }).first()
|
||||
await firstSave.click()
|
||||
await expect(firstSave).toBeDisabled()
|
||||
await expect(firstArgs).toHaveValue('--model provider/exact-model')
|
||||
await page.screenshot({ path: path.join(output, 'omp-explicit-model-saved.png') })
|
||||
report.hidden = await app.evaluate(({ BrowserWindow }) =>
|
||||
BrowserWindow.getAllWindows().every((window) => !window.isVisible())
|
||||
)
|
||||
expect(report.hidden).toBe(true)
|
||||
expect(report.errors).toEqual([])
|
||||
report.passed = true
|
||||
} finally {
|
||||
writeFileSync(path.join(output, 'report.json'), `${JSON.stringify(report, null, 2)}\n`)
|
||||
console.log(output)
|
||||
await app.close()
|
||||
}
|
||||
Reference in New Issue
Block a user