browsertrack 0.0.1 → 0.1.1

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.
Files changed (110) hide show
  1. package/.github/workflows/docs.yml +54 -0
  2. package/AGENTS.md +133 -0
  3. package/README.md +143 -0
  4. package/dist/chunk-6VA7GBAO.js +246 -0
  5. package/dist/chunk-6VA7GBAO.js.map +1 -0
  6. package/dist/chunk-G2Y3CXCY.js +659 -0
  7. package/dist/chunk-G2Y3CXCY.js.map +1 -0
  8. package/dist/chunk-ILRYKMME.js +1228 -0
  9. package/dist/chunk-ILRYKMME.js.map +1 -0
  10. package/dist/chunk-SKCMT2DE.js +2641 -0
  11. package/dist/chunk-SKCMT2DE.js.map +1 -0
  12. package/dist/chunk-SPCIROIU.js +616 -0
  13. package/dist/chunk-SPCIROIU.js.map +1 -0
  14. package/dist/cli/index.js +2942 -0
  15. package/dist/cli/index.js.map +1 -0
  16. package/dist/client/index.cjs +2780 -0
  17. package/dist/client/index.d.ts +211 -0
  18. package/dist/client/index.js +20 -0
  19. package/dist/client/index.js.map +1 -0
  20. package/dist/client.iife.js +630 -0
  21. package/dist/core/index.d.ts +86 -0
  22. package/dist/core/index.js +37 -0
  23. package/dist/core/index.js.map +1 -0
  24. package/dist/daemon/index.d.ts +58 -0
  25. package/dist/daemon/index.js +32 -0
  26. package/dist/daemon/index.js.map +1 -0
  27. package/dist/engine-CeT9URuN.d.ts +161 -0
  28. package/dist/index.d.ts +11 -0
  29. package/dist/index.js +56 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/mcp/index.d.ts +11 -0
  32. package/dist/mcp/index.js +12 -0
  33. package/dist/mcp/index.js.map +1 -0
  34. package/dist/notes-CBvN91Wf.d.ts +260 -0
  35. package/dist/projects-CY8ungMt.d.ts +99 -0
  36. package/dist/server-Dd8NX2Mk.d.ts +55 -0
  37. package/docboot.config.js +16 -0
  38. package/docs/cli.md +51 -0
  39. package/docs/closed-loop-verification.md +57 -0
  40. package/docs/getting-started.md +122 -0
  41. package/docs/incidents-diagnostics.md +61 -0
  42. package/docs/index.md +46 -0
  43. package/docs/mcp-reference.md +128 -0
  44. package/docs/security-privacy.md +32 -0
  45. package/docs/visual-notes.md +67 -0
  46. package/examples/test-app/index.html +299 -0
  47. package/package.json +56 -7
  48. package/packages/cli/src/index.ts +413 -0
  49. package/packages/client/package.json +28 -0
  50. package/packages/client/src/breadcrumbs.ts +30 -0
  51. package/packages/client/src/client.ts +278 -0
  52. package/packages/client/src/commands/handler.ts +301 -0
  53. package/packages/client/src/config.ts +37 -0
  54. package/packages/client/src/index.ts +50 -0
  55. package/packages/client/src/interceptors/console.ts +68 -0
  56. package/packages/client/src/interceptors/interaction.ts +119 -0
  57. package/packages/client/src/interceptors/navigation.ts +93 -0
  58. package/packages/client/src/interceptors/network.ts +165 -0
  59. package/packages/client/src/interceptors/runtime.ts +65 -0
  60. package/packages/client/src/notes/inspector.ts +1553 -0
  61. package/packages/client/src/screenshot/browser-script-driver.ts +192 -0
  62. package/packages/client/src/screenshot/driver.ts +14 -0
  63. package/packages/client/src/transport/websocket.ts +194 -0
  64. package/packages/client/tsconfig.json +8 -0
  65. package/packages/core/package.json +26 -0
  66. package/packages/core/src/fingerprint.ts +120 -0
  67. package/packages/core/src/index.ts +9 -0
  68. package/packages/core/src/redaction.ts +174 -0
  69. package/packages/core/src/selector.ts +77 -0
  70. package/packages/core/src/types/commands.ts +101 -0
  71. package/packages/core/src/types/events.ts +108 -0
  72. package/packages/core/src/types/incidents.ts +54 -0
  73. package/packages/core/src/types/notes.ts +106 -0
  74. package/packages/core/src/types/probes.ts +44 -0
  75. package/packages/core/src/types/projects.ts +20 -0
  76. package/packages/core/tsconfig.json +8 -0
  77. package/packages/daemon/src/config.ts +29 -0
  78. package/packages/daemon/src/incidents/engine.ts +211 -0
  79. package/packages/daemon/src/index.ts +18 -0
  80. package/packages/daemon/src/notes/engine.ts +92 -0
  81. package/packages/daemon/src/notes/verification.ts +191 -0
  82. package/packages/daemon/src/server/daemon.ts +112 -0
  83. package/packages/daemon/src/server/http.ts +165 -0
  84. package/packages/daemon/src/server/ws.ts +246 -0
  85. package/packages/daemon/src/session/manager.ts +147 -0
  86. package/packages/daemon/src/storage/db.ts +742 -0
  87. package/packages/daemon/src/storage/screenshot-store.ts +49 -0
  88. package/packages/daemon/src/verification/engine.ts +264 -0
  89. package/packages/mcp/src/handlers.ts +398 -0
  90. package/packages/mcp/src/index.ts +3 -0
  91. package/packages/mcp/src/server.ts +86 -0
  92. package/packages/mcp/src/tools.ts +236 -0
  93. package/src/index.ts +4 -0
  94. package/test/client/interceptors.test.ts +130 -0
  95. package/test/core/fingerprint.test.ts +53 -0
  96. package/test/core/notes.test.ts +66 -0
  97. package/test/core/redaction.test.ts +60 -0
  98. package/test/daemon/incident-engine.test.ts +98 -0
  99. package/test/daemon/notes-storage.test.ts +130 -0
  100. package/test/daemon/notes-verification.test.ts +135 -0
  101. package/test/daemon/storage.test.ts +123 -0
  102. package/test/daemon/verification-engine.test.ts +88 -0
  103. package/test/e2e/daemon-mcp-e2e.test.ts +153 -0
  104. package/test/e2e/visual-notes-e2e.test.ts +205 -0
  105. package/test/mcp/handlers.test.ts +116 -0
  106. package/test/mcp/notes.test.ts +107 -0
  107. package/tsconfig.base.json +17 -0
  108. package/tsconfig.json +26 -0
  109. package/tsup.config.ts +54 -0
  110. package/vitest.config.ts +9 -0
