mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
* feat(worktree): show checkout progress while a worktree is created A local create spends most of its time in the checkout `git worktree add` performs, and the card only said "Creating worktree…" for that whole time. Git already writes its "Updating files: N%" meter into our stderr pipe, so the add call reads it passively (same command line and environment) and the create card shows "Checking out files… N%" with a thin bar. All six create labels are translated so a create never switches language mid-flight. Rebuilt on main from the version stacked on the create-gating branch. On main a create can also claim a prepared checkout; that path never runs `git worktree add`, so it keeps today's "Creating worktree…" label with no bar, and a missed claim falls back to the plain add, which shows the bar. * test(worktree): pin that finalizing a prepared checkout reports no checkout progress Claiming a prepared checkout never runs `git worktree add`, so the create card keeps its label with no bar; this pins that at the finalize step, including the retarget `reset --hard`. * test(worktree): keep only the checkout-progress tests that pin a contract Drops unit tests that mirror the implementation; the real-git flow, the parser captures, the cross-version Git contract and the create card test remain. * test(worktree): pin the checkout meter's path from git to the create card * test(worktree): merge the narrow-terminal capture's pipe chunks
90 lines
4.4 KiB
TypeScript
90 lines
4.4 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { createGitProgressRecordReader, type GitProgressRecord } from './git-progress-records'
|
|
|
|
// Captured through a Node execFile pipe from `git worktree add` of a 25-file repo with
|
|
// GIT_PROGRESS_DELAY=0, chunked exactly as the pipe delivered them.
|
|
const GIT_2_25_5_CHUNKS: string[] = [
|
|
'Updating files: 4% (1/25)\rUpdating files: 8% (2/25)\rUpdating files: 12% (3/25)\r',
|
|
'Updating files: 16% (4/25)\rUpdating files: 20% (5/25)\rUpdating files: 24% (6/25)\rUpdating files: 28% (7/25)\rUpdating files: 32% (8/25)\rUpdating files: 36% (9/25)\rUpdating files: 40% (10/25)\r',
|
|
'Updating files: 44% (11/25)\r',
|
|
'Updating files: 48% (12/25)\rUpdating files: 52% (13/25)\rUpdating files: 56% (14/25)\rUpdating files: 60% (15/25)\rUpdating files: 64% (16/25)\rUpdating files: 68% (17/25)\rUpdating files: 72% (18/25)\rUpdating files: 76% (19/25)\rUpdating files: 80% (20/25)\rUpdating files: 84% (21/25)\rUpdating files: 88% (22/25)\rUpdating files: 92% (23/25)\rUpdating files: 96% (24/25)\rUpdating files: 100% (25/25)\rUpdating files: 100% (25/25), done.\n'
|
|
]
|
|
// Same capture with COLUMNS=20 (pipe chunks merged): git prints the title once, then bare counters.
|
|
const GIT_2_25_5_NARROW_CHUNKS: string[] = [
|
|
'Updating files: \n 4% (1/25)\r',
|
|
' 8% (2/25)\r 12% (3/25)\r 16% (4/25)\r 20% (5/25)\r 24% (6/25)\r 28% (7/25)\r 32% (8/25)\r 36% (9/25)\r 40% (10/25)\r 44% (11/25)\r 48% (12/25)\r 52% (13/25)\r 56% (14/25)\r 60% (15/25)\r 64% (16/25)\r 68% (17/25)\r 72% (18/25)\r 76% (19/25)\r 80% (20/25)\r 84% (21/25)\r 88% (22/25)\r 92% (23/25)\r 96% (24/25)\r 100% (25/25)\r',
|
|
' 100% (25/25), done.\n'
|
|
]
|
|
|
|
function readAll(chunks: string[]): GitProgressRecord[] {
|
|
const records: GitProgressRecord[] = []
|
|
const read = createGitProgressRecordReader('Updating files', (record) => records.push(record))
|
|
for (const chunk of chunks) {
|
|
read(chunk)
|
|
}
|
|
return records
|
|
}
|
|
|
|
function expectFullCheckout(records: GitProgressRecord[]): void {
|
|
expect(records.map((record) => record.completed)).toEqual([
|
|
...Array.from({ length: 25 }, (_, index) => index + 1),
|
|
25
|
|
])
|
|
expect(records.every((record) => record.total === 25)).toBe(true)
|
|
expect(records.at(-2)).toEqual({ percent: 100, completed: 25, total: 25, done: false })
|
|
expect(records.at(-1)).toEqual({ percent: 100, completed: 25, total: 25, done: true })
|
|
expect(records.filter((record) => record.done)).toHaveLength(1)
|
|
}
|
|
|
|
describe('createGitProgressRecordReader', () => {
|
|
it.each([
|
|
['2.25.5', GIT_2_25_5_CHUNKS],
|
|
['2.25.5 with a narrow COLUMNS', GIT_2_25_5_NARROW_CHUNKS]
|
|
])('reads every record git %s wrote, as the pipe chunked it', (_version, chunks) => {
|
|
expectFullCheckout(readAll(chunks))
|
|
})
|
|
|
|
it.each([
|
|
['2.25.5', GIT_2_25_5_CHUNKS],
|
|
['2.25.5 with a narrow COLUMNS', GIT_2_25_5_NARROW_CHUNKS]
|
|
])('reads the same records from git %s wherever a chunk boundary falls', (_version, chunks) => {
|
|
const stream = chunks.join('')
|
|
const expected = readAll([stream])
|
|
for (let cut = 1; cut < stream.length; cut += 1) {
|
|
expect(readAll([stream.slice(0, cut), stream.slice(cut)])).toEqual(expected)
|
|
}
|
|
expect(readAll([...stream])).toEqual(expected)
|
|
})
|
|
|
|
it('accepts CRLF line ends, even split between chunks', () => {
|
|
expect(readAll(['Updating files:\r', '\n 50% (1/2)\r\n 100% (2/2), done.\r\n'])).toEqual([
|
|
{ percent: 50, completed: 1, total: 2, done: false },
|
|
{ percent: 100, completed: 2, total: 2, done: true }
|
|
])
|
|
})
|
|
|
|
it('ignores other stderr, including other progress meters', () => {
|
|
expect(
|
|
readAll([
|
|
"Preparing worktree (new branch 'feature')\n",
|
|
'HEAD is now at 1234567 init\n',
|
|
'Filtering content: 50% (1/2)\r',
|
|
'hook: Updating files is not a record\n',
|
|
' 40% (2/5)\n',
|
|
'Updating files: 0% (0/0)\n'
|
|
])
|
|
).toEqual([])
|
|
})
|
|
|
|
it('keeps only a bounded tail of a line that never ends', () => {
|
|
const records: GitProgressRecord[] = []
|
|
const read = createGitProgressRecordReader('Updating files', (record) => records.push(record))
|
|
// The record's head falls out of the bounded buffer before its line ends.
|
|
read(`Updating files: 10% (1/10)${' '.repeat(300)}`)
|
|
read('\r')
|
|
expect(records).toEqual([])
|
|
read('Updating files: 20% (2/10)\r')
|
|
expect(records).toEqual([{ percent: 20, completed: 2, total: 10, done: false }])
|
|
})
|
|
})
|