Skip to content

Commit e1e5401

Browse files
committed
Add tests and UI updates
1 parent 2589bda commit e1e5401

File tree

12 files changed

+1736
-13
lines changed

12 files changed

+1736
-13
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,7 @@ grd_files_bundled_sources = [
656656
"front_end/panels/ai_chat/ui/mcp/MCPConnectionsDialog.js",
657657
"front_end/panels/ai_chat/ui/mcp/MCPConnectorsCatalogDialog.js",
658658
"front_end/panels/ai_chat/ui/EvaluationDialog.js",
659+
"front_end/panels/ai_chat/ui/WebAppCodeViewer.js",
659660
"front_end/panels/ai_chat/core/AgentService.js",
660661
"front_end/panels/ai_chat/core/State.js",
661662
"front_end/panels/ai_chat/core/Graph.js",
@@ -676,6 +677,7 @@ grd_files_bundled_sources = [
676677
"front_end/panels/ai_chat/core/AgentDescriptorRegistry.js",
677678
"front_end/panels/ai_chat/core/Version.js",
678679
"front_end/panels/ai_chat/core/VersionChecker.js",
680+
"front_end/panels/ai_chat/core/LLMConfigurationManager.js",
679681
"front_end/panels/ai_chat/LLM/LLMTypes.js",
680682
"front_end/panels/ai_chat/LLM/LLMProvider.js",
681683
"front_end/panels/ai_chat/LLM/LLMProviderRegistry.js",
@@ -702,6 +704,15 @@ grd_files_bundled_sources = [
702704
"front_end/panels/ai_chat/tools/BookmarkStoreTool.js",
703705
"front_end/panels/ai_chat/tools/DocumentSearchTool.js",
704706
"front_end/panels/ai_chat/tools/ThinkingTool.js",
707+
"front_end/panels/ai_chat/tools/RenderWebAppTool.js",
708+
"front_end/panels/ai_chat/tools/GetWebAppDataTool.js",
709+
"front_end/panels/ai_chat/tools/RemoveWebAppTool.js",
710+
"front_end/panels/ai_chat/tools/FileStorageManager.js",
711+
"front_end/panels/ai_chat/tools/CreateFileTool.js",
712+
"front_end/panels/ai_chat/tools/UpdateFileTool.js",
713+
"front_end/panels/ai_chat/tools/DeleteFileTool.js",
714+
"front_end/panels/ai_chat/tools/ReadFileTool.js",
715+
"front_end/panels/ai_chat/tools/ListFilesTool.js",
705716
"front_end/panels/ai_chat/common/utils.js",
706717
"front_end/panels/ai_chat/common/log.js",
707718
"front_end/panels/ai_chat/common/context.js",

front_end/panels/ai_chat/BUILD.gn

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ devtools_module("ai_chat") {
4242
"ui/SettingsDialog.ts",
4343
"ui/PromptEditDialog.ts",
4444
"ui/EvaluationDialog.ts",
45+
"ui/WebAppCodeViewer.ts",
4546
"ai_chat_impl.ts",
4647
"models/ChatTypes.ts",
4748
"core/Graph.ts",
@@ -206,6 +207,7 @@ _ai_chat_sources = [
206207
"ui/PromptEditDialog.ts",
207208
"ui/SettingsDialog.ts",
208209
"ui/EvaluationDialog.ts",
210+
"ui/WebAppCodeViewer.ts",
209211
"ui/mcp/MCPConnectionsDialog.ts",
210212
"ui/mcp/MCPConnectorsCatalogDialog.ts",
211213
"ai_chat_impl.ts",
@@ -265,6 +267,9 @@ _ai_chat_sources = [
265267
"tools/ListFilesTool.ts",
266268
"tools/SequentialThinkingTool.ts",
267269
"tools/ThinkingTool.ts",
270+
"tools/RenderWebAppTool.ts",
271+
"tools/GetWebAppDataTool.ts",
272+
"tools/RemoveWebAppTool.ts",
268273
"agent_framework/ConfigurableAgentTool.ts",
269274
"agent_framework/AgentRunner.ts",
270275
"agent_framework/AgentRunnerEventBus.ts",
@@ -408,6 +413,7 @@ ts_library("unittests") {
408413
"ui/__tests__/ChatViewSequentialSessionsTransition.test.ts",
409414
"ui/__tests__/ChatViewInputClear.test.ts",
410415
"ui/__tests__/SettingsDialogOpenRouterCache.test.ts",
416+
"ui/__tests__/WebAppCodeViewer.test.ts",
411417
"ui/input/__tests__/InputBarClear.test.ts",
412418
"ui/message/__tests__/MessageCombiner.test.ts",
413419
"ui/message/__tests__/StructuredResponseController.test.ts",
@@ -428,6 +434,11 @@ ts_library("unittests") {
428434
"ui/__tests__/AIChatPanel.test.ts",
429435
"ui/__tests__/LiveAgentSessionComponent.test.ts",
430436
"ui/message/__tests__/MessageList.test.ts",
437+
"tools/__tests__/CreateFileTool.test.ts",
438+
"tools/__tests__/UpdateFileTool.test.ts",
439+
"tools/__tests__/ReadFileTool.test.ts",
440+
"tools/__tests__/ListFilesTool.test.ts",
441+
"tools/__tests__/FileStorageManager.test.ts",
431442
]
432443

433444
deps = [
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright 2025 The Chromium Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import {CreateFileTool} from '../CreateFileTool.js';
6+
import {FileStorageManager, type StoredFile} from '../FileStorageManager.js';
7+
8+
describe('CreateFileTool', () => {
9+
afterEach(() => {
10+
sinon.restore();
11+
});
12+
13+
it('creates a file successfully', async () => {
14+
const stored: StoredFile = {
15+
id: 'id-123',
16+
sessionId: 'sess-1',
17+
fileName: 'note.txt',
18+
content: 'hello',
19+
mimeType: 'text/plain',
20+
createdAt: Date.now(),
21+
updatedAt: Date.now(),
22+
size: 5,
23+
};
24+
25+
const fakeManager = {createFile: sinon.stub().resolves(stored)} as unknown as FileStorageManager;
26+
sinon.stub(FileStorageManager, 'getInstance').returns(fakeManager);
27+
28+
const tool = new CreateFileTool();
29+
const result = await tool.execute({fileName: 'note.txt', content: 'hello', reasoning: 'store note'});
30+
31+
assert.strictEqual(result.success, true);
32+
assert.strictEqual(result.fileName, 'note.txt');
33+
assert.strictEqual(result.fileId, 'id-123');
34+
assert.isString(result.message);
35+
assert.match(result.message!, /Created file "note\.txt" \(\d+ bytes\)\./);
36+
37+
sinon.assert.calledOnce((fakeManager as any).createFile);
38+
sinon.assert.calledWithExactly((fakeManager as any).createFile, 'note.txt', 'hello', undefined);
39+
});
40+
41+
it('returns an error when creation fails', async () => {
42+
const fakeManager = {createFile: sinon.stub().rejects(new Error('boom'))} as unknown as FileStorageManager;
43+
sinon.stub(FileStorageManager, 'getInstance').returns(fakeManager);
44+
45+
const tool = new CreateFileTool();
46+
const result = await tool.execute({fileName: 'note.txt', content: 'hello', reasoning: 'store note'});
47+
48+
assert.strictEqual(result.success, false);
49+
assert.strictEqual(result.error, 'boom');
50+
});
51+
});
52+
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright 2025 The Chromium Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import {FileStorageManager, type StoredFile, type FileSummary} from '../FileStorageManager.js';
6+
7+
describe('FileStorageManager', () => {
8+
beforeEach(() => {
9+
// Reset singleton to avoid session bleed between tests
10+
(FileStorageManager as unknown as {instance: FileStorageManager|null}).instance = null;
11+
});
12+
13+
it('creates, reads, lists and deletes files end-to-end', async () => {
14+
const mgr = FileStorageManager.getInstance();
15+
16+
const f1 = await mgr.createFile('f1.txt', 'Hello', 'text/plain');
17+
assert.strictEqual(f1.fileName, 'f1.txt');
18+
assert.strictEqual(f1.content, 'Hello');
19+
assert.strictEqual(f1.mimeType, 'text/plain');
20+
21+
const f2 = await mgr.createFile('f2.txt', 'World');
22+
assert.strictEqual(f2.fileName, 'f2.txt');
23+
24+
const read = await mgr.readFile('f1.txt');
25+
assert.isNotNull(read);
26+
assert.strictEqual(read?.content, 'Hello');
27+
28+
const files: FileSummary[] = await mgr.listFiles();
29+
const names = new Set(files.map(f => f.fileName));
30+
assert.isTrue(names.has('f1.txt'));
31+
assert.isTrue(names.has('f2.txt'));
32+
if (files.length >= 2) {
33+
assert.isAtLeast(files[0].createdAt, files[1].createdAt);
34+
}
35+
36+
await mgr.deleteFile('f1.txt');
37+
const afterDelete = await mgr.readFile('f1.txt');
38+
assert.isNull(afterDelete);
39+
});
40+
41+
it('prevents duplicate filenames within the same session', async () => {
42+
const mgr = FileStorageManager.getInstance();
43+
await mgr.createFile('dup.txt', 'a');
44+
try {
45+
await mgr.createFile('dup.txt', 'b');
46+
assert.fail('Expected duplicate create to throw');
47+
} catch (e: any) {
48+
assert.match(String(e?.message || e), /already exists/i);
49+
}
50+
});
51+
52+
it('validates file name rules', async () => {
53+
const mgr = FileStorageManager.getInstance();
54+
55+
async function expectRejected(p: Promise<unknown>, re: RegExp) {
56+
try {
57+
await p;
58+
assert.fail('Expected promise to reject');
59+
} catch (e: any) {
60+
assert.match(String(e?.message || e), re);
61+
}
62+
}
63+
64+
await expectRejected(mgr.createFile('', 'x'), /cannot be empty/i);
65+
await expectRejected(mgr.createFile('a/b', 'x'), /path separators/i);
66+
const tooLong = 'a'.repeat(256);
67+
await expectRejected(mgr.createFile(tooLong, 'x'), /255 characters or fewer/i);
68+
});
69+
70+
it('updateFile supports append and replace semantics', async () => {
71+
const mgr = FileStorageManager.getInstance();
72+
await mgr.createFile('note.txt', 'A');
73+
74+
const appended: StoredFile = await mgr.updateFile('note.txt', 'B', true);
75+
assert.strictEqual(appended.content, 'AB');
76+
assert.isAtLeast(appended.size, 2);
77+
78+
const replaced: StoredFile = await mgr.updateFile('note.txt', 'C', false);
79+
assert.strictEqual(replaced.content, 'C');
80+
assert.strictEqual(replaced.size, new TextEncoder().encode('C').length);
81+
});
82+
83+
it('readFile returns null when missing', async () => {
84+
const mgr = FileStorageManager.getInstance();
85+
const missing = await mgr.readFile('does-not-exist.txt');
86+
assert.isNull(missing);
87+
});
88+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright 2025 The Chromium Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import {ListFilesTool} from '../ListFilesTool.js';
6+
import {FileStorageManager, type FileSummary} from '../FileStorageManager.js';
7+
8+
describe('ListFilesTool', () => {
9+
afterEach(() => {
10+
sinon.restore();
11+
});
12+
13+
it('lists files successfully and returns count', async () => {
14+
const now = Date.now();
15+
const files: FileSummary[] = [
16+
{fileName: 'b.txt', size: 2, mimeType: 'text/plain', createdAt: now - 2, updatedAt: now - 1},
17+
{fileName: 'a.txt', size: 1, mimeType: 'text/plain', createdAt: now - 3, updatedAt: now - 2},
18+
];
19+
const fakeManager = {listFiles: sinon.stub().resolves(files)} as unknown as FileStorageManager;
20+
sinon.stub(FileStorageManager, 'getInstance').returns(fakeManager);
21+
22+
const tool = new ListFilesTool();
23+
const result = await tool.execute({reasoning: 'enumerate'});
24+
25+
assert.strictEqual(result.success, true);
26+
assert.deepEqual(result.files, files);
27+
assert.strictEqual(result.count, files.length);
28+
sinon.assert.calledOnce((fakeManager as any).listFiles);
29+
});
30+
31+
it('returns an error when listing fails', async () => {
32+
const fakeManager = {listFiles: sinon.stub().rejects(new Error('no db'))} as unknown as FileStorageManager;
33+
sinon.stub(FileStorageManager, 'getInstance').returns(fakeManager);
34+
35+
const tool = new ListFilesTool();
36+
const result = await tool.execute({reasoning: 'enumerate'});
37+
38+
assert.strictEqual(result.success, false);
39+
assert.strictEqual(result.error, 'no db');
40+
});
41+
});
42+
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright 2025 The Chromium Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import {ReadFileTool} from '../ReadFileTool.js';
6+
import {FileStorageManager, type StoredFile} from '../FileStorageManager.js';
7+
8+
describe('ReadFileTool', () => {
9+
afterEach(() => {
10+
sinon.restore();
11+
});
12+
13+
function makeStored(overrides: Partial<StoredFile> = {}): StoredFile {
14+
const now = Date.now();
15+
return {
16+
id: 'id-1',
17+
sessionId: 'sess-1',
18+
fileName: 'note.txt',
19+
content: 'hello',
20+
mimeType: 'text/plain',
21+
createdAt: now - 100,
22+
updatedAt: now,
23+
size: 5,
24+
...overrides,
25+
};
26+
}
27+
28+
it('reads an existing file and returns metadata', async () => {
29+
const stored = makeStored();
30+
const fakeManager = {readFile: sinon.stub().resolves(stored)} as unknown as FileStorageManager;
31+
sinon.stub(FileStorageManager, 'getInstance').returns(fakeManager);
32+
33+
const tool = new ReadFileTool();
34+
const result = await tool.execute({fileName: 'note.txt', reasoning: 'inspect'});
35+
36+
assert.strictEqual(result.success, true);
37+
assert.strictEqual(result.fileName, 'note.txt');
38+
assert.strictEqual(result.content, 'hello');
39+
assert.strictEqual(result.mimeType, 'text/plain');
40+
assert.strictEqual(result.size, 5);
41+
assert.isNumber(result.createdAt);
42+
assert.isNumber(result.updatedAt);
43+
});
44+
45+
it('returns a not found error when manager returns null', async () => {
46+
const fakeManager = {readFile: sinon.stub().resolves(null)} as unknown as FileStorageManager;
47+
sinon.stub(FileStorageManager, 'getInstance').returns(fakeManager);
48+
49+
const tool = new ReadFileTool();
50+
const result = await tool.execute({fileName: 'missing.txt', reasoning: 'inspect'});
51+
52+
assert.strictEqual(result.success, false);
53+
assert.strictEqual(result.error, 'File "missing.txt" was not found in the current session.');
54+
});
55+
56+
it('returns an error when read fails', async () => {
57+
const fakeManager = {readFile: sinon.stub().rejects(new Error('disk error'))} as unknown as FileStorageManager;
58+
sinon.stub(FileStorageManager, 'getInstance').returns(fakeManager);
59+
60+
const tool = new ReadFileTool();
61+
const result = await tool.execute({fileName: 'note.txt', reasoning: 'inspect'});
62+
63+
assert.strictEqual(result.success, false);
64+
assert.strictEqual(result.error, 'disk error');
65+
});
66+
});
67+

0 commit comments

Comments
 (0)