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,130 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import type { VisualNote } from '../../packages/core/src/index.js';
|
|
3
|
+
import { StorageDB } from '../../packages/daemon/src/storage/db.js';
|
|
4
|
+
|
|
5
|
+
describe('Daemon StorageDB - Visual Notes', () => {
|
|
6
|
+
let db: StorageDB;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
db = new StorageDB(':memory:');
|
|
10
|
+
db.upsertProject({ id: 'proj_1', name: 'my-app', origin: 'http://localhost:3000' });
|
|
11
|
+
db.upsertSession({
|
|
12
|
+
id: 'sess_1',
|
|
13
|
+
projectId: 'proj_1',
|
|
14
|
+
origin: 'http://localhost:3000',
|
|
15
|
+
connectedAt: new Date().toISOString(),
|
|
16
|
+
lastSeenAt: new Date().toISOString(),
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('inserts and retrieves a visual note', () => {
|
|
21
|
+
const note: VisualNote = {
|
|
22
|
+
id: 'note_abc123',
|
|
23
|
+
projectId: 'proj_1',
|
|
24
|
+
sessionId: 'sess_1',
|
|
25
|
+
type: 'element',
|
|
26
|
+
message: 'Card text is truncated on small screens',
|
|
27
|
+
route: '/settings',
|
|
28
|
+
url: 'http://localhost:3000/settings',
|
|
29
|
+
viewport: { width: 375, height: 667, devicePixelRatio: 2 },
|
|
30
|
+
scroll: { scrollX: 0, scrollY: 0 },
|
|
31
|
+
target: {
|
|
32
|
+
selector: '.card-title',
|
|
33
|
+
boundingRect: { x: 10, y: 50, width: 200, height: 30, top: 50, left: 10, bottom: 80, right: 210 },
|
|
34
|
+
visible: true,
|
|
35
|
+
},
|
|
36
|
+
elementContext: {
|
|
37
|
+
selector: '.card-title',
|
|
38
|
+
tag: 'h3',
|
|
39
|
+
attributes: { class: 'card-title' },
|
|
40
|
+
outerHTML: '<h3 class="card-title">Settings</h3>',
|
|
41
|
+
},
|
|
42
|
+
status: 'OPEN',
|
|
43
|
+
createdAt: new Date().toISOString(),
|
|
44
|
+
updatedAt: new Date().toISOString(),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
db.insertNote(note);
|
|
48
|
+
|
|
49
|
+
const fetched = db.getNote('note_abc123');
|
|
50
|
+
expect(fetched).toBeDefined();
|
|
51
|
+
expect(fetched?.id).toBe('note_abc123');
|
|
52
|
+
expect(fetched?.message).toBe('Card text is truncated on small screens');
|
|
53
|
+
expect(fetched?.viewport.width).toBe(375);
|
|
54
|
+
expect(fetched?.target?.selector).toBe('.card-title');
|
|
55
|
+
expect(fetched?.status).toBe('OPEN');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('updates note status to RESOLVED and back to OPEN', () => {
|
|
59
|
+
const note: VisualNote = {
|
|
60
|
+
id: 'note_status_test',
|
|
61
|
+
projectId: 'proj_1',
|
|
62
|
+
sessionId: 'sess_1',
|
|
63
|
+
type: 'page',
|
|
64
|
+
message: 'Header background should be transparent',
|
|
65
|
+
route: '/',
|
|
66
|
+
url: 'http://localhost:3000/',
|
|
67
|
+
viewport: { width: 1440, height: 900, devicePixelRatio: 1 },
|
|
68
|
+
scroll: { scrollX: 0, scrollY: 0 },
|
|
69
|
+
status: 'OPEN',
|
|
70
|
+
createdAt: new Date().toISOString(),
|
|
71
|
+
updatedAt: new Date().toISOString(),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
db.insertNote(note);
|
|
75
|
+
expect(db.getNote('note_status_test')?.status).toBe('OPEN');
|
|
76
|
+
|
|
77
|
+
db.updateNoteStatus('note_status_test', 'RESOLVED');
|
|
78
|
+
const resolved = db.getNote('note_status_test');
|
|
79
|
+
expect(resolved?.status).toBe('RESOLVED');
|
|
80
|
+
expect(resolved?.resolvedAt).toBeDefined();
|
|
81
|
+
|
|
82
|
+
db.updateNoteStatus('note_status_test', 'OPEN');
|
|
83
|
+
const reopened = db.getNote('note_status_test');
|
|
84
|
+
expect(reopened?.status).toBe('OPEN');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('stores and retrieves note verification records', () => {
|
|
88
|
+
db.insertNote({
|
|
89
|
+
id: 'note_ver_test',
|
|
90
|
+
projectId: 'proj_1',
|
|
91
|
+
sessionId: 'sess_1',
|
|
92
|
+
type: 'element',
|
|
93
|
+
message: 'Fix overflow',
|
|
94
|
+
route: '/home',
|
|
95
|
+
url: 'http://localhost:3000/home',
|
|
96
|
+
viewport: { width: 390, height: 844, devicePixelRatio: 3 },
|
|
97
|
+
scroll: { scrollX: 0, scrollY: 0 },
|
|
98
|
+
status: 'OPEN',
|
|
99
|
+
createdAt: new Date().toISOString(),
|
|
100
|
+
updatedAt: new Date().toISOString(),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
db.insertNoteVerification({
|
|
104
|
+
noteId: 'note_ver_test',
|
|
105
|
+
status: 'VERIFIED',
|
|
106
|
+
checks: [
|
|
107
|
+
{ type: 'element_exists', passed: true, details: 'Element found' },
|
|
108
|
+
{ type: 'no_viewport_overflow', passed: true, details: 'Fits inside 390px viewport' },
|
|
109
|
+
],
|
|
110
|
+
geometryDiff: {
|
|
111
|
+
overflowFixed: true,
|
|
112
|
+
viewportWidth: 390,
|
|
113
|
+
overflowPx: 0,
|
|
114
|
+
},
|
|
115
|
+
screenshots: {
|
|
116
|
+
before: '/path/to/before.webp',
|
|
117
|
+
after: '/path/to/after.webp',
|
|
118
|
+
},
|
|
119
|
+
timestamp: new Date().toISOString(),
|
|
120
|
+
message: 'Visual note verified successfully',
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const latest = db.getLatestNoteVerification('note_ver_test');
|
|
124
|
+
expect(latest).toBeDefined();
|
|
125
|
+
expect(latest?.status).toBe('VERIFIED');
|
|
126
|
+
expect(latest?.checks.length).toBe(2);
|
|
127
|
+
expect(latest?.geometryDiff?.overflowFixed).toBe(true);
|
|
128
|
+
expect(latest?.screenshots?.after).toBe('/path/to/after.webp');
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
|
|
8
|
+
describe('NoteVerificationEngine - Layout Probes & Verification', () => {
|
|
9
|
+
let db: StorageDB;
|
|
10
|
+
let screenshotStore: ScreenshotStore;
|
|
11
|
+
let sessionManager: SessionManager;
|
|
12
|
+
let notesEngine: NotesEngine;
|
|
13
|
+
let noteVerifier: NoteVerificationEngine;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
db = new StorageDB(':memory:');
|
|
17
|
+
screenshotStore = new ScreenshotStore('/tmp/test-notes-screenshots');
|
|
18
|
+
sessionManager = new SessionManager(db);
|
|
19
|
+
notesEngine = new NotesEngine(db, screenshotStore);
|
|
20
|
+
noteVerifier = new NoteVerificationEngine(db, sessionManager, notesEngine);
|
|
21
|
+
|
|
22
|
+
db.upsertProject({ id: 'proj_notes', name: 'notes-app', origin: 'http://localhost:3000' });
|
|
23
|
+
db.upsertSession({
|
|
24
|
+
id: 'sess_live',
|
|
25
|
+
projectId: 'proj_notes',
|
|
26
|
+
origin: 'http://localhost:3000',
|
|
27
|
+
connectedAt: new Date().toISOString(),
|
|
28
|
+
lastSeenAt: new Date().toISOString(),
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('returns INCONCLUSIVE when no active session is connected', async () => {
|
|
33
|
+
db.insertNote({
|
|
34
|
+
id: 'note_no_sess',
|
|
35
|
+
projectId: 'proj_notes',
|
|
36
|
+
sessionId: 'sess_live',
|
|
37
|
+
type: 'element',
|
|
38
|
+
message: 'Check responsive card',
|
|
39
|
+
route: '/',
|
|
40
|
+
url: 'http://localhost:3000/',
|
|
41
|
+
viewport: { width: 390, height: 844, devicePixelRatio: 3 },
|
|
42
|
+
scroll: { scrollX: 0, scrollY: 0 },
|
|
43
|
+
target: {
|
|
44
|
+
selector: '.card',
|
|
45
|
+
boundingRect: { x: 0, y: 0, width: 420, height: 200, top: 0, left: 0, bottom: 200, right: 420 },
|
|
46
|
+
visible: true,
|
|
47
|
+
},
|
|
48
|
+
status: 'OPEN',
|
|
49
|
+
createdAt: new Date().toISOString(),
|
|
50
|
+
updatedAt: new Date().toISOString(),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const res = await noteVerifier.verifyNote('note_no_sess');
|
|
54
|
+
expect(res.status).toBe('INCONCLUSIVE');
|
|
55
|
+
expect(res.message).toContain('No active browser session');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('verifies note with layout overflow fixed when session responds', async () => {
|
|
59
|
+
// Mock connected socket
|
|
60
|
+
const fakeSocket: any = {
|
|
61
|
+
readyState: 1,
|
|
62
|
+
send: (data: string) => {
|
|
63
|
+
const wrapper = JSON.parse(data);
|
|
64
|
+
const cmd = wrapper.command;
|
|
65
|
+
if (!cmd) return;
|
|
66
|
+
|
|
67
|
+
if (cmd.type === 'get_page_state') {
|
|
68
|
+
sessionManager.handleCommandResponse('sess_live', {
|
|
69
|
+
id: cmd.id,
|
|
70
|
+
ok: true,
|
|
71
|
+
result: { route: '/', title: 'Test App', readyState: 'complete', url: 'http://localhost:3000/' },
|
|
72
|
+
});
|
|
73
|
+
} else if (cmd.type === 'query_element') {
|
|
74
|
+
sessionManager.handleCommandResponse('sess_live', {
|
|
75
|
+
id: cmd.id,
|
|
76
|
+
ok: true,
|
|
77
|
+
result: {
|
|
78
|
+
exists: true,
|
|
79
|
+
visible: true,
|
|
80
|
+
boundingRect: { x: 0, y: 0, width: 350, height: 180, top: 0, left: 0, bottom: 180, right: 350 },
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
} else if (cmd.type === 'check_overflow') {
|
|
84
|
+
sessionManager.handleCommandResponse('sess_live', {
|
|
85
|
+
id: cmd.id,
|
|
86
|
+
ok: true,
|
|
87
|
+
result: {
|
|
88
|
+
selector: '.card',
|
|
89
|
+
overflow: false,
|
|
90
|
+
viewportWidth: 390,
|
|
91
|
+
viewportHeight: 844,
|
|
92
|
+
overflowRightPx: 0,
|
|
93
|
+
overflowBottomPx: 0,
|
|
94
|
+
rect: { x: 0, y: 0, width: 350, height: 180, top: 0, left: 0, bottom: 180, right: 350 },
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
} else if (cmd.type === 'capture_element') {
|
|
98
|
+
sessionManager.handleCommandResponse('sess_live', {
|
|
99
|
+
id: cmd.id,
|
|
100
|
+
ok: true,
|
|
101
|
+
result: { dataUrl: 'data:image/webp;base64,UklGRmYAAABXRUJQVlA4IFoAAAAwAQCdASoBAAEAD8D+JaQAA3AA/ua2wAAA', format: 'webp', width: 350, height: 180 },
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
sessionManager.registerSocket('sess_live', fakeSocket, 'http://localhost:3000', 'proj_notes');
|
|
108
|
+
|
|
109
|
+
db.insertNote({
|
|
110
|
+
id: 'note_fixed_ovf',
|
|
111
|
+
projectId: 'proj_notes',
|
|
112
|
+
sessionId: 'sess_live',
|
|
113
|
+
type: 'element',
|
|
114
|
+
message: "Mobile'da buraya bak, sağa taşıyor",
|
|
115
|
+
route: '/',
|
|
116
|
+
url: 'http://localhost:3000/',
|
|
117
|
+
viewport: { width: 390, height: 844, devicePixelRatio: 3 },
|
|
118
|
+
scroll: { scrollX: 0, scrollY: 0 },
|
|
119
|
+
target: {
|
|
120
|
+
selector: '.card',
|
|
121
|
+
boundingRect: { x: 0, y: 0, width: 440, height: 200, top: 0, left: 0, bottom: 200, right: 440 },
|
|
122
|
+
visible: true,
|
|
123
|
+
},
|
|
124
|
+
status: 'OPEN',
|
|
125
|
+
createdAt: new Date().toISOString(),
|
|
126
|
+
updatedAt: new Date().toISOString(),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const res = await noteVerifier.verifyNote('note_fixed_ovf');
|
|
130
|
+
expect(res.status).toBe('VERIFIED');
|
|
131
|
+
expect(res.geometryDiff?.overflowFixed).toBe(true);
|
|
132
|
+
expect(res.geometryDiff?.current?.width).toBe(350);
|
|
133
|
+
expect(res.checks.some((c) => c.type === 'no_viewport_overflow' && c.passed)).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { StorageDB } from '../../packages/daemon/src/storage/db.js';
|
|
6
|
+
|
|
7
|
+
describe('StorageDB (SQLite)', () => {
|
|
8
|
+
let tempDbPath: string;
|
|
9
|
+
let db: StorageDB;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
tempDbPath = path.join(os.tmpdir(), `bt_test_${Date.now()}_${Math.random().toString(36).slice(2)}.db`);
|
|
13
|
+
db = new StorageDB(tempDbPath);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
db.close();
|
|
18
|
+
if (fs.existsSync(tempDbPath)) {
|
|
19
|
+
fs.unlinkSync(tempDbPath);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should upsert and retrieve projects', () => {
|
|
24
|
+
const project = db.upsertProject({
|
|
25
|
+
id: 'proj_euix',
|
|
26
|
+
name: 'euix',
|
|
27
|
+
origin: 'http://localhost:5173',
|
|
28
|
+
path: '/Users/dev/projects/euix',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(project.id).toBe('proj_euix');
|
|
32
|
+
expect(project.name).toBe('euix');
|
|
33
|
+
|
|
34
|
+
const fetched = db.getProject('euix');
|
|
35
|
+
expect(fetched).not.toBeNull();
|
|
36
|
+
expect(fetched?.origin).toBe('http://localhost:5173');
|
|
37
|
+
expect(fetched?.path).toBe('/Users/dev/projects/euix');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should track sessions and query by project', () => {
|
|
41
|
+
db.upsertProject({ id: 'proj_1', name: 'app1', origin: 'http://localhost:3000' });
|
|
42
|
+
|
|
43
|
+
db.upsertSession({
|
|
44
|
+
id: 'sess_1',
|
|
45
|
+
projectId: 'proj_1',
|
|
46
|
+
origin: 'http://localhost:3000',
|
|
47
|
+
url: 'http://localhost:3000/dashboard',
|
|
48
|
+
title: 'App Dashboard',
|
|
49
|
+
userAgent: 'Mozilla/5.0 Chrome/120.0',
|
|
50
|
+
connectedAt: new Date().toISOString(),
|
|
51
|
+
lastSeenAt: new Date().toISOString(),
|
|
52
|
+
active: true,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const activeSessions = db.listSessions('proj_1', true);
|
|
56
|
+
expect(activeSessions.length).toBe(1);
|
|
57
|
+
expect(activeSessions[0].id).toBe('sess_1');
|
|
58
|
+
|
|
59
|
+
db.deactivateSession('sess_1');
|
|
60
|
+
const afterDeactivate = db.listSessions('proj_1', true);
|
|
61
|
+
expect(afterDeactivate.length).toBe(0);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should enforce event retention per session', () => {
|
|
65
|
+
const sessionId = 'sess_retention';
|
|
66
|
+
for (let i = 1; i <= 20; i++) {
|
|
67
|
+
db.insertEvent({
|
|
68
|
+
id: `evt_${i}`,
|
|
69
|
+
sessionId,
|
|
70
|
+
eventType: 'console',
|
|
71
|
+
payload: { message: `Log ${i}` },
|
|
72
|
+
timestamp: 1000 + i,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Prune to keep only last 5
|
|
77
|
+
db.pruneSessionEvents(sessionId, 5);
|
|
78
|
+
|
|
79
|
+
const remaining = db.getEvents({ sessionId, limit: 50 });
|
|
80
|
+
expect(remaining.length).toBe(5);
|
|
81
|
+
expect(remaining[0].id).toBe('evt_20');
|
|
82
|
+
expect(remaining[4].id).toBe('evt_16');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should insert, update, and list incidents', () => {
|
|
86
|
+
const incident = {
|
|
87
|
+
id: 'inc_1',
|
|
88
|
+
projectId: 'proj_test',
|
|
89
|
+
sessionId: 'sess_test',
|
|
90
|
+
type: 'TypeError',
|
|
91
|
+
severity: 'error' as const,
|
|
92
|
+
message: 'Cannot read properties of undefined',
|
|
93
|
+
source: { file: '/src/main.ts', line: 42 },
|
|
94
|
+
fingerprint: 'fp_abc123',
|
|
95
|
+
route: '/home',
|
|
96
|
+
firstSeen: new Date().toISOString(),
|
|
97
|
+
lastSeen: new Date().toISOString(),
|
|
98
|
+
occurrences: 1,
|
|
99
|
+
status: 'OPEN' as const,
|
|
100
|
+
breadcrumbs: [],
|
|
101
|
+
networkFailures: [],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
db.insertIncident(incident);
|
|
105
|
+
|
|
106
|
+
const found = db.findIncidentByFingerprint('fp_abc123');
|
|
107
|
+
expect(found).not.toBeNull();
|
|
108
|
+
expect(found?.id).toBe('inc_1');
|
|
109
|
+
expect(found?.occurrences).toBe(1);
|
|
110
|
+
|
|
111
|
+
// Increment occurrence
|
|
112
|
+
db.updateIncidentOccurrence('inc_1', {
|
|
113
|
+
sessionId: 'sess_test',
|
|
114
|
+
lastSeen: new Date().toISOString(),
|
|
115
|
+
occurrences: 2,
|
|
116
|
+
route: '/home',
|
|
117
|
+
breadcrumbs: [],
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const updated = db.getIncident('inc_1');
|
|
121
|
+
expect(updated?.occurrences).toBe(2);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
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 { SessionManager } from '../../packages/daemon/src/session/manager.js';
|
|
6
|
+
import { StorageDB } from '../../packages/daemon/src/storage/db.js';
|
|
7
|
+
import { ScreenshotStore } from '../../packages/daemon/src/storage/screenshot-store.js';
|
|
8
|
+
import { VerificationEngine } from '../../packages/daemon/src/verification/engine.js';
|
|
9
|
+
|
|
10
|
+
describe('VerificationEngine', () => {
|
|
11
|
+
let tempDir: string;
|
|
12
|
+
let db: StorageDB;
|
|
13
|
+
let screenshotStore: ScreenshotStore;
|
|
14
|
+
let sessionManager: SessionManager;
|
|
15
|
+
let verificationEngine: VerificationEngine;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bt_ver_test_'));
|
|
19
|
+
const dbPath = path.join(tempDir, 'test.db');
|
|
20
|
+
const screenshotsDir = path.join(tempDir, 'screenshots');
|
|
21
|
+
|
|
22
|
+
db = new StorageDB(dbPath);
|
|
23
|
+
screenshotStore = new ScreenshotStore(screenshotsDir);
|
|
24
|
+
sessionManager = new SessionManager(db);
|
|
25
|
+
verificationEngine = new VerificationEngine(db, sessionManager, screenshotStore);
|
|
26
|
+
|
|
27
|
+
db.upsertProject({ id: 'proj_demo', name: 'demo', origin: 'http://localhost:5173' });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
db.close();
|
|
32
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should return INCONCLUSIVE when no active session is connected to browser', async () => {
|
|
36
|
+
db.insertIncident({
|
|
37
|
+
id: 'inc_test_1',
|
|
38
|
+
projectId: 'proj_demo',
|
|
39
|
+
sessionId: 'sess_inactive',
|
|
40
|
+
type: 'TypeError',
|
|
41
|
+
severity: 'error',
|
|
42
|
+
message: 'Cannot read properties of null',
|
|
43
|
+
source: { file: '/src/main.ts', line: 10 },
|
|
44
|
+
fingerprint: 'fp_test_1',
|
|
45
|
+
route: '/app',
|
|
46
|
+
firstSeen: new Date().toISOString(),
|
|
47
|
+
lastSeen: new Date().toISOString(),
|
|
48
|
+
occurrences: 1,
|
|
49
|
+
status: 'OPEN',
|
|
50
|
+
breadcrumbs: [],
|
|
51
|
+
networkFailures: [],
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const result = await verificationEngine.verifyIncident('inc_test_1');
|
|
55
|
+
expect(result.status).toBe('INCONCLUSIVE');
|
|
56
|
+
expect(result.message).toContain('No active browser session');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should record verification result in database and update incident status', async () => {
|
|
60
|
+
db.insertIncident({
|
|
61
|
+
id: 'inc_test_2',
|
|
62
|
+
projectId: 'proj_demo',
|
|
63
|
+
sessionId: 'sess_inactive',
|
|
64
|
+
type: 'ReferenceError',
|
|
65
|
+
severity: 'error',
|
|
66
|
+
message: 'Button not found',
|
|
67
|
+
source: { file: '/src/app.ts', line: 20 },
|
|
68
|
+
fingerprint: 'fp_test_2',
|
|
69
|
+
route: '/app',
|
|
70
|
+
firstSeen: new Date().toISOString(),
|
|
71
|
+
lastSeen: new Date().toISOString(),
|
|
72
|
+
occurrences: 1,
|
|
73
|
+
status: 'OPEN',
|
|
74
|
+
breadcrumbs: [],
|
|
75
|
+
networkFailures: [],
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
await verificationEngine.verifyIncident('inc_test_2');
|
|
79
|
+
|
|
80
|
+
const latest = db.getLatestVerification('inc_test_2');
|
|
81
|
+
expect(latest).not.toBeNull();
|
|
82
|
+
expect(latest?.incidentId).toBe('inc_test_2');
|
|
83
|
+
expect(latest?.status).toBe('INCONCLUSIVE');
|
|
84
|
+
|
|
85
|
+
const updatedInc = db.getIncident('inc_test_2');
|
|
86
|
+
expect(updatedInc?.status).toBe('INCONCLUSIVE');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
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 WebSocket from 'ws';
|
|
6
|
+
import { createDaemon } from '../../packages/daemon/src/index.js';
|
|
7
|
+
import { handleToolCall } from '../../packages/mcp/src/handlers.js';
|
|
8
|
+
|
|
9
|
+
describe('BrowserTrack Full E2E Lifecycle (Daemon + WebSocket Client + MCP)', () => {
|
|
10
|
+
let tempDir: string;
|
|
11
|
+
let daemon: ReturnType<typeof createDaemon>;
|
|
12
|
+
let wsClient: WebSocket | null = null;
|
|
13
|
+
const testPort = 7339;
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bt_e2e_'));
|
|
17
|
+
const dbPath = path.join(tempDir, 'e2e.db');
|
|
18
|
+
const screenshotsDir = path.join(tempDir, 'screenshots');
|
|
19
|
+
|
|
20
|
+
daemon = createDaemon({
|
|
21
|
+
port: testPort,
|
|
22
|
+
host: '127.0.0.1',
|
|
23
|
+
dbPath,
|
|
24
|
+
screenshotsDir,
|
|
25
|
+
verbose: false,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
await daemon.start();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(async () => {
|
|
32
|
+
if (wsClient) {
|
|
33
|
+
wsClient.close();
|
|
34
|
+
wsClient = null;
|
|
35
|
+
}
|
|
36
|
+
await daemon.stop();
|
|
37
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should complete full cycle: handshake -> error ingestion -> incident creation -> MCP tool query -> command dispatch', async () => {
|
|
41
|
+
let receivedSessionId = '';
|
|
42
|
+
|
|
43
|
+
// 1. Connect WebSocket client (simulating browser)
|
|
44
|
+
wsClient = new WebSocket(`ws://127.0.0.1:${testPort}`);
|
|
45
|
+
|
|
46
|
+
await new Promise<void>((resolve) => {
|
|
47
|
+
wsClient!.on('open', () => {
|
|
48
|
+
// Send hello
|
|
49
|
+
wsClient!.send(
|
|
50
|
+
JSON.stringify({
|
|
51
|
+
type: 'hello',
|
|
52
|
+
origin: 'http://localhost:5173',
|
|
53
|
+
url: 'http://localhost:5173/products/123',
|
|
54
|
+
title: 'Product Details',
|
|
55
|
+
userAgent: 'Mozilla/5.0 TestBrowser',
|
|
56
|
+
timestamp: Date.now(),
|
|
57
|
+
})
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
wsClient!.on('message', (raw: string | Buffer) => {
|
|
62
|
+
const msg = JSON.parse(raw.toString());
|
|
63
|
+
if (msg.type === 'hello_ack') {
|
|
64
|
+
receivedSessionId = msg.sessionId;
|
|
65
|
+
resolve();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(receivedSessionId.startsWith('sess_')).toBe(true);
|
|
71
|
+
|
|
72
|
+
// 2. Browser client triggers runtime error
|
|
73
|
+
wsClient.send(
|
|
74
|
+
JSON.stringify({
|
|
75
|
+
type: 'event',
|
|
76
|
+
sessionId: receivedSessionId,
|
|
77
|
+
eventType: 'runtime_error',
|
|
78
|
+
payload: {
|
|
79
|
+
errorType: 'TypeError',
|
|
80
|
+
message: 'Cannot read property price of undefined',
|
|
81
|
+
filename: 'http://localhost:5173/src/components/ProductCard.tsx',
|
|
82
|
+
lineno: 45,
|
|
83
|
+
colno: 12,
|
|
84
|
+
timestamp: Date.now(),
|
|
85
|
+
},
|
|
86
|
+
breadcrumbs: [
|
|
87
|
+
{ type: 'navigation', message: 'navigate /products/123', timestamp: Date.now() - 200 },
|
|
88
|
+
{ type: 'click', message: 'click button.add-to-cart', timestamp: Date.now() - 100 },
|
|
89
|
+
],
|
|
90
|
+
lastElement: {
|
|
91
|
+
selector: 'button.add-to-cart',
|
|
92
|
+
tag: 'button',
|
|
93
|
+
visible: true,
|
|
94
|
+
innerText: 'Add to Cart',
|
|
95
|
+
},
|
|
96
|
+
route: '/products/123',
|
|
97
|
+
url: 'http://localhost:5173/products/123',
|
|
98
|
+
timestamp: Date.now(),
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Wait a brief tick for SQLite write
|
|
103
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
104
|
+
|
|
105
|
+
// 3. MCP query: list_incidents
|
|
106
|
+
const listRes = await handleToolCall('list_incidents', {}, { db: daemon.db });
|
|
107
|
+
expect(listRes.total).toBe(1);
|
|
108
|
+
const incidentId = listRes.incidents[0].id;
|
|
109
|
+
expect(incidentId.startsWith('inc_')).toBe(true);
|
|
110
|
+
expect(listRes.incidents[0].message).toBe('Cannot read property price of undefined');
|
|
111
|
+
|
|
112
|
+
// 4. MCP query: get_incident
|
|
113
|
+
const getRes = await handleToolCall('get_incident', { incidentId }, { db: daemon.db });
|
|
114
|
+
expect(getRes.id).toBe(incidentId);
|
|
115
|
+
expect(getRes.source.file).toBe('/src/components/ProductCard.tsx');
|
|
116
|
+
expect(getRes.source.line).toBe(45);
|
|
117
|
+
expect(getRes.lastInteractedElement?.selector).toBe('button.add-to-cart');
|
|
118
|
+
expect(getRes.recentBreadcrumbs.length).toBe(2);
|
|
119
|
+
|
|
120
|
+
// 5. Test Live Command: get_page_state via daemon session manager
|
|
121
|
+
// Setup client listener to reply to get_page_state command
|
|
122
|
+
wsClient.on('message', (raw: string | Buffer) => {
|
|
123
|
+
const msg = JSON.parse(raw.toString());
|
|
124
|
+
if (msg.type === 'command' && msg.command.type === 'get_page_state') {
|
|
125
|
+
wsClient!.send(
|
|
126
|
+
JSON.stringify({
|
|
127
|
+
type: 'command_response',
|
|
128
|
+
sessionId: receivedSessionId,
|
|
129
|
+
response: {
|
|
130
|
+
id: msg.command.id,
|
|
131
|
+
ok: true,
|
|
132
|
+
result: {
|
|
133
|
+
url: 'http://localhost:5173/products/123',
|
|
134
|
+
route: '/products/123',
|
|
135
|
+
title: 'Product Details',
|
|
136
|
+
readyState: 'complete',
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const pageState = await handleToolCall(
|
|
145
|
+
'get_page_state',
|
|
146
|
+
{ sessionId: receivedSessionId },
|
|
147
|
+
{ db: daemon.db, sessionManager: daemon.sessionManager }
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
expect(pageState.url).toBe('http://localhost:5173/products/123');
|
|
151
|
+
expect(pageState.title).toBe('Product Details');
|
|
152
|
+
});
|
|
153
|
+
});
|