diff --git a/main.ts b/main.ts index 367f6a1..994f6ac 100644 --- a/main.ts +++ b/main.ts @@ -39,6 +39,7 @@ import type { ObsidianMenuItem, PluginSettings, ResolvedCard, + RunForFileOptions, } from './src/types'; import { copyToClipboard } from './src/ui-helpers'; import { ParallelReaderView, VIEW_TYPE_PARALLEL } from './src/view'; @@ -427,7 +428,7 @@ class ParallelReaderPlugin extends Plugin { return this.runForFile(mdView.file, force); } - async runForFile(file: TFile | null, force: boolean) { + async runForFile(file: TFile | null, force: boolean, options: RunForFileOptions = {}) { if (!file) { new Notice(this.t('openNoteFirst')); return; @@ -494,7 +495,10 @@ class ParallelReaderPlugin extends Plugin { }), ); }) - .catch((e: unknown) => this.handleGenerationError(e, file, view)); + .catch((e: unknown) => { + this.handleGenerationError(e, file, view); + if (options.rethrowErrors) throw e; + }); } private streamProgressFor( @@ -564,9 +568,11 @@ class ParallelReaderPlugin extends Plugin { continue; } try { - await this.runForFile(file, false); + await this.runForFile(file, false, { rethrowErrors: true }); + if (batch.cancelled) break; stats = recordBatchProcessed(stats); } catch (e: unknown) { + if (batch.cancelled && e instanceof GenerationJobCancelledError) break; stats = recordBatchError(stats); console.error('[parallel-reader] batch error for', file.path, e); } diff --git a/src/test-exports.ts b/src/test-exports.ts index 8c9e04c..2c8482c 100644 --- a/src/test-exports.ts +++ b/src/test-exports.ts @@ -1,5 +1,6 @@ 'use strict'; +export { default as ParallelReaderPlugin } from '../main'; export { findLineForAnchor } from './anchor'; export { batchProgressVars, diff --git a/src/types.ts b/src/types.ts index a519a0c..9d67b3c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -103,6 +103,10 @@ export type GenerationPhase = export type ErrorKind = 'auth' | 'timeout' | 'rate-limit' | 'schema' | 'config' | 'cancelled' | 'unknown'; +export interface RunForFileOptions { + rethrowErrors?: boolean; +} + /* ---------- Prompt types ---------- */ export interface PromptPair { @@ -156,7 +160,7 @@ export interface PluginHost { t(key: string, vars?: Record): string; isGeneratingFile(file: TFile | null): boolean; cancelGenerationForFile(file: TFile | null): boolean; - runForFile(file: TFile | null, force: boolean): Promise; + runForFile(file: TFile | null, force: boolean, options?: RunForFileOptions): Promise; copyCurrentViewMarkdown(): Promise; scrollEditorToLine(line: number, file: TFile | null): Promise; cacheReplaceCards(filePath: string, cards: ResolvedCard[]): Promise; diff --git a/tests/plugin-batch.test.js b/tests/plugin-batch.test.js new file mode 100644 index 0000000..955044d --- /dev/null +++ b/tests/plugin-batch.test.js @@ -0,0 +1,34 @@ +const { assert, t } = require('./test-setup'); + +const plugin = new t.ParallelReaderPlugin(); +const failedFile = { path: 'failed.md' }; +const originalConsoleError = console.error; + +plugin.cacheManager = { get: () => null }; +plugin.jobs = { + isRunning: () => false, + start: async () => { + throw new Error('backend down'); + }, +}; +plugin.t = (key) => key; + +(async () => { + console.error = () => {}; + try { + await assert.doesNotReject( + () => plugin.runForFile(failedFile, false), + 'single-file UI path should keep handling errors internally', + ); + + await assert.rejects( + () => plugin.runForFile(failedFile, false, { rethrowErrors: true }), + /backend down/, + 'batch path should be able to count failed generation attempts', + ); + } finally { + console.error = originalConsoleError; + } + + console.log('plugin batch tests passed'); +})();