mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 16:03:26 +00:00
Addresses 13 of the 14 findings from the {linkedIssue} code review
(6 minor, 8 nit, 0 critical, 0 major); Issue 5 (GitLab provider naming)
is deferred to design Open Question 3 as product expansion.
Behavior:
- Dialog previews the workspace's real linked issue instead of the
synthetic 123, in both the chip hover card and the plan preview, so an
unlinked workspace previews the `Fixes #` it will actually generate.
Settings dry-runs stay fully synthetic.
- Reject non-positive, fractional and unsafe-integer issue numbers at the
IPC resolver via a shared isLinkedIssueNumber predicate, so corrupt meta
never reaches a draft context (previously -7 rendered `Fixes #-7` and
1e21 rendered `Fixes #1e+21`).
- Fail closed on an empty-string repoId instead of skipping the cross-check.
Structure:
- Split the variable registry into source-control-ai-action-variables.ts
and re-export it, restoring max-lines headroom with no consumer churn
and no lint disable.
- Constrain withLinkedIssueDraftContext to contexts declaring linkedIssue.
- Move the misplaced shared imports into their import group.
Docs and tests:
- Document that the IPC id/path validator guards relay/CLI/future callers,
not the renderer (whose path is id-derived), and rename the three tests
that read as proof of a protection that cannot fire.
- Add PR-side coverage that was missing: three git:generatePullRequestFields
handler tests, a built-in PR prompt no-leak guard, and the runtime PR
unlinked case.
- Replace the coincidental '42' assertion with a fixture-unique sentinel.
- Type the runtime worktree fixture with satisfies, which surfaced and
fixed pre-existing drift in its git sub-object.
- Add an e2e case covering the preload -> main -> meta -> template chain.
Co-authored-by: Orca <help@stably.ai>
162 lines
5.8 KiB
TypeScript
162 lines
5.8 KiB
TypeScript
import { execFileSync } from 'node:child_process'
|
|
import { rmSync, writeFileSync } from 'node:fs'
|
|
import os from 'node:os'
|
|
import path from 'node:path'
|
|
import { test, expect } from './helpers/orca-app'
|
|
import { waitForSessionReady } from './helpers/store'
|
|
import { openSourceControlForWorktree } from './helpers/worktree-registration'
|
|
|
|
function createWorktreeWithStagedChange(repoPath: string): {
|
|
branchName: string
|
|
worktreePath: string
|
|
} {
|
|
const branchName = `e2e-ai-commit-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
|
const worktreePath = path.join(os.tmpdir(), branchName)
|
|
execFileSync('git', ['worktree', 'add', worktreePath, '-b', branchName], {
|
|
cwd: repoPath,
|
|
stdio: 'pipe'
|
|
})
|
|
writeFileSync(
|
|
path.join(worktreePath, 'README.md'),
|
|
'# AI Commit Message E2E\n\nGenerated flow.\n'
|
|
)
|
|
execFileSync('git', ['add', 'README.md'], { cwd: worktreePath, stdio: 'pipe' })
|
|
return { branchName, worktreePath }
|
|
}
|
|
|
|
function cleanupWorktree(repoPath: string, worktreePath: string, branchName: string): void {
|
|
try {
|
|
execFileSync('git', ['worktree', 'remove', '--force', worktreePath], {
|
|
cwd: repoPath,
|
|
stdio: 'pipe'
|
|
})
|
|
} catch {
|
|
rmSync(worktreePath, { recursive: true, force: true })
|
|
}
|
|
try {
|
|
execFileSync('git', ['branch', '-D', branchName], { cwd: repoPath, stdio: 'pipe' })
|
|
} catch {
|
|
// The branch is already gone when git prunes it with the worktree.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Echoes back whichever issue number reached the prompt, so the assertion covers the
|
|
* whole chain (renderer → IPC → worktree meta → template render → agent stdin) rather
|
|
* than any single hop.
|
|
*/
|
|
function writeLinkedIssueEchoGenerator(scriptPath: 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')",
|
|
' const match = prompt.match(/ORCA_E2E_ISSUE=(\\d*)/)',
|
|
" process.stdout.write(`saw-issue:${match ? match[1] || 'empty' : 'missing'}`)",
|
|
'})'
|
|
].join('\n')
|
|
)
|
|
}
|
|
|
|
test.describe('Source Control AI commit messages', () => {
|
|
test('substitutes the workspace-linked issue into the commit-message recipe', async ({
|
|
orcaPage,
|
|
testRepoPath
|
|
}) => {
|
|
const { branchName, worktreePath } = createWorktreeWithStagedChange(testRepoPath)
|
|
const generatorPath = path.join(os.tmpdir(), `${branchName}-linked-issue-generator.cjs`)
|
|
writeLinkedIssueEchoGenerator(generatorPath)
|
|
|
|
try {
|
|
await waitForSessionReady(orcaPage)
|
|
await openSourceControlForWorktree(orcaPage, testRepoPath, worktreePath)
|
|
|
|
await orcaPage.evaluate(
|
|
async ({ generatorPath, linkedIssue }) => {
|
|
const store = window.__store
|
|
if (!store) {
|
|
throw new Error('window.__store is not available')
|
|
}
|
|
const worktreeId = store.getState().activeWorktreeId
|
|
if (!worktreeId) {
|
|
throw new Error('No worktree was active after opening Source Control')
|
|
}
|
|
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: {
|
|
commitMessage: {
|
|
agentId: 'custom' as const,
|
|
commandInputTemplate: 'ORCA_E2E_ISSUE={linkedIssue}\n\n{basePrompt}'
|
|
}
|
|
}
|
|
}
|
|
})
|
|
},
|
|
{ generatorPath, linkedIssue: 4242 }
|
|
)
|
|
|
|
const textarea = orcaPage.getByRole('textbox', { name: 'Commit message' })
|
|
await expect(textarea).toBeVisible({ timeout: 10_000 })
|
|
|
|
const generate = orcaPage.getByRole('button', { name: 'Generate commit message with AI' })
|
|
await expect(generate).toBeEnabled()
|
|
await generate.click()
|
|
|
|
await expect(textarea).toHaveValue('saw-issue:4242', { timeout: 15_000 })
|
|
} finally {
|
|
rmSync(generatorPath, { force: true })
|
|
cleanupWorktree(testRepoPath, worktreePath, branchName)
|
|
}
|
|
})
|
|
|
|
test('generates a commit message from staged changes through the Source Control UI', async ({
|
|
orcaPage,
|
|
testRepoPath
|
|
}) => {
|
|
const { branchName, worktreePath } = createWorktreeWithStagedChange(testRepoPath)
|
|
const agentCommand =
|
|
'node -e "setTimeout(() => process.stdout.write(\'Add generated E2E message\'), 250)"'
|
|
|
|
try {
|
|
await waitForSessionReady(orcaPage)
|
|
await openSourceControlForWorktree(orcaPage, testRepoPath, worktreePath, {
|
|
commitMessageAi: {
|
|
enabled: true,
|
|
agentId: 'custom',
|
|
selectedModelByAgent: {},
|
|
selectedThinkingByModel: {},
|
|
customPrompt: '',
|
|
customAgentCommand: agentCommand
|
|
}
|
|
})
|
|
|
|
const textarea = orcaPage.getByRole('textbox', { name: 'Commit message' })
|
|
await expect(textarea).toBeVisible({ timeout: 10_000 })
|
|
await expect(textarea).toHaveValue('')
|
|
|
|
const generate = orcaPage.getByRole('button', { name: 'Generate commit message with AI' })
|
|
await expect(generate).toBeVisible()
|
|
await expect(generate).toBeEnabled()
|
|
await generate.click()
|
|
|
|
await expect(
|
|
orcaPage.getByRole('button', { name: 'Stop generating commit message' })
|
|
).toBeVisible()
|
|
await expect(textarea).toHaveValue('Add generated E2E message', { timeout: 10_000 })
|
|
} finally {
|
|
cleanupWorktree(testRepoPath, worktreePath, branchName)
|
|
}
|
|
})
|
|
})
|