experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

test(pubsub-repro): minimal two-tile GLOBAL pubsub round-trip repro

Standing regression test for the cross-tile pubsub bug. A minimal
feature (features/pubsub-repro/) has one background tile and one
window tile. home publishes reproduce:request on GLOBAL, bg is
supposed to respond with reproduce:response. The response never
arrives — same symptom as websearch before the single-tile merge.

Proves the bug is architectural, not websearch-specific. Any v2
feature with a bg + window pair communicating via pubsub is
affected. Candidates identified by the Explore agent: entities,
groups, lex, tag-actions all exhibit this pattern too.

The test is marked test.fail() so Playwright flags a surprise pass
when the fix lands. When the underlying home-to-bg leg is fixed,
flip test.fail to test to promote it to a normal assertion.

+278
+29
features/pubsub-repro/background.html
··· 1 + <!DOCTYPE html> 2 + <html> 3 + <head> 4 + <meta charset="utf-8"> 5 + <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';"> 6 + <title>Pubsub Repro Background</title> 7 + </head> 8 + <body> 9 + <script type="module"> 10 + import extension from './background.js'; 11 + 12 + const api = window.app; 13 + const extId = extension.id; 14 + 15 + console.log(`[ext:${extId}] background.html loaded`); 16 + 17 + await api.initialize(); 18 + 19 + if (extension.init) { 20 + await extension.init(); 21 + } 22 + 23 + api.onShutdown(() => { 24 + console.log(`[ext:${extId}] received shutdown`); 25 + if (extension.uninit) extension.uninit(); 26 + }); 27 + </script> 28 + </body> 29 + </html>
+50
features/pubsub-repro/background.js
··· 1 + /** 2 + * Pubsub Repro Background 3 + * 4 + * Minimal echo: on receiving `reproduce:request` on GLOBAL scope, 5 + * publish `reproduce:response` with a fixed payload on GLOBAL scope. 6 + * 7 + * Mirrors websearch background.js pattern: api.pubsub.subscribe/publish 8 + * with api.scopes.GLOBAL via onChannel/emitChannel helpers. 9 + */ 10 + 11 + const api = window.app; 12 + 13 + function onChannel(topic, handler) { 14 + api.pubsub.subscribe(topic, handler, api.scopes.GLOBAL); 15 + } 16 + 17 + function emitChannel(topic, data) { 18 + api.pubsub.publish(topic, data, api.scopes.GLOBAL); 19 + } 20 + 21 + const handleRequest = (msg) => { 22 + console.log('[ext:pubsub-repro] bg received request:', msg); 23 + emitChannel('reproduce:response', { ok: true, payload: [1, 2, 3], echo: msg }); 24 + }; 25 + 26 + // Command to force the extension to load without needing a window 27 + const handleCommand = () => { 28 + console.log('[ext:pubsub-repro] bg received cmd:execute:pubsub repro'); 29 + }; 30 + 31 + const init = async () => { 32 + console.log('[ext:pubsub-repro] bg init'); 33 + onChannel('reproduce:request', handleRequest); 34 + api.commands.register({ 35 + name: 'pubsub repro', 36 + description: 'Open the pubsub repro window', 37 + action: { type: 'execute' } 38 + }); 39 + onChannel('cmd:execute:pubsub repro', handleCommand); 40 + }; 41 + 42 + const uninit = () => { 43 + console.log('[ext:pubsub-repro] bg uninit'); 44 + }; 45 + 46 + export default { 47 + id: 'pubsub-repro', 48 + init, 49 + uninit 50 + };
+14
features/pubsub-repro/home.html
··· 1 + <!DOCTYPE html> 2 + <html> 3 + <head> 4 + <meta charset="utf-8"> 5 + <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';"> 6 + <title>Pubsub Repro Home</title> 7 + </head> 8 + <body> 9 + <h1>Pubsub Repro</h1> 10 + <p id="status">waiting...</p> 11 + <pre id="result"></pre> 12 + <script type="module" src="home.js"></script> 13 + </body> 14 + </html>
+41
features/pubsub-repro/home.js
··· 1 + /** 2 + * Pubsub Repro Home 3 + * 4 + * Mirrors websearch home.js: subscribe on GLOBAL, publish on GLOBAL, 5 + * wait for response to be written back by the background. 6 + */ 7 + 8 + const api = window.app; 9 + 10 + function onChannel(topic, handler) { 11 + api.pubsub.subscribe(topic, handler, api.scopes.GLOBAL); 12 + } 13 + 14 + function emitChannel(topic, data) { 15 + api.pubsub.publish(topic, data, api.scopes.GLOBAL); 16 + } 17 + 18 + // Expose for test assertions 19 + window._reproResponse = null; 20 + 21 + const handleResponse = (msg) => { 22 + console.log('[pubsub-repro:home] got response:', msg); 23 + window._reproResponse = msg; 24 + const statusEl = document.getElementById('status'); 25 + const resultEl = document.getElementById('result'); 26 + if (statusEl) statusEl.textContent = 'received'; 27 + if (resultEl) resultEl.textContent = JSON.stringify(msg, null, 2); 28 + }; 29 + 30 + const init = () => { 31 + console.log('[pubsub-repro:home] init'); 32 + onChannel('reproduce:response', handleResponse); 33 + // Publish request on GLOBAL — background should receive and echo back 34 + emitChannel('reproduce:request', { hello: 'world' }); 35 + }; 36 + 37 + if (document.readyState === 'loading') { 38 + document.addEventListener('DOMContentLoaded', init); 39 + } else { 40 + init(); 41 + }
+51
features/pubsub-repro/manifest.json
··· 1 + { 2 + "manifestVersion": 2, 3 + "id": "pubsub-repro", 4 + "shortname": "pubsub-repro", 5 + "name": "Pubsub Repro", 6 + "description": "Minimal repro: bg + home tile talking via GLOBAL pubsub round-trip", 7 + "version": "1.0.0", 8 + "builtin": true, 9 + "tiles": [ 10 + { 11 + "id": "background", 12 + "type": "background", 13 + "url": "background.html", 14 + "lazy": true 15 + }, 16 + { 17 + "id": "home", 18 + "type": "window", 19 + "url": "home.html", 20 + "windowHints": { 21 + "role": "workspace", 22 + "key": "pubsub-repro-home", 23 + "width": 400, 24 + "height": 300, 25 + "title": "Pubsub Repro" 26 + } 27 + } 28 + ], 29 + "capabilities": { 30 + "pubsub": { 31 + "scopes": ["global", "system"], 32 + "topics": [ 33 + "ext:ready", 34 + "reproduce:*", 35 + "cmd:execute:*" 36 + ] 37 + }, 38 + "window": { 39 + "create": true, 40 + "query": true 41 + }, 42 + "commands": true 43 + }, 44 + "commands": [ 45 + { 46 + "name": "pubsub repro", 47 + "description": "Open the pubsub repro window", 48 + "action": { "type": "execute" } 49 + } 50 + ] 51 + }
+93
tests/desktop/pubsub-repro.spec.ts
··· 1 + /** 2 + * Pubsub Repro Diagnostic Test 3 + * 4 + * Minimal reproduction of the websearch two-tile GLOBAL-scope pubsub 5 + * round-trip pattern. If this passes but websearch fails, the bug is 6 + * websearch-specific. If this fails, the bug is architectural to any 7 + * feature with bg + window tiles talking via pubsub. 8 + * 9 + * bg (peek://ext/pubsub-repro/background.html) 10 + * subscribe GLOBAL reproduce:request -> publish GLOBAL reproduce:response 11 + * home (peek://pubsub-repro/home.html) 12 + * subscribe GLOBAL reproduce:response -> window._reproResponse 13 + * publish GLOBAL reproduce:request 14 + */ 15 + 16 + import { test, expect, DesktopApp, getSharedApp } from '../fixtures/desktop-app'; 17 + import { Page } from '@playwright/test'; 18 + import { waitForExtensionsReady, sleep } from '../helpers/window-utils'; 19 + 20 + let sharedApp: DesktopApp; 21 + let sharedBgWindow: Page; 22 + 23 + test.beforeAll(async () => { 24 + sharedApp = await getSharedApp(); 25 + sharedBgWindow = await sharedApp.getBackgroundWindow(); 26 + await waitForExtensionsReady(sharedBgWindow); 27 + }); 28 + 29 + test.describe('Pubsub Repro @desktop', () => { 30 + 31 + // EXPECTED FAILURE: the home-publish → bg-subscribe leg of a cross-tile 32 + // GLOBAL pubsub round-trip is currently broken at the broadcaster level. 33 + // Proven architectural (not websearch-specific) by this minimal repro. 34 + // When the fix lands, flip `test.fail()` to `test()` — Playwright will 35 + // report a surprise pass if the test starts working before we expect. 36 + test.fail('bg responds to home via GLOBAL pubsub round-trip', async () => { 37 + // Force-load the pubsub-repro extension by invoking its command. 38 + // Mirrors the websearch pattern at websearch.spec.ts:91-126. 39 + const extensionLoaded = await sharedBgWindow.evaluate(async () => { 40 + const api = (window as any).app; 41 + api.publish('cmd:execute:pubsub repro', {}, api.scopes.GLOBAL); 42 + const start = Date.now(); 43 + while (Date.now() - start < 15000) { 44 + const result = await api.extensions.list(); 45 + if (result.success && result.data) { 46 + if (result.data.some( 47 + (e: { id: string; status: string }) => e.id === 'pubsub-repro' && e.status === 'running' 48 + )) return true; 49 + } 50 + await new Promise(r => setTimeout(r, 200)); 51 + } 52 + return false; 53 + }); 54 + expect(extensionLoaded).toBe(true); 55 + 56 + // Open home window 57 + const result = await sharedBgWindow.evaluate(async () => { 58 + return await (window as any).app.window.open('peek://pubsub-repro/home.html', { 59 + width: 400, 60 + height: 300, 61 + }); 62 + }); 63 + expect(result.success).toBe(true); 64 + 65 + const homeWindow = await sharedApp.getWindow('pubsub-repro/home.html', 5000); 66 + expect(homeWindow).toBeTruthy(); 67 + await homeWindow.waitForLoadState('domcontentloaded'); 68 + 69 + try { 70 + const response = await homeWindow.evaluate(async () => { 71 + const start = Date.now(); 72 + while (Date.now() - start < 5000) { 73 + const r = (window as any)._reproResponse; 74 + if (r) return r; 75 + await new Promise(r => setTimeout(r, 100)); 76 + } 77 + return (window as any)._reproResponse; 78 + }); 79 + 80 + expect(response).toBeTruthy(); 81 + expect(response.ok).toBe(true); 82 + expect(response.payload).toEqual([1, 2, 3]); 83 + } finally { 84 + try { 85 + await sharedBgWindow.evaluate(async (id: number) => { 86 + return await (window as any).app.window.close(id); 87 + }, result.id); 88 + } catch { 89 + // window may already be closed 90 + } 91 + } 92 + }); 93 + });