@@ -0,0 +1,98 @@
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 { IncidentEngine } from '../../packages/daemon/src/incidents/engine.js';
6
+ import { StorageDB } from '../../packages/daemon/src/storage/db.js';
7
+ import { ScreenshotStore } from '../../packages/daemon/src/storage/screenshot-store.js';
8
+
9
+ describe('IncidentEngine', () => {
10
+ let tempDir: string;
11
+ let db: StorageDB;
12
+ let screenshotStore: ScreenshotStore;
13
+ let engine: IncidentEngine;
14
+
15
+ beforeEach(() => {
16
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bt_inc_test_'));
17
+ const dbPath = path.join(tempDir, 'test.db');
18
+ const screenshotsDir = path.join(tempDir, 'screenshots');
19
+
20
+ db = new StorageDB(dbPath);
21
+ screenshotStore = new ScreenshotStore(screenshotsDir);
22
+ engine = new IncidentEngine(db, screenshotStore);
23
+
24
+ // Setup project and session
25
+ db.upsertProject({ id: 'proj_app', name: 'app', origin: 'http://localhost:5173' });
26
+ db.upsertSession({
27
+ id: 'sess_100',
28
+ projectId: 'proj_app',
29
+ origin: 'http://localhost:5173',
30
+ url: 'http://localhost:5173/dashboard',
31
+ title: 'Dashboard',
32
+ userAgent: 'Chrome/120',
33
+ connectedAt: new Date().toISOString(),
34
+ lastSeenAt: new Date().toISOString(),
35
+ active: true,
36
+ });
37
+ });
38
+
39
+ afterEach(() => {
40
+ db.close();
41
+ fs.rmSync(tempDir, { recursive: true, force: true });
42
+ });
43
+
44
+ it('should create a new incident from a runtime error event', () => {
45
+ const incident = engine.processClientEvent({
46
+ type: 'event',
47
+ sessionId: 'sess_100',
48
+ eventType: 'runtime_error',
49
+ payload: {
50
+ errorType: 'TypeError',
51
+ message: "Cannot read properties of null (reading 'userName')",
52
+ filename: 'http://localhost:5173/src/User.tsx?t=12345',
53
+ lineno: 28,
54
+ colno: 5,
55
+ timestamp: Date.now(),
56
+ },
57
+ route: '/dashboard',
58
+ url: 'http://localhost:5173/dashboard',
59
+ timestamp: Date.now(),
60
+ });
61
+
62
+ expect(incident).not.toBeNull();
63
+ expect(incident?.id.startsWith('inc_')).toBe(true);
64
+ expect(incident?.occurrences).toBe(1);
65
+ expect(incident?.status).toBe('OPEN');
66
+ expect(incident?.source.file).toBe('/src/User.tsx');
67
+ expect(incident?.source.line).toBe(28);
68
+ });
69
+
70
+ it('should deduplicate recurring errors and increment occurrences', () => {
71
+ const eventPayload = {
72
+ type: 'event' as const,
73
+ sessionId: 'sess_100',
74
+ eventType: 'runtime_error' as const,
75
+ payload: {
76
+ errorType: 'ReferenceError',
77
+ message: 'AuthService is not defined',
78
+ filename: '/src/services/auth.ts',
79
+ lineno: 14,
80
+ timestamp: Date.now(),
81
+ },
82
+ route: '/login',
83
+ url: 'http://localhost:5173/login',
84
+ timestamp: Date.now(),
85
+ };
86
+
87
+ const first = engine.processClientEvent(eventPayload);
88
+ const second = engine.processClientEvent(eventPayload);
89
+ const third = engine.processClientEvent(eventPayload);
90
+
91
+ expect(first?.id).toBe(second?.id);
92
+ expect(first?.id).toBe(third?.id);
93
+ expect(third?.occurrences).toBe(3);
94
+
95
+ const fromDb = db.getIncident(first!.id);
96
+ expect(fromDb?.occurrences).toBe(3);
97
+ });
98
+ });
@@ -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
+ });