diff --git a/.project-docs/30-worklog/tasks/20260823-pi-product-tools-a9c4e7d2.md b/.project-docs/30-worklog/tasks/20260823-pi-product-tools-a9c4e7d2.md index 480bcd7..f4fe064 100644 --- a/.project-docs/30-worklog/tasks/20260823-pi-product-tools-a9c4e7d2.md +++ b/.project-docs/30-worklog/tasks/20260823-pi-product-tools-a9c4e7d2.md @@ -98,6 +98,10 @@ `changes.touched` ACK bridge; full diff/preview data stays Main-only; and untracked reads plus previews are both limited to 8 KiB while size/mtime detect equal-length changes beyond that window. +- Closed the follow-up review finding by making per-candidate Git diff failure, + non-zero exit, or output-limit rejection non-fatal: the Main snapshot keeps + the changed relative path and status while omitting only the unavailable + diff. The regression uses the same `Git output is too large` failure shape. - The packaged smoke now has two halves: a staged production-closure Pi process check and a controlled Electron-Node authenticated bridge check that actually executes browser status/screenshot attachment and game asset @@ -114,7 +118,7 @@ `Makelore/index.tsx`; none are in this task's changed scope). - Final focused PI-090/Pi projection/bridge/game/resource suite: 12 files, 71 tests passed. The planner's five minimal failure shapes are also covered - by the 4-file 19-test tracker/bridge/runtime subset. + by the final 4-file 20-test tracker/bridge/runtime subset. - Final serial full suite (`corepack pnpm vitest run --maxWorkers=1 --no-file-parallelism`): 203 files, 2226 tests passed, 2 skipped. Two preceding parallel full-suite runs each diff --git a/electron/coding-projects/conversation-change-tracker.ts b/electron/coding-projects/conversation-change-tracker.ts index 0a22ee1..65711d1 100644 --- a/electron/coding-projects/conversation-change-tracker.ts +++ b/electron/coding-projects/conversation-change-tracker.ts @@ -325,10 +325,17 @@ export class ConversationChangeTracker { diff?: string; truncated?: boolean; }> { - const [working, staged] = await Promise.all([ - this.git.run(projectPath, ['diff', '--no-ext-diff', '--no-color', '--relative', '--', relativePath]), - this.git.run(projectPath, ['diff', '--cached', '--no-ext-diff', '--no-color', '--relative', '--', relativePath]), - ]); + let working: GitCommandResult; + let staged: GitCommandResult; + try { + [working, staged] = await Promise.all([ + this.git.run(projectPath, ['diff', '--no-ext-diff', '--no-color', '--relative', '--', relativePath]), + this.git.run(projectPath, ['diff', '--cached', '--no-ext-diff', '--no-color', '--relative', '--', relativePath]), + ]); + } catch { + return {}; + } + if (working.code !== 0 || staged.code !== 0) return {}; const bounded = boundedText(`${staged.stdout}${working.stdout}`, MAX_DIFF_BYTES); return { ...(bounded.text ? { diff: bounded.text } : {}), diff --git a/tests/unit/pi-product-tools.test.ts b/tests/unit/pi-product-tools.test.ts index c6645c0..229f86b 100644 --- a/tests/unit/pi-product-tools.test.ts +++ b/tests/unit/pi-product-tools.test.ts @@ -143,6 +143,40 @@ describe('PI-090 product tools', () => { )).files).toEqual([expect.objectContaining({ path: 'notes.txt', preview: 'local notes\n' })]); }); + it('keeps the changed path when a candidate diff is unavailable or oversized', async () => { + const root = await temporaryRoot('makelore-pi-diff-unavailable-'); + await writeFile(path.join(root, 'tracked.txt'), 'changed\n', 'utf8'); + let statusReads = 0; + const tracker = new ConversationChangeTracker({ + async run(_projectPath, args) { + if (args[0] === 'rev-parse' && args[1] === '--is-inside-work-tree') { + return { code: 0, stdout: 'true\n' }; + } + if (args[0] === 'rev-parse') return { code: 0, stdout: `${'a'.repeat(40)}\n` }; + if (args[0] === 'status') { + statusReads += 1; + return { + code: 0, + stdout: statusReads === 1 + ? '' + : '1 .M N... 100644 100644 100644 abc abc tracked.txt\0', + }; + } + if (args[0] === 'diff') throw new Error('Git output is too large'); + throw new Error(`Unexpected Git call: ${args.join(' ')}`); + }, + }); + await tracker.beginRun({ + conversationId: 'conversation-a', runId: 'run-a', projectPath: root, + }); + const snapshot = await tracker.recordTouchedPaths( + 'conversation-a', 'run-a', ['tracked.txt'], + ); + expect(snapshot.files).toEqual([{ + path: 'tracked.txt', status: 'modified', + }]); + }); + it('supports no-git projects and rejects paths outside the project', async () => { const root = await temporaryRoot('makelore-pi-no-git-'); await writeFile(path.join(root, 'notes.txt'), 'local notes\n', 'utf8');