browsertrack 0.0.1 → 0.1.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/README.md +143 -0
- package/dist/chunk-6A7FFDIB.js +221 -0
- package/dist/chunk-6A7FFDIB.js.map +1 -0
- package/dist/chunk-ANMR5WBH.js +659 -0
- package/dist/chunk-ANMR5WBH.js.map +1 -0
- package/dist/chunk-BSKNG4V7.js +1199 -0
- package/dist/chunk-BSKNG4V7.js.map +1 -0
- package/dist/chunk-X7C5CHBO.js +2080 -0
- package/dist/chunk-X7C5CHBO.js.map +1 -0
- package/dist/chunk-ZLNGOOH2.js +554 -0
- package/dist/chunk-ZLNGOOH2.js.map +1 -0
- package/dist/cli/index.js +2829 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/client/index.cjs +2218 -0
- package/dist/client/index.d.ts +197 -0
- package/dist/client/index.js +20 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client.iife.js +309 -0
- package/dist/core/index.d.ts +78 -0
- package/dist/core/index.js +31 -0
- package/dist/core/index.js.map +1 -0
- package/dist/daemon/index.d.ts +58 -0
- package/dist/daemon/index.js +32 -0
- package/dist/daemon/index.js.map +1 -0
- package/dist/engine-DKloFjvs.d.ts +157 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +50 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +11 -0
- package/dist/mcp/index.js +12 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/notes-CBvN91Wf.d.ts +260 -0
- package/dist/projects-CY8ungMt.d.ts +99 -0
- package/dist/server-CN-se8td.d.ts +55 -0
- package/examples/test-app/index.html +299 -0
- package/package.json +52 -7
- package/packages/cli/src/index.ts +413 -0
- package/packages/client/package.json +28 -0
- package/packages/client/src/breadcrumbs.ts +30 -0
- package/packages/client/src/client.ts +278 -0
- package/packages/client/src/commands/handler.ts +301 -0
- package/packages/client/src/config.ts +37 -0
- package/packages/client/src/index.ts +50 -0
- package/packages/client/src/interceptors/console.ts +68 -0
- package/packages/client/src/interceptors/interaction.ts +119 -0
- package/packages/client/src/interceptors/navigation.ts +93 -0
- package/packages/client/src/interceptors/network.ts +165 -0
- package/packages/client/src/interceptors/runtime.ts +65 -0
- package/packages/client/src/notes/inspector.ts +943 -0
- package/packages/client/src/screenshot/browser-script-driver.ts +192 -0
- package/packages/client/src/screenshot/driver.ts +14 -0
- package/packages/client/src/transport/websocket.ts +179 -0
- package/packages/client/tsconfig.json +8 -0
- package/packages/core/dist/index.d.ts +317 -0
- package/packages/core/dist/index.js +221 -0
- package/packages/core/package.json +23 -0
- package/packages/core/src/fingerprint.ts +120 -0
- package/packages/core/src/index.ts +9 -0
- package/packages/core/src/redaction.ts +139 -0
- package/packages/core/src/selector.ts +77 -0
- package/packages/core/src/types/commands.ts +101 -0
- package/packages/core/src/types/events.ts +108 -0
- package/packages/core/src/types/incidents.ts +54 -0
- package/packages/core/src/types/notes.ts +106 -0
- package/packages/core/src/types/probes.ts +44 -0
- package/packages/core/src/types/projects.ts +20 -0
- package/packages/core/tsconfig.json +8 -0
- package/packages/daemon/src/config.ts +29 -0
- package/packages/daemon/src/incidents/engine.ts +211 -0
- package/packages/daemon/src/index.ts +18 -0
- package/packages/daemon/src/notes/engine.ts +92 -0
- package/packages/daemon/src/notes/verification.ts +191 -0
- package/packages/daemon/src/server/daemon.ts +112 -0
- package/packages/daemon/src/server/http.ts +165 -0
- package/packages/daemon/src/server/ws.ts +175 -0
- package/packages/daemon/src/session/manager.ts +126 -0
- package/packages/daemon/src/storage/db.ts +733 -0
- package/packages/daemon/src/storage/screenshot-store.ts +49 -0
- package/packages/daemon/src/verification/engine.ts +264 -0
- package/packages/mcp/src/handlers.ts +398 -0
- package/packages/mcp/src/index.ts +3 -0
- package/packages/mcp/src/server.ts +86 -0
- package/packages/mcp/src/tools.ts +236 -0
- package/src/index.ts +4 -0
- package/test/client/interceptors.test.ts +69 -0
- package/test/core/fingerprint.test.ts +53 -0
- package/test/core/notes.test.ts +66 -0
- package/test/core/redaction.test.ts +48 -0
- package/test/daemon/incident-engine.test.ts +98 -0
- package/test/daemon/notes-storage.test.ts +130 -0
- package/test/daemon/notes-verification.test.ts +135 -0
- package/test/daemon/storage.test.ts +123 -0
- package/test/daemon/verification-engine.test.ts +88 -0
- package/test/e2e/daemon-mcp-e2e.test.ts +153 -0
- package/test/e2e/visual-notes-e2e.test.ts +205 -0
- package/test/mcp/handlers.test.ts +116 -0
- package/test/mcp/notes.test.ts +107 -0
- package/tsconfig.base.json +17 -0
- package/tsconfig.json +26 -0
- package/tsup.config.ts +54 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
+
import { WebSocket } from 'ws';
|
|
3
|
+
import { BrowserTrackDaemon } from '../../packages/daemon/src/server/daemon.js';
|
|
4
|
+
import { handleToolCall } from '../../packages/mcp/src/handlers.js';
|
|
5
|
+
|
|
6
|
+
describe('E2E Visual Notes Flow (Client -> Daemon -> MCP -> Verification -> Resolution)', () => {
|
|
7
|
+
let daemon: BrowserTrackDaemon;
|
|
8
|
+
const testPort = 7345;
|
|
9
|
+
let clientWs: WebSocket;
|
|
10
|
+
let assignedSessionId = '';
|
|
11
|
+
|
|
12
|
+
beforeAll(async () => {
|
|
13
|
+
daemon = new BrowserTrackDaemon({
|
|
14
|
+
port: testPort,
|
|
15
|
+
host: '127.0.0.1',
|
|
16
|
+
dbPath: ':memory:',
|
|
17
|
+
screenshotsDir: '/tmp/test-e2e-notes-screenshots',
|
|
18
|
+
verbose: false,
|
|
19
|
+
});
|
|
20
|
+
await daemon.start();
|
|
21
|
+
|
|
22
|
+
// Connect WebSocket simulating browser client
|
|
23
|
+
await new Promise<void>((resolve, reject) => {
|
|
24
|
+
clientWs = new WebSocket(`ws://127.0.0.1:${testPort}`);
|
|
25
|
+
clientWs.on('open', () => {
|
|
26
|
+
clientWs.send(
|
|
27
|
+
JSON.stringify({
|
|
28
|
+
type: 'hello',
|
|
29
|
+
origin: 'http://localhost:5173',
|
|
30
|
+
url: 'http://localhost:5173/dashboard',
|
|
31
|
+
title: 'Analytics Dashboard',
|
|
32
|
+
})
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
clientWs.on('message', (raw) => {
|
|
37
|
+
const msg = JSON.parse(raw.toString());
|
|
38
|
+
if (msg.type === 'hello_ack') {
|
|
39
|
+
assignedSessionId = msg.sessionId;
|
|
40
|
+
resolve();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Handle daemon command dispatches during verify_note
|
|
44
|
+
if (msg.type === 'command') {
|
|
45
|
+
const cmd = msg.command;
|
|
46
|
+
if (cmd.type === 'get_page_state') {
|
|
47
|
+
clientWs.send(
|
|
48
|
+
JSON.stringify({
|
|
49
|
+
type: 'command_response',
|
|
50
|
+
sessionId: assignedSessionId,
|
|
51
|
+
response: {
|
|
52
|
+
id: cmd.id,
|
|
53
|
+
ok: true,
|
|
54
|
+
result: {
|
|
55
|
+
url: 'http://localhost:5173/dashboard',
|
|
56
|
+
route: '/dashboard',
|
|
57
|
+
title: 'Analytics Dashboard',
|
|
58
|
+
readyState: 'complete',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
);
|
|
63
|
+
} else if (cmd.type === 'query_element') {
|
|
64
|
+
clientWs.send(
|
|
65
|
+
JSON.stringify({
|
|
66
|
+
type: 'command_response',
|
|
67
|
+
sessionId: assignedSessionId,
|
|
68
|
+
response: {
|
|
69
|
+
id: cmd.id,
|
|
70
|
+
ok: true,
|
|
71
|
+
result: {
|
|
72
|
+
exists: true,
|
|
73
|
+
visible: true,
|
|
74
|
+
boundingRect: { x: 20, y: 80, width: 350, height: 160, top: 80, left: 20, bottom: 240, right: 370 },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
);
|
|
79
|
+
} else if (cmd.type === 'check_overflow') {
|
|
80
|
+
clientWs.send(
|
|
81
|
+
JSON.stringify({
|
|
82
|
+
type: 'command_response',
|
|
83
|
+
sessionId: assignedSessionId,
|
|
84
|
+
response: {
|
|
85
|
+
id: cmd.id,
|
|
86
|
+
ok: true,
|
|
87
|
+
result: {
|
|
88
|
+
selector: '#stats-banner',
|
|
89
|
+
overflow: false, // Overflow fixed!
|
|
90
|
+
viewportWidth: 390,
|
|
91
|
+
viewportHeight: 844,
|
|
92
|
+
overflowRightPx: 0,
|
|
93
|
+
overflowBottomPx: 0,
|
|
94
|
+
rect: { x: 20, y: 80, width: 350, height: 160, top: 80, left: 20, bottom: 240, right: 370 },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
} else if (cmd.type === 'capture_element') {
|
|
100
|
+
clientWs.send(
|
|
101
|
+
JSON.stringify({
|
|
102
|
+
type: 'command_response',
|
|
103
|
+
sessionId: assignedSessionId,
|
|
104
|
+
response: {
|
|
105
|
+
id: cmd.id,
|
|
106
|
+
ok: true,
|
|
107
|
+
result: {
|
|
108
|
+
format: 'webp',
|
|
109
|
+
dataUrl: 'data:image/webp;base64,UklGRmYAAABXRUJQVlA4IFoAAAAwAQCdASoBAAEAD8D+JaQAA3AA/ua2wAAA',
|
|
110
|
+
width: 350,
|
|
111
|
+
height: 160,
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
})
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
clientWs.on('error', reject);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
afterAll(async () => {
|
|
125
|
+
clientWs?.close();
|
|
126
|
+
await daemon.stop();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('runs complete lifecycle of a visual note', async () => {
|
|
130
|
+
// 1. Client creates a note on overflowing stats card
|
|
131
|
+
const noteCreatedPromise = new Promise<string>((resolve) => {
|
|
132
|
+
const listener = (raw: any) => {
|
|
133
|
+
const msg = JSON.parse(raw.toString());
|
|
134
|
+
if (msg.type === 'note_created_ack') {
|
|
135
|
+
clientWs.removeListener('message', listener);
|
|
136
|
+
resolve(msg.noteId);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
clientWs.on('message', listener);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
clientWs.send(
|
|
143
|
+
JSON.stringify({
|
|
144
|
+
type: 'create_note',
|
|
145
|
+
sessionId: assignedSessionId,
|
|
146
|
+
noteType: 'element',
|
|
147
|
+
message: "Mobile'da buraya bak, sağa taşıyor",
|
|
148
|
+
route: '/dashboard',
|
|
149
|
+
url: 'http://localhost:5173/dashboard',
|
|
150
|
+
viewport: { width: 390, height: 844, devicePixelRatio: 3 },
|
|
151
|
+
scroll: { scrollX: 0, scrollY: 0 },
|
|
152
|
+
target: {
|
|
153
|
+
selector: '#stats-banner',
|
|
154
|
+
boundingRect: { x: 20, y: 80, width: 440, height: 160, top: 80, left: 20, bottom: 240, right: 460 },
|
|
155
|
+
visible: true,
|
|
156
|
+
confidence: 'high',
|
|
157
|
+
},
|
|
158
|
+
elementContext: {
|
|
159
|
+
selector: '#stats-banner',
|
|
160
|
+
tag: 'div',
|
|
161
|
+
attributes: { id: 'stats-banner' },
|
|
162
|
+
outerHTML: '<div id="stats-banner"><input type="password" value="[REDACTED]" /></div>',
|
|
163
|
+
},
|
|
164
|
+
screenshot: 'data:image/webp;base64,UklGRmYAAABXRUJQVlA4IFoAAAAwAQCdASoBAAEAD8D+JaQAA3AA/ua2wAAA',
|
|
165
|
+
})
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const noteId = await noteCreatedPromise;
|
|
169
|
+
expect(noteId).toMatch(/^note_/);
|
|
170
|
+
|
|
171
|
+
// 2. MCP list_notes returns the note
|
|
172
|
+
const mcpCtx = {
|
|
173
|
+
db: daemon.db,
|
|
174
|
+
sessionManager: daemon.sessionManager,
|
|
175
|
+
noteVerificationEngine: daemon.noteVerificationEngine,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const listRes = await handleToolCall('list_notes', {}, mcpCtx);
|
|
179
|
+
expect(listRes.total).toBe(1);
|
|
180
|
+
expect(listRes.notes[0].id).toBe(noteId);
|
|
181
|
+
expect(listRes.notes[0].status).toBe('OPEN');
|
|
182
|
+
expect(listRes.notes[0].target).toBe('#stats-banner');
|
|
183
|
+
|
|
184
|
+
// 3. MCP get_note returns sanitized DOM and screenshot
|
|
185
|
+
const getRes = await handleToolCall('get_note', { noteId }, mcpCtx);
|
|
186
|
+
expect(getRes.id).toBe(noteId);
|
|
187
|
+
expect(getRes.elementContext.outerHTML).toContain('[REDACTED]');
|
|
188
|
+
expect(getRes.screenshot.available).toBe(true);
|
|
189
|
+
|
|
190
|
+
// 4. Verify note with layout probes (checks element & viewport overflow)
|
|
191
|
+
const verifyRes = await handleToolCall('verify_note', { noteId, observationWindowMs: 50 }, mcpCtx);
|
|
192
|
+
expect(verifyRes.noteId).toBe(noteId);
|
|
193
|
+
expect(verifyRes.status).toBe('VERIFIED');
|
|
194
|
+
expect(verifyRes.geometryDiff.overflowFixed).toBe(true);
|
|
195
|
+
expect(verifyRes.screenshots.after).toBeDefined();
|
|
196
|
+
|
|
197
|
+
// 5. MCP resolve_note marks note as RESOLVED
|
|
198
|
+
const resolveRes = await handleToolCall('resolve_note', { noteId }, mcpCtx);
|
|
199
|
+
expect(resolveRes.ok).toBe(true);
|
|
200
|
+
expect(resolveRes.status).toBe('RESOLVED');
|
|
201
|
+
|
|
202
|
+
const finalNote = daemon.db.getNote(noteId);
|
|
203
|
+
expect(finalNote?.status).toBe('RESOLVED');
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { StorageDB } from '../../packages/daemon/src/storage/db.js';
|
|
6
|
+
import { handleToolCall } from '../../packages/mcp/src/handlers.js';
|
|
7
|
+
|
|
8
|
+
describe('MCP Tool Handlers', () => {
|
|
9
|
+
let tempDir: string;
|
|
10
|
+
let db: StorageDB;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bt_mcp_test_'));
|
|
14
|
+
const dbPath = path.join(tempDir, 'test.db');
|
|
15
|
+
db = new StorageDB(dbPath);
|
|
16
|
+
|
|
17
|
+
// Seed test data
|
|
18
|
+
db.upsertProject({
|
|
19
|
+
id: 'proj_mcp',
|
|
20
|
+
name: 'mcp-app',
|
|
21
|
+
origin: 'http://localhost:3000',
|
|
22
|
+
path: '/Users/test/mcp-app',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
db.upsertSession({
|
|
26
|
+
id: 'sess_mcp_1',
|
|
27
|
+
projectId: 'proj_mcp',
|
|
28
|
+
origin: 'http://localhost:3000',
|
|
29
|
+
url: 'http://localhost:3000/users',
|
|
30
|
+
title: 'Users List',
|
|
31
|
+
userAgent: 'Chrome/120',
|
|
32
|
+
connectedAt: new Date().toISOString(),
|
|
33
|
+
lastSeenAt: new Date().toISOString(),
|
|
34
|
+
active: true,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
db.insertIncident({
|
|
38
|
+
id: 'inc_42',
|
|
39
|
+
projectId: 'proj_mcp',
|
|
40
|
+
sessionId: 'sess_mcp_1',
|
|
41
|
+
type: 'TypeError',
|
|
42
|
+
severity: 'error',
|
|
43
|
+
message: "Cannot read properties of undefined (reading 'avatar')",
|
|
44
|
+
source: { file: '/src/components/UserAvatar.tsx', line: 18, column: 5 },
|
|
45
|
+
fingerprint: 'fp_mcp_user_avatar',
|
|
46
|
+
route: '/users/42',
|
|
47
|
+
firstSeen: new Date().toISOString(),
|
|
48
|
+
lastSeen: new Date().toISOString(),
|
|
49
|
+
occurrences: 4,
|
|
50
|
+
status: 'OPEN',
|
|
51
|
+
stack: 'TypeError: Cannot read properties...\n at UserAvatar (/src/components/UserAvatar.tsx:18:5)',
|
|
52
|
+
breadcrumbs: [
|
|
53
|
+
{ type: 'navigation', message: 'navigate /users', timestamp: Date.now() - 1000 },
|
|
54
|
+
{ type: 'click', message: 'click button[data-testid="user-42"]', timestamp: Date.now() - 500 },
|
|
55
|
+
],
|
|
56
|
+
networkFailures: [
|
|
57
|
+
{ url: '/api/users/42', method: 'GET', status: 500, durationMs: 120, timestamp: Date.now() - 300 },
|
|
58
|
+
],
|
|
59
|
+
lastElement: {
|
|
60
|
+
selector: '[data-testid="user-42"]',
|
|
61
|
+
tag: 'button',
|
|
62
|
+
visible: true,
|
|
63
|
+
innerText: 'View Profile',
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
db.insertEvent({
|
|
68
|
+
id: 'evt_log_1',
|
|
69
|
+
sessionId: 'sess_mcp_1',
|
|
70
|
+
eventType: 'console',
|
|
71
|
+
payload: { level: 'error', message: 'Failed to load user image' },
|
|
72
|
+
timestamp: Date.now(),
|
|
73
|
+
route: '/users/42',
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
afterEach(() => {
|
|
78
|
+
db.close();
|
|
79
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should handle list_projects', async () => {
|
|
83
|
+
const res = await handleToolCall('list_projects', {}, { db });
|
|
84
|
+
expect(res.projects.length).toBe(1);
|
|
85
|
+
expect(res.projects[0].name).toBe('mcp-app');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should handle list_incidents', async () => {
|
|
89
|
+
const res = await handleToolCall('list_incidents', { limit: 10 }, { db });
|
|
90
|
+
expect(res.total).toBe(1);
|
|
91
|
+
expect(res.incidents[0].id).toBe('inc_42');
|
|
92
|
+
expect(res.incidents[0].occurrences).toBe(4);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should handle get_incident with rich debugging context', async () => {
|
|
96
|
+
const res = await handleToolCall('get_incident', { incidentId: 'inc_42' }, { db });
|
|
97
|
+
expect(res.id).toBe('inc_42');
|
|
98
|
+
expect(res.source.file).toBe('/src/components/UserAvatar.tsx');
|
|
99
|
+
expect(res.source.line).toBe(18);
|
|
100
|
+
expect(res.lastInteractedElement?.selector).toBe('[data-testid="user-42"]');
|
|
101
|
+
expect(res.recentBreadcrumbs.length).toBe(2);
|
|
102
|
+
expect(res.networkFailures.length).toBe(1);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should handle get_console', async () => {
|
|
106
|
+
const res = await handleToolCall('get_console', { sessionId: 'sess_mcp_1' }, { db });
|
|
107
|
+
expect(res.logs.length).toBe(1);
|
|
108
|
+
expect(res.logs[0].message).toBe('Failed to load user image');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should handle get_breadcrumbs', async () => {
|
|
112
|
+
const res = await handleToolCall('get_breadcrumbs', { incidentId: 'inc_42' }, { db });
|
|
113
|
+
expect(res.breadcrumbs.length).toBe(2);
|
|
114
|
+
expect(res.breadcrumbs[0].type).toBe('navigation');
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { StorageDB } from '../../packages/daemon/src/storage/db.js';
|
|
3
|
+
import { ScreenshotStore } from '../../packages/daemon/src/storage/screenshot-store.js';
|
|
4
|
+
import { SessionManager } from '../../packages/daemon/src/session/manager.js';
|
|
5
|
+
import { NotesEngine } from '../../packages/daemon/src/notes/engine.js';
|
|
6
|
+
import { NoteVerificationEngine } from '../../packages/daemon/src/notes/verification.js';
|
|
7
|
+
import { handleToolCall } from '../../packages/mcp/src/handlers.js';
|
|
8
|
+
|
|
9
|
+
describe('MCP Tools - Visual Notes', () => {
|
|
10
|
+
let db: StorageDB;
|
|
11
|
+
let screenshotStore: ScreenshotStore;
|
|
12
|
+
let sessionManager: SessionManager;
|
|
13
|
+
let notesEngine: NotesEngine;
|
|
14
|
+
let noteVerificationEngine: NoteVerificationEngine;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
db = new StorageDB(':memory:');
|
|
18
|
+
screenshotStore = new ScreenshotStore('/tmp/test-mcp-notes');
|
|
19
|
+
sessionManager = new SessionManager(db);
|
|
20
|
+
notesEngine = new NotesEngine(db, screenshotStore);
|
|
21
|
+
noteVerificationEngine = new NoteVerificationEngine(db, sessionManager, notesEngine);
|
|
22
|
+
|
|
23
|
+
db.upsertProject({ id: 'proj_mcp', name: 'store-app', origin: 'http://localhost:5173' });
|
|
24
|
+
db.upsertSession({
|
|
25
|
+
id: 'sess_mcp',
|
|
26
|
+
projectId: 'proj_mcp',
|
|
27
|
+
origin: 'http://localhost:5173',
|
|
28
|
+
connectedAt: new Date().toISOString(),
|
|
29
|
+
lastSeenAt: new Date().toISOString(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
db.insertNote({
|
|
33
|
+
id: 'note_mcp_1',
|
|
34
|
+
projectId: 'proj_mcp',
|
|
35
|
+
sessionId: 'sess_mcp',
|
|
36
|
+
type: 'element',
|
|
37
|
+
message: 'Mobile nav hamburger button is misaligned',
|
|
38
|
+
route: '/checkout',
|
|
39
|
+
url: 'http://localhost:5173/checkout',
|
|
40
|
+
viewport: { width: 390, height: 844, devicePixelRatio: 3 },
|
|
41
|
+
scroll: { scrollX: 0, scrollY: 0 },
|
|
42
|
+
target: {
|
|
43
|
+
selector: '#nav-hamburger',
|
|
44
|
+
boundingRect: { x: 340, y: 15, width: 40, height: 40, top: 15, left: 340, bottom: 55, right: 380 },
|
|
45
|
+
visible: true,
|
|
46
|
+
},
|
|
47
|
+
elementContext: {
|
|
48
|
+
selector: '#nav-hamburger',
|
|
49
|
+
tag: 'button',
|
|
50
|
+
attributes: { id: 'nav-hamburger', 'aria-label': 'Menu' },
|
|
51
|
+
outerHTML: '<button id="nav-hamburger" aria-label="Menu"><span></span></button>',
|
|
52
|
+
},
|
|
53
|
+
status: 'OPEN',
|
|
54
|
+
createdAt: new Date().toISOString(),
|
|
55
|
+
updatedAt: new Date().toISOString(),
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('lists visual notes via list_notes tool', async () => {
|
|
60
|
+
const res = await handleToolCall(
|
|
61
|
+
'list_notes',
|
|
62
|
+
{ projectId: 'proj_mcp' },
|
|
63
|
+
{ db, sessionManager, noteVerificationEngine }
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(res.total).toBe(1);
|
|
67
|
+
expect(res.notes[0].id).toBe('note_mcp_1');
|
|
68
|
+
expect(res.notes[0].route).toBe('/checkout');
|
|
69
|
+
expect(res.notes[0].target).toBe('#nav-hamburger');
|
|
70
|
+
expect(res.notes[0].message).toBe('Mobile nav hamburger button is misaligned');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('retrieves note debugging context via get_note tool', async () => {
|
|
74
|
+
const res = await handleToolCall(
|
|
75
|
+
'get_note',
|
|
76
|
+
{ noteId: 'note_mcp_1' },
|
|
77
|
+
{ db, sessionManager, noteVerificationEngine }
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
expect(res.id).toBe('note_mcp_1');
|
|
81
|
+
expect(res.type).toBe('element');
|
|
82
|
+
expect(res.viewport.width).toBe(390);
|
|
83
|
+
expect(res.elementContext.tag).toBe('button');
|
|
84
|
+
expect(res.project.name).toBe('store-app');
|
|
85
|
+
expect(res.status).toBe('OPEN');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('resolves and reopens note via resolve_note and reopen_note tools', async () => {
|
|
89
|
+
const resolveRes = await handleToolCall(
|
|
90
|
+
'resolve_note',
|
|
91
|
+
{ noteId: 'note_mcp_1' },
|
|
92
|
+
{ db, sessionManager, noteVerificationEngine }
|
|
93
|
+
);
|
|
94
|
+
expect(resolveRes.ok).toBe(true);
|
|
95
|
+
expect(resolveRes.status).toBe('RESOLVED');
|
|
96
|
+
expect(db.getNote('note_mcp_1')?.status).toBe('RESOLVED');
|
|
97
|
+
|
|
98
|
+
const reopenRes = await handleToolCall(
|
|
99
|
+
'reopen_note',
|
|
100
|
+
{ noteId: 'note_mcp_1' },
|
|
101
|
+
{ db, sessionManager, noteVerificationEngine }
|
|
102
|
+
);
|
|
103
|
+
expect(reopenRes.ok).toBe(true);
|
|
104
|
+
expect(reopenRes.status).toBe('OPEN');
|
|
105
|
+
expect(db.getNote('note_mcp_1')?.status).toBe('OPEN');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"declarationMap": true,
|
|
9
|
+
"sourceMap": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"forceConsistentCasingInFileNames": true,
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"isolatedModules": true
|
|
16
|
+
}
|
|
17
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"declarationMap": true,
|
|
9
|
+
"sourceMap": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"forceConsistentCasingInFileNames": true,
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"isolatedModules": true,
|
|
16
|
+
"baseUrl": ".",
|
|
17
|
+
"paths": {
|
|
18
|
+
"@browserdiag/core": ["./packages/core/src/index.ts"],
|
|
19
|
+
"browsertrack/core": ["./packages/core/src/index.ts"],
|
|
20
|
+
"browsertrack/client": ["./packages/client/src/index.ts"],
|
|
21
|
+
"browsertrack/daemon": ["./packages/daemon/src/index.ts"],
|
|
22
|
+
"browsertrack/mcp": ["./packages/mcp/src/index.ts"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"include": ["src/**/*", "packages/**/*", "test/**/*"]
|
|
26
|
+
}
|
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { defineConfig } from 'tsup';
|
|
2
|
+
|
|
3
|
+
export default defineConfig([
|
|
4
|
+
// Core, Client, Daemon, MCP Library exports
|
|
5
|
+
{
|
|
6
|
+
entry: {
|
|
7
|
+
index: 'src/index.ts',
|
|
8
|
+
'core/index': 'packages/core/src/index.ts',
|
|
9
|
+
'client/index': 'packages/client/src/index.ts',
|
|
10
|
+
'daemon/index': 'packages/daemon/src/index.ts',
|
|
11
|
+
'mcp/index': 'packages/mcp/src/index.ts',
|
|
12
|
+
},
|
|
13
|
+
format: ['esm'],
|
|
14
|
+
dts: true,
|
|
15
|
+
sourcemap: true,
|
|
16
|
+
clean: true,
|
|
17
|
+
target: 'node20',
|
|
18
|
+
},
|
|
19
|
+
// CLI Binary
|
|
20
|
+
{
|
|
21
|
+
entry: {
|
|
22
|
+
'cli/index': 'packages/cli/src/index.ts',
|
|
23
|
+
},
|
|
24
|
+
format: ['esm'],
|
|
25
|
+
banner: {
|
|
26
|
+
js: '#!/usr/bin/env node',
|
|
27
|
+
},
|
|
28
|
+
sourcemap: true,
|
|
29
|
+
target: 'node20',
|
|
30
|
+
},
|
|
31
|
+
// Client browser CJS bundle
|
|
32
|
+
{
|
|
33
|
+
entry: {
|
|
34
|
+
'client/index': 'packages/client/src/index.ts',
|
|
35
|
+
},
|
|
36
|
+
format: ['cjs'],
|
|
37
|
+
dts: false,
|
|
38
|
+
target: 'es2020',
|
|
39
|
+
},
|
|
40
|
+
// Standalone browser client script (for http://127.0.0.1:7331/client.js)
|
|
41
|
+
{
|
|
42
|
+
entry: {
|
|
43
|
+
'client.iife': 'packages/client/src/index.ts',
|
|
44
|
+
},
|
|
45
|
+
format: ['iife'],
|
|
46
|
+
globalName: 'BrowserTrack',
|
|
47
|
+
platform: 'browser',
|
|
48
|
+
target: 'es2020',
|
|
49
|
+
outExtension() {
|
|
50
|
+
return { js: '.js' };
|
|
51
|
+
},
|
|
52
|
+
minify: true,
|
|
53
|
+
},
|
|
54
|
+
]);
|