basant307/AI_Governance_Project
048
1/**2 * @license3 * Copyright 2025 Qwen Team4 * SPDX-License-Identifier: Apache-2.05 */6 7import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';8import { mkdtempSync, rmSync } from 'node:fs';9import { tmpdir } from 'node:os';10import { join } from 'node:path';11import {12 runBootstrap,13 parsePermissionsStatus,14 type BootstrapDeps,15 type StatusDaemon,16} from './bootstrap.js';17 18const KEY = 'cua-driver-rs@0.5.2';19 20function makeFakeClient() {21 const start = vi.fn(async () => {});22 const stop = vi.fn(async () => {});23 return {24 isStarted: vi.fn(() => start.mock.calls.length > stop.mock.calls.length),25 start,26 stop,27 callTool: vi.fn(),28 };29}30 31describe('runBootstrap', () => {32 let tmpHome: string;33 let daemon: StatusDaemon & { kill: ReturnType<typeof vi.fn> };34 let deps: BootstrapDeps;35 36 beforeEach(() => {37 tmpHome = mkdtempSync(join(tmpdir(), 'qwen-cu-bs-'));38 daemon = { kill: vi.fn() };39 deps = {40 homeDir: tmpHome,41 approvalKey: KEY,42 platform: 'darwin',43 promptInstallApproval: vi.fn(async () => true),44 install: vi.fn(async () => '/fake/cua-driver'),45 startStatusDaemon: vi.fn(() => daemon),46 probePermissions: vi.fn(async () => 'ok' as const),47 openPermissionPane: vi.fn(),48 pollIntervalMs: 1,49 pollTimeoutMs: 1000,50 };51 });52 53 afterEach(() => {54 rmSync(tmpHome, { recursive: true, force: true });55 });56 57 it('starts the proxy directly when already granted (no panes opened)', async () => {58 const { saveInstallState } = await import('./install-state.js');59 await saveInstallState(tmpHome, {60 approvedPackageSpec: KEY,61 approvedAtIso: '2026-06-12T10:00:00Z',62 });63 64 const client = makeFakeClient();65 await runBootstrap(66 client as never,67 { signal: new AbortController().signal },68 deps,69 );70 71 expect(deps.install).toHaveBeenCalledOnce();72 expect(deps.openPermissionPane).not.toHaveBeenCalled();73 expect(daemon.kill).toHaveBeenCalled(); // status daemon torn down74 expect(client.start).toHaveBeenCalledOnce();75 });76 77 it('prompts for install approval on first call', async () => {78 const client = makeFakeClient();79 await runBootstrap(80 client as never,81 { signal: new AbortController().signal },82 deps,83 );84 expect(deps.promptInstallApproval).toHaveBeenCalledOnce();85 expect(client.start).toHaveBeenCalledOnce();86 });87 88 it('throws and does NOT download when user declines install', async () => {89 deps.promptInstallApproval = vi.fn(async () => false);90 const client = makeFakeClient();91 await expect(92 runBootstrap(93 client as never,94 { signal: new AbortController().signal },95 deps,96 ),97 ).rejects.toThrow(/declined/i);98 expect(deps.install).not.toHaveBeenCalled();99 expect(client.start).not.toHaveBeenCalled();100 });101 102 it('auto-approves the install (no prompt) and persists state when autoApproveInstall is set', async () => {103 // review #1 (⑤): an auto-approve mode or an always-allow-ruled call passes104 // autoApproveInstall. The gate must skip promptInstallApproval, persist the105 // install state (so later cold calls also skip the gate), and proceed —106 // this is what closes the DEFAULT-mode "install declined" dead-end.107 const { isPackageSpecApproved } = await import('./install-state.js');108 const client = makeFakeClient();109 await runBootstrap(110 client as never,111 { signal: new AbortController().signal, autoApproveInstall: true },112 deps,113 );114 expect(deps.promptInstallApproval).not.toHaveBeenCalled();115 expect(await isPackageSpecApproved(tmpHome, KEY)).toBe(true);116 expect(deps.install).toHaveBeenCalledOnce();117 expect(client.start).toHaveBeenCalledOnce();118 });119 120 it('guides one permission at a time: Accessibility pane, then Screen Recording pane', async () => {121 const { saveInstallState } = await import('./install-state.js');122 await saveInstallState(tmpHome, {123 approvedPackageSpec: KEY,124 approvedAtIso: '2026-06-12T10:00:00Z',125 });126 127 // accessibility missing → screen recording missing → ok128 let n = 0;129 deps.probePermissions = vi.fn(async () => {130 n++;131 if (n === 1) return 'accessibility' as const;132 if (n === 2) return 'screenRecording' as const;133 return 'ok' as const;134 });135 136 const client = makeFakeClient();137 await runBootstrap(138 client as never,139 { signal: new AbortController().signal },140 deps,141 );142 143 const panes = (deps.openPermissionPane as ReturnType<typeof vi.fn>).mock144 .calls;145 expect(panes).toEqual([['accessibility'], ['screenRecording']]); // in order, one each146 expect(client.start).toHaveBeenCalledOnce();147 });148 149 it('relaunches the status daemon when status reads unknown (e.g. SR restart)', async () => {150 const { saveInstallState } = await import('./install-state.js');151 await saveInstallState(tmpHome, {152 approvedPackageSpec: KEY,153 approvedAtIso: '2026-06-12T10:00:00Z',154 });155 156 let n = 0;157 deps.probePermissions = vi.fn(async () => {158 n++;159 if (n === 1) return 'unknown' as const; // daemon coming up / restarted160 if (n === 2) return 'accessibility' as const;161 return 'ok' as const;162 });163 164 const client = makeFakeClient();165 await runBootstrap(166 client as never,167 { signal: new AbortController().signal },168 deps,169 );170 171 // initial launch + one relaunch after 'unknown'.172 expect(deps.startStatusDaemon).toHaveBeenCalledTimes(2);173 expect(client.start).toHaveBeenCalledOnce();174 });175 176 it('times out (and tears down the daemon) if permissions never arrive', async () => {177 const { saveInstallState } = await import('./install-state.js');178 await saveInstallState(tmpHome, {179 approvedPackageSpec: KEY,180 approvedAtIso: '2026-06-12T10:00:00Z',181 });182 deps.probePermissions = vi.fn(async () => 'accessibility' as const);183 deps.pollTimeoutMs = 30;184 185 const client = makeFakeClient();186 await expect(187 runBootstrap(188 client as never,189 { signal: new AbortController().signal },190 deps,191 ),192 ).rejects.toThrow(/timed out/i);193 expect(daemon.kill).toHaveBeenCalled();194 expect(client.start).not.toHaveBeenCalled();195 });196 197 it('skips the permission flow on non-darwin platforms', async () => {198 const { saveInstallState } = await import('./install-state.js');199 await saveInstallState(tmpHome, {200 approvedPackageSpec: KEY,201 approvedAtIso: '2026-06-12T10:00:00Z',202 });203 deps.platform = 'linux';204 205 const client = makeFakeClient();206 await runBootstrap(207 client as never,208 { signal: new AbortController().signal },209 deps,210 );211 expect(deps.startStatusDaemon).not.toHaveBeenCalled();212 expect(deps.probePermissions).not.toHaveBeenCalled();213 expect(client.start).toHaveBeenCalledOnce();214 });215 216 it('does nothing extra when the client is already started (warm)', async () => {217 const { saveInstallState } = await import('./install-state.js');218 await saveInstallState(tmpHome, {219 approvedPackageSpec: KEY,220 approvedAtIso: '2026-06-12T10:00:00Z',221 });222 const client = {223 isStarted: vi.fn(() => true),224 start: vi.fn(async () => {}),225 stop: vi.fn(async () => {}),226 callTool: vi.fn(),227 };228 await runBootstrap(229 client as never,230 { signal: new AbortController().signal },231 deps,232 );233 expect(client.start).not.toHaveBeenCalled();234 expect(deps.startStatusDaemon).not.toHaveBeenCalled();235 // The warm-client short-circuit must precede the install step: a started236 // client implies the binary is present, so the downloader must NOT run237 // (otherwise unit tests trigger a real ~20MB download). (review round 1)238 expect(deps.install).not.toHaveBeenCalled();239 });240});241 242describe('parsePermissionsStatus', () => {243 it("returns 'ok' when both grants are true", () => {244 expect(245 parsePermissionsStatus('{"accessibility":true,"screen_recording":true}'),246 ).toBe('ok');247 });248 it("returns 'accessibility' when accessibility is false", () => {249 expect(250 parsePermissionsStatus('{"accessibility":false,"screen_recording":true}'),251 ).toBe('accessibility');252 });253 it("returns 'screenRecording' when only screen recording is false", () => {254 expect(255 parsePermissionsStatus('{"accessibility":true,"screen_recording":false}'),256 ).toBe('screenRecording');257 });258 it("returns 'unknown' for daemon-less / unparseable payloads", () => {259 expect(parsePermissionsStatus('{"status":"unknown"}')).toBe('unknown');260 expect(parsePermissionsStatus('not json')).toBe('unknown');261 });262});263 