diff --git a/src/providers/codexCellEditorProvider/codexCellEditorMessagehandling.ts b/src/providers/codexCellEditorProvider/codexCellEditorMessagehandling.ts index 760083a4f..4e8ed7fc4 100644 --- a/src/providers/codexCellEditorProvider/codexCellEditorMessagehandling.ts +++ b/src/providers/codexCellEditorProvider/codexCellEditorMessagehandling.ts @@ -315,6 +315,43 @@ async function getAudioFilePathForCell( return null; } +/** + * True when the source notebook has a resolvable audio file for this cell (metadata or legacy paths). + * Used so we only show "Transcribing source audio…" when transcription can actually run — not when + * source text is missing from the index or empty with no audio. + */ +async function sourceCellHasResolvableAudioForTranscription( + sourceUri: vscode.Uri, + cellId: string, + workspaceFolder: vscode.WorkspaceFolder +): Promise { + const cancellationTokenSource = new vscode.CancellationTokenSource(); + try { + const sourceDoc = await CodexCellDocument.create( + sourceUri, + undefined, + cancellationTokenSource.token + ); + try { + const cell = sourceDoc.getCell(cellId); + const audioPath = await getAudioFilePathForCell( + cell ?? {}, + cellId, + workspaceFolder, + sourceUri + ); + return audioPath !== null; + } finally { + sourceDoc.dispose(); + } + } catch (e) { + debug("sourceCellHasResolvableAudioForTranscription: could not read source or check audio", e); + return false; + } finally { + cancellationTokenSource.dispose(); + } +} + // Individual message handlers const messageHandlers: Record Promise | void> = { webviewReady: () => { /* no-op */ }, @@ -816,6 +853,23 @@ const messageHandlers: Record Promise true); + if (!readyNoAudio) { + vscode.window.showWarningMessage( + "LLM is not configured. Set an API key or sign in to generate predictions." + ); + } + await provider.addCellToSingleCellQueue(cellId, document, webviewPanel, addContentToValue); + return; + } + await vscode.commands.executeCommand( "vscode.openWith", sourcePath, diff --git a/src/test/suite/codexCellEditorProvider.test.ts b/src/test/suite/codexCellEditorProvider.test.ts index 2f7530452..9ea61eed7 100644 --- a/src/test/suite/codexCellEditorProvider.test.ts +++ b/src/test/suite/codexCellEditorProvider.test.ts @@ -2922,6 +2922,100 @@ suite("CodexCellEditorProvider Test Suite", () => { } }); + test("llmCompletion skips source transcription when source is empty and no source audio exists", async function () { + this.timeout(15000); + + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if (!workspaceFolder) { + this.skip(); + } + + const baseName = `llm-skip-transcribe-${Date.now()}`; + const codexName = `${baseName}.codex`; + const sourcePath = vscode.Uri.joinPath(workspaceFolder.uri, ".project", "sourceTexts", `${baseName}.source`); + const codexPath = vscode.Uri.joinPath(workspaceFolder.uri, "files", "target", codexName); + + fs.mkdirSync(path.dirname(sourcePath.fsPath), { recursive: true }); + fs.mkdirSync(path.dirname(codexPath.fsPath), { recursive: true }); + + const sourceNotebook = JSON.parse(JSON.stringify(codexSubtitleContent)) as CodexNotebookAsJSONData; + const firstCell = sourceNotebook.cells[0] as any; + firstCell.value = ""; + if (firstCell.metadata) { + delete firstCell.metadata.attachments; + delete firstCell.metadata.selectedAudioId; + } + + const encoder = new TextEncoder(); + await vscode.workspace.fs.writeFile( + sourcePath, + encoder.encode(JSON.stringify(sourceNotebook, null, 2)) + ); + await vscode.workspace.fs.writeFile( + codexPath, + encoder.encode(JSON.stringify(codexSubtitleContent, null, 2)) + ); + + const cellId = codexSubtitleContent.cells[0].metadata.id; + + const testProvider = new CodexCellEditorProvider(context); + const document = await testProvider.openCustomDocument( + codexPath, + { backupId: undefined }, + new vscode.CancellationTokenSource().token + ); + + const { panel: webviewPanel } = createMockWebviewPanel(); + await testProvider.resolveCustomEditor( + document, + webviewPanel, + new vscode.CancellationTokenSource().token + ); + + const addQueueStub = sinon.stub(testProvider as any, "addCellToSingleCellQueue").resolves(); + + const originalExecuteCommand = vscode.commands.executeCommand; + let openWithCallCount = 0; + (vscode.commands as any).executeCommand = async (command: string, ...args: any[]) => { + if (command === "vscode.openWith") { + openWithCallCount++; + return undefined; + } + if (command === "codex-editor-extension.getSourceCellByCellIdFromAllSourceCells") { + return { cellId: args[0] as string, content: "" }; + } + return originalExecuteCommand.apply(vscode.commands, [command, ...args]); + }; + + try { + await handleMessages( + { command: "llmCompletion", content: { currentLineId: cellId } } as any, + webviewPanel, + document, + () => { /* no-op */ }, + testProvider as any + ); + await sleep(300); + + assert.strictEqual( + openWithCallCount, + 0, + "Must not open the source editor for transcription when there is no source audio" + ); + assert.strictEqual( + addQueueStub.called, + true, + "Should enqueue LLM completion directly when transcription is not possible" + ); + } finally { + (vscode.commands as any).executeCommand = originalExecuteCommand; + addQueueStub.restore(); + document.dispose(); + await deleteIfExists(sourcePath); + await deleteIfExists(codexPath); + } + }); + test("validateCellContent persists validatedBy on latest edit", async () => { const provider = new CodexCellEditorProvider(context); const document = await provider.openCustomDocument( diff --git a/src/test/suite/startupFlowProvider_updateSync.test.ts b/src/test/suite/startupFlowProvider_updateSync.test.ts index eaa3f739b..f58e8c767 100644 --- a/src/test/suite/startupFlowProvider_updateSync.test.ts +++ b/src/test/suite/startupFlowProvider_updateSync.test.ts @@ -10,6 +10,7 @@ import { createMockExtensionContext, swallowDuplicateCommandRegistrations } from import * as directoryConflicts from "../../projectManager/utils/merge/directoryConflicts"; import * as mergeResolvers from "../../projectManager/utils/merge/resolvers"; +import * as connectivityChecker from "../../utils/connectivityChecker"; import * as projectLocationUtils from "../../utils/projectLocationUtils"; suite("StartupFlowProvider Update - triggers LFS-aware sync", () => { @@ -45,6 +46,10 @@ suite("StartupFlowProvider Update - triggers LFS-aware sync", () => { vscode.Uri.file(tempProjectsDir) ); + // Avoid indefinite wait: performProjectUpdate calls ensureConnectivity(), which polls until + // isOnline() succeeds — in tests fetch often fails, so the update never finishes. + const ensureConnectivityStub = sinon.stub(connectivityChecker, "ensureConnectivity").resolves(); + // Make merge steps no-op const buildConflictsStub = sinon.stub(directoryConflicts, "buildConflictsFromDirectories").resolves({ textConflicts: [], @@ -156,6 +161,7 @@ suite("StartupFlowProvider Update - triggers LFS-aware sync", () => { initStub.restore(); getExtensionStub.restore(); getCodexProjectsDirectoryStub.restore(); + ensureConnectivityStub.restore(); buildConflictsStub.restore(); resolveConflictFilesStub.restore(); if (fsStatStub) {