From e0f06a295c03e43159cce3b886f32c961e4a6954 Mon Sep 17 00:00:00 2001 From: zknpr Date: Wed, 20 May 2026 17:17:58 +0200 Subject: [PATCH 1/2] chore: post-Jules-triage followups Tighten internal sql.js types in WasmDatabaseEngine to CellValue[], removing the `params as unknown[]` cast at the iterateStatements bind site (gap noted while closing #312). Export WorkerPort interface so #326's connectWorkerPort tests typecheck under `tsc --noEmit`, and apply matching variance/structural casts to rpc.test.ts and webviewMessageHandler.test.ts so the tightened types from #305 (`unknown[]`) and #330 (`HostBridge`) don't introduce new tsc errors. Switch the two remaining bare `import.meta.env.VSCODE_BROWSER_EXT` reads in editorController.ts to optional chaining, matching the pattern already used in threadPool.ts and cryptoShim.ts. This unblocks testing the read-only vs read-write registration branch. Add tests/unit/editorController.test.ts covering registerEditorProvider: - readOnly=true picks DatabaseViewerProvider regardless of verified - verified=false picks DatabaseViewerProvider - verified=true + readOnly=false picks DatabaseEditorProvider - retainContextWhenHidden=false is set - returns a disposable This closes the coverage gap from #292 that I closed without iterating. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/core/engine/wasm/WasmDatabaseEngine.ts | 14 ++-- src/core/rpc.ts | 2 +- src/editorController.ts | 4 +- tests/unit/editorController.test.ts | 94 ++++++++++++++++++++++ tests/unit/rpc.test.ts | 4 +- tests/unit/webviewMessageHandler.test.ts | 6 +- 6 files changed, 109 insertions(+), 15 deletions(-) create mode 100644 tests/unit/editorController.test.ts 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..19c807e --- /dev/null +++ b/tests/unit/editorController.test.ts @@ -0,0 +1,94 @@ +import './vscode_mock_setup'; + +import { describe, it, before, after, 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.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', From 2b0a1b7b8ed252af834660224b05839c28ac18f1 Mon Sep 17 00:00:00 2001 From: zknpr Date: Wed, 20 May 2026 17:44:38 +0200 Subject: [PATCH 2/2] test: fix tsc error and drop unused imports in editorController.test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply review feedback from PR #338: line 39 read `mockVscode.window.registerCustomEditorProvider` without the `as any` cast that line 40 has — the mock window object doesn't declare this property, so it introduced a fresh TS2339, regressing the tsc-clean invariant that this PR was meant to restore. Add the matching cast. Also drop `before, after` from the import list since the test only uses `beforeEach`/`afterEach`. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/unit/editorController.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/editorController.test.ts b/tests/unit/editorController.test.ts index 19c807e..43eebb6 100644 --- a/tests/unit/editorController.test.ts +++ b/tests/unit/editorController.test.ts @@ -1,6 +1,6 @@ import './vscode_mock_setup'; -import { describe, it, before, after, beforeEach, afterEach } from 'node:test'; +import { describe, it, beforeEach, afterEach } from 'node:test'; import assert from 'node:assert'; import { mockVscode } from './mocks/vscode'; @@ -36,7 +36,7 @@ describe('registerEditorProvider', () => { beforeEach(() => { calls = []; - originalRegister = mockVscode.window.registerCustomEditorProvider; + originalRegister = (mockVscode.window as any).registerCustomEditorProvider; (mockVscode.window as any).registerCustomEditorProvider = ( viewType: string, provider: unknown,