Skip to content

Commit 7d1475d

Browse files
Merge pull request Patrick-Ehimen#30 from Patrick-Ehimen/feat/shared-extension-core
feat: Implement shared IDE extension core module
2 parents b723cb0 + 02abccc commit 7d1475d

24 files changed

Lines changed: 4123 additions & 4 deletions
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* Command registry tests
3+
* @fileoverview Tests for the command registry functionality
4+
*/
5+
6+
import { CommandRegistryImpl } from "../src/core/command-registry";
7+
import { ExtensionEventEmitter } from "../src/events/event-emitter";
8+
import type { CommandDefinition } from "../src/types/index";
9+
10+
describe("CommandRegistry", () => {
11+
let commandRegistry: CommandRegistryImpl;
12+
let eventEmitter: ExtensionEventEmitter;
13+
14+
beforeEach(() => {
15+
eventEmitter = new ExtensionEventEmitter();
16+
commandRegistry = new CommandRegistryImpl(eventEmitter);
17+
});
18+
19+
describe("command registration", () => {
20+
it("should register a command successfully", () => {
21+
const command: CommandDefinition = {
22+
id: "test.command",
23+
title: "Test Command",
24+
handler: jest.fn(),
25+
};
26+
27+
commandRegistry.registerCommand(command);
28+
29+
expect(commandRegistry.hasCommand("test.command")).toBe(true);
30+
expect(commandRegistry.getCommands()).toHaveLength(1);
31+
});
32+
33+
it("should throw error for invalid command definition", () => {
34+
const invalidCommand = {
35+
title: "Invalid Command",
36+
handler: jest.fn(),
37+
} as CommandDefinition;
38+
39+
expect(() => commandRegistry.registerCommand(invalidCommand)).toThrow();
40+
});
41+
42+
it("should unregister a command", () => {
43+
const command: CommandDefinition = {
44+
id: "test.command",
45+
title: "Test Command",
46+
handler: jest.fn(),
47+
};
48+
49+
commandRegistry.registerCommand(command);
50+
expect(commandRegistry.hasCommand("test.command")).toBe(true);
51+
52+
commandRegistry.unregisterCommand("test.command");
53+
expect(commandRegistry.hasCommand("test.command")).toBe(false);
54+
});
55+
});
56+
57+
describe("command execution", () => {
58+
it("should execute a registered command", async () => {
59+
const mockHandler = jest.fn().mockResolvedValue("success");
60+
const command: CommandDefinition = {
61+
id: "test.command",
62+
title: "Test Command",
63+
handler: mockHandler,
64+
};
65+
66+
commandRegistry.registerCommand(command);
67+
68+
const result = await commandRegistry.executeCommand("test.command", "arg1", "arg2");
69+
70+
expect(mockHandler).toHaveBeenCalledWith("arg1", "arg2");
71+
expect(result).toBe("success");
72+
});
73+
74+
it("should throw error for unregistered command", async () => {
75+
await expect(commandRegistry.executeCommand("nonexistent.command")).rejects.toThrow(
76+
"Command nonexistent.command is not registered",
77+
);
78+
});
79+
80+
it("should throw error for disabled command", async () => {
81+
const command: CommandDefinition = {
82+
id: "test.command",
83+
title: "Test Command",
84+
handler: jest.fn(),
85+
enabled: false,
86+
};
87+
88+
commandRegistry.registerCommand(command);
89+
90+
await expect(commandRegistry.executeCommand("test.command")).rejects.toThrow(
91+
"Command test.command is disabled",
92+
);
93+
});
94+
});
95+
});
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Extension core tests
3+
* @fileoverview Tests for the main extension core functionality
4+
*/
5+
6+
import { ExtensionCoreImpl, createExtensionCore } from "../src/core/extension-core";
7+
8+
describe("ExtensionCore", () => {
9+
let extensionCore: ExtensionCoreImpl;
10+
11+
beforeEach(() => {
12+
extensionCore = new ExtensionCoreImpl();
13+
});
14+
15+
afterEach(async () => {
16+
if (extensionCore.isInitialized()) {
17+
await extensionCore.dispose();
18+
}
19+
});
20+
21+
describe("initialization", () => {
22+
it("should initialize successfully", async () => {
23+
expect(extensionCore.isInitialized()).toBe(false);
24+
25+
await extensionCore.initialize();
26+
27+
expect(extensionCore.isInitialized()).toBe(true);
28+
});
29+
30+
it("should not initialize twice", async () => {
31+
await extensionCore.initialize();
32+
33+
// Second initialization should not throw
34+
await expect(extensionCore.initialize()).resolves.toBeUndefined();
35+
expect(extensionCore.isInitialized()).toBe(true);
36+
});
37+
38+
it("should provide access to all components", async () => {
39+
await extensionCore.initialize();
40+
41+
expect(extensionCore.getCommandRegistry()).toBeDefined();
42+
expect(extensionCore.getWorkspaceContextProvider()).toBeDefined();
43+
expect(extensionCore.getAICommandHandler()).toBeDefined();
44+
expect(extensionCore.getProgressStreamer()).toBeDefined();
45+
expect(extensionCore.getConfigurationManager()).toBeDefined();
46+
});
47+
});
48+
49+
describe("disposal", () => {
50+
it("should dispose successfully", async () => {
51+
await extensionCore.initialize();
52+
expect(extensionCore.isInitialized()).toBe(true);
53+
54+
await extensionCore.dispose();
55+
expect(extensionCore.isInitialized()).toBe(false);
56+
});
57+
58+
it("should handle disposal when not initialized", async () => {
59+
expect(extensionCore.isInitialized()).toBe(false);
60+
61+
await expect(extensionCore.dispose()).resolves.toBeUndefined();
62+
});
63+
});
64+
65+
describe("factory function", () => {
66+
it("should create extension core instance", () => {
67+
const core = createExtensionCore();
68+
expect(core).toBeInstanceOf(ExtensionCoreImpl);
69+
expect(core.isInitialized()).toBe(false);
70+
});
71+
});
72+
});
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/**
2+
* Progress streamer tests
3+
* @fileoverview Tests for the progress streaming functionality
4+
*/
5+
6+
import { ProgressStreamerImpl } from "../src/core/progress-streamer";
7+
import { ExtensionEventEmitter } from "../src/events/event-emitter";
8+
9+
describe("ProgressStreamer", () => {
10+
let progressStreamer: ProgressStreamerImpl;
11+
let eventEmitter: ExtensionEventEmitter;
12+
13+
beforeEach(() => {
14+
eventEmitter = new ExtensionEventEmitter();
15+
progressStreamer = new ProgressStreamerImpl(eventEmitter);
16+
});
17+
18+
describe("progress stream management", () => {
19+
it("should start a progress stream", () => {
20+
const stream = progressStreamer.startProgress("test-op", "Test Operation");
21+
22+
expect(stream.operationId).toBe("test-op");
23+
expect(stream.title).toBe("Test Operation");
24+
expect(stream.isActive()).toBe(true);
25+
expect(progressStreamer.getProgress("test-op")).toBe(stream);
26+
});
27+
28+
it("should update progress", () => {
29+
const stream = progressStreamer.startProgress("test-op", "Test Operation");
30+
31+
stream.update({
32+
progress: 50,
33+
message: "Half way done",
34+
});
35+
36+
const currentProgress = stream.getCurrentProgress();
37+
expect(currentProgress.progress).toBe(50);
38+
expect(currentProgress.message).toBe("Half way done");
39+
});
40+
41+
it("should complete progress", () => {
42+
const stream = progressStreamer.startProgress("test-op", "Test Operation");
43+
44+
stream.complete({ result: "success" });
45+
46+
expect(stream.isActive()).toBe(false);
47+
const currentProgress = stream.getCurrentProgress();
48+
expect(currentProgress.completed).toBe(true);
49+
expect(currentProgress.result).toEqual({ result: "success" });
50+
});
51+
52+
it("should fail progress", () => {
53+
const stream = progressStreamer.startProgress("test-op", "Test Operation");
54+
const error = new Error("Test error");
55+
56+
stream.fail(error);
57+
58+
expect(stream.isActive()).toBe(false);
59+
const currentProgress = stream.getCurrentProgress();
60+
expect(currentProgress.completed).toBe(true);
61+
expect(currentProgress.error).toBe("Test error");
62+
});
63+
64+
it("should cancel progress", () => {
65+
const stream = progressStreamer.startProgress("test-op", "Test Operation");
66+
67+
stream.cancel();
68+
69+
expect(stream.isActive()).toBe(false);
70+
const currentProgress = stream.getCurrentProgress();
71+
expect(currentProgress.cancelled).toBe(true);
72+
});
73+
});
74+
75+
describe("stream collection management", () => {
76+
it("should get active streams", () => {
77+
const stream1 = progressStreamer.startProgress("op1", "Operation 1");
78+
const stream2 = progressStreamer.startProgress("op2", "Operation 2");
79+
80+
expect(progressStreamer.getActiveStreams()).toHaveLength(2);
81+
82+
stream1.complete();
83+
expect(progressStreamer.getActiveStreams()).toHaveLength(1);
84+
});
85+
86+
it("should stop progress stream", () => {
87+
const stream = progressStreamer.startProgress("test-op", "Test Operation");
88+
expect(stream.isActive()).toBe(true);
89+
90+
progressStreamer.stopProgress("test-op");
91+
expect(stream.isActive()).toBe(false);
92+
expect(progressStreamer.getProgress("test-op")).toBeUndefined();
93+
});
94+
95+
it("should clear all streams", () => {
96+
progressStreamer.startProgress("op1", "Operation 1");
97+
progressStreamer.startProgress("op2", "Operation 2");
98+
99+
expect(progressStreamer.getActiveStreams()).toHaveLength(2);
100+
101+
progressStreamer.clearAllStreams();
102+
expect(progressStreamer.getActiveStreams()).toHaveLength(0);
103+
});
104+
});
105+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import baseConfig from "../../eslint.config.mjs";
2+
3+
export default [
4+
...baseConfig,
5+
{
6+
files: ["**/*.ts"],
7+
rules: {
8+
// Extension-specific rules can be added here
9+
},
10+
},
11+
];
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/** @type {import('jest').Config} */
2+
module.exports = {
3+
preset: "ts-jest",
4+
testEnvironment: "node",
5+
roots: ["<rootDir>/src", "<rootDir>/__tests__"],
6+
testMatch: ["**/__tests__/**/*.test.ts", "**/*.test.ts"],
7+
collectCoverageFrom: ["src/**/*.ts", "!src/**/*.d.ts", "!src/index.ts"],
8+
coverageDirectory: "coverage",
9+
coverageReporters: ["text", "lcov", "html"],
10+
setupFilesAfterEnv: ["../../jest.setup.js"],
11+
moduleNameMapper: {
12+
"^(\\.{1,2}/.*)\\.js$": "$1",
13+
},
14+
};
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "@lighthouse-tooling/extension-core",
3+
"version": "0.1.0",
4+
"description": "Shared IDE extension core module for Lighthouse AI integration",
5+
"main": "dist/index.js",
6+
"types": "dist/index.d.ts",
7+
"scripts": {
8+
"build": "tsc -p tsconfig.build.json",
9+
"test": "jest",
10+
"test:watch": "jest --watch",
11+
"test:coverage": "jest --coverage",
12+
"lint": "eslint .",
13+
"lint:fix": "eslint . --fix",
14+
"format": "prettier --write .",
15+
"format:check": "prettier --check .",
16+
"clean": "rm -rf dist"
17+
},
18+
"devDependencies": {
19+
"jest": "^29.7.0",
20+
"ts-jest": "^29.1.1",
21+
"@types/jest": "^29.5.5",
22+
"typescript": "^5.0.0"
23+
},
24+
"dependencies": {
25+
"@lighthouse-tooling/types": "workspace:*",
26+
"@lighthouse-tooling/shared": "workspace:*",
27+
"@lighthouse-tooling/sdk-wrapper": "workspace:*"
28+
}
29+
}

0 commit comments

Comments
 (0)