From 54b2616fa45968d4a844ca65b881411ae0c563c1 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Sat, 18 Apr 2026 09:58:49 +0200 Subject: [PATCH] fix: restore manual commit dialog when autogit is off --- src/App.tsx | 16 +- src/utils/commitEntryAction.test.ts | 32 ++++ src/utils/commitEntryAction.ts | 21 +++ .../manual-commit-modal-autogit-off.spec.ts | 168 ++++++++++++++++++ 4 files changed, 232 insertions(+), 5 deletions(-) create mode 100644 src/utils/commitEntryAction.test.ts create mode 100644 src/utils/commitEntryAction.ts create mode 100644 tests/smoke/manual-commit-modal-autogit-off.spec.ts diff --git a/src/App.tsx b/src/App.tsx index 78feb6f3..995a2767 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -35,6 +35,7 @@ import { useGitRemoteStatus } from './hooks/useGitRemoteStatus' import { useViewMode, type ViewMode } from './hooks/useViewMode' import { useEntryActions } from './hooks/useEntryActions' import { useAppCommands } from './hooks/useAppCommands' +import { triggerCommitEntryAction } from './utils/commitEntryAction' import { generateCommitMessage } from './utils/commitMessage' import { useDialogs } from './hooks/useDialogs' import { useVaultSwitcher } from './hooks/useVaultSwitcher' @@ -660,6 +661,7 @@ function App() { onCheckpoint: () => commitFlow.runAutomaticCheckpoint(), }) const recordAutoGitActivity = autoGit.recordActivity + const openCommitDialog = commitFlow.openCommitDialog const runAutomaticCheckpoint = commitFlow.runAutomaticCheckpoint const handleAppContentChange = appSave.handleContentChange const handleAppSave = appSave.handleSave @@ -670,9 +672,13 @@ function App() { recordAutoGitActivity() }, [modifiedFilesSignature, recordAutoGitActivity]) - const handleQuickCommitPush = useCallback(() => { - void runAutomaticCheckpoint({ savePendingBeforeCommit: true }) - }, [runAutomaticCheckpoint]) + const handleCommitPush = useCallback(() => { + triggerCommitEntryAction({ + autoGitEnabled: settings.autogit_enabled === true, + openCommitDialog, + runAutomaticCheckpoint, + }) + }, [openCommitDialog, runAutomaticCheckpoint, settings.autogit_enabled]) const handleTrackedContentChange = useCallback((path: string, content: string) => { recordAutoGitActivity() @@ -953,7 +959,7 @@ function App() { onOpenFeedback: openFeedback, onDeleteNote: deleteActions.handleDeleteNote, onArchiveNote: entryActions.handleArchiveNote, onUnarchiveNote: entryActions.handleUnarchiveNote, - onCommitPush: commitFlow.openCommitDialog, + onCommitPush: handleCommitPush, onPull: autoSync.triggerSync, onResolveConflicts: conflictFlow.handleOpenConflictResolver, onSetViewMode: handleSetViewMode, @@ -1174,7 +1180,7 @@ function App() { - handleSetSelection({ kind: 'filter', filter: 'changes' })} onClickPulse={() => handleSetSelection({ kind: 'filter', filter: 'pulse' })} onCommitPush={handleQuickCommitPush} isOffline={networkStatus.isOffline} isGitVault={isGitVault} syncStatus={autoSync.syncStatus} lastSyncTime={autoSync.lastSyncTime} conflictCount={autoSync.conflictFiles.length} remoteStatus={autoSync.remoteStatus} onTriggerSync={autoSync.triggerSync} onPullAndPush={autoSync.pullAndPush} onOpenConflictResolver={conflictFlow.handleOpenConflictResolver} zoomLevel={zoom.zoomLevel} onZoomReset={zoom.zoomReset} buildNumber={buildNumber} onCheckForUpdates={handleCheckForUpdates} onRemoveVault={vaultSwitcher.removeVault} mcpStatus={mcpStatus} onInstallMcp={installMcp} aiAgentsStatus={aiAgentsStatus} vaultAiGuidanceStatus={vaultAiGuidanceStatus} defaultAiAgent={aiAgentPreferences.defaultAiAgent} onSetDefaultAiAgent={aiAgentPreferences.setDefaultAiAgent} onRestoreVaultAiGuidance={() => { void restoreVaultAiGuidance() }} /> + handleSetSelection({ kind: 'filter', filter: 'changes' })} onClickPulse={() => handleSetSelection({ kind: 'filter', filter: 'pulse' })} onCommitPush={handleCommitPush} isOffline={networkStatus.isOffline} isGitVault={isGitVault} syncStatus={autoSync.syncStatus} lastSyncTime={autoSync.lastSyncTime} conflictCount={autoSync.conflictFiles.length} remoteStatus={autoSync.remoteStatus} onTriggerSync={autoSync.triggerSync} onPullAndPush={autoSync.pullAndPush} onOpenConflictResolver={conflictFlow.handleOpenConflictResolver} zoomLevel={zoom.zoomLevel} onZoomReset={zoom.zoomReset} buildNumber={buildNumber} onCheckForUpdates={handleCheckForUpdates} onRemoveVault={vaultSwitcher.removeVault} mcpStatus={mcpStatus} onInstallMcp={installMcp} aiAgentsStatus={aiAgentsStatus} vaultAiGuidanceStatus={vaultAiGuidanceStatus} defaultAiAgent={aiAgentPreferences.defaultAiAgent} onSetDefaultAiAgent={aiAgentPreferences.setDefaultAiAgent} onRestoreVaultAiGuidance={() => { void restoreVaultAiGuidance() }} /> setToastMessage(null)} /> diff --git a/src/utils/commitEntryAction.test.ts b/src/utils/commitEntryAction.test.ts new file mode 100644 index 00000000..a39342ae --- /dev/null +++ b/src/utils/commitEntryAction.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it, vi } from 'vitest' +import { triggerCommitEntryAction } from './commitEntryAction' + +describe('triggerCommitEntryAction', () => { + it('runs the automatic checkpoint when AutoGit is enabled', () => { + const openCommitDialog = vi.fn().mockResolvedValue(undefined) + const runAutomaticCheckpoint = vi.fn().mockResolvedValue(true) + + triggerCommitEntryAction({ + autoGitEnabled: true, + openCommitDialog, + runAutomaticCheckpoint, + }) + + expect(runAutomaticCheckpoint).toHaveBeenCalledWith({ savePendingBeforeCommit: true }) + expect(openCommitDialog).not.toHaveBeenCalled() + }) + + it('opens the manual commit dialog when AutoGit is disabled', () => { + const openCommitDialog = vi.fn().mockResolvedValue(undefined) + const runAutomaticCheckpoint = vi.fn().mockResolvedValue(true) + + triggerCommitEntryAction({ + autoGitEnabled: false, + openCommitDialog, + runAutomaticCheckpoint, + }) + + expect(openCommitDialog).toHaveBeenCalledTimes(1) + expect(runAutomaticCheckpoint).not.toHaveBeenCalled() + }) +}) diff --git a/src/utils/commitEntryAction.ts b/src/utils/commitEntryAction.ts new file mode 100644 index 00000000..8d82f277 --- /dev/null +++ b/src/utils/commitEntryAction.ts @@ -0,0 +1,21 @@ +type RunAutomaticCheckpoint = (options?: { savePendingBeforeCommit?: boolean }) => Promise +type OpenCommitDialog = () => Promise + +interface CommitEntryActionConfig { + autoGitEnabled: boolean + openCommitDialog: OpenCommitDialog + runAutomaticCheckpoint: RunAutomaticCheckpoint +} + +export function triggerCommitEntryAction({ + autoGitEnabled, + openCommitDialog, + runAutomaticCheckpoint, +}: CommitEntryActionConfig): void { + if (autoGitEnabled) { + void runAutomaticCheckpoint({ savePendingBeforeCommit: true }) + return + } + + void openCommitDialog() +} diff --git a/tests/smoke/manual-commit-modal-autogit-off.spec.ts b/tests/smoke/manual-commit-modal-autogit-off.spec.ts new file mode 100644 index 00000000..b492a4fa --- /dev/null +++ b/tests/smoke/manual-commit-modal-autogit-off.spec.ts @@ -0,0 +1,168 @@ +import { expect, test, type Page } from '@playwright/test' +import { executeCommand, openCommandPalette } from './helpers' +import { seedAutoGitSavedChange } from './testBridge' + +type MockHandler = (args?: Record) => unknown + +function installCommitFlowMocks() { + type BrowserWindow = Window & typeof globalThis & { + __gitCommitMessages?: string[] + __gitPushCalls?: number + __mockHandlers?: Record + } + + const browserWindow = window as BrowserWindow + const dirtyPaths = new Set() + let ahead = 0 + + const createModifiedFiles = () => [...dirtyPaths].map((path) => ({ + path, + relativePath: path.split('/').pop() ?? path, + status: 'modified', + })) + + const isPatched = (handlers?: Record | null) => + !handlers || (handlers as Record).__manualCommitPatched === true + + const patchSaveNoteContent = (handlers: Record) => { + const originalSaveNoteContent = handlers.save_note_content + handlers.save_note_content = (args?: Record) => { + const path = typeof args?.path === 'string' ? args.path : null + if (path) dirtyPaths.add(path) + return originalSaveNoteContent?.(args) + } + } + + const patchGitHandlers = (handlers: Record) => { + handlers.get_modified_files = () => createModifiedFiles() + handlers.git_commit = (args?: Record) => { + const message = typeof args?.message === 'string' ? args.message : '' + browserWindow.__gitCommitMessages?.push(message) + dirtyPaths.clear() + ahead = 1 + return `[main abc1234] ${message}` + } + + handlers.git_push = () => { + browserWindow.__gitPushCalls = (browserWindow.__gitPushCalls ?? 0) + 1 + ahead = 0 + return { status: 'ok', message: 'Pushed to remote' } + } + + handlers.git_remote_status = () => ({ + branch: 'main', + ahead, + behind: 0, + hasRemote: true, + }) + } + + const markPatched = (handlers: Record) => { + Object.defineProperty(handlers, '__manualCommitPatched', { + configurable: true, + enumerable: false, + value: true, + }) + } + + const patchHandlers = (handlers?: Record | null) => { + if (isPatched(handlers)) { + return handlers ?? null + } + + patchSaveNoteContent(handlers) + patchGitHandlers(handlers) + markPatched(handlers) + return handlers + } + + browserWindow.__gitCommitMessages = [] + browserWindow.__gitPushCalls = 0 + + let ref = patchHandlers(browserWindow.__mockHandlers) ?? null + Object.defineProperty(browserWindow, '__mockHandlers', { + configurable: true, + get() { + return patchHandlers(ref) ?? ref + }, + set(value) { + ref = patchHandlers(value as Record | undefined) ?? null + }, + }) +} + +async function openFirstNote(page: Page) { + const noteList = page.locator('[data-testid="note-list-container"]') + await noteList.waitFor({ timeout: 5_000 }) + await noteList.locator('.cursor-pointer').first().click() + await expect(page.locator('.bn-editor')).toBeVisible({ timeout: 5_000 }) +} + +async function setAutoGitEnabled(page: Page, enabled: boolean) { + await openCommandPalette(page) + await executeCommand(page, 'Open Settings') + + const settingsPanel = page.getByTestId('settings-panel') + await expect(settingsPanel).toBeVisible({ timeout: 5_000 }) + + const toggle = page.getByRole('switch', { name: 'AutoGit' }) + const isEnabled = (await toggle.getAttribute('aria-checked')) === 'true' + if (isEnabled !== enabled) { + await toggle.click() + } + + await page.getByTestId('settings-save').click() + await expect(settingsPanel).not.toBeVisible({ timeout: 5_000 }) +} + +async function triggerCommitButton(page: Page) { + const commitButton = page.getByTestId('status-commit-push') + await commitButton.focus() + await expect(commitButton).toBeFocused() + await page.keyboard.press('Enter') +} + +async function expectCommitMessage(page: Page, index: number, expectedMessage: string) { + await expect.poll(async () => + page.evaluate( + ({ targetIndex }) => (window as Window & { __gitCommitMessages?: string[] }).__gitCommitMessages?.[targetIndex] ?? '', + { targetIndex: index }, + ), + ).toBe(expectedMessage) +} + +async function expectPushCount(page: Page, expectedCount: number) { + await expect.poll(async () => + page.evaluate(() => (window as Window & { __gitPushCalls?: number }).__gitPushCalls ?? 0), + ).toBe(expectedCount) +} + +test('@smoke commit entry opens the manual modal when AutoGit is off and switches back immediately when enabled', async ({ page }) => { + await page.addInitScript(installCommitFlowMocks) + await page.goto('/') + await page.waitForLoadState('networkidle') + + await openFirstNote(page) + await setAutoGitEnabled(page, false) + await seedAutoGitSavedChange(page) + await triggerCommitButton(page) + + await expect(page.getByRole('heading', { name: 'Commit & Push' })).toBeVisible() + const messageInput = page.locator('textarea[placeholder="Commit message..."]') + await expect(messageInput).toBeFocused() + await page.keyboard.press('Meta+A') + await page.keyboard.type('Manual commit from keyboard') + await page.keyboard.press('Meta+Enter') + + await expect(page.locator('.fixed.bottom-8')).toContainText('Committed and pushed', { timeout: 5_000 }) + await expectCommitMessage(page, 0, 'Manual commit from keyboard') + await expectPushCount(page, 1) + + await setAutoGitEnabled(page, true) + await seedAutoGitSavedChange(page) + await triggerCommitButton(page) + + await expect(messageInput).not.toBeVisible() + await expectCommitMessage(page, 1, 'Updated 1 note') + await expectPushCount(page, 2) +})