katashiro-vscode 0.2.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/package.json +184 -0
- package/src/commands/command-manager.ts +452 -0
- package/src/commands/index.ts +5 -0
- package/src/extension.ts +48 -0
- package/src/index.ts +36 -0
- package/src/katashiro-extension.ts +125 -0
- package/src/ui/index.ts +9 -0
- package/src/ui/output-channel-manager.ts +105 -0
- package/src/ui/status-bar-manager.ts +157 -0
- package/src/views/history-view-provider.ts +221 -0
- package/src/views/index.ts +15 -0
- package/src/views/knowledge-view-provider.ts +204 -0
- package/src/views/research-view-provider.ts +141 -0
- package/tests/mocks/vscode.ts +121 -0
- package/tests/unit/history-view-provider.test.ts +150 -0
- package/tests/unit/knowledge-view-provider.test.ts +120 -0
- package/tests/unit/output-channel-manager.test.ts +75 -0
- package/tests/unit/research-view-provider.test.ts +111 -0
- package/tests/unit/status-bar-manager.test.ts +101 -0
- package/tsconfig.json +20 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KnowledgeViewProvider - Knowledge Graph panel tree view
|
|
3
|
+
*
|
|
4
|
+
* @module katashiro
|
|
5
|
+
* @task TSK-072
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as vscode from 'vscode';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Knowledge item for tree view
|
|
12
|
+
*/
|
|
13
|
+
export class KnowledgeItem extends vscode.TreeItem {
|
|
14
|
+
constructor(
|
|
15
|
+
public readonly label: string,
|
|
16
|
+
public readonly collapsibleState: vscode.TreeItemCollapsibleState,
|
|
17
|
+
public readonly itemType: 'category' | 'node' | 'edge',
|
|
18
|
+
public readonly description?: string
|
|
19
|
+
) {
|
|
20
|
+
super(label, collapsibleState);
|
|
21
|
+
this.description = description;
|
|
22
|
+
this.contextValue = itemType;
|
|
23
|
+
|
|
24
|
+
// Set icon based on type
|
|
25
|
+
switch (itemType) {
|
|
26
|
+
case 'category':
|
|
27
|
+
this.iconPath = new vscode.ThemeIcon('folder');
|
|
28
|
+
break;
|
|
29
|
+
case 'node':
|
|
30
|
+
this.iconPath = new vscode.ThemeIcon('circle-filled');
|
|
31
|
+
break;
|
|
32
|
+
case 'edge':
|
|
33
|
+
this.iconPath = new vscode.ThemeIcon('arrow-right');
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* KnowledgeViewProvider
|
|
41
|
+
*
|
|
42
|
+
* Provides tree data for the Knowledge Graph panel
|
|
43
|
+
*/
|
|
44
|
+
export class KnowledgeViewProvider
|
|
45
|
+
implements vscode.TreeDataProvider<KnowledgeItem>
|
|
46
|
+
{
|
|
47
|
+
private _onDidChangeTreeData = new vscode.EventEmitter<
|
|
48
|
+
KnowledgeItem | undefined | null | void
|
|
49
|
+
>();
|
|
50
|
+
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
|
|
51
|
+
|
|
52
|
+
private nodes: Array<{ id: string; label: string; type: string }> = [];
|
|
53
|
+
private edges: Array<{ source: string; target: string; relation: string }> =
|
|
54
|
+
[];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Refresh the tree view
|
|
58
|
+
*/
|
|
59
|
+
refresh(): void {
|
|
60
|
+
this._onDidChangeTreeData.fire();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Add a node
|
|
65
|
+
*/
|
|
66
|
+
addNode(id: string, label: string, type: string): void {
|
|
67
|
+
this.nodes.push({ id, label, type });
|
|
68
|
+
this.refresh();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Add an edge
|
|
73
|
+
*/
|
|
74
|
+
addEdge(source: string, target: string, relation: string): void {
|
|
75
|
+
this.edges.push({ source, target, relation });
|
|
76
|
+
this.refresh();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Clear all data
|
|
81
|
+
*/
|
|
82
|
+
clear(): void {
|
|
83
|
+
this.nodes = [];
|
|
84
|
+
this.edges = [];
|
|
85
|
+
this.refresh();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get statistics
|
|
90
|
+
*/
|
|
91
|
+
getStats(): { nodes: number; edges: number } {
|
|
92
|
+
return {
|
|
93
|
+
nodes: this.nodes.length,
|
|
94
|
+
edges: this.edges.length,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get tree item
|
|
100
|
+
*/
|
|
101
|
+
getTreeItem(element: KnowledgeItem): vscode.TreeItem {
|
|
102
|
+
return element;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get children
|
|
107
|
+
*/
|
|
108
|
+
getChildren(element?: KnowledgeItem): Thenable<KnowledgeItem[]> {
|
|
109
|
+
if (!element) {
|
|
110
|
+
// Root level - categories
|
|
111
|
+
const stats = this.getStats();
|
|
112
|
+
return Promise.resolve([
|
|
113
|
+
new KnowledgeItem(
|
|
114
|
+
'Nodes',
|
|
115
|
+
vscode.TreeItemCollapsibleState.Collapsed,
|
|
116
|
+
'category',
|
|
117
|
+
`${stats.nodes} items`
|
|
118
|
+
),
|
|
119
|
+
new KnowledgeItem(
|
|
120
|
+
'Edges',
|
|
121
|
+
vscode.TreeItemCollapsibleState.Collapsed,
|
|
122
|
+
'category',
|
|
123
|
+
`${stats.edges} items`
|
|
124
|
+
),
|
|
125
|
+
new KnowledgeItem(
|
|
126
|
+
'Actions',
|
|
127
|
+
vscode.TreeItemCollapsibleState.Collapsed,
|
|
128
|
+
'category'
|
|
129
|
+
),
|
|
130
|
+
]);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Children based on category
|
|
134
|
+
switch (element.label) {
|
|
135
|
+
case 'Nodes':
|
|
136
|
+
if (this.nodes.length === 0) {
|
|
137
|
+
return Promise.resolve([
|
|
138
|
+
new KnowledgeItem(
|
|
139
|
+
'No nodes yet',
|
|
140
|
+
vscode.TreeItemCollapsibleState.None,
|
|
141
|
+
'node',
|
|
142
|
+
'Start researching to add knowledge'
|
|
143
|
+
),
|
|
144
|
+
]);
|
|
145
|
+
}
|
|
146
|
+
return Promise.resolve(
|
|
147
|
+
this.nodes.map(
|
|
148
|
+
(node) =>
|
|
149
|
+
new KnowledgeItem(
|
|
150
|
+
node.label,
|
|
151
|
+
vscode.TreeItemCollapsibleState.None,
|
|
152
|
+
'node',
|
|
153
|
+
node.type
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
case 'Edges':
|
|
159
|
+
if (this.edges.length === 0) {
|
|
160
|
+
return Promise.resolve([
|
|
161
|
+
new KnowledgeItem(
|
|
162
|
+
'No edges yet',
|
|
163
|
+
vscode.TreeItemCollapsibleState.None,
|
|
164
|
+
'edge',
|
|
165
|
+
'Connections between nodes'
|
|
166
|
+
),
|
|
167
|
+
]);
|
|
168
|
+
}
|
|
169
|
+
return Promise.resolve(
|
|
170
|
+
this.edges.map(
|
|
171
|
+
(edge) =>
|
|
172
|
+
new KnowledgeItem(
|
|
173
|
+
`${edge.source} → ${edge.target}`,
|
|
174
|
+
vscode.TreeItemCollapsibleState.None,
|
|
175
|
+
'edge',
|
|
176
|
+
edge.relation
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
case 'Actions':
|
|
182
|
+
return Promise.resolve([
|
|
183
|
+
new KnowledgeItem(
|
|
184
|
+
'View Graph',
|
|
185
|
+
vscode.TreeItemCollapsibleState.None,
|
|
186
|
+
'category'
|
|
187
|
+
),
|
|
188
|
+
new KnowledgeItem(
|
|
189
|
+
'Export',
|
|
190
|
+
vscode.TreeItemCollapsibleState.None,
|
|
191
|
+
'category'
|
|
192
|
+
),
|
|
193
|
+
new KnowledgeItem(
|
|
194
|
+
'Clear All',
|
|
195
|
+
vscode.TreeItemCollapsibleState.None,
|
|
196
|
+
'category'
|
|
197
|
+
),
|
|
198
|
+
]);
|
|
199
|
+
|
|
200
|
+
default:
|
|
201
|
+
return Promise.resolve([]);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResearchViewProvider - Research panel tree view
|
|
3
|
+
*
|
|
4
|
+
* @module katashiro
|
|
5
|
+
* @task TSK-072
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as vscode from 'vscode';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Research item for tree view
|
|
12
|
+
*/
|
|
13
|
+
export class ResearchItem extends vscode.TreeItem {
|
|
14
|
+
constructor(
|
|
15
|
+
public readonly label: string,
|
|
16
|
+
public readonly collapsibleState: vscode.TreeItemCollapsibleState,
|
|
17
|
+
public readonly description?: string,
|
|
18
|
+
public readonly command?: vscode.Command
|
|
19
|
+
) {
|
|
20
|
+
super(label, collapsibleState);
|
|
21
|
+
this.description = description;
|
|
22
|
+
this.command = command;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* ResearchViewProvider
|
|
28
|
+
*
|
|
29
|
+
* Provides tree data for the Research panel
|
|
30
|
+
*/
|
|
31
|
+
export class ResearchViewProvider
|
|
32
|
+
implements vscode.TreeDataProvider<ResearchItem>
|
|
33
|
+
{
|
|
34
|
+
private _onDidChangeTreeData = new vscode.EventEmitter<
|
|
35
|
+
ResearchItem | undefined | null | void
|
|
36
|
+
>();
|
|
37
|
+
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
|
|
38
|
+
|
|
39
|
+
private recentSearches: Array<{ query: string; timestamp: Date }> = [];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Refresh the tree view
|
|
43
|
+
*/
|
|
44
|
+
refresh(): void {
|
|
45
|
+
this._onDidChangeTreeData.fire();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Add a recent search
|
|
50
|
+
*/
|
|
51
|
+
addSearch(query: string): void {
|
|
52
|
+
this.recentSearches.unshift({ query, timestamp: new Date() });
|
|
53
|
+
if (this.recentSearches.length > 10) {
|
|
54
|
+
this.recentSearches.pop();
|
|
55
|
+
}
|
|
56
|
+
this.refresh();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get tree item
|
|
61
|
+
*/
|
|
62
|
+
getTreeItem(element: ResearchItem): vscode.TreeItem {
|
|
63
|
+
return element;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get children
|
|
68
|
+
*/
|
|
69
|
+
getChildren(element?: ResearchItem): Thenable<ResearchItem[]> {
|
|
70
|
+
if (!element) {
|
|
71
|
+
// Root level items
|
|
72
|
+
return Promise.resolve([
|
|
73
|
+
new ResearchItem(
|
|
74
|
+
'New Search',
|
|
75
|
+
vscode.TreeItemCollapsibleState.None,
|
|
76
|
+
'',
|
|
77
|
+
{
|
|
78
|
+
command: 'katashiro.webSearch',
|
|
79
|
+
title: 'New Search',
|
|
80
|
+
}
|
|
81
|
+
),
|
|
82
|
+
new ResearchItem(
|
|
83
|
+
'Research Topic',
|
|
84
|
+
vscode.TreeItemCollapsibleState.None,
|
|
85
|
+
'',
|
|
86
|
+
{
|
|
87
|
+
command: 'katashiro.researchTopic',
|
|
88
|
+
title: 'Research Topic',
|
|
89
|
+
}
|
|
90
|
+
),
|
|
91
|
+
new ResearchItem(
|
|
92
|
+
'Recent Searches',
|
|
93
|
+
vscode.TreeItemCollapsibleState.Expanded
|
|
94
|
+
),
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Recent searches children
|
|
99
|
+
if (element.label === 'Recent Searches') {
|
|
100
|
+
if (this.recentSearches.length === 0) {
|
|
101
|
+
return Promise.resolve([
|
|
102
|
+
new ResearchItem(
|
|
103
|
+
'No recent searches',
|
|
104
|
+
vscode.TreeItemCollapsibleState.None,
|
|
105
|
+
'Start a new search'
|
|
106
|
+
),
|
|
107
|
+
]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return Promise.resolve(
|
|
111
|
+
this.recentSearches.map(
|
|
112
|
+
(search) =>
|
|
113
|
+
new ResearchItem(
|
|
114
|
+
search.query,
|
|
115
|
+
vscode.TreeItemCollapsibleState.None,
|
|
116
|
+
this.formatTime(search.timestamp)
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return Promise.resolve([]);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Format timestamp
|
|
127
|
+
*/
|
|
128
|
+
private formatTime(date: Date): string {
|
|
129
|
+
const now = new Date();
|
|
130
|
+
const diffMs = now.getTime() - date.getTime();
|
|
131
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
132
|
+
|
|
133
|
+
if (diffMins < 1) return 'just now';
|
|
134
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
135
|
+
|
|
136
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
137
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
138
|
+
|
|
139
|
+
return date.toLocaleDateString();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VS Code Mock - テスト用モック
|
|
3
|
+
*
|
|
4
|
+
* VS Code APIのモックを提供
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const window = {
|
|
8
|
+
showInformationMessage: vi.fn(),
|
|
9
|
+
showWarningMessage: vi.fn(),
|
|
10
|
+
showErrorMessage: vi.fn(),
|
|
11
|
+
showInputBox: vi.fn(),
|
|
12
|
+
showQuickPick: vi.fn(),
|
|
13
|
+
withProgress: vi.fn(async (_options, task) => {
|
|
14
|
+
return task({ report: vi.fn() }, { isCancellationRequested: false });
|
|
15
|
+
}),
|
|
16
|
+
createOutputChannel: vi.fn(() => ({
|
|
17
|
+
appendLine: vi.fn(),
|
|
18
|
+
clear: vi.fn(),
|
|
19
|
+
show: vi.fn(),
|
|
20
|
+
dispose: vi.fn(),
|
|
21
|
+
})),
|
|
22
|
+
createStatusBarItem: vi.fn(() => ({
|
|
23
|
+
text: '',
|
|
24
|
+
tooltip: '',
|
|
25
|
+
command: undefined,
|
|
26
|
+
backgroundColor: undefined,
|
|
27
|
+
show: vi.fn(),
|
|
28
|
+
hide: vi.fn(),
|
|
29
|
+
dispose: vi.fn(),
|
|
30
|
+
})),
|
|
31
|
+
activeTextEditor: undefined,
|
|
32
|
+
registerTreeDataProvider: vi.fn(() => ({ dispose: vi.fn() })),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const workspace = {
|
|
36
|
+
getConfiguration: vi.fn(() => ({
|
|
37
|
+
get: vi.fn((key: string, defaultValue: unknown) => defaultValue),
|
|
38
|
+
})),
|
|
39
|
+
openTextDocument: vi.fn(async (options) => ({
|
|
40
|
+
getText: vi.fn(() => options.content || ''),
|
|
41
|
+
uri: { fsPath: '/test/file.md' },
|
|
42
|
+
})),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const commands = {
|
|
46
|
+
registerCommand: vi.fn(() => ({ dispose: vi.fn() })),
|
|
47
|
+
executeCommand: vi.fn(),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export enum TreeItemCollapsibleState {
|
|
51
|
+
None = 0,
|
|
52
|
+
Collapsed = 1,
|
|
53
|
+
Expanded = 2,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class TreeItem {
|
|
57
|
+
label: string;
|
|
58
|
+
collapsibleState: TreeItemCollapsibleState;
|
|
59
|
+
description?: string;
|
|
60
|
+
tooltip?: string;
|
|
61
|
+
command?: Command;
|
|
62
|
+
iconPath?: ThemeIcon;
|
|
63
|
+
contextValue?: string;
|
|
64
|
+
|
|
65
|
+
constructor(
|
|
66
|
+
label: string,
|
|
67
|
+
collapsibleState: TreeItemCollapsibleState = TreeItemCollapsibleState.None
|
|
68
|
+
) {
|
|
69
|
+
this.label = label;
|
|
70
|
+
this.collapsibleState = collapsibleState;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface Command {
|
|
75
|
+
command: string;
|
|
76
|
+
title: string;
|
|
77
|
+
arguments?: unknown[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export class ThemeIcon {
|
|
81
|
+
constructor(public readonly id: string) {}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class ThemeColor {
|
|
85
|
+
constructor(public readonly id: string) {}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export enum StatusBarAlignment {
|
|
89
|
+
Left = 1,
|
|
90
|
+
Right = 2,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class EventEmitter<T> {
|
|
94
|
+
private listeners: Array<(e: T) => void> = [];
|
|
95
|
+
|
|
96
|
+
event = (listener: (e: T) => void) => {
|
|
97
|
+
this.listeners.push(listener);
|
|
98
|
+
return { dispose: () => {} };
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
fire(data: T): void {
|
|
102
|
+
this.listeners.forEach((l) => l(data));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export enum ProgressLocation {
|
|
107
|
+
Notification = 15,
|
|
108
|
+
Window = 10,
|
|
109
|
+
SourceControl = 1,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export enum ViewColumn {
|
|
113
|
+
Active = -1,
|
|
114
|
+
Beside = -2,
|
|
115
|
+
One = 1,
|
|
116
|
+
Two = 2,
|
|
117
|
+
Three = 3,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Re-export vi for test files
|
|
121
|
+
import { vi } from 'vitest';
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HistoryViewProvider テスト
|
|
3
|
+
*
|
|
4
|
+
* @task TSK-072
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
8
|
+
|
|
9
|
+
// Mock vscode module
|
|
10
|
+
vi.mock('vscode', () => import('../mocks/vscode.js'));
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
HistoryViewProvider,
|
|
14
|
+
HistoryItem,
|
|
15
|
+
} from '../../src/views/history-view-provider.js';
|
|
16
|
+
|
|
17
|
+
describe('HistoryViewProvider', () => {
|
|
18
|
+
let provider: HistoryViewProvider;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
provider = new HistoryViewProvider();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('initialization', () => {
|
|
25
|
+
it('should create provider', () => {
|
|
26
|
+
expect(provider).toBeDefined();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should have empty history initially', () => {
|
|
30
|
+
const all = provider.getAll();
|
|
31
|
+
expect(all.length).toBe(0);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('addEntry', () => {
|
|
36
|
+
it('should add entry', () => {
|
|
37
|
+
const entry = provider.addEntry('search', 'Test Search', 'Details');
|
|
38
|
+
|
|
39
|
+
expect(entry.id).toBeDefined();
|
|
40
|
+
expect(entry.type).toBe('search');
|
|
41
|
+
expect(entry.title).toBe('Test Search');
|
|
42
|
+
expect(entry.details).toBe('Details');
|
|
43
|
+
expect(entry.timestamp).toBeInstanceOf(Date);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should add multiple entries in order', () => {
|
|
47
|
+
provider.addEntry('search', 'First');
|
|
48
|
+
provider.addEntry('analysis', 'Second');
|
|
49
|
+
provider.addEntry('summary', 'Third');
|
|
50
|
+
|
|
51
|
+
const all = provider.getAll();
|
|
52
|
+
expect(all.length).toBe(3);
|
|
53
|
+
expect(all[0].title).toBe('Third'); // Most recent first
|
|
54
|
+
expect(all[2].title).toBe('First');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should limit to 50 entries', () => {
|
|
58
|
+
for (let i = 0; i < 60; i++) {
|
|
59
|
+
provider.addEntry('search', `Entry ${i}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const all = provider.getAll();
|
|
63
|
+
expect(all.length).toBe(50);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('getEntry', () => {
|
|
68
|
+
it('should get entry by ID', () => {
|
|
69
|
+
const added = provider.addEntry('search', 'Test');
|
|
70
|
+
const found = provider.getEntry(added.id);
|
|
71
|
+
|
|
72
|
+
expect(found).toBeDefined();
|
|
73
|
+
expect(found?.id).toBe(added.id);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should return undefined for unknown ID', () => {
|
|
77
|
+
const found = provider.getEntry('unknown-id');
|
|
78
|
+
expect(found).toBeUndefined();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('clear', () => {
|
|
83
|
+
it('should clear all entries', () => {
|
|
84
|
+
provider.addEntry('search', 'Test 1');
|
|
85
|
+
provider.addEntry('analysis', 'Test 2');
|
|
86
|
+
|
|
87
|
+
provider.clear();
|
|
88
|
+
|
|
89
|
+
const all = provider.getAll();
|
|
90
|
+
expect(all.length).toBe(0);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('getChildren', () => {
|
|
95
|
+
it('should return empty array when no history', async () => {
|
|
96
|
+
const children = await provider.getChildren();
|
|
97
|
+
expect(children.length).toBe(0);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should return history items', async () => {
|
|
101
|
+
provider.addEntry('search', 'Search 1');
|
|
102
|
+
provider.addEntry('analysis', 'Analysis 1');
|
|
103
|
+
|
|
104
|
+
const children = await provider.getChildren();
|
|
105
|
+
expect(children.length).toBe(2);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should return no children for items', async () => {
|
|
109
|
+
provider.addEntry('search', 'Test');
|
|
110
|
+
const children = await provider.getChildren();
|
|
111
|
+
const itemChildren = await provider.getChildren(children[0]);
|
|
112
|
+
|
|
113
|
+
expect(itemChildren.length).toBe(0);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('HistoryItem', () => {
|
|
119
|
+
it('should create item from entry', () => {
|
|
120
|
+
const entry = {
|
|
121
|
+
id: 'test-id',
|
|
122
|
+
type: 'search' as const,
|
|
123
|
+
title: 'Test Search',
|
|
124
|
+
timestamp: new Date(),
|
|
125
|
+
details: 'Test details',
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const item = new HistoryItem(entry, 0);
|
|
129
|
+
|
|
130
|
+
expect(item.entry).toBe(entry);
|
|
131
|
+
expect(item.label).toBe('Test Search');
|
|
132
|
+
expect(item.contextValue).toBe('search');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should set icon based on type', () => {
|
|
136
|
+
const types = ['search', 'analysis', 'summary', 'research', 'report'] as const;
|
|
137
|
+
|
|
138
|
+
for (const type of types) {
|
|
139
|
+
const entry = {
|
|
140
|
+
id: `${type}-id`,
|
|
141
|
+
type,
|
|
142
|
+
title: `Test ${type}`,
|
|
143
|
+
timestamp: new Date(),
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const item = new HistoryItem(entry, 0);
|
|
147
|
+
expect(item.iconPath).toBeDefined();
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
});
|