basant307/AI_Governance_Project
048
1/**2 * @license3 * Copyright 2025 Qwen4 * SPDX-License-Identifier: Apache-2.05 */6 7import * as fs from 'node:fs';8import * as os from 'node:os';9import * as path from 'node:path';10 11import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';12 13// Mock cleanup.ts to avoid pulling in @qwen-code/qwen-code-core dependency chain14vi.mock('./cleanup.js', () => ({15 registerCleanup: vi.fn(),16}));17 18import {19 _resetCpuProfilerForTest,20 _setSessionFactoryForTest,21 clearCpuProfileRateLimit,22 isCpuProfileRecording,23 startCpuProfile,24 stopCpuProfile,25} from './cpuProfiler.js';26 27function createMockSession() {28 const mockProfile = {29 nodes: [30 {31 id: 1,32 callFrame: {33 functionName: 'test',34 scriptId: '1',35 url: '',36 lineNumber: 0,37 columnNumber: 0,38 },39 hitCount: 10,40 children: [],41 },42 ],43 startTime: 0,44 endTime: 1000000,45 samples: [1],46 timeDeltas: [100],47 };48 49 const post = vi.fn().mockImplementation((method: string) => {50 if (method === 'Profiler.stop') {51 return Promise.resolve({ profile: mockProfile });52 }53 return Promise.resolve(undefined);54 });55 const connect = vi.fn();56 const disconnect = vi.fn();57 58 return { post, connect, disconnect };59}60 61describe('cpuProfiler', () => {62 let tmpDir: string;63 let mockSession: ReturnType<typeof createMockSession>;64 65 beforeEach(() => {66 _resetCpuProfilerForTest();67 clearCpuProfileRateLimit();68 69 mockSession = createMockSession();70 _setSessionFactoryForTest(async () => mockSession);71 72 tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cpu-profiler-test-'));73 });74 75 afterEach(() => {76 _resetCpuProfilerForTest();77 _setSessionFactoryForTest(null);78 vi.restoreAllMocks();79 try {80 fs.rmSync(tmpDir, { recursive: true, force: true });81 } catch {82 // ignore83 }84 });85 86 describe('isCpuProfileRecording', () => {87 it('returns false when not recording', () => {88 expect(isCpuProfileRecording()).toBe(false);89 });90 91 it('returns true when recording', async () => {92 await startCpuProfile();93 expect(isCpuProfileRecording()).toBe(true);94 });95 });96 97 describe('startCpuProfile', () => {98 it('starts profiling successfully', async () => {99 const result = await startCpuProfile();100 expect(result).toEqual({ ok: true });101 expect(mockSession.post).toHaveBeenCalledWith('Profiler.enable');102 expect(mockSession.post).toHaveBeenCalledWith(103 'Profiler.setSamplingInterval',104 { interval: 1000 },105 );106 expect(mockSession.post).toHaveBeenCalledWith('Profiler.start');107 });108 109 it('accepts custom sampling interval', async () => {110 await startCpuProfile({ samplingInterval: 500 });111 expect(mockSession.post).toHaveBeenCalledWith(112 'Profiler.setSamplingInterval',113 { interval: 500 },114 );115 });116 117 it('returns error when already recording', async () => {118 await startCpuProfile();119 const result = await startCpuProfile();120 expect(result).toEqual({121 ok: false,122 error: 'CPU profiling is already in progress.',123 });124 });125 126 it('returns error and resets state on session failure', async () => {127 _setSessionFactoryForTest(async () => {128 throw new Error('Connection refused');129 });130 131 const result = await startCpuProfile();132 expect(result.ok).toBe(false);133 if (!result.ok) {134 expect(result.error).toContain('Connection refused');135 }136 expect(isCpuProfileRecording()).toBe(false);137 });138 });139 140 describe('stopCpuProfile', () => {141 it('stops and writes profile file', async () => {142 await startCpuProfile();143 const result = await stopCpuProfile({ outputDir: tmpDir });144 145 expect(result.ok).toBe(true);146 if (result.ok) {147 expect(result.filePath).toMatch(/qwen-code-cpu-\d+-.*\.cpuprofile$/);148 expect(fs.existsSync(result.filePath)).toBe(true);149 150 const content = JSON.parse(fs.readFileSync(result.filePath, 'utf8'));151 expect(content.nodes).toBeDefined();152 expect(content.startTime).toBeDefined();153 }154 });155 156 it('returns error when not recording', async () => {157 const result = await stopCpuProfile({ outputDir: tmpDir });158 expect(result).toEqual({159 ok: false,160 error: 'CPU profiler is not recording.',161 });162 });163 164 it('calls Profiler.stop and Profiler.disable', async () => {165 await startCpuProfile();166 mockSession.post.mockClear();167 await stopCpuProfile({ outputDir: tmpDir });168 169 expect(mockSession.post).toHaveBeenCalledWith('Profiler.stop');170 expect(mockSession.post).toHaveBeenCalledWith('Profiler.disable');171 });172 173 it('sets file permissions to 0o600', async () => {174 await startCpuProfile();175 const result = await stopCpuProfile({ outputDir: tmpDir });176 177 if (result.ok && process.platform !== 'win32') {178 const stats = fs.statSync(result.filePath);179 expect(stats.mode & 0o777).toBe(0o600);180 }181 });182 });183 184 describe('rate limiting', () => {185 it('enforces rate limit between writes', async () => {186 const now = new Date('2026-05-29T10:00:00.000Z');187 188 await startCpuProfile();189 const first = await stopCpuProfile({ outputDir: tmpDir, now });190 expect(first.ok).toBe(true);191 192 // Second write within rate limit window193 await startCpuProfile();194 const second = await stopCpuProfile({195 outputDir: tmpDir,196 now: new Date(now.getTime() + 5000), // 5s later, within 30s limit197 });198 expect(second.ok).toBe(false);199 if (!second.ok) {200 expect(second.error).toContain('rate limit');201 }202 });203 204 it('allows write after rate limit expires', async () => {205 const now = new Date('2026-05-29T10:00:00.000Z');206 207 await startCpuProfile();208 await stopCpuProfile({ outputDir: tmpDir, now });209 210 // After rate limit window211 await startCpuProfile();212 const result = await stopCpuProfile({213 outputDir: tmpDir,214 now: new Date(now.getTime() + 31000), // 31s later215 });216 expect(result.ok).toBe(true);217 });218 219 it('resets state to idle when rate-limited so user can retry', async () => {220 const now = new Date('2026-05-29T10:00:00.000Z');221 222 await startCpuProfile();223 await stopCpuProfile({ outputDir: tmpDir, now });224 225 // Start a new recording, then try to stop within rate limit window226 await startCpuProfile();227 const rateLimited = await stopCpuProfile({228 outputDir: tmpDir,229 now: new Date(now.getTime() + 5000),230 });231 expect(rateLimited.ok).toBe(false);232 233 // State should be reset — a new startCpuProfile() must succeed234 const restart = await startCpuProfile();235 expect(restart.ok).toBe(true);236 });237 });238 239 describe('old profile cleanup', () => {240 it('removes old profiles beyond max count', async () => {241 // Create 5 existing profiles242 for (let i = 0; i < 5; i++) {243 const name = `qwen-code-cpu-99999-2026-05-29T0${i}-00-00-000Z.cpuprofile`;244 fs.writeFileSync(path.join(tmpDir, name), '{}');245 // Stagger mtime so sort is deterministic246 const mtime = new Date(Date.now() - (5 - i) * 1000);247 fs.utimesSync(path.join(tmpDir, name), mtime, mtime);248 }249 250 clearCpuProfileRateLimit();251 await startCpuProfile();252 const result = await stopCpuProfile({253 outputDir: tmpDir,254 maxProfiles: 5,255 });256 expect(result.ok).toBe(true);257 258 const files = fs259 .readdirSync(tmpDir)260 .filter((f) => f.endsWith('.cpuprofile'));261 // Should have at most 5 files (new one replaces oldest)262 expect(files.length).toBeLessThanOrEqual(5);263 });264 });265 266 describe('conflict handling', () => {267 it('rejects second start while recording', async () => {268 const first = await startCpuProfile();269 expect(first.ok).toBe(true);270 271 const second = await startCpuProfile();272 expect(second.ok).toBe(false);273 if (!second.ok) {274 expect(second.error).toContain('already in progress');275 }276 });277 278 it('resets state after stop so new recording can start', async () => {279 await startCpuProfile();280 await stopCpuProfile({ outputDir: tmpDir });281 282 clearCpuProfileRateLimit();283 const result = await startCpuProfile();284 expect(result.ok).toBe(true);285 });286 });287 288 describe('initCpuProfiler', () => {289 it('is idempotent — calling twice does not error', async () => {290 const { initCpuProfiler } = await import('./cpuProfiler.js');291 // First call292 initCpuProfiler();293 // Second call should be a no-op294 initCpuProfiler();295 // No error thrown means success296 });297 298 it('does not start recording when env var is unset', async () => {299 _resetCpuProfilerForTest();300 delete process.env['QWEN_CODE_CPU_PROFILE'];301 const { initCpuProfiler } = await import('./cpuProfiler.js');302 _resetCpuProfilerForTest();303 initCpuProfiler();304 expect(isCpuProfileRecording()).toBe(false);305 });306 });307 308 describe('SIGUSR1 toggle (via start/stop cycle)', () => {309 it('simulates signal toggle: start then stop', async () => {310 // Simulate what handleSigusr1 does internally311 expect(isCpuProfileRecording()).toBe(false);312 313 // First signal: start314 const startResult = await startCpuProfile();315 expect(startResult.ok).toBe(true);316 expect(isCpuProfileRecording()).toBe(true);317 318 // Second signal: stop319 const stopResult = await stopCpuProfile({ outputDir: tmpDir });320 expect(stopResult.ok).toBe(true);321 expect(isCpuProfileRecording()).toBe(false);322 });323 324 it('ignores stop when in idle state', async () => {325 const result = await stopCpuProfile({ outputDir: tmpDir });326 expect(result.ok).toBe(false);327 if (!result.ok) {328 expect(result.error).toContain('not recording');329 }330 });331 });332 333 describe('empty profile guard', () => {334 it('returns error when V8 returns empty profile', async () => {335 _resetCpuProfilerForTest();336 const emptyMock = {337 post: vi.fn().mockImplementation((method: string) => {338 if (method === 'Profiler.stop') {339 return Promise.resolve({ profile: undefined });340 }341 return Promise.resolve(undefined);342 }),343 connect: vi.fn(),344 disconnect: vi.fn(),345 };346 _setSessionFactoryForTest(async () => emptyMock);347 348 await startCpuProfile();349 const result = await stopCpuProfile({ outputDir: tmpDir });350 351 expect(result.ok).toBe(false);352 if (!result.ok) {353 expect(result.error).toContain('empty profile');354 }355 });356 });357});358 