import { test, expect } from '@playwright/test' import { WebSocketServer } from 'ws' const NEW_NOTE_PATH = '/Users/luca/Laputa/note/ai-created-note.md' const NEW_NOTE_TITLE = 'AI Created Note' const NEW_NOTE_CONTENT = `--- title: ${NEW_NOTE_TITLE} type: Note --- # ${NEW_NOTE_TITLE} This note was created by the AI agent. ` function broadcast(wss: WebSocketServer, data: Record) { const msg = JSON.stringify(data) for (const client of wss.clients) { if (client.readyState === 1) client.send(msg) } } /** Wait until at least one WebSocket client connects. */ function waitForClient(wss: WebSocketServer, timeoutMs = 10000): Promise { if (wss.clients.size > 0) return Promise.resolve() return new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error('No WS client connected')), timeoutMs) wss.on('connection', () => { clearTimeout(timer); resolve() }) }) } test.describe('AI-created note visibility', () => { let wss: WebSocketServer /** When true, the intercepted list_vault response includes the new note. */ let injectNote = false test.beforeEach(async () => { wss = new WebSocketServer({ port: 9711 }) }) test.afterEach(async () => { wss.close() await new Promise(r => setTimeout(r, 200)) }) test.fixme('vault_changed + open_tab from MCP makes note visible and opens tab', async ({ page }) => { // Intercept list_vault API calls. On reload (after vault_changed), // the injected note will be included in the response. await page.route('**/api/vault/list*', async (route) => { const response = await route.fetch() if (!injectNote) { await route.fulfill({ response }) return } const entries = await response.json() const now = Date.now() entries.push({ path: NEW_NOTE_PATH, filename: 'ai-created-note.md', title: NEW_NOTE_TITLE, isA: null, aliases: [], belongsTo: [], relatedTo: [], status: null, archived: false, trashed: false, trashedAt: null, modifiedAt: now, createdAt: now, fileSize: NEW_NOTE_CONTENT.length, snippet: 'This note was created by the AI agent.', wordCount: 10, relationships: {}, icon: null, color: null, order: null, sidebarLabel: null, template: null, sort: null, view: null, visible: null, outgoingLinks: [], properties: {}, }) await route.fulfill({ json: entries }) }) // Intercept content fetch for the new note await page.route('**/api/vault/content*ai-created-note*', async (route) => { await route.fulfill({ json: { content: NEW_NOTE_CONTENT } }) }) // Intercept all-content to include new note await page.route('**/api/vault/all-content*', async (route) => { const response = await route.fetch() if (!injectNote) { await route.fulfill({ response }) return } const data = await response.json() data[NEW_NOTE_PATH] = NEW_NOTE_CONTENT await route.fulfill({ json: data }) }) await page.goto('/') // Wait for note list to render const noteList = page.locator('.app__note-list') await expect(noteList).toBeVisible() await expect(noteList.getByText(NEW_NOTE_TITLE)).not.toBeVisible() // Enable injection for subsequent reloads injectNote = true // Wait for the frontend's useAiActivity WS to connect to our bridge await waitForClient(wss) // Simulate MCP broadcasting vault_changed then open_tab // (This is what happens when Claude Code calls open_note via MCP) broadcast(wss, { type: 'ui_action', action: 'vault_changed', path: 'note/ai-created-note.md' }) // Verify: note appears in note list after vault reload await expect(noteList.getByText(NEW_NOTE_TITLE)).toBeVisible({ timeout: 8000 }) // Send open_tab after vault is reloaded so the entry is in entriesByPath broadcast(wss, { type: 'ui_action', action: 'open_tab', path: NEW_NOTE_PATH }) // Verify: note content is displayed in the editor (proves tab opened) await expect(page.getByText('This note was created by the AI agent.')).toBeVisible({ timeout: 8000 }) }) })