Skip to content

Commit cbdbe5d

Browse files
author
SentienceDEV
committed
Sync extension bundle and quiet tests
Add a reproducible extension sync script, track dist assets, and clean up test warnings while keeping download tracking resilient to async mocks.
1 parent 796dbfd commit cbdbe5d

File tree

10 files changed

+3495
-8
lines changed

10 files changed

+3495
-8
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,7 @@ Thumbs.db
6060
# Temporary directories from sync workflows
6161
extension-temp/
6262
playground/
63+
64+
# Allow bundled extension assets under sentience/extension/dist
65+
!sentience/extension/dist/
66+
!sentience/extension/dist/**

scripts/sync_extension.sh

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
6+
CHROME_DIR="${REPO_ROOT}/sentience-chrome"
7+
SDK_EXT_DIR="${REPO_ROOT}/sdk-python/sentience/extension"
8+
9+
if [[ ! -d "${CHROME_DIR}" ]]; then
10+
echo "[sync_extension] sentience-chrome not found at ${CHROME_DIR}"
11+
exit 1
12+
fi
13+
14+
if [[ ! -f "${CHROME_DIR}/package.json" ]]; then
15+
echo "[sync_extension] package.json missing in sentience-chrome"
16+
exit 1
17+
fi
18+
19+
echo "[sync_extension] Building sentience-chrome..."
20+
pushd "${CHROME_DIR}" >/dev/null
21+
npm run build
22+
popd >/dev/null
23+
24+
echo "[sync_extension] Syncing dist/ and pkg/ to sdk-python..."
25+
mkdir -p "${SDK_EXT_DIR}/dist" "${SDK_EXT_DIR}/pkg"
26+
cp "${CHROME_DIR}/dist/"* "${SDK_EXT_DIR}/dist/"
27+
cp "${CHROME_DIR}/pkg/"* "${SDK_EXT_DIR}/pkg/"
28+
29+
echo "[sync_extension] Done."

sentience/backends/playwright_backend.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"""
2323

2424
import asyncio
25+
import inspect
2526
import mimetypes
2627
import os
2728
import time
@@ -56,7 +57,14 @@ def __init__(self, page: "AsyncPage") -> None:
5657
# Best-effort download tracking (does not change behavior unless a download occurs).
5758
# pylint: disable=broad-exception-caught
5859
try:
59-
self._page.on("download", lambda d: asyncio.create_task(self._track_download(d)))
60+
result = self._page.on(
61+
"download", lambda d: asyncio.create_task(self._track_download(d))
62+
)
63+
if inspect.isawaitable(result):
64+
try:
65+
asyncio.get_running_loop().create_task(result)
66+
except RuntimeError:
67+
pass
6068
except Exception:
6169
pass
6270

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
// Sentience Chrome Extension - Background Service Worker
2+
// Auto-generated from modular source
3+
import init, { analyze_page_with_options, analyze_page, prune_for_api } from '../pkg/sentience_core.js';
4+
5+
// background.js - Service Worker with WASM (CSP-Immune!)
6+
// This runs in an isolated environment, completely immune to page CSP policies
7+
8+
9+
console.log('[Sentience Background] Initializing...');
10+
11+
// Global WASM initialization state
12+
let wasmReady = false;
13+
let wasmInitPromise = null;
14+
15+
/**
16+
* Initialize WASM module - called once on service worker startup
17+
* Uses static imports (not dynamic import()) which is required for Service Workers
18+
*/
19+
async function initWASM() {
20+
if (wasmReady) return;
21+
if (wasmInitPromise) return wasmInitPromise;
22+
23+
wasmInitPromise = (async () => {
24+
try {
25+
console.log('[Sentience Background] Loading WASM module...');
26+
27+
// Define the js_click_element function that WASM expects
28+
// In Service Workers, use 'globalThis' instead of 'window'
29+
// In background context, we can't actually click, so we log a warning
30+
globalThis.js_click_element = () => {
31+
console.warn('[Sentience Background] js_click_element called in background (ignored)');
32+
};
33+
34+
// Initialize WASM - this calls the init() function from the static import
35+
// The init() function handles fetching and instantiating the .wasm file
36+
await init();
37+
38+
wasmReady = true;
39+
console.log('[Sentience Background] ✓ WASM ready!');
40+
console.log(
41+
'[Sentience Background] Available functions: analyze_page, analyze_page_with_options, prune_for_api'
42+
);
43+
} catch (error) {
44+
console.error('[Sentience Background] WASM initialization failed:', error);
45+
throw error;
46+
}
47+
})();
48+
49+
return wasmInitPromise;
50+
}
51+
52+
// Initialize WASM on service worker startup
53+
initWASM().catch((err) => {
54+
console.error('[Sentience Background] Failed to initialize WASM:', err);
55+
});
56+
57+
/**
58+
* Message handler for all extension communication
59+
* Includes global error handling to prevent extension crashes
60+
*/
61+
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
62+
// Global error handler to prevent extension crashes
63+
try {
64+
// Handle screenshot requests (existing functionality)
65+
if (request.action === 'captureScreenshot') {
66+
handleScreenshotCapture(sender.tab.id, request.options)
67+
.then((screenshot) => {
68+
sendResponse({ success: true, screenshot });
69+
})
70+
.catch((error) => {
71+
console.error('[Sentience Background] Screenshot capture failed:', error);
72+
sendResponse({
73+
success: false,
74+
error: error.message || 'Screenshot capture failed',
75+
});
76+
});
77+
return true; // Async response
78+
}
79+
80+
// Handle WASM processing requests (NEW!)
81+
if (request.action === 'processSnapshot') {
82+
handleSnapshotProcessing(request.rawData, request.options)
83+
.then((result) => {
84+
sendResponse({ success: true, result });
85+
})
86+
.catch((error) => {
87+
console.error('[Sentience Background] Snapshot processing failed:', error);
88+
sendResponse({
89+
success: false,
90+
error: error.message || 'Snapshot processing failed',
91+
});
92+
});
93+
return true; // Async response
94+
}
95+
96+
// Unknown action
97+
console.warn('[Sentience Background] Unknown action:', request.action);
98+
sendResponse({ success: false, error: 'Unknown action' });
99+
return false;
100+
} catch (error) {
101+
// Catch any synchronous errors that might crash the extension
102+
console.error('[Sentience Background] Fatal error in message handler:', error);
103+
try {
104+
sendResponse({
105+
success: false,
106+
error: `Fatal error: ${error.message || 'Unknown error'}`,
107+
});
108+
} catch (e) {
109+
// If sendResponse already called, ignore
110+
}
111+
return false;
112+
}
113+
});
114+
115+
/**
116+
* Handle screenshot capture (existing functionality)
117+
*/
118+
async function handleScreenshotCapture(_tabId, options = {}) {
119+
try {
120+
const { format = 'png', quality = 90 } = options;
121+
122+
const dataUrl = await chrome.tabs.captureVisibleTab(null, {
123+
format,
124+
quality,
125+
});
126+
127+
console.log(
128+
`[Sentience Background] Screenshot captured: ${format}, size: ${dataUrl.length} bytes`
129+
);
130+
return dataUrl;
131+
} catch (error) {
132+
console.error('[Sentience Background] Screenshot error:', error);
133+
throw new Error(`Failed to capture screenshot: ${error.message}`);
134+
}
135+
}
136+
137+
/**
138+
* Handle snapshot processing with WASM (NEW!)
139+
* This is where the magic happens - completely CSP-immune!
140+
* Includes safeguards to prevent crashes and hangs.
141+
*
142+
* @param {Array} rawData - Raw element data from injected_api.js
143+
* @param {Object} options - Snapshot options (limit, filter, etc.)
144+
* @returns {Promise<Object>} Processed snapshot result
145+
*/
146+
async function handleSnapshotProcessing(rawData, options = {}) {
147+
const MAX_ELEMENTS = 10000; // Safety limit to prevent hangs
148+
const startTime = performance.now();
149+
150+
try {
151+
// Safety check: limit element count to prevent hangs
152+
if (!Array.isArray(rawData)) {
153+
throw new Error('rawData must be an array');
154+
}
155+
156+
if (rawData.length > MAX_ELEMENTS) {
157+
console.warn(
158+
`[Sentience Background] ⚠️ Large dataset: ${rawData.length} elements. Limiting to ${MAX_ELEMENTS} to prevent hangs.`
159+
);
160+
rawData = rawData.slice(0, MAX_ELEMENTS);
161+
}
162+
163+
// Ensure WASM is initialized
164+
await initWASM();
165+
if (!wasmReady) {
166+
throw new Error('WASM module not initialized');
167+
}
168+
169+
console.log(
170+
`[Sentience Background] Processing ${rawData.length} elements with options:`,
171+
options
172+
);
173+
174+
// Run WASM processing using the imported functions directly
175+
// Wrap in try-catch with timeout protection
176+
let analyzedElements;
177+
try {
178+
// Use a timeout wrapper to prevent infinite hangs
179+
const wasmPromise = new Promise((resolve, reject) => {
180+
try {
181+
let result;
182+
if (options.limit || options.filter) {
183+
result = analyze_page_with_options(rawData, options);
184+
} else {
185+
result = analyze_page(rawData);
186+
}
187+
resolve(result);
188+
} catch (e) {
189+
reject(e);
190+
}
191+
});
192+
193+
// Add timeout protection (18 seconds - less than content.js timeout)
194+
analyzedElements = await Promise.race([
195+
wasmPromise,
196+
new Promise((_, reject) =>
197+
setTimeout(() => reject(new Error('WASM processing timeout (>18s)')), 18000)
198+
),
199+
]);
200+
} catch (e) {
201+
const errorMsg = e.message || 'Unknown WASM error';
202+
console.error(`[Sentience Background] WASM analyze_page failed: ${errorMsg}`, e);
203+
throw new Error(`WASM analyze_page failed: ${errorMsg}`);
204+
}
205+
206+
// Prune elements for API (prevents 413 errors on large sites)
207+
let prunedRawData;
208+
try {
209+
prunedRawData = prune_for_api(rawData);
210+
} catch (e) {
211+
console.warn('[Sentience Background] prune_for_api failed, using original data:', e);
212+
prunedRawData = rawData;
213+
}
214+
215+
const duration = performance.now() - startTime;
216+
console.log(
217+
`[Sentience Background] ✓ Processed: ${analyzedElements.length} analyzed, ${prunedRawData.length} pruned (${duration.toFixed(1)}ms)`
218+
);
219+
220+
return {
221+
elements: analyzedElements,
222+
raw_elements: prunedRawData,
223+
};
224+
} catch (error) {
225+
const duration = performance.now() - startTime;
226+
console.error(`[Sentience Background] Processing error after ${duration.toFixed(1)}ms:`, error);
227+
throw error;
228+
}
229+
}
230+
231+
console.log('[Sentience Background] Service worker ready');
232+
233+
// Global error handlers to prevent extension crashes
234+
self.addEventListener('error', (event) => {
235+
console.error('[Sentience Background] Global error caught:', event.error);
236+
event.preventDefault(); // Prevent extension crash
237+
});
238+
239+
self.addEventListener('unhandledrejection', (event) => {
240+
console.error('[Sentience Background] Unhandled promise rejection:', event.reason);
241+
event.preventDefault(); // Prevent extension crash
242+
});

0 commit comments

Comments
 (0)