Resolve {linkedIssue} from live metadata, not cache

Resolved worktrees are cached for a second, causing commit and PR
generation to use stale linked-issue state. Hosts now implement
getWorktreeLinkedIssue to provide fresh issue metadata by worktree id,
with proper fallback for unlinked workspaces. Updates both commit
message and PR field generation paths; includes integration and e2e
coverage.
This commit is contained in:
Jinjing
2026-07-25 18:01:30 -07:00
parent f17c509be4
commit f405e68934
9 changed files with 461 additions and 66 deletions
+33
View File
@@ -2102,6 +2102,39 @@ describe('registerFilesystemHandlers', () => {
)
})
// Why: folder-repo instances keep `::workspace:<uuid>` on the meta key while the
// request path is the stripped cwd. A strip-before-lookup "cleanup" would still
// pass plain-id tests and silently lose enrichment on second workspaces.
it('enriches local commit context when the worktree id carries a folder-repo workspace suffix', async () => {
const context = {
branch: 'feature/ai',
stagedSummary: 'M\tREADME.md',
stagedPatch: '+hello'
}
const params = { agentId: 'codex', model: 'gpt-5.4-mini' }
const instanceId = `repo-1::${WORKTREE_FEATURE_PATH}::workspace:${'0'.repeat(8)}-0000-0000-0000-${'0'.repeat(12)}`
resolveCommitMessageSettingsMock.mockReturnValue({ ok: true, params })
getStagedCommitContextMock.mockResolvedValue(context)
generateCommitMessageFromContextMock.mockResolvedValue({ success: true, message: 'Update' })
const getWorktreeMeta = vi.fn((id: string) =>
id === instanceId ? { linkedIssue: 9 } : undefined
)
registerFilesystemHandlers({ ...store, getWorktreeMeta } as never)
await handlers.get('git:generateCommitMessage')!(null, {
worktreePath: WORKTREE_FEATURE_PATH,
worktreeId: instanceId
})
expect(getWorktreeMeta).toHaveBeenCalledWith(instanceId)
expect(generateCommitMessageFromContextMock).toHaveBeenCalledWith(
{ ...context, linkedIssue: 9 },
params,
expect.objectContaining({ kind: 'local' })
)
})
// Why: the renderer derives worktreePath from worktreeId, so a mismatched pair
// models an independent caller (relay/CLI/future), not a stale renderer context.
it('ignores an independently supplied id that does not own the requested worktree path', async () => {
+81
View File
@@ -644,6 +644,87 @@ describe('RuntimeGitCommands', () => {
)
})
it('prefers live meta over the linked issue projected onto the resolved worktree', async () => {
const worktreePath = mkdtempSync(join(tmpdir(), 'orca-runtime-git-'))
tempDirs.push(worktreePath)
const context = { branch: 'main', stagedSummary: 'M\tREADME.md', stagedPatch: '+hello' }
const params = { agentId: 'codex', model: 'gpt-5.4-mini' }
mocks.resolveCommitMessageSettings.mockReturnValue({ ok: true, params })
mocks.getStagedCommitContext.mockResolvedValue(context)
mocks.generateCommitMessageFromContext.mockResolvedValue({ success: true, message: 'docs' })
const getWorktreeLinkedIssue = vi.fn(() => 321)
const commands = new RuntimeGitCommands({
resolveRuntimeGitTarget: async () => ({ worktree: makeWorktree(worktreePath, 123) }),
getRuntimeSettings: () => ({}) as GlobalSettings,
getWorktreeLinkedIssue
})
await commands.generateRuntimeCommitMessage('id:wt-1')
expect(getWorktreeLinkedIssue).toHaveBeenCalledWith('wt-1')
expect(mocks.generateCommitMessageFromContext).toHaveBeenCalledWith(
{ ...context, linkedIssue: 321 },
params,
expect.objectContaining({ kind: 'local' })
)
})
it('drops a stale worktree issue number when live meta reports the workspace unlinked', async () => {
const worktreePath = mkdtempSync(join(tmpdir(), 'orca-runtime-git-'))
tempDirs.push(worktreePath)
const context = { branch: 'main', stagedSummary: 'M\tREADME.md', stagedPatch: '+hello' }
mocks.resolveCommitMessageSettings.mockReturnValue({
ok: true,
params: { agentId: 'codex', model: 'gpt-5.4-mini' }
})
mocks.getStagedCommitContext.mockResolvedValue(context)
mocks.generateCommitMessageFromContext.mockResolvedValue({ success: true, message: 'docs' })
const commands = new RuntimeGitCommands({
resolveRuntimeGitTarget: async () => ({ worktree: makeWorktree(worktreePath, 123) }),
getRuntimeSettings: () => ({}) as GlobalSettings,
getWorktreeLinkedIssue: () => null
})
await commands.generateRuntimeCommitMessage('id:wt-1')
expect(mocks.generateCommitMessageFromContext.mock.calls[0][0]).not.toHaveProperty(
'linkedIssue'
)
})
it('reads pull-request linked issues from live meta too', async () => {
const worktreePath = mkdtempSync(join(tmpdir(), 'orca-runtime-git-'))
tempDirs.push(worktreePath)
const context = {
base: 'main',
branch: 'feature/login',
commitSummary: 'abc123 feat: test',
changeSummary: 'M README.md',
patch: '+hello',
currentTitle: '',
currentBody: '',
currentDraft: false
}
mocks.getPullRequestDraftContext.mockResolvedValue(context)
mocks.generatePullRequestFieldsFromContext.mockResolvedValue({ success: true, fields: {} })
const commands = new RuntimeGitCommands({
resolveRuntimeGitTarget: async () => ({ worktree: makeWorktree(worktreePath, 123) }),
getRuntimeSettings: () => ({}) as GlobalSettings,
getWorktreeLinkedIssue: () => 321
})
await commands.generateRuntimePullRequestFields(
'id:wt-1',
{ base: 'main', title: '', body: '', draft: false },
{ sourceControlAiResolvedParams: { agentId: 'codex' as const, model: 'gpt-5.5' } }
)
expect(mocks.generatePullRequestFieldsFromContext.mock.calls[0][0]).toEqual({
...context,
linkedIssue: 321
})
})
it('leaves the commit context untouched when no issue is linked', async () => {
const worktreePath = mkdtempSync(join(tmpdir(), 'orca-runtime-git-'))
tempDirs.push(worktreePath)
+15 -3
View File
@@ -161,11 +161,23 @@ export type RuntimeGitCommandHost = {
resolveRuntimeGitTarget(selector: string): Promise<RuntimeGitTarget>
getRuntimeSettings(): GlobalSettings
getCommitMessageAgentEnvironment?(): CommitMessageAgentEnvironmentResolvers | undefined
/**
* Live linked-issue read by worktree id. Resolved worktrees come from a
* short-TTL cache, so link/unlink would otherwise lag generation; hosts that
* implement this are authoritative, including the `null` unlinked answer.
*/
getWorktreeLinkedIssue?(worktreeId: string): number | null
}
export class RuntimeGitCommands {
constructor(private readonly host: RuntimeGitCommandHost) {}
private linkedIssueForTarget(target: RuntimeGitTarget): number | null | undefined {
return this.host.getWorktreeLinkedIssue
? this.host.getWorktreeLinkedIssue(target.worktree.id)
: target.worktree.linkedIssue
}
async getRuntimeGitStatus(
worktreeSelector: string,
options?: GitProviderStatusOptions
@@ -616,7 +628,7 @@ export class RuntimeGitCommands {
if (!context) {
return { success: false, error: 'No staged changes to summarize.' }
}
context = withLinkedIssueDraftContext(context, target.worktree.linkedIssue)
context = withLinkedIssueDraftContext(context, this.linkedIssueForTarget(target))
return generateCommitMessageFromContext(context, resolvedSettings.params, {
kind: 'remote',
cwd: target.worktree.path,
@@ -636,7 +648,7 @@ export class RuntimeGitCommands {
if (!context) {
return { success: false, error: 'No staged changes to summarize.' }
}
context = withLinkedIssueDraftContext(context, target.worktree.linkedIssue)
context = withLinkedIssueDraftContext(context, this.linkedIssueForTarget(target))
const localEnv = await prepareLocalCommitMessageAgentEnv(
resolvedSettings.params.agentId,
this.host.getCommitMessageAgentEnvironment?.(),
@@ -742,7 +754,7 @@ export class RuntimeGitCommands {
return { success: false, error: 'No branch changes to summarize.' }
}
// Why: both SSH and local branches share this context, so one attach covers each.
context = withLinkedIssueDraftContext(context, target.worktree.linkedIssue)
context = withLinkedIssueDraftContext(context, this.linkedIssueForTarget(target))
if (target.connectionId) {
return generatePullRequestFieldsFromContext(context, resolvedSettings.params, {
@@ -0,0 +1,141 @@
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { WorktreeMeta } from '../../shared/types'
import type * as GitStatusModule from '../git/status'
import type * as CommitMessageTextGenerationModule from '../text-generation/commit-message-text-generation'
import type * as WorktreeModule from '../git/worktree'
import { OrcaRuntimeService } from './orca-runtime'
const mocks = vi.hoisted(() => ({
listWorktrees: vi.fn(),
getStagedCommitContext: vi.fn(),
generateCommitMessageFromContext: vi.fn(),
resolveCommitMessageSettings: vi.fn()
}))
vi.mock('../git/worktree', async () => ({
...(await vi.importActual<typeof WorktreeModule>('../git/worktree')),
listWorktrees: mocks.listWorktrees
}))
vi.mock('../git/status', async () => ({
...(await vi.importActual<typeof GitStatusModule>('../git/status')),
getStagedCommitContext: mocks.getStagedCommitContext
}))
vi.mock('../text-generation/commit-message-text-generation', async () => ({
...(await vi.importActual<typeof CommitMessageTextGenerationModule>(
'../text-generation/commit-message-text-generation'
)),
generateCommitMessageFromContext: mocks.generateCommitMessageFromContext,
resolveCommitMessageSettings: mocks.resolveCommitMessageSettings
}))
const REPO_ID = 'repo-1'
const STAGED_CONTEXT = { branch: 'main', stagedSummary: 'M\tREADME.md', stagedPatch: '+hello' }
const PARAMS = { agentId: 'codex', model: 'gpt-5.4-mini' }
const tempDirs: string[] = []
/**
* Store double narrow enough to drive worktree resolution, so the runtime runs
* its real hydration (`listResolvedWorktrees` → `mergeWorktree` → cache) instead
* of a hand-built worktree fixture carrying `linkedIssue`.
*/
function makeStore(worktreePath: string) {
const worktreeId = `${REPO_ID}::${worktreePath}`
const worktreeMeta: Record<string, WorktreeMeta> = {
[worktreeId]: {
instanceId: worktreeId,
displayName: 'wt',
comment: '',
linkedIssue: null,
linkedPR: null,
linkedLinearIssue: null,
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 0
}
}
return {
worktreeId,
updateLinkedIssue: (linkedIssue: number | null): void => {
// Why: mirrors `worktrees:updateMeta`, which persists the link without
// invalidating the runtime's resolved-worktree cache.
worktreeMeta[worktreeId] = { ...worktreeMeta[worktreeId], linkedIssue }
},
store: {
getRepos: () => [
{ id: REPO_ID, path: worktreePath, displayName: 'repo', badgeColor: 'blue', addedAt: 1 }
],
getRepo: (id: string) =>
id === REPO_ID
? { id: REPO_ID, path: worktreePath, displayName: 'repo', badgeColor: 'blue', addedAt: 1 }
: undefined,
getAllWorktreeMeta: () => worktreeMeta,
getWorktreeMeta: (id: string) => worktreeMeta[id],
setWorktreeMeta: (id: string, updates: Partial<WorktreeMeta>) => {
worktreeMeta[id] = { ...worktreeMeta[id], ...updates }
return worktreeMeta[id]
},
getSettings: () => ({})
}
}
}
async function generatedCommitContext(
runtime: OrcaRuntimeService,
worktreeId: string
): Promise<Record<string, unknown>> {
mocks.generateCommitMessageFromContext.mockClear()
await runtime.generateRuntimeCommitMessage(`id:${worktreeId}`)
return mocks.generateCommitMessageFromContext.mock.calls[0][0]
}
describe('runtime commit-message generation linked-issue freshness', () => {
beforeEach(() => {
mocks.listWorktrees.mockReset()
mocks.getStagedCommitContext.mockReset()
mocks.generateCommitMessageFromContext.mockReset()
mocks.resolveCommitMessageSettings.mockReset()
mocks.getStagedCommitContext.mockResolvedValue(STAGED_CONTEXT)
mocks.generateCommitMessageFromContext.mockResolvedValue({ success: true, message: 'docs' })
mocks.resolveCommitMessageSettings.mockReturnValue({ ok: true, params: PARAMS })
})
afterEach(() => {
while (tempDirs.length > 0) {
rmSync(tempDirs.pop()!, { recursive: true, force: true })
}
})
it('substitutes the linked issue persisted since the last worktree resolution', async () => {
const worktreePath = mkdtempSync(join(tmpdir(), 'orca-linked-issue-'))
tempDirs.push(worktreePath)
const { store, worktreeId, updateLinkedIssue } = makeStore(worktreePath)
mocks.listWorktrees.mockResolvedValue([
{
path: worktreePath,
head: 'a'.repeat(40),
branch: 'main',
isBare: false,
isMainWorktree: true
}
])
const runtime = new OrcaRuntimeService(store as never)
// Why: the first generation warms the resolved-worktree cache with the
// unlinked projection, so a stale read would still answer `unlinked` below.
expect(await generatedCommitContext(runtime, worktreeId)).not.toHaveProperty('linkedIssue')
updateLinkedIssue(321)
expect(await generatedCommitContext(runtime, worktreeId)).toMatchObject({ linkedIssue: 321 })
updateLinkedIssue(null)
expect(await generatedCommitContext(runtime, worktreeId)).not.toHaveProperty('linkedIssue')
})
})
+5 -1
View File
@@ -7062,7 +7062,11 @@ export class OrcaRuntimeService {
private readonly gitCommands = new RuntimeGitCommands({
resolveRuntimeGitTarget: (selector) => this.resolveRuntimeGitTarget(selector),
getRuntimeSettings: () => this.requireStore().getSettings() as GlobalSettings,
getCommitMessageAgentEnvironment: () => this.commitMessageAgentEnv ?? undefined
getCommitMessageAgentEnvironment: () => this.commitMessageAgentEnv ?? undefined,
// Why: resolved worktrees are cached for a second, so link/unlink would lag
// generation; meta is keyed by the same id the resolver returns.
getWorktreeLinkedIssue: (worktreeId) =>
this.store?.getWorktreeMeta?.(worktreeId)?.linkedIssue ?? null
})
getRuntimeGitStatus: RuntimeGitCommands['getRuntimeGitStatus'] =
@@ -2,7 +2,8 @@ import type React from 'react'
import { Braces } from 'lucide-react'
import {
SOURCE_CONTROL_ACTION_VARIABLE_INFO,
SOURCE_CONTROL_ACTION_VARIABLES
SOURCE_CONTROL_ACTION_VARIABLES,
type SourceControlActionVariable
} from '../../../../shared/source-control-ai-action-variables'
import type { SourceControlActionId } from '../../../../shared/source-control-ai-actions'
import { Button } from '../ui/button'
@@ -55,7 +56,7 @@ function SourceControlVariableDetails({
variable,
preview
}: {
variable: string
variable: SourceControlActionVariable
preview?: string
}): React.JSX.Element {
// Why: for `basePrompt` the preview *is* the content — the static card only restates it.
@@ -13,8 +13,11 @@ import {
describe('source-control AI variable registry', () => {
it('documents every registered variable so chip hover cards cannot crash', () => {
// Why: the registry type already makes an undocumented chip a compile error;
// this keeps the guarantee falsifiable at runtime if that type ever loosens.
const documented = new Set<string>(Object.keys(SOURCE_CONTROL_ACTION_VARIABLE_INFO))
const undocumented = [...new Set(Object.values(SOURCE_CONTROL_ACTION_VARIABLES).flat())].filter(
(variable) => SOURCE_CONTROL_ACTION_VARIABLE_INFO[variable] === undefined
(variable) => !documented.has(variable)
)
expect(undocumented).toEqual([])
@@ -1,6 +1,14 @@
import type { SourceControlActionId } from './source-control-ai-actions'
export const SOURCE_CONTROL_ACTION_VARIABLES: Record<SourceControlActionId, string[]> = {
/**
* Registering a variable a hover card cannot describe is a compile error: the
* element type is keyed off `SOURCE_CONTROL_ACTION_VARIABLE_INFO`, so chips can
* never index a missing entry.
*/
export const SOURCE_CONTROL_ACTION_VARIABLES: Record<
SourceControlActionId,
SourceControlActionVariable[]
> = {
commitMessage: ['basePrompt', 'branch', 'stagedFiles', 'stagedPatch', 'linkedIssue'],
pullRequest: [
'basePrompt',
@@ -26,65 +34,66 @@ export type SourceControlActionVariableInfo = {
example: string
}
export const SOURCE_CONTROL_ACTION_VARIABLE_INFO: Record<string, SourceControlActionVariableInfo> =
{
basePrompt: {
description:
'Orca’s built-in prompt for this action, including the context Orca knows how to gather safely.',
example:
'Commit messages include staged diff guidance; PR details include branch comparison guidance; fix actions include the failure summary.'
},
branch: {
description: 'The current source-control branch name.',
example: 'feature/source-control-ai-recipes'
},
stagedFiles: {
description: 'A newline-separated list of staged files for commit-message generation.',
example: 'M src/shared/source-control-ai.ts\nA src/shared/source-control-ai-actions.ts'
},
stagedPatch: {
description: 'The staged git patch used for commit-message generation.',
example: 'diff --git a/src/app.ts b/src/app.ts\n+addActionRecipeDefaults()'
},
baseBranch: {
description: 'The target branch selected in the Create PR composer.',
example: 'main'
},
currentTitle: {
description: 'The PR title currently typed in the composer before generation starts.',
example: 'Improve Source Control AI customization'
},
currentBody: {
description: 'The PR description currently typed in the composer before generation starts.',
example: 'Adds configurable agents and command templates for Source Control actions.'
},
commitSummary: {
description: 'A newline-separated list of commits on the branch compared to the base.',
example: 'a1b2c3d Add action recipe defaults\nd4e5f6a Render command templates'
},
changedFiles: {
description: 'A summary of files changed between the branch and the base branch.',
example:
'src/shared/source-control-ai-actions.ts | 24 +++++\nsrc/main/text-generation.ts | 8 +-'
},
patch: {
description: 'The branch diff against the base branch used for PR-details generation.',
example: 'diff --git a/src/app.ts b/src/app.ts\n+renderSourceControlActionCommandTemplate()'
},
firstPrompt: {
description: 'The first user request that created the Orca workspace.',
example: 'Fix CI and commit the result'
},
assistantMessage: {
description: 'The initial agent response, when Orca has one available.',
example: 'I will inspect the failing check, patch the issue, and run tests.'
},
linkedIssue: {
description:
'The GitHub issue number linked to this workspace. Empty when no GitHub issue is linked (including GitLab-linked workspaces). Prefer instructional templates: a bare "Fixes #{linkedIssue}" becomes "Fixes #" when unlinked.',
example: '123'
}
export const SOURCE_CONTROL_ACTION_VARIABLE_INFO = {
basePrompt: {
description:
'Orca’s built-in prompt for this action, including the context Orca knows how to gather safely.',
example:
'Commit messages include staged diff guidance; PR details include branch comparison guidance; fix actions include the failure summary.'
},
branch: {
description: 'The current source-control branch name.',
example: 'feature/source-control-ai-recipes'
},
stagedFiles: {
description: 'A newline-separated list of staged files for commit-message generation.',
example: 'M src/shared/source-control-ai.ts\nA src/shared/source-control-ai-actions.ts'
},
stagedPatch: {
description: 'The staged git patch used for commit-message generation.',
example: 'diff --git a/src/app.ts b/src/app.ts\n+addActionRecipeDefaults()'
},
baseBranch: {
description: 'The target branch selected in the Create PR composer.',
example: 'main'
},
currentTitle: {
description: 'The PR title currently typed in the composer before generation starts.',
example: 'Improve Source Control AI customization'
},
currentBody: {
description: 'The PR description currently typed in the composer before generation starts.',
example: 'Adds configurable agents and command templates for Source Control actions.'
},
commitSummary: {
description: 'A newline-separated list of commits on the branch compared to the base.',
example: 'a1b2c3d Add action recipe defaults\nd4e5f6a Render command templates'
},
changedFiles: {
description: 'A summary of files changed between the branch and the base branch.',
example:
'src/shared/source-control-ai-actions.ts | 24 +++++\nsrc/main/text-generation.ts | 8 +-'
},
patch: {
description: 'The branch diff against the base branch used for PR-details generation.',
example: 'diff --git a/src/app.ts b/src/app.ts\n+renderSourceControlActionCommandTemplate()'
},
firstPrompt: {
description: 'The first user request that created the Orca workspace.',
example: 'Fix CI and commit the result'
},
assistantMessage: {
description: 'The initial agent response, when Orca has one available.',
example: 'I will inspect the failing check, patch the issue, and run tests.'
},
linkedIssue: {
description:
'The GitHub issue number linked to this workspace. Empty when no GitHub issue is linked (including GitLab-linked workspaces). Prefer instructional templates: a bare "Fixes #{linkedIssue}" becomes "Fixes #" when unlinked.',
example: '123'
}
} satisfies Record<string, SourceControlActionVariableInfo>
export type SourceControlActionVariable = keyof typeof SOURCE_CONTROL_ACTION_VARIABLE_INFO
/**
* Issue numbers are positive integers on every supported provider, so anything
@@ -0,0 +1,111 @@
import { rmSync, writeFileSync } from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { test, expect } from './helpers/orca-app'
import {
createBranchCommit,
openSourceControl,
seedCreatePrComposer
} from './helpers/source-control-ai-generation'
import { waitForSessionReady } from './helpers/store'
/**
* Echoes back whichever issue number reached the PR prompt as the generated
* title, so the assertion covers renderer → IPC → meta → template → agent stdin
* for the pull-request path (the commit twin lives in source-control-commit-message-ai).
*/
function writeLinkedIssuePrEchoGenerator(scriptPath: string, base: string): void {
writeFileSync(
scriptPath,
[
'const chunks = []',
"process.stdin.on('data', (chunk) => chunks.push(chunk))",
"process.stdin.on('end', () => {",
" const prompt = Buffer.concat(chunks).toString('utf8')",
// Why: capture the whole line, not `\d*` — a `\d*` capture matches zero digits before
// an unexpanded `{linkedIssue}` and reports it as `empty`, hiding a literal token.
' const match = prompt.match(/ORCA_E2E_ISSUE=([^\\r\\n]*)/)',
" const issue = match ? match[1] || 'empty' : 'missing'",
' process.stdout.write(JSON.stringify({',
` base: ${JSON.stringify(base)},`,
' title: `saw-issue:${issue}`,',
" body: 'linked-issue e2e body',",
' draft: false',
' }))',
'})'
].join('\n')
)
}
test.describe('Source Control AI pull request linkedIssue', () => {
// Why: the unlinked case separates a real resolver from one that always returns a number,
// and — because the generator echoes the whole line into the title — a literal
// `{linkedIssue}` reaches the assertion as `saw-issue:{linkedIssue}` instead of
// masquerading as the empty expansion.
for (const { label, linkedIssue, expected } of [
{ label: 'substitutes the workspace-linked issue into', linkedIssue: 4242, expected: '4242' },
{
label: 'expands the issue token to nothing for an unlinked workspace in',
linkedIssue: null,
expected: 'empty'
}
]) {
test(`${label} the pull-request recipe`, async ({ orcaPage }) => {
await waitForSessionReady(orcaPage)
const { prWorktreeId, prWorktreePath, primaryBranch } = await seedCreatePrComposer(orcaPage)
createBranchCommit(prWorktreePath)
const generatorPath = path.join(
os.tmpdir(),
`e2e-pr-linked-issue-${Date.now()}-${Math.random().toString(16).slice(2)}.cjs`
)
writeLinkedIssuePrEchoGenerator(generatorPath, primaryBranch)
try {
await orcaPage.evaluate(
async ({ generatorPath, linkedIssue, worktreeId }) => {
const store = window.__store
if (!store) {
throw new Error('window.__store is not available')
}
await window.api.worktrees.updateMeta({ worktreeId, updates: { linkedIssue } })
const customAgentCommand = `node ${JSON.stringify(generatorPath)}`
await store.getState().updateSettings({
activeRuntimeEnvironmentId: null,
sourceControlAi: {
enabled: true,
agentId: 'custom' as const,
selectedModelByAgent: {},
selectedThinkingByModel: {},
customAgentCommand,
instructionsByOperation: {},
actions: {
pullRequest: {
agentId: 'custom' as const,
commandInputTemplate: 'ORCA_E2E_ISSUE={linkedIssue}\n\n{basePrompt}'
}
}
}
})
},
{ generatorPath, linkedIssue, worktreeId: prWorktreeId }
)
await openSourceControl(orcaPage, prWorktreeId)
const title = orcaPage.getByRole('textbox', { name: 'Pull request title' })
await expect(title).toBeVisible({ timeout: 10_000 })
const generate = orcaPage.getByRole('button', {
name: 'Generate pull request details with AI'
})
await expect(generate).toBeEnabled()
await generate.click()
await expect(title).toHaveValue(`saw-issue:${expected}`, { timeout: 15_000 })
} finally {
rmSync(generatorPath, { force: true })
}
})
}
})