* Copyright (c) 2026 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as assert from 'assert';
import * as path from 'path';
import * as vscode from 'vscode';
import { DebugProtocol } from 'vscode-debugprotocol';
import { createKeelsDebugAdapterForTest, KeelsConfigurationProvider } from '../../src/keelsRun';
import { RemotePlatform } from '../../src/types';
import { Device, KeelsDeviceManager } from '../../src/utils';
import { activateCjmpExtension } from '../helpers/extensionHost';
suite('Keels debug basic protocol', () => {
* @function testResolvesAnEmptyDebugConfigurationFromTheSelectedDevice
* @description Tests whether an empty debug configuration can be resolved from the selected device.
* @returns {Promise<void>} - Returns a Promise that resolves when the async test assertions have completed.
*/
test('resolves an empty debug configuration from the selected device', async () => {
await activateCjmpExtension();
const provider = new KeelsConfigurationProvider(
new StaticDeviceManager() as unknown as KeelsDeviceManager,
);
const document = await vscode.workspace.openTextDocument(
vscode.Uri.joinPath(vscode.workspace.workspaceFolders![0].uri, 'lib', 'index.cj'),
);
await vscode.window.showTextDocument(document);
const config = await provider.resolveDebugConfiguration(undefined, {} as vscode.DebugConfiguration);
assert.strictEqual(config.type, 'keels');
assert.strictEqual(config.name, 'Launch');
assert.strictEqual(config.request, 'launch');
assert.strictEqual(config.program, '${file}');
assert.strictEqual(config.deviceId, 'test-android-device');
assert.strictEqual(config.remotePlatform, RemotePlatform.ANDROID);
});
* @function testRespondsToInitializeSetBreakpointsAndDisconnectRequests
* @description Tests whether initialize, setBreakpoints, and disconnect requests are handled correctly.
* @returns {Promise<void>} - Returns a Promise that resolves when the async test assertions have completed.
*/
test('responds to initialize, setBreakpoints, and disconnect requests', async () => {
await activateCjmpExtension();
const adapter = createKeelsDebugAdapterForTest(
new StaticDeviceManager() as unknown as KeelsDeviceManager,
{ configuration: {} } as vscode.DebugSession,
createTestExtensionContext(),
);
const messages: DebugProtocol.ProtocolMessage[] = [];
const subscription = adapter.onDidSendMessage((message) => {
messages.push(message as DebugProtocol.ProtocolMessage);
});
await adapter.handleMessage({ type: 'request', seq: 1, command: 'initialize' } as DebugProtocol.Request);
await adapter.handleMessage({
type: 'request',
seq: 2,
command: 'setBreakpoints',
arguments: { lines: [3, 7] },
} as DebugProtocol.Request);
await adapter.handleMessage({ type: 'request', seq: 3, command: 'disconnect' } as DebugProtocol.Request);
subscription.dispose();
adapter.dispose();
const initialize = messages.find((message) => message.type === 'response' && (message as DebugProtocol.Response).command === 'initialize') as DebugProtocol.InitializeResponse;
assert.strictEqual(initialize.success, true);
assert.ok(initialize.body);
assert.strictEqual(initialize.body.supportsConfigurationDoneRequest, true);
const breakpoints = messages.find((message) => message.type === 'response' && (message as DebugProtocol.Response).command === 'setBreakpoints') as DebugProtocol.SetBreakpointsResponse;
assert.strictEqual(breakpoints.success, true);
assert.deepStrictEqual(breakpoints.body.breakpoints, [
{ verified: true, line: 3 },
{ verified: true, line: 7 },
]);
const disconnect = messages.find((message) => message.type === 'response' && (message as DebugProtocol.Response).command === 'disconnect') as DebugProtocol.DisconnectResponse;
assert.strictEqual(disconnect.success, true);
});
});
class StaticDeviceManager {
async showDevicePicker(): Promise<Device> {
return {
name: 'Android Test Device',
id: 'test-android-device',
isSupported: true,
targetPlatform: 'android-arm64',
emulator: false,
category: 'mobile',
platform: 'ANDROID',
};
}
}
function createTestExtensionContext(): vscode.ExtensionContext {
return {
subscriptions: [],
workspaceState: createMemento(),
globalState: createMemento(),
extensionUri: vscode.Uri.file(process.cwd()),
extensionPath: process.cwd(),
environmentVariableCollection: {} as vscode.GlobalEnvironmentVariableCollection,
extensionMode: vscode.ExtensionMode.Test,
extension: {} as vscode.Extension<unknown>,
secrets: {} as vscode.SecretStorage,
storageUri: undefined,
storagePath: undefined,
globalStorageUri: vscode.Uri.file(process.cwd()),
globalStoragePath: process.cwd(),
logUri: vscode.Uri.file(process.cwd()),
logPath: process.cwd(),
asAbsolutePath: (relativePath: string) => path.join(process.cwd(), relativePath),
} as unknown as vscode.ExtensionContext;
}
function createMemento(): vscode.Memento {
const values = new Map<string, unknown>();
return {
keys: () => [...values.keys()],
get: <T>(key: string, defaultValue?: T) => (values.has(key) ? values.get(key) : defaultValue) as T,
update: async (key: string, value: unknown) => {
values.set(key, value);
},
};
}