Skip to content

Commit 0ec6620

Browse files
committed
test(desktop): cover consent revalidation and independent cleanup
1 parent 9390e0a commit 0ec6620

1 file changed

Lines changed: 117 additions & 1 deletion

File tree

‎apps/desktop/e2e/background-executor.spec.ts‎

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
1+
import {
2+
existsSync,
3+
mkdirSync,
4+
mkdtempSync,
5+
readFileSync,
6+
renameSync,
7+
writeFileSync,
8+
} from 'node:fs'
29
import { tmpdir } from 'node:os'
310
import { join } from 'node:path'
411
import { type ElectronApplication, expect, test } from '@playwright/test'
@@ -203,6 +210,115 @@ test.describe('background executor', () => {
203210
})
204211
})
205212

213+
test('background consent cannot grant access when Sim cannot verify the call', async () => {
214+
const userData = mkdtempSync(join(tmpdir(), 'sim-executor-offline-consent-'))
215+
const launched = await launch(sim, userData)
216+
app = launched.app
217+
const deviceId = await registeredDevice(sim)
218+
const readable = join(userData, 'private.txt')
219+
writeFileSync(readable, 'offline consent fixture')
220+
221+
await check('offline approval returns an error without remembering a grant', async () => {
222+
const shown = launched.app.waitForEvent('window')
223+
const call = sim.issue(deviceId, CHAT_B, 'read_local_file', { path: readable })
224+
const prompt = await shown
225+
await expect(prompt.getByRole('button', { name: 'Allow folder', exact: true })).toBeVisible()
226+
sim.disconnect()
227+
await prompt.getByRole('button', { name: 'Allow folder', exact: true }).click()
228+
await expect
229+
.poll(() =>
230+
launched.app.evaluate(({ safeStorage }, toolCallId) => {
231+
const fs = process.getBuiltinModule('node:fs')
232+
const path = `${process.env.SIM_DESKTOP_USER_DATA}/desktop-executor-journal.json`
233+
const envelope = JSON.parse(fs.readFileSync(path, 'utf8'))
234+
const journal = JSON.parse(
235+
safeStorage.decryptString(Buffer.from(envelope.ciphertext, 'base64'))
236+
) as { entries: { toolCallId: string; state: string }[] }
237+
return journal.entries.find((entry) => entry.toolCallId === toolCallId)?.state
238+
}, call)
239+
)
240+
.toBe('result')
241+
sim.reconnect()
242+
const completion = await settled(sim, call)
243+
expect(completion.status).toBe('error')
244+
expect(JSON.stringify(completion)).not.toContain('offline consent fixture')
245+
const nextPrompt = launched.app.waitForEvent('window')
246+
const next = sim.issue(deviceId, CHAT_B, 'read_local_file', { path: readable })
247+
await (await nextPrompt).getByRole('button', { name: "Don't allow", exact: true }).click()
248+
expect((await settled(sim, next)).status).toBe('error')
249+
})
250+
})
251+
252+
test('background consent does not reopen the main app after its windows are closed', async () => {
253+
test.skip(process.platform !== 'darwin', 'The macOS app remains running without a window.')
254+
const userData = mkdtempSync(join(tmpdir(), 'sim-executor-windowless-consent-'))
255+
const launched = await launch(sim, userData)
256+
app = launched.app
257+
const deviceId = await registeredDevice(sim)
258+
const readable = join(userData, 'private.txt')
259+
writeFileSync(readable, 'windowless consent fixture')
260+
await launched.window.close()
261+
262+
await check('only the standalone consent window opens for a background read', async () => {
263+
const shown = launched.app.waitForEvent('window')
264+
const call = sim.issue(deviceId, CHAT_B, 'read_local_file', { path: readable })
265+
const prompt = await shown
266+
await expect(prompt.getByRole('button', { name: 'Allow folder', exact: true })).toBeVisible()
267+
expect(
268+
await launched.app.evaluate(({ BrowserWindow }) =>
269+
BrowserWindow.getAllWindows().map((window) => window.getParentWindow() === null)
270+
)
271+
).toEqual([true])
272+
await prompt.getByRole('button', { name: "Don't allow", exact: true }).click()
273+
expect((await settled(sim, call)).status).toBe('error')
274+
})
275+
})
276+
277+
test('sign-out clears folder grants even when desktop settings cannot be saved', async () => {
278+
const userData = mkdtempSync(join(tmpdir(), 'sim-executor-grant-cleanup-'))
279+
const launched = await launch(sim, userData)
280+
app = launched.app
281+
const deviceId = await registeredDevice(sim)
282+
const readable = join(userData, 'private.txt')
283+
writeFileSync(readable, 'cleanup consent fixture')
284+
const shown = launched.app.waitForEvent('window')
285+
const call = sim.issue(deviceId, CHAT_B, 'read_local_file', { path: readable })
286+
await (await shown).getByRole('button', { name: 'Allow folder', exact: true }).click()
287+
expect((await settled(sim, call)).status).toBe('success')
288+
const grants = join(userData, 'local-filesystem-grants.json')
289+
expect(existsSync(grants)).toBe(true)
290+
await launched.window.evaluate(() => {
291+
const button = document.createElement('button')
292+
button.textContent = 'Enable full file access'
293+
button.onclick = async () => {
294+
const api = (globalThis as typeof globalThis & { simDesktop: SimDesktopApi }).simDesktop
295+
await api.settings.setFullFileAccess?.(true)
296+
button.textContent = 'Full file access enabled'
297+
}
298+
document.body.append(button)
299+
})
300+
await launched.window
301+
.getByRole('button', { name: 'Enable full file access', exact: true })
302+
.click()
303+
await expect(
304+
launched.window.getByRole('button', { name: 'Full file access enabled', exact: true })
305+
).toBeVisible()
306+
const settings = join(userData, 'settings.json')
307+
if (existsSync(settings)) renameSync(settings, `${settings}.backup`)
308+
mkdirSync(settings)
309+
310+
await check('failed settings persistence does not skip independent grant cleanup', async () => {
311+
await launched.app.evaluate(({ Menu }) => {
312+
const item = Menu.getApplicationMenu()
313+
?.items.flatMap((entry) => entry.submenu?.items ?? [])
314+
.find((entry) => entry.label === 'Sign Out')
315+
if (!item) throw new Error('Missing Sign Out menu item')
316+
item.click()
317+
})
318+
await expect.poll(() => existsSync(grants)).toBe(false)
319+
})
320+
})
321+
206322
test('B: a result produced while the network is cut is delivered once after reconnecting', async () => {
207323
const userData = mkdtempSync(join(tmpdir(), 'sim-executor-b-'))
208324
app = (await launch(sim, userData)).app

0 commit comments

Comments
 (0)