@theia/plugin-ext 1.71.0-next.8 → 1.71.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/common/plugin-api-rpc.d.ts +76 -0
- package/lib/common/plugin-api-rpc.d.ts.map +1 -1
- package/lib/common/plugin-api-rpc.js +9 -1
- package/lib/common/plugin-api-rpc.js.map +1 -1
- package/lib/hosted/browser/hosted-plugin.d.ts.map +1 -1
- package/lib/hosted/browser/hosted-plugin.js +13 -6
- package/lib/hosted/browser/hosted-plugin.js.map +1 -1
- package/lib/main/browser/main-context.d.ts.map +1 -1
- package/lib/main/browser/main-context.js +2 -6
- package/lib/main/browser/main-context.js.map +1 -1
- package/lib/main/browser/main-file-system-event-service.d.ts +10 -2
- package/lib/main/browser/main-file-system-event-service.d.ts.map +1 -1
- package/lib/main/browser/main-file-system-event-service.js +19 -2
- package/lib/main/browser/main-file-system-event-service.js.map +1 -1
- package/lib/main/browser/menus/menus-contribution-handler.d.ts.map +1 -1
- package/lib/main/browser/menus/menus-contribution-handler.js +2 -0
- package/lib/main/browser/menus/menus-contribution-handler.js.map +1 -1
- package/lib/main/browser/menus/plugin-menu-command-adapter.d.ts +1 -0
- package/lib/main/browser/menus/plugin-menu-command-adapter.d.ts.map +1 -1
- package/lib/main/browser/menus/plugin-menu-command-adapter.js +13 -1
- package/lib/main/browser/menus/plugin-menu-command-adapter.js.map +1 -1
- package/lib/main/browser/menus/vscode-theia-menu-mappings.d.ts +1 -1
- package/lib/main/browser/menus/vscode-theia-menu-mappings.d.ts.map +1 -1
- package/lib/main/browser/menus/vscode-theia-menu-mappings.js +9 -2
- package/lib/main/browser/menus/vscode-theia-menu-mappings.js.map +1 -1
- package/lib/main/browser/scm-main.d.ts +33 -2
- package/lib/main/browser/scm-main.d.ts.map +1 -1
- package/lib/main/browser/scm-main.js +237 -3
- package/lib/main/browser/scm-main.js.map +1 -1
- package/lib/main/browser/scm-main.spec.d.ts +2 -0
- package/lib/main/browser/scm-main.spec.d.ts.map +1 -0
- package/lib/main/browser/scm-main.spec.js +87 -0
- package/lib/main/browser/scm-main.spec.js.map +1 -0
- package/lib/main/browser/test-main.d.ts +3 -2
- package/lib/main/browser/test-main.d.ts.map +1 -1
- package/lib/main/browser/test-main.js +12 -1
- package/lib/main/browser/test-main.js.map +1 -1
- package/lib/plugin/file-system-event-service-ext-impl.d.ts +11 -5
- package/lib/plugin/file-system-event-service-ext-impl.d.ts.map +1 -1
- package/lib/plugin/file-system-event-service-ext-impl.js +28 -9
- package/lib/plugin/file-system-event-service-ext-impl.js.map +1 -1
- package/lib/plugin/plugin-context.js +4 -4
- package/lib/plugin/plugin-context.js.map +1 -1
- package/lib/plugin/scm.d.ts +8 -2
- package/lib/plugin/scm.d.ts.map +1 -1
- package/lib/plugin/scm.js +188 -5
- package/lib/plugin/scm.js.map +1 -1
- package/lib/plugin/scm.spec.d.ts +2 -0
- package/lib/plugin/scm.spec.d.ts.map +1 -0
- package/lib/plugin/scm.spec.js +461 -0
- package/lib/plugin/scm.spec.js.map +1 -0
- package/lib/plugin/terminal-ext.d.ts +13 -3
- package/lib/plugin/terminal-ext.d.ts.map +1 -1
- package/lib/plugin/terminal-ext.js +51 -10
- package/lib/plugin/terminal-ext.js.map +1 -1
- package/lib/plugin/terminal-ext.spec.d.ts +2 -0
- package/lib/plugin/terminal-ext.spec.d.ts.map +1 -0
- package/lib/plugin/terminal-ext.spec.js +285 -0
- package/lib/plugin/terminal-ext.spec.js.map +1 -0
- package/lib/plugin/test-item.d.ts.map +1 -1
- package/lib/plugin/test-item.js +8 -3
- package/lib/plugin/test-item.js.map +1 -1
- package/lib/plugin/tests.d.ts.map +1 -1
- package/lib/plugin/tests.js +15 -3
- package/lib/plugin/tests.js.map +1 -1
- package/lib/plugin/type-converters.d.ts +2 -2
- package/lib/plugin/type-converters.d.ts.map +1 -1
- package/lib/plugin/type-converters.js +3 -9
- package/lib/plugin/type-converters.js.map +1 -1
- package/lib/plugin/types-impl.d.ts +1 -1
- package/lib/plugin/types-impl.d.ts.map +1 -1
- package/lib/plugin/types-impl.js +1 -1
- package/lib/plugin/types-impl.js.map +1 -1
- package/lib/plugin/workspace.d.ts.map +1 -1
- package/lib/plugin/workspace.js +17 -3
- package/lib/plugin/workspace.js.map +1 -1
- package/package.json +39 -39
- package/src/common/plugin-api-rpc.ts +78 -0
- package/src/hosted/browser/hosted-plugin.ts +13 -6
- package/src/main/browser/main-context.ts +3 -7
- package/src/main/browser/main-file-system-event-service.ts +26 -6
- package/src/main/browser/menus/menus-contribution-handler.ts +2 -0
- package/src/main/browser/menus/plugin-menu-command-adapter.ts +15 -2
- package/src/main/browser/menus/vscode-theia-menu-mappings.ts +12 -3
- package/src/main/browser/scm-main.spec.ts +105 -0
- package/src/main/browser/scm-main.ts +272 -4
- package/src/main/browser/test-main.ts +13 -3
- package/src/plugin/file-system-event-service-ext-impl.ts +40 -14
- package/src/plugin/plugin-context.ts +7 -7
- package/src/plugin/scm.spec.ts +615 -0
- package/src/plugin/scm.ts +224 -6
- package/src/plugin/terminal-ext.spec.ts +350 -0
- package/src/plugin/terminal-ext.ts +58 -12
- package/src/plugin/test-item.ts +8 -3
- package/src/plugin/tests.ts +14 -3
- package/src/plugin/type-converters.ts +7 -13
- package/src/plugin/types-impl.ts +2 -2
- package/src/plugin/workspace.ts +17 -3
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Tests for the SCM history provider bridge.
|
|
19
|
+
*
|
|
20
|
+
* These tests validate the plugin-side logic in isolation without requiring
|
|
21
|
+
* the full Theia runtime — following the pattern from workspace.spec.ts.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import * as assert from 'assert';
|
|
25
|
+
import { Emitter } from '@theia/core/lib/common/event';
|
|
26
|
+
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
|
27
|
+
import { ScmCommandArg, ScmHistoryItemCommandArg } from '../common/plugin-api-rpc';
|
|
28
|
+
|
|
29
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
30
|
+
|
|
31
|
+
interface HistoryItemRef {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
readonly name: string;
|
|
34
|
+
readonly description?: string;
|
|
35
|
+
readonly revision?: string;
|
|
36
|
+
readonly category?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface HistoryItemRefsChangeEvent {
|
|
40
|
+
readonly added: readonly HistoryItemRef[];
|
|
41
|
+
readonly removed: readonly HistoryItemRef[];
|
|
42
|
+
readonly modified: readonly HistoryItemRef[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface HistoryItem {
|
|
46
|
+
readonly id: string;
|
|
47
|
+
readonly parentIds?: readonly string[];
|
|
48
|
+
readonly subject: string;
|
|
49
|
+
readonly author?: string;
|
|
50
|
+
readonly authorEmail?: string;
|
|
51
|
+
readonly displayId?: string;
|
|
52
|
+
readonly timestamp?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface HistoryOptions {
|
|
56
|
+
readonly skip?: number;
|
|
57
|
+
readonly limit?: number | { id?: string };
|
|
58
|
+
readonly historyItemRefs?: readonly string[];
|
|
59
|
+
readonly filterText?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Extracted from SourceControlImpl.historyProvider setter logic.
|
|
64
|
+
* Tests the event subscription and proxy notification pattern.
|
|
65
|
+
*
|
|
66
|
+
* Note: SourceControlImpl cannot be instantiated directly in unit tests because
|
|
67
|
+
* it depends on ScmMain (RPC proxy) and CommandRegistryImpl, both of which
|
|
68
|
+
* require a full Theia runtime with inversify bindings and RPC channels.
|
|
69
|
+
* Instead, the bridge pattern extracts the logic under test into a standalone
|
|
70
|
+
* class that mirrors the real implementation, following the same pattern used
|
|
71
|
+
* in workspace.spec.ts for other plugin-side logic.
|
|
72
|
+
*/
|
|
73
|
+
class HistoryProviderBridge {
|
|
74
|
+
private _historyProvider: any;
|
|
75
|
+
private _historyProviderDisposables = new DisposableCollection();
|
|
76
|
+
|
|
77
|
+
readonly updateSourceControlCalls: Array<{ features: any }> = [];
|
|
78
|
+
readonly onDidChangeCurrentHistoryItemRefsCalls: number[] = [];
|
|
79
|
+
readonly onDidChangeHistoryItemRefsCalls: Array<{ event: any }> = [];
|
|
80
|
+
|
|
81
|
+
private historyItemRefToDto(ref: HistoryItemRef): any {
|
|
82
|
+
return { id: ref.id, name: ref.name, description: ref.description, revision: ref.revision, category: ref.category };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
setHistoryProvider(provider: any): void {
|
|
86
|
+
this._historyProviderDisposables.dispose();
|
|
87
|
+
this._historyProviderDisposables = new DisposableCollection();
|
|
88
|
+
this._historyProvider = provider;
|
|
89
|
+
|
|
90
|
+
if (provider) {
|
|
91
|
+
this._historyProviderDisposables.push(
|
|
92
|
+
provider.onDidChangeCurrentHistoryItemRefs(() => {
|
|
93
|
+
this.updateSourceControlCalls.push({
|
|
94
|
+
features: {
|
|
95
|
+
hasHistoryProvider: true,
|
|
96
|
+
currentHistoryItemRef: provider.currentHistoryItemRef ? this.historyItemRefToDto(provider.currentHistoryItemRef) : undefined,
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
this.onDidChangeCurrentHistoryItemRefsCalls.push(1);
|
|
100
|
+
})
|
|
101
|
+
);
|
|
102
|
+
this._historyProviderDisposables.push(
|
|
103
|
+
provider.onDidChangeHistoryItemRefs((event: HistoryItemRefsChangeEvent) => {
|
|
104
|
+
this.onDidChangeHistoryItemRefsCalls.push({
|
|
105
|
+
event: {
|
|
106
|
+
added: event.added.map((r: HistoryItemRef) => this.historyItemRefToDto(r)),
|
|
107
|
+
removed: event.removed.map((r: HistoryItemRef) => this.historyItemRefToDto(r)),
|
|
108
|
+
modified: event.modified.map((r: HistoryItemRef) => this.historyItemRefToDto(r)),
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
})
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.updateSourceControlCalls.push({
|
|
116
|
+
features: {
|
|
117
|
+
hasHistoryProvider: !!provider,
|
|
118
|
+
currentHistoryItemRef: provider?.currentHistoryItemRef ? this.historyItemRefToDto(provider.currentHistoryItemRef) : undefined,
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async provideHistoryItems(options: HistoryOptions): Promise<any[] | undefined> {
|
|
124
|
+
if (!this._historyProvider) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
const result = await this._historyProvider.provideHistoryItems(options);
|
|
128
|
+
if (!result) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
return result.map((item: HistoryItem) => ({
|
|
132
|
+
id: item.id,
|
|
133
|
+
parentIds: item.parentIds ? [...item.parentIds] : undefined,
|
|
134
|
+
subject: item.subject,
|
|
135
|
+
author: item.author,
|
|
136
|
+
authorEmail: item.authorEmail,
|
|
137
|
+
displayId: item.displayId,
|
|
138
|
+
timestamp: item.timestamp,
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async provideHistoryItemRefs(refs: string[] | undefined): Promise<HistoryItemRef[] | undefined> {
|
|
143
|
+
if (!this._historyProvider) {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
const result = await this._historyProvider.provideHistoryItemRefs(refs);
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
dispose(): void {
|
|
151
|
+
this._historyProviderDisposables.dispose();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
describe('SCM history provider bridge (plugin side)', () => {
|
|
156
|
+
let bridge: HistoryProviderBridge;
|
|
157
|
+
let bridgeDisposables: DisposableCollection;
|
|
158
|
+
|
|
159
|
+
beforeEach(() => {
|
|
160
|
+
bridgeDisposables = new DisposableCollection();
|
|
161
|
+
bridge = new HistoryProviderBridge();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
afterEach(() => {
|
|
165
|
+
bridge.dispose();
|
|
166
|
+
bridgeDisposables.dispose();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('setting historyProvider sends hasHistoryProvider: true', () => {
|
|
170
|
+
const onCurrentRefs = new Emitter<void>();
|
|
171
|
+
const onHistoryRefs = new Emitter<HistoryItemRefsChangeEvent>();
|
|
172
|
+
bridgeDisposables.push(onCurrentRefs);
|
|
173
|
+
bridgeDisposables.push(onHistoryRefs);
|
|
174
|
+
|
|
175
|
+
const provider = {
|
|
176
|
+
currentHistoryItemRef: { id: 'refs/heads/main', name: 'main' },
|
|
177
|
+
onDidChangeCurrentHistoryItemRefs: onCurrentRefs.event,
|
|
178
|
+
onDidChangeHistoryItemRefs: onHistoryRefs.event,
|
|
179
|
+
provideHistoryItemRefs: async () => [],
|
|
180
|
+
provideHistoryItems: async () => [],
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
bridge.setHistoryProvider(provider);
|
|
184
|
+
|
|
185
|
+
const lastCall = bridge.updateSourceControlCalls[bridge.updateSourceControlCalls.length - 1];
|
|
186
|
+
assert.ok(lastCall, 'Expected $updateSourceControl to be called');
|
|
187
|
+
assert.strictEqual(lastCall.features.hasHistoryProvider, true);
|
|
188
|
+
assert.strictEqual(lastCall.features.currentHistoryItemRef?.id, 'refs/heads/main');
|
|
189
|
+
assert.strictEqual(lastCall.features.currentHistoryItemRef?.name, 'main');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('setting historyProvider to undefined sends hasHistoryProvider: false', () => {
|
|
193
|
+
const onCurrentRefs = new Emitter<void>();
|
|
194
|
+
const onHistoryRefs = new Emitter<HistoryItemRefsChangeEvent>();
|
|
195
|
+
bridgeDisposables.push(onCurrentRefs);
|
|
196
|
+
bridgeDisposables.push(onHistoryRefs);
|
|
197
|
+
|
|
198
|
+
const provider = {
|
|
199
|
+
currentHistoryItemRef: undefined,
|
|
200
|
+
onDidChangeCurrentHistoryItemRefs: onCurrentRefs.event,
|
|
201
|
+
onDidChangeHistoryItemRefs: onHistoryRefs.event,
|
|
202
|
+
provideHistoryItemRefs: async () => [],
|
|
203
|
+
provideHistoryItems: async () => [],
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
bridge.setHistoryProvider(provider);
|
|
207
|
+
bridge.setHistoryProvider(undefined);
|
|
208
|
+
|
|
209
|
+
const lastCall = bridge.updateSourceControlCalls[bridge.updateSourceControlCalls.length - 1];
|
|
210
|
+
assert.ok(lastCall, 'Expected $updateSourceControl to be called');
|
|
211
|
+
assert.strictEqual(lastCall.features.hasHistoryProvider, false);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('onDidChangeCurrentHistoryItemRefs fires proxy notification', () => {
|
|
215
|
+
const onCurrentRefs = new Emitter<void>();
|
|
216
|
+
const onHistoryRefs = new Emitter<HistoryItemRefsChangeEvent>();
|
|
217
|
+
bridgeDisposables.push(onCurrentRefs);
|
|
218
|
+
bridgeDisposables.push(onHistoryRefs);
|
|
219
|
+
|
|
220
|
+
const provider = {
|
|
221
|
+
currentHistoryItemRef: { id: 'refs/heads/main', name: 'main' },
|
|
222
|
+
onDidChangeCurrentHistoryItemRefs: onCurrentRefs.event,
|
|
223
|
+
onDidChangeHistoryItemRefs: onHistoryRefs.event,
|
|
224
|
+
provideHistoryItemRefs: async () => [],
|
|
225
|
+
provideHistoryItems: async () => [],
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
bridge.setHistoryProvider(provider);
|
|
229
|
+
const callsBefore = bridge.onDidChangeCurrentHistoryItemRefsCalls.length;
|
|
230
|
+
onCurrentRefs.fire();
|
|
231
|
+
|
|
232
|
+
assert.strictEqual(bridge.onDidChangeCurrentHistoryItemRefsCalls.length, callsBefore + 1);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('onDidChangeHistoryItemRefs fires proxy notification with mapped DTOs', () => {
|
|
236
|
+
const onCurrentRefs = new Emitter<void>();
|
|
237
|
+
const onHistoryRefs = new Emitter<HistoryItemRefsChangeEvent>();
|
|
238
|
+
bridgeDisposables.push(onCurrentRefs);
|
|
239
|
+
bridgeDisposables.push(onHistoryRefs);
|
|
240
|
+
|
|
241
|
+
const provider = {
|
|
242
|
+
currentHistoryItemRef: undefined,
|
|
243
|
+
onDidChangeCurrentHistoryItemRefs: onCurrentRefs.event,
|
|
244
|
+
onDidChangeHistoryItemRefs: onHistoryRefs.event,
|
|
245
|
+
provideHistoryItemRefs: async () => [],
|
|
246
|
+
provideHistoryItems: async () => [],
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
bridge.setHistoryProvider(provider);
|
|
250
|
+
|
|
251
|
+
const changeEvent: HistoryItemRefsChangeEvent = {
|
|
252
|
+
added: [{ id: 'refs/heads/feature', name: 'feature' }],
|
|
253
|
+
removed: [],
|
|
254
|
+
modified: [],
|
|
255
|
+
};
|
|
256
|
+
onHistoryRefs.fire(changeEvent);
|
|
257
|
+
|
|
258
|
+
assert.strictEqual(bridge.onDidChangeHistoryItemRefsCalls.length, 1);
|
|
259
|
+
assert.strictEqual(bridge.onDidChangeHistoryItemRefsCalls[0].event.added[0].id, 'refs/heads/feature');
|
|
260
|
+
assert.strictEqual(bridge.onDidChangeHistoryItemRefsCalls[0].event.added[0].name, 'feature');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('provideHistoryItems delegates to historyProvider and maps results to DTOs', async () => {
|
|
264
|
+
const onCurrentRefs = new Emitter<void>();
|
|
265
|
+
const onHistoryRefs = new Emitter<HistoryItemRefsChangeEvent>();
|
|
266
|
+
bridgeDisposables.push(onCurrentRefs);
|
|
267
|
+
bridgeDisposables.push(onHistoryRefs);
|
|
268
|
+
|
|
269
|
+
const expectedItems: HistoryItem[] = [
|
|
270
|
+
{
|
|
271
|
+
id: 'abc123',
|
|
272
|
+
subject: 'Initial commit',
|
|
273
|
+
parentIds: [],
|
|
274
|
+
author: 'Test User',
|
|
275
|
+
authorEmail: 'test@example.com',
|
|
276
|
+
displayId: 'abc1234',
|
|
277
|
+
timestamp: 1700000000000,
|
|
278
|
+
}
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
const provider = {
|
|
282
|
+
currentHistoryItemRef: undefined,
|
|
283
|
+
onDidChangeCurrentHistoryItemRefs: onCurrentRefs.event,
|
|
284
|
+
onDidChangeHistoryItemRefs: onHistoryRefs.event,
|
|
285
|
+
provideHistoryItemRefs: async () => [],
|
|
286
|
+
provideHistoryItems: async (_opts: HistoryOptions) => expectedItems,
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
bridge.setHistoryProvider(provider);
|
|
290
|
+
|
|
291
|
+
const result = await bridge.provideHistoryItems({ limit: 10 });
|
|
292
|
+
|
|
293
|
+
assert.ok(result, 'Expected result to be defined');
|
|
294
|
+
assert.strictEqual(result.length, 1);
|
|
295
|
+
assert.strictEqual(result[0].id, 'abc123');
|
|
296
|
+
assert.strictEqual(result[0].subject, 'Initial commit');
|
|
297
|
+
assert.strictEqual(result[0].author, 'Test User');
|
|
298
|
+
assert.strictEqual(result[0].displayId, 'abc1234');
|
|
299
|
+
assert.strictEqual(result[0].timestamp, 1700000000000);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('provideHistoryItems returns undefined when no history provider set', async () => {
|
|
303
|
+
const result = await bridge.provideHistoryItems({ limit: 10 });
|
|
304
|
+
assert.strictEqual(result, undefined);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('provideHistoryItemRefs delegates to historyProvider', async () => {
|
|
308
|
+
const onCurrentRefs = new Emitter<void>();
|
|
309
|
+
const onHistoryRefs = new Emitter<HistoryItemRefsChangeEvent>();
|
|
310
|
+
bridgeDisposables.push(onCurrentRefs);
|
|
311
|
+
bridgeDisposables.push(onHistoryRefs);
|
|
312
|
+
|
|
313
|
+
const expectedRefs: HistoryItemRef[] = [
|
|
314
|
+
{ id: 'refs/heads/main', name: 'main' },
|
|
315
|
+
{ id: 'refs/heads/feature', name: 'feature' },
|
|
316
|
+
];
|
|
317
|
+
|
|
318
|
+
const provider = {
|
|
319
|
+
currentHistoryItemRef: undefined,
|
|
320
|
+
onDidChangeCurrentHistoryItemRefs: onCurrentRefs.event,
|
|
321
|
+
onDidChangeHistoryItemRefs: onHistoryRefs.event,
|
|
322
|
+
provideHistoryItemRefs: async () => expectedRefs,
|
|
323
|
+
provideHistoryItems: async () => [],
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
bridge.setHistoryProvider(provider);
|
|
327
|
+
|
|
328
|
+
const result = await bridge.provideHistoryItemRefs(undefined);
|
|
329
|
+
|
|
330
|
+
assert.ok(result, 'Expected result to be defined');
|
|
331
|
+
assert.strictEqual(result.length, 2);
|
|
332
|
+
assert.strictEqual(result[0].id, 'refs/heads/main');
|
|
333
|
+
assert.strictEqual(result[1].id, 'refs/heads/feature');
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe('SCM history command arg type guards and processors', () => {
|
|
338
|
+
|
|
339
|
+
describe('ScmHistoryItemCommandArg.is()', () => {
|
|
340
|
+
it('returns true for historyItem arg with all required fields', () => {
|
|
341
|
+
const arg: ScmHistoryItemCommandArg = { sourceControlHandle: 0, id: 'abc', type: 'historyItem' };
|
|
342
|
+
assert.strictEqual(ScmHistoryItemCommandArg.is(arg), true);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('returns true for historyItemRef arg with all required fields', () => {
|
|
346
|
+
const arg: ScmHistoryItemCommandArg = { sourceControlHandle: 1, id: 'refs/heads/main', type: 'historyItemRef' };
|
|
347
|
+
assert.strictEqual(ScmHistoryItemCommandArg.is(arg), true);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('returns false for plain ScmCommandArg (sourceControlHandle only)', () => {
|
|
351
|
+
const arg: ScmCommandArg = { sourceControlHandle: 0 };
|
|
352
|
+
assert.strictEqual(ScmHistoryItemCommandArg.is(arg), false);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('returns false for null and non-objects', () => {
|
|
356
|
+
assert.strictEqual(ScmHistoryItemCommandArg.is(undefined), false);
|
|
357
|
+
assert.strictEqual(ScmHistoryItemCommandArg.is('string'), false);
|
|
358
|
+
assert.strictEqual(ScmHistoryItemCommandArg.is(42), false);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('returns false for object missing id field', () => {
|
|
362
|
+
assert.strictEqual(ScmHistoryItemCommandArg.is({ sourceControlHandle: 0, type: 'historyItem' }), false);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('returns false for object missing sourceControlHandle', () => {
|
|
366
|
+
assert.strictEqual(ScmHistoryItemCommandArg.is({ id: 'abc', type: 'historyItem' }), false);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('returns false for object missing type field', () => {
|
|
370
|
+
assert.strictEqual(ScmHistoryItemCommandArg.is({ sourceControlHandle: 0, id: 'abc' }), false);
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
describe('Guard specificity: ScmCommandArg.is() vs ScmHistoryItemCommandArg.is()', () => {
|
|
375
|
+
it('ScmCommandArg.is() matches history item args (broad guard)', () => {
|
|
376
|
+
// ScmCommandArg only checks for sourceControlHandle — it DOES match history args.
|
|
377
|
+
// This is why history processors are registered before the generic processor,
|
|
378
|
+
// so the more-specific guard wins by running first.
|
|
379
|
+
const historyItemArg: ScmHistoryItemCommandArg = { sourceControlHandle: 0, id: 'abc', type: 'historyItem' };
|
|
380
|
+
assert.strictEqual(ScmCommandArg.is(historyItemArg), true,
|
|
381
|
+
'ScmCommandArg.is() matches history item args because it only checks sourceControlHandle');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('ScmHistoryItemCommandArg.is() does NOT match plain ScmCommandArg', () => {
|
|
385
|
+
const plainArg: ScmCommandArg = { sourceControlHandle: 0 };
|
|
386
|
+
assert.strictEqual(ScmHistoryItemCommandArg.is(plainArg), false,
|
|
387
|
+
'ScmHistoryItemCommandArg.is() requires id and type to be present');
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
describe('History item argument processor', () => {
|
|
392
|
+
interface MockRef {
|
|
393
|
+
id: string;
|
|
394
|
+
name: string;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
interface MockHistoryProvider {
|
|
398
|
+
id: string;
|
|
399
|
+
currentHistoryItemRef?: MockRef;
|
|
400
|
+
currentHistoryItemRemoteRef?: MockRef;
|
|
401
|
+
currentHistoryItemBaseRef?: MockRef;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
interface MockSourceControl {
|
|
405
|
+
historyProvider: MockHistoryProvider | undefined;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function makeHistoryItemProcessor(
|
|
409
|
+
sourceControls: Map<number, MockSourceControl>
|
|
410
|
+
): (arg: unknown) => unknown {
|
|
411
|
+
return (arg: unknown) => {
|
|
412
|
+
if (!ScmHistoryItemCommandArg.is(arg) || arg.type !== 'historyItem') {
|
|
413
|
+
return arg;
|
|
414
|
+
}
|
|
415
|
+
const sourceControl = sourceControls.get(arg.sourceControlHandle);
|
|
416
|
+
if (!sourceControl?.historyProvider) {
|
|
417
|
+
return undefined;
|
|
418
|
+
}
|
|
419
|
+
return { id: arg.id };
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function makeHistoryItemRefProcessor(
|
|
424
|
+
sourceControls: Map<number, MockSourceControl>
|
|
425
|
+
): (arg: unknown) => unknown {
|
|
426
|
+
return (arg: unknown) => {
|
|
427
|
+
if (!ScmHistoryItemCommandArg.is(arg) || arg.type !== 'historyItemRef') {
|
|
428
|
+
return arg;
|
|
429
|
+
}
|
|
430
|
+
const sourceControl = sourceControls.get(arg.sourceControlHandle);
|
|
431
|
+
if (!sourceControl?.historyProvider) {
|
|
432
|
+
return undefined;
|
|
433
|
+
}
|
|
434
|
+
const provider = sourceControl.historyProvider as any;
|
|
435
|
+
const ref = [provider.currentHistoryItemRef, provider.currentHistoryItemRemoteRef, provider.currentHistoryItemBaseRef]
|
|
436
|
+
.find((r: any) => r?.id === arg.id);
|
|
437
|
+
return ref ?? { id: arg.id };
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function makeGenericScmProcessor(sourceControls: Map<number, MockSourceControl>): (arg: unknown) => unknown {
|
|
442
|
+
return (arg: unknown) => {
|
|
443
|
+
if (!ScmCommandArg.is(arg)) {
|
|
444
|
+
return arg;
|
|
445
|
+
}
|
|
446
|
+
const sourceControl = sourceControls.get(arg.sourceControlHandle);
|
|
447
|
+
if (!sourceControl) {
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
450
|
+
return sourceControl;
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
it('history item processor returns { id } for a known history item', () => {
|
|
455
|
+
const sourceControls = new Map<number, MockSourceControl>();
|
|
456
|
+
sourceControls.set(0, { historyProvider: { id: 'git' } });
|
|
457
|
+
|
|
458
|
+
const processor = makeHistoryItemProcessor(sourceControls);
|
|
459
|
+
const arg: ScmHistoryItemCommandArg = { sourceControlHandle: 0, id: 'abc123', type: 'historyItem' };
|
|
460
|
+
const result = processor(arg);
|
|
461
|
+
|
|
462
|
+
assert.deepStrictEqual(result, { id: 'abc123' });
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('history item processor returns undefined when source control has no historyProvider', () => {
|
|
466
|
+
const sourceControls = new Map<number, MockSourceControl>();
|
|
467
|
+
sourceControls.set(0, { historyProvider: undefined });
|
|
468
|
+
|
|
469
|
+
const processor = makeHistoryItemProcessor(sourceControls);
|
|
470
|
+
const arg: ScmHistoryItemCommandArg = { sourceControlHandle: 0, id: 'abc123', type: 'historyItem' };
|
|
471
|
+
const result = processor(arg);
|
|
472
|
+
|
|
473
|
+
assert.strictEqual(result, undefined);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it('history item ref processor returns currentHistoryItemRef from provider when id matches', () => {
|
|
477
|
+
const currentRef = { id: 'refs/heads/main', name: 'main' };
|
|
478
|
+
const sourceControls = new Map<number, MockSourceControl>();
|
|
479
|
+
sourceControls.set(0, { historyProvider: { id: 'git', currentHistoryItemRef: currentRef } });
|
|
480
|
+
|
|
481
|
+
const processor = makeHistoryItemRefProcessor(sourceControls);
|
|
482
|
+
const arg: ScmHistoryItemCommandArg = { sourceControlHandle: 0, id: 'refs/heads/main', type: 'historyItemRef' };
|
|
483
|
+
const result = processor(arg);
|
|
484
|
+
|
|
485
|
+
assert.strictEqual(result, currentRef, 'should return the exact provider ref object');
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it('history item ref processor returns { id } when ref id is not among current provider refs', () => {
|
|
489
|
+
const sourceControls = new Map<number, MockSourceControl>();
|
|
490
|
+
sourceControls.set(0, { historyProvider: { id: 'git', currentHistoryItemRef: { id: 'refs/heads/main', name: 'main' } } });
|
|
491
|
+
|
|
492
|
+
const processor = makeHistoryItemRefProcessor(sourceControls);
|
|
493
|
+
const arg: ScmHistoryItemCommandArg = { sourceControlHandle: 0, id: 'refs/heads/other', type: 'historyItemRef' };
|
|
494
|
+
const result = processor(arg);
|
|
495
|
+
|
|
496
|
+
assert.deepStrictEqual(result, { id: 'refs/heads/other' });
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('type discriminator: historyItem arg is NOT processed by ref processor', () => {
|
|
500
|
+
const sourceControls = new Map<number, MockSourceControl>();
|
|
501
|
+
const currentRef = { id: 'refs/heads/main', name: 'main' };
|
|
502
|
+
sourceControls.set(0, { historyProvider: { id: 'git', currentHistoryItemRef: currentRef } });
|
|
503
|
+
|
|
504
|
+
const refProcessor = makeHistoryItemRefProcessor(sourceControls);
|
|
505
|
+
const historyItemArg: ScmHistoryItemCommandArg = { sourceControlHandle: 0, id: 'refs/heads/main', type: 'historyItem' };
|
|
506
|
+
|
|
507
|
+
// Even though the id matches a known ref, the type discriminator prevents the ref
|
|
508
|
+
// processor from consuming it — it should be passed through unchanged.
|
|
509
|
+
const result = refProcessor(historyItemArg);
|
|
510
|
+
assert.strictEqual(result, historyItemArg,
|
|
511
|
+
'ref processor must not consume an arg with type historyItem');
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it('type discriminator: historyItemRef arg is NOT processed by item processor', () => {
|
|
515
|
+
const sourceControls = new Map<number, MockSourceControl>();
|
|
516
|
+
sourceControls.set(0, { historyProvider: { id: 'git' } });
|
|
517
|
+
|
|
518
|
+
const itemProcessor = makeHistoryItemProcessor(sourceControls);
|
|
519
|
+
const refArg: ScmHistoryItemCommandArg = { sourceControlHandle: 0, id: 'refs/heads/main', type: 'historyItemRef' };
|
|
520
|
+
|
|
521
|
+
// The item processor must not consume an arg with type historyItemRef.
|
|
522
|
+
const result = itemProcessor(refArg);
|
|
523
|
+
assert.strictEqual(result, refArg,
|
|
524
|
+
'item processor must not consume an arg with type historyItemRef');
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('type discriminator fixes ordering bug: ref arg survives processor 1 intact', () => {
|
|
528
|
+
// Regression test for the ordering bug described in the task:
|
|
529
|
+
// Before the fix, Processor 1 (historyItem) would intercept the ref arg because
|
|
530
|
+
// both processors used the same ScmHistoryItemCommandArg.is() guard without
|
|
531
|
+
// checking the type discriminator. The ref arg would be "resolved" as a missing
|
|
532
|
+
// history item and returned as { id: ref.id }, stripping sourceControlHandle.
|
|
533
|
+
// Processor 2 would then fail to match (no sourceControlHandle) and pass through
|
|
534
|
+
// the degraded { id } object.
|
|
535
|
+
//
|
|
536
|
+
// With the fix, Processor 1 sees type === 'historyItemRef', skips, and passes the
|
|
537
|
+
// arg through unchanged so Processor 2 can correctly resolve it.
|
|
538
|
+
const sourceControls = new Map<number, MockSourceControl>();
|
|
539
|
+
const mainRef = { id: 'refs/heads/main', name: 'main' };
|
|
540
|
+
sourceControls.set(0, { historyProvider: { id: 'git', currentHistoryItemRef: mainRef } });
|
|
541
|
+
|
|
542
|
+
const itemProcessor = makeHistoryItemProcessor(sourceControls);
|
|
543
|
+
const refProcessor = makeHistoryItemRefProcessor(sourceControls);
|
|
544
|
+
|
|
545
|
+
const refArg: ScmHistoryItemCommandArg = { sourceControlHandle: 0, id: 'refs/heads/main', type: 'historyItemRef' };
|
|
546
|
+
|
|
547
|
+
// Simulate the reduce chain: arg passes through processor 1, then processor 2
|
|
548
|
+
const afterP1 = itemProcessor(refArg);
|
|
549
|
+
// Processor 1 must pass it through unchanged (not strip sourceControlHandle)
|
|
550
|
+
assert.strictEqual(afterP1, refArg,
|
|
551
|
+
'Processor 1 must not consume the historyItemRef arg');
|
|
552
|
+
|
|
553
|
+
const afterP2 = refProcessor(afterP1 as ScmHistoryItemCommandArg);
|
|
554
|
+
// Processor 2 must resolve it to the full ref object
|
|
555
|
+
assert.strictEqual(afterP2, mainRef,
|
|
556
|
+
'Processor 2 must resolve the ref arg to the full SourceControlHistoryItemRef');
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it('discriminated interface works for both history items and refs: type field routes correctly', () => {
|
|
560
|
+
const sourceControls = new Map<number, MockSourceControl>();
|
|
561
|
+
const remoteRef = { id: 'refs/remotes/origin/main', name: 'origin/main' };
|
|
562
|
+
sourceControls.set(1, { historyProvider: { id: 'git', currentHistoryItemRemoteRef: remoteRef } });
|
|
563
|
+
|
|
564
|
+
const historyItemArg: ScmHistoryItemCommandArg = { sourceControlHandle: 1, id: 'deadbeef', type: 'historyItem' };
|
|
565
|
+
const historyRefArg: ScmHistoryItemCommandArg = { sourceControlHandle: 1, id: 'refs/remotes/origin/main', type: 'historyItemRef' };
|
|
566
|
+
|
|
567
|
+
// Both are valid ScmHistoryItemCommandArg
|
|
568
|
+
assert.strictEqual(ScmHistoryItemCommandArg.is(historyItemArg), true);
|
|
569
|
+
assert.strictEqual(ScmHistoryItemCommandArg.is(historyRefArg), true);
|
|
570
|
+
|
|
571
|
+
// Item processor resolves a commit by id (skips ref arg)
|
|
572
|
+
const itemProcessor = makeHistoryItemProcessor(sourceControls);
|
|
573
|
+
assert.deepStrictEqual(itemProcessor(historyItemArg), { id: 'deadbeef' });
|
|
574
|
+
assert.strictEqual(itemProcessor(historyRefArg), historyRefArg,
|
|
575
|
+
'item processor must pass through historyItemRef args unchanged');
|
|
576
|
+
|
|
577
|
+
// Ref processor resolves a ref by id (skips item arg)
|
|
578
|
+
const refProcessor = makeHistoryItemRefProcessor(sourceControls);
|
|
579
|
+
assert.strictEqual(refProcessor(historyRefArg), remoteRef);
|
|
580
|
+
assert.strictEqual(refProcessor(historyItemArg), historyItemArg,
|
|
581
|
+
'ref processor must pass through historyItem args unchanged');
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it('generic ScmCommandArg processor does NOT match history item args (guard specificity)', () => {
|
|
585
|
+
// Because we rely on registration order (history processors run first),
|
|
586
|
+
// verify here that if the generic processor receives a history item arg,
|
|
587
|
+
// it would treat it as a plain ScmCommandArg and NOT pass it through unchanged.
|
|
588
|
+
const sourceControls = new Map<number, MockSourceControl>();
|
|
589
|
+
sourceControls.set(0, { historyProvider: { id: 'git' } });
|
|
590
|
+
|
|
591
|
+
const genericProcessor = makeGenericScmProcessor(sourceControls);
|
|
592
|
+
const historyItemArg: ScmHistoryItemCommandArg = { sourceControlHandle: 0, id: 'abc123', type: 'historyItem' };
|
|
593
|
+
|
|
594
|
+
// The generic processor DOES match (ScmCommandArg.is() is true for history args),
|
|
595
|
+
// but it returns the sourceControl object — not the historyItem shape.
|
|
596
|
+
// This confirms why history processors must be registered first.
|
|
597
|
+
const result = genericProcessor(historyItemArg);
|
|
598
|
+
assert.ok(result !== undefined, 'Generic processor matched history item arg');
|
|
599
|
+
assert.ok(!('id' in (result as object) && Object.keys(result as object).length === 1),
|
|
600
|
+
'Generic processor should NOT return the history item shape { id }');
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
it('history item processor passes through non-matching args unchanged', () => {
|
|
604
|
+
const sourceControls = new Map<number, MockSourceControl>();
|
|
605
|
+
const processor = makeHistoryItemProcessor(sourceControls);
|
|
606
|
+
|
|
607
|
+
const plainArg: ScmCommandArg = { sourceControlHandle: 42 };
|
|
608
|
+
const result = processor(plainArg);
|
|
609
|
+
|
|
610
|
+
// Not a ScmHistoryItemCommandArg (missing 'id' and 'type'), so returned unchanged
|
|
611
|
+
assert.strictEqual(result, plainArg);
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
|