diff --git a/src/core/engine/wasm/WasmDatabaseEngine.ts b/src/core/engine/wasm/WasmDatabaseEngine.ts index f80d4ec..7c952b5 100644 --- a/src/core/engine/wasm/WasmDatabaseEngine.ts +++ b/src/core/engine/wasm/WasmDatabaseEngine.ts @@ -32,9 +32,9 @@ import { getNodeFs } from '../../platform/fs'; // ============================================================================ interface WasmPreparedStatement { - run(params?: unknown[]): void; - bind(params?: unknown[]): boolean; - get(params?: unknown[]): CellValue[] | undefined; + run(params?: CellValue[]): void; + bind(params?: CellValue[]): boolean; + get(params?: CellValue[]): CellValue[] | undefined; step(): boolean; reset(): void; free(): boolean; @@ -42,8 +42,8 @@ interface WasmPreparedStatement { } export interface WasmDatabaseInstance { - exec(sql: string, params?: unknown[]): Array<{ columns: string[]; values: unknown[][] }>; - prepare(sql: string, params?: unknown[]): WasmPreparedStatement; + exec(sql: string, params?: CellValue[]): Array<{ columns: string[]; values: CellValue[][] }>; + prepare(sql: string, params?: CellValue[]): WasmPreparedStatement; iterateStatements(sql: string): Iterable; export(): Uint8Array; close(): void; @@ -122,7 +122,7 @@ export class WasmDatabaseEngine implements DatabaseOperations { // Bind parameters only to the first statement to match exec behavior if (isFirstStatement && params && params.length > 0) { - stmt.bind(params as unknown[]); + stmt.bind(params); } isFirstStatement = false; @@ -301,7 +301,7 @@ export class WasmDatabaseEngine implements DatabaseOperations { const stmt = this.instance.prepare(sql); try { for (const [rId, rowObj] of rowUpdates.entries()) { - const params: any[] = deletedColumns.map(c => rowObj[c.name] ?? null); + const params: CellValue[] = deletedColumns.map(c => rowObj[c.name] ?? null); params.push(rId); stmt.run(params); } diff --git a/src/core/rpc.ts b/src/core/rpc.ts index 3ebad8a..6538b8e 100644 --- a/src/core/rpc.ts +++ b/src/core/rpc.ts @@ -345,7 +345,7 @@ export function processProtocolMessage( /** * Worker-like interface for message passing. */ -interface WorkerPort { +export interface WorkerPort { postMessage(data: unknown, transfer?: Transferable[]): void; on(event: 'message', handler: (data: unknown) => void): void; } diff --git a/src/editorController.ts b/src/editorController.ts index fea0964..f2f7812 100644 --- a/src/editorController.ts +++ b/src/editorController.ts @@ -260,7 +260,7 @@ export class DatabaseViewerProvider extends Disposable implements vsc.CustomRead // Build environment data for webview const vscodeEnv = { webviewId, - browserExt: toBoolString(!!import.meta.env.VSCODE_BROWSER_EXT), + browserExt: toBoolString(!!import.meta.env?.VSCODE_BROWSER_EXT), uriScheme, appHost, appName, extensionUrl, uiKind: uiKindToString(uiKind), firstInstall: doTry(() => new Date(this.context.globalState.get(FirstInstallMs) ?? Date.now()).toISOString()), @@ -378,7 +378,7 @@ export function registerEditorProvider( outputChannel: vsc.OutputChannel | null, { verified, accessToken, readOnly }: { verified: boolean, accessToken?: string, readOnly?: boolean } ) { - const enableReadWrite = !import.meta.env.VSCODE_BROWSER_EXT && verified && !readOnly && SupportsWriteMode; + const enableReadWrite = !import.meta.env?.VSCODE_BROWSER_EXT && verified && !readOnly && SupportsWriteMode; const Provider = enableReadWrite ? DatabaseEditorProvider : DatabaseViewerProvider; return vsc.window.registerCustomEditorProvider( viewType, diff --git a/tests/unit/editorController.test.ts b/tests/unit/editorController.test.ts new file mode 100644 index 0000000..43eebb6 --- /dev/null +++ b/tests/unit/editorController.test.ts @@ -0,0 +1,94 @@ +import './vscode_mock_setup'; + +import { describe, it, beforeEach, afterEach } from 'node:test'; +import assert from 'node:assert'; +import { mockVscode } from './mocks/vscode'; + +// SupportsWriteMode in databaseModel.ts is evaluated at module-load time from +// `vsc.env.remoteName` and `CurrentExtension?.extensionKind`. Set both so the +// read-write path resolves to true regardless of which test loads databaseModel first. +(mockVscode as any).ExtensionKind = { Workspace: 2, UI: 1 }; +mockVscode.env.remoteName = 'remote'; +(mockVscode as any).extensions = { + getExtension: () => ({ extensionKind: 2 }) +}; + +// workerFactory imports threadPool which crashes due to bare `import.meta.env` at +// module load. Mock it in require cache before editorController is required. +const moduleCache = require('module')._cache; +const workerFactoryPath = require.resolve('../../src/workerFactory'); +moduleCache[workerFactoryPath] = { + id: workerFactoryPath, + filename: workerFactoryPath, + loaded: true, + exports: { + createDatabaseConnection: () => {} + } +}; + +const editorControllerModule = require('../../src/editorController'); +const { registerEditorProvider, DatabaseViewerProvider, DatabaseEditorProvider } = editorControllerModule; + +describe('registerEditorProvider', () => { + type RegisterCall = { viewType: string; provider: unknown; options: unknown }; + let calls: RegisterCall[]; + let originalRegister: any; + + beforeEach(() => { + calls = []; + originalRegister = (mockVscode.window as any).registerCustomEditorProvider; + (mockVscode.window as any).registerCustomEditorProvider = ( + viewType: string, + provider: unknown, + options: unknown + ) => { + calls.push({ viewType, provider, options }); + return { dispose: () => {} }; + }; + }); + + afterEach(() => { + (mockVscode.window as any).registerCustomEditorProvider = originalRegister; + }); + + const ctx = { extensionUri: mockVscode.Uri.file('/ext'), globalState: { get: () => undefined } } as any; + + it('registers DatabaseViewerProvider when readOnly=true regardless of verified', () => { + registerEditorProvider('sqlite-explorer.view', ctx, undefined, null, { verified: true, readOnly: true }); + + assert.strictEqual(calls.length, 1); + assert.strictEqual(calls[0].viewType, 'sqlite-explorer.view'); + assert.ok(calls[0].provider instanceof DatabaseViewerProvider); + assert.ok(!(calls[0].provider instanceof DatabaseEditorProvider)); + }); + + it('registers DatabaseViewerProvider when verified=false', () => { + registerEditorProvider('sqlite-explorer.view', ctx, undefined, null, { verified: false, readOnly: false }); + + assert.strictEqual(calls.length, 1); + assert.ok(calls[0].provider instanceof DatabaseViewerProvider); + assert.ok(!(calls[0].provider instanceof DatabaseEditorProvider)); + }); + + it('registers DatabaseEditorProvider when verified=true and readOnly=false (write mode enabled)', () => { + registerEditorProvider('sqlite-explorer.edit', ctx, undefined, null, { verified: true, readOnly: false }); + + assert.strictEqual(calls.length, 1); + assert.strictEqual(calls[0].viewType, 'sqlite-explorer.edit'); + // DatabaseEditorProvider extends DatabaseViewerProvider, so check the more specific type. + assert.ok(calls[0].provider instanceof DatabaseEditorProvider); + }); + + it('passes retainContextWhenHidden=false in webview options', () => { + registerEditorProvider('sqlite-explorer.view', ctx, undefined, null, { verified: true, readOnly: true }); + + const options = calls[0].options as { webviewOptions: { retainContextWhenHidden: boolean } }; + assert.strictEqual(options.webviewOptions.retainContextWhenHidden, false); + }); + + it('returns a disposable from registerCustomEditorProvider', () => { + const result = registerEditorProvider('sqlite-explorer.view', ctx, undefined, null, { verified: true, readOnly: true }); + + assert.ok(result && typeof result.dispose === 'function'); + }); +}); diff --git a/tests/unit/rpc.test.ts b/tests/unit/rpc.test.ts index 8c7ff46..6117c32 100644 --- a/tests/unit/rpc.test.ts +++ b/tests/unit/rpc.test.ts @@ -11,8 +11,8 @@ describe('RPC', () => { }); it('should handle invocations', (context) => { - const methods = { - add: (a: number, b: number) => a + b + const methods: Record unknown> = { + add: (...args: unknown[]) => (args[0] as number) + (args[1] as number) }; let response: any = null; diff --git a/tests/unit/webviewMessageHandler.test.ts b/tests/unit/webviewMessageHandler.test.ts index 6716daf..b25d850 100644 --- a/tests/unit/webviewMessageHandler.test.ts +++ b/tests/unit/webviewMessageHandler.test.ts @@ -13,7 +13,7 @@ describe('WebviewMessageHandler', () => { return true; }; - const handler = new WebviewMessageHandler(postMessage, hostBridge); + const handler = new WebviewMessageHandler(postMessage, hostBridge as unknown as import('../../src/hostBridge').HostBridge); handler.handleMessage({ channel: 'rpc', @@ -51,7 +51,7 @@ describe('WebviewMessageHandler', () => { return true; }; - const handler = new WebviewMessageHandler(postMessage, hostBridge); + const handler = new WebviewMessageHandler(postMessage, hostBridge as unknown as import('../../src/hostBridge').HostBridge); handler.handleMessage({ channel: 'rpc', @@ -77,7 +77,7 @@ describe('WebviewMessageHandler', () => { return true; }; - const handler = new WebviewMessageHandler(postMessage, hostBridge); + const handler = new WebviewMessageHandler(postMessage, hostBridge as unknown as import('../../src/hostBridge').HostBridge); handler.handleMessage({ type: 'rpc-request',