@auxiora/os-bridge 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@auxiora/os-bridge",
3
+ "version": "1.0.0",
4
+ "description": "Deep OS integration: clipboard, file watching, app control, system state",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "dependencies": {
15
+ "nanoid": "^5.1.2",
16
+ "@auxiora/logger": "1.0.0",
17
+ "@auxiora/audit": "1.0.0"
18
+ },
19
+ "engines": {
20
+ "node": ">=22.0.0"
21
+ },
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "clean": "rm -rf dist",
25
+ "typecheck": "tsc --noEmit"
26
+ }
27
+ }
@@ -0,0 +1,80 @@
1
+ import type { Platform } from './types.js';
2
+
3
+ export class AppController {
4
+ private platform: Platform;
5
+
6
+ constructor(platform: Platform) {
7
+ this.platform = platform;
8
+ }
9
+
10
+ getCommand(action: 'launch' | 'focus' | 'close' | 'list', appName?: string): string {
11
+ switch (this.platform) {
12
+ case 'darwin':
13
+ return this.getDarwinCommand(action, appName);
14
+ case 'linux':
15
+ return this.getLinuxCommand(action, appName);
16
+ case 'win32':
17
+ return this.getWin32Command(action, appName);
18
+ }
19
+ }
20
+
21
+ private getDarwinCommand(action: string, appName?: string): string {
22
+ switch (action) {
23
+ case 'launch':
24
+ return `open -a "${appName}"`;
25
+ case 'focus':
26
+ return `osascript -e 'tell app "${appName}" to activate'`;
27
+ case 'close':
28
+ return `osascript -e 'tell app "${appName}" to quit'`;
29
+ case 'list':
30
+ return 'ps aux';
31
+ default:
32
+ return '';
33
+ }
34
+ }
35
+
36
+ private getLinuxCommand(action: string, appName?: string): string {
37
+ switch (action) {
38
+ case 'launch':
39
+ return `xdg-open "${appName}" || ${appName}`;
40
+ case 'focus':
41
+ return `wmctrl -a "${appName}"`;
42
+ case 'close':
43
+ return `pkill -f "${appName}"`;
44
+ case 'list':
45
+ return 'ps aux';
46
+ default:
47
+ return '';
48
+ }
49
+ }
50
+
51
+ private getWin32Command(action: string, appName?: string): string {
52
+ switch (action) {
53
+ case 'launch':
54
+ return `start "" "${appName}"`;
55
+ case 'focus':
56
+ return `powershell -c "(New-Object -ComObject WScript.Shell).AppActivate('${appName}')"`;
57
+ case 'close':
58
+ return `taskkill /IM "${appName}" /F`;
59
+ case 'list':
60
+ return 'tasklist';
61
+ default:
62
+ return '';
63
+ }
64
+ }
65
+
66
+ async launch(appName: string): Promise<{ success: boolean; command: string }> {
67
+ const command = this.getCommand('launch', appName);
68
+ return { success: true, command };
69
+ }
70
+
71
+ async focus(appName: string): Promise<{ success: boolean; command: string }> {
72
+ const command = this.getCommand('focus', appName);
73
+ return { success: true, command };
74
+ }
75
+
76
+ async close(appName: string): Promise<{ success: boolean; command: string }> {
77
+ const command = this.getCommand('close', appName);
78
+ return { success: true, command };
79
+ }
80
+ }
@@ -0,0 +1,73 @@
1
+ import type { ClipboardEntry } from './types.js';
2
+
3
+ const MAX_HISTORY = 100;
4
+
5
+ type ClipboardListener = (entry: ClipboardEntry) => void;
6
+
7
+ export class ClipboardMonitor {
8
+ private history: ClipboardEntry[] = [];
9
+ private listeners: ClipboardListener[] = [];
10
+ private intervalId: ReturnType<typeof setInterval> | null = null;
11
+
12
+ getContent(): ClipboardEntry {
13
+ if (this.history.length > 0) {
14
+ return this.history[this.history.length - 1]!;
15
+ }
16
+ return { content: '', type: 'text', timestamp: Date.now() };
17
+ }
18
+
19
+ addEntry(content: string, type: 'text' | 'image' | 'html' = 'text'): ClipboardEntry {
20
+ const entry: ClipboardEntry = { content, type, timestamp: Date.now() };
21
+ this.history.push(entry);
22
+ if (this.history.length > MAX_HISTORY) {
23
+ this.history.shift();
24
+ }
25
+ for (const listener of this.listeners) {
26
+ listener(entry);
27
+ }
28
+ return entry;
29
+ }
30
+
31
+ onchange(cb: ClipboardListener): () => void {
32
+ this.listeners.push(cb);
33
+ return () => {
34
+ const idx = this.listeners.indexOf(cb);
35
+ if (idx !== -1) {
36
+ this.listeners.splice(idx, 1);
37
+ }
38
+ };
39
+ }
40
+
41
+ startWatching(): void {
42
+ this.intervalId = setInterval(() => {
43
+ // No-op polling — actual clipboard reading would be platform-specific
44
+ }, 1000);
45
+ }
46
+
47
+ stopWatching(): void {
48
+ if (this.intervalId !== null) {
49
+ clearInterval(this.intervalId);
50
+ this.intervalId = null;
51
+ }
52
+ }
53
+
54
+ transform(content: string, op: 'uppercase' | 'lowercase' | 'trim' | 'json-format'): string {
55
+ switch (op) {
56
+ case 'uppercase':
57
+ return content.toUpperCase();
58
+ case 'lowercase':
59
+ return content.toLowerCase();
60
+ case 'trim':
61
+ return content.trim();
62
+ case 'json-format':
63
+ return JSON.stringify(JSON.parse(content), null, 2);
64
+ }
65
+ }
66
+
67
+ getHistory(limit?: number): ClipboardEntry[] {
68
+ if (limit !== undefined) {
69
+ return this.history.slice(-limit);
70
+ }
71
+ return [...this.history];
72
+ }
73
+ }
@@ -0,0 +1,102 @@
1
+ import path from 'node:path';
2
+ import type { FileEvent, FileClassification } from './types.js';
3
+
4
+ type FileEventListener = (event: FileEvent) => void;
5
+
6
+ const EXTENSION_MAP: Record<string, FileClassification> = {
7
+ '.pdf': 'document',
8
+ '.doc': 'document',
9
+ '.docx': 'document',
10
+ '.txt': 'document',
11
+ '.md': 'document',
12
+ '.rtf': 'document',
13
+ '.jpg': 'image',
14
+ '.jpeg': 'image',
15
+ '.png': 'image',
16
+ '.gif': 'image',
17
+ '.svg': 'image',
18
+ '.webp': 'image',
19
+ '.bmp': 'image',
20
+ '.mp4': 'video',
21
+ '.avi': 'video',
22
+ '.mov': 'video',
23
+ '.mkv': 'video',
24
+ '.webm': 'video',
25
+ '.mp3': 'audio',
26
+ '.wav': 'audio',
27
+ '.flac': 'audio',
28
+ '.aac': 'audio',
29
+ '.ogg': 'audio',
30
+ '.ts': 'code',
31
+ '.js': 'code',
32
+ '.py': 'code',
33
+ '.go': 'code',
34
+ '.rs': 'code',
35
+ '.java': 'code',
36
+ '.c': 'code',
37
+ '.cpp': 'code',
38
+ '.h': 'code',
39
+ '.rb': 'code',
40
+ '.php': 'code',
41
+ '.sh': 'code',
42
+ '.zip': 'archive',
43
+ '.tar': 'archive',
44
+ '.gz': 'archive',
45
+ '.7z': 'archive',
46
+ '.rar': 'archive',
47
+ '.bz2': 'archive',
48
+ '.xls': 'spreadsheet',
49
+ '.xlsx': 'spreadsheet',
50
+ '.csv': 'spreadsheet',
51
+ '.ppt': 'presentation',
52
+ '.pptx': 'presentation',
53
+ '.key': 'presentation',
54
+ };
55
+
56
+ export class FileWatcher {
57
+ private directories: string[];
58
+ private listeners: FileEventListener[] = [];
59
+ private watching = false;
60
+
61
+ constructor(config: { directories: string[] }) {
62
+ this.directories = config.directories;
63
+ }
64
+
65
+ watch(): void {
66
+ this.watching = true;
67
+ }
68
+
69
+ stop(): void {
70
+ this.watching = false;
71
+ }
72
+
73
+ onEvent(cb: FileEventListener): () => void {
74
+ this.listeners.push(cb);
75
+ return () => {
76
+ const idx = this.listeners.indexOf(cb);
77
+ if (idx !== -1) {
78
+ this.listeners.splice(idx, 1);
79
+ }
80
+ };
81
+ }
82
+
83
+ emitEvent(event: FileEvent): void {
84
+ for (const listener of this.listeners) {
85
+ listener(event);
86
+ }
87
+ }
88
+
89
+ classifyFile(filePath: string): FileClassification {
90
+ const ext = path.extname(filePath).toLowerCase();
91
+ return EXTENSION_MAP[ext] ?? 'other';
92
+ }
93
+
94
+ suggestDestination(filePath: string, baseDir?: string): { classification: FileClassification; suggestedDir: string } {
95
+ const classification = this.classifyFile(filePath);
96
+ const base = baseDir || '~/Documents';
97
+ return {
98
+ classification,
99
+ suggestedDir: `${base}/${classification}s/`,
100
+ };
101
+ }
102
+ }
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ export type {
2
+ Platform,
3
+ ClipboardEntry,
4
+ FileEvent,
5
+ FileClassification,
6
+ AppInfo,
7
+ SystemState,
8
+ OsBridgeConfig,
9
+ } from './types.js';
10
+ export { ClipboardMonitor } from './clipboard.js';
11
+ export { FileWatcher } from './file-watcher.js';
12
+ export { AppController } from './app-controller.js';
13
+ export { SystemStateMonitor } from './system-state.js';
@@ -0,0 +1,36 @@
1
+ import os from 'node:os';
2
+ import type { Platform, SystemState } from './types.js';
3
+
4
+ export class SystemStateMonitor {
5
+ private platform: Platform;
6
+
7
+ constructor(platform?: Platform) {
8
+ this.platform = platform ?? (os.platform() as Platform);
9
+ }
10
+
11
+ getState(): SystemState {
12
+ return {
13
+ platform: this.platform,
14
+ hostname: os.hostname(),
15
+ uptime: os.uptime(),
16
+ memory: this.getMemoryUsage(),
17
+ cpu: this.getCpuInfo(),
18
+ };
19
+ }
20
+
21
+ getMemoryUsage(): { total: number; free: number; usedPercent: number } {
22
+ const total = os.totalmem();
23
+ const free = os.freemem();
24
+ const usedPercent = ((total - free) / total) * 100;
25
+ return { total, free, usedPercent };
26
+ }
27
+
28
+ getCpuInfo(): { model: string; cores: number; loadAvg: number[] } {
29
+ const cpus = os.cpus();
30
+ return {
31
+ model: cpus[0]?.model ?? 'unknown',
32
+ cores: cpus.length,
33
+ loadAvg: os.loadavg(),
34
+ };
35
+ }
36
+ }
package/src/types.ts ADDED
@@ -0,0 +1,62 @@
1
+ export type Platform = 'darwin' | 'linux' | 'win32';
2
+
3
+ export interface ClipboardEntry {
4
+ content: string;
5
+ type: 'text' | 'image' | 'html';
6
+ timestamp: number;
7
+ }
8
+
9
+ export interface FileEvent {
10
+ type: 'created' | 'modified' | 'deleted';
11
+ path: string;
12
+ filename: string;
13
+ timestamp: number;
14
+ }
15
+
16
+ export type FileClassification =
17
+ | 'document'
18
+ | 'image'
19
+ | 'video'
20
+ | 'audio'
21
+ | 'code'
22
+ | 'archive'
23
+ | 'spreadsheet'
24
+ | 'presentation'
25
+ | 'other';
26
+
27
+ export interface AppInfo {
28
+ name: string;
29
+ pid: number;
30
+ focused: boolean;
31
+ }
32
+
33
+ export interface SystemState {
34
+ platform: Platform;
35
+ hostname: string;
36
+ uptime: number;
37
+ memory: {
38
+ total: number;
39
+ free: number;
40
+ usedPercent: number;
41
+ };
42
+ cpu: {
43
+ model: string;
44
+ cores: number;
45
+ loadAvg: number[];
46
+ };
47
+ disk?: {
48
+ total: number;
49
+ free: number;
50
+ usedPercent: number;
51
+ };
52
+ battery?: {
53
+ level: number;
54
+ charging: boolean;
55
+ } | null;
56
+ }
57
+
58
+ export interface OsBridgeConfig {
59
+ watchDirs?: string[];
60
+ clipboardPollMs?: number;
61
+ platform?: Platform;
62
+ }
@@ -0,0 +1,69 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { AppController } from '../src/app-controller.js';
3
+
4
+ describe('AppController', () => {
5
+ it('darwin launch command correct', () => {
6
+ const ctrl = new AppController('darwin');
7
+ expect(ctrl.getCommand('launch', 'Safari')).toBe('open -a "Safari"');
8
+ });
9
+
10
+ it('linux launch command correct', () => {
11
+ const ctrl = new AppController('linux');
12
+ expect(ctrl.getCommand('launch', 'firefox')).toBe('xdg-open "firefox" || firefox');
13
+ });
14
+
15
+ it('win32 launch command correct', () => {
16
+ const ctrl = new AppController('win32');
17
+ expect(ctrl.getCommand('launch', 'notepad')).toBe('start "" "notepad"');
18
+ });
19
+
20
+ it('darwin close command uses osascript', () => {
21
+ const ctrl = new AppController('darwin');
22
+ const cmd = ctrl.getCommand('close', 'Safari');
23
+ expect(cmd).toContain('osascript');
24
+ expect(cmd).toContain('quit');
25
+ });
26
+
27
+ it('linux close command uses pkill', () => {
28
+ const ctrl = new AppController('linux');
29
+ const cmd = ctrl.getCommand('close', 'firefox');
30
+ expect(cmd).toContain('pkill');
31
+ });
32
+
33
+ it('win32 close command uses taskkill', () => {
34
+ const ctrl = new AppController('win32');
35
+ const cmd = ctrl.getCommand('close', 'notepad');
36
+ expect(cmd).toContain('taskkill');
37
+ });
38
+
39
+ it('getCommand returns string for all actions', () => {
40
+ const ctrl = new AppController('darwin');
41
+ const actions = ['launch', 'focus', 'close', 'list'] as const;
42
+ for (const action of actions) {
43
+ const cmd = ctrl.getCommand(action, 'App');
44
+ expect(typeof cmd).toBe('string');
45
+ expect(cmd.length).toBeGreaterThan(0);
46
+ }
47
+ });
48
+
49
+ it('launch returns command without executing', async () => {
50
+ const ctrl = new AppController('linux');
51
+ const result = await ctrl.launch('firefox');
52
+ expect(result.success).toBe(true);
53
+ expect(result.command).toBe('xdg-open "firefox" || firefox');
54
+ });
55
+
56
+ it('focus returns command without executing', async () => {
57
+ const ctrl = new AppController('darwin');
58
+ const result = await ctrl.focus('Safari');
59
+ expect(result.success).toBe(true);
60
+ expect(result.command).toContain('activate');
61
+ });
62
+
63
+ it('close returns command without executing', async () => {
64
+ const ctrl = new AppController('win32');
65
+ const result = await ctrl.close('notepad');
66
+ expect(result.success).toBe(true);
67
+ expect(result.command).toContain('taskkill');
68
+ });
69
+ });
@@ -0,0 +1,75 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { ClipboardMonitor } from '../src/clipboard.js';
3
+
4
+ describe('ClipboardMonitor', () => {
5
+ it('addEntry adds to history', () => {
6
+ const monitor = new ClipboardMonitor();
7
+ monitor.addEntry('hello');
8
+ expect(monitor.getHistory()).toHaveLength(1);
9
+ expect(monitor.getHistory()[0]!.content).toBe('hello');
10
+ });
11
+
12
+ it('getContent returns latest entry', () => {
13
+ const monitor = new ClipboardMonitor();
14
+ monitor.addEntry('first');
15
+ monitor.addEntry('second');
16
+ expect(monitor.getContent().content).toBe('second');
17
+ });
18
+
19
+ it('getContent returns empty entry when no history', () => {
20
+ const monitor = new ClipboardMonitor();
21
+ const entry = monitor.getContent();
22
+ expect(entry.content).toBe('');
23
+ expect(entry.type).toBe('text');
24
+ });
25
+
26
+ it('getHistory returns limited entries', () => {
27
+ const monitor = new ClipboardMonitor();
28
+ monitor.addEntry('a');
29
+ monitor.addEntry('b');
30
+ monitor.addEntry('c');
31
+ const limited = monitor.getHistory(2);
32
+ expect(limited).toHaveLength(2);
33
+ expect(limited[0]!.content).toBe('b');
34
+ expect(limited[1]!.content).toBe('c');
35
+ });
36
+
37
+ it('transform uppercase works', () => {
38
+ const monitor = new ClipboardMonitor();
39
+ expect(monitor.transform('hello', 'uppercase')).toBe('HELLO');
40
+ });
41
+
42
+ it('transform lowercase works', () => {
43
+ const monitor = new ClipboardMonitor();
44
+ expect(monitor.transform('HELLO', 'lowercase')).toBe('hello');
45
+ });
46
+
47
+ it('transform trim works', () => {
48
+ const monitor = new ClipboardMonitor();
49
+ expect(monitor.transform(' hello ', 'trim')).toBe('hello');
50
+ });
51
+
52
+ it('transform json-format pretty-prints JSON', () => {
53
+ const monitor = new ClipboardMonitor();
54
+ const result = monitor.transform('{"a":1}', 'json-format');
55
+ expect(result).toBe('{\n "a": 1\n}');
56
+ });
57
+
58
+ it('onchange listener fires on addEntry', () => {
59
+ const monitor = new ClipboardMonitor();
60
+ const listener = vi.fn();
61
+ monitor.onchange(listener);
62
+ monitor.addEntry('test');
63
+ expect(listener).toHaveBeenCalledOnce();
64
+ expect(listener.mock.calls[0]![0]!.content).toBe('test');
65
+ });
66
+
67
+ it('onchange unsubscribe stops notifications', () => {
68
+ const monitor = new ClipboardMonitor();
69
+ const listener = vi.fn();
70
+ const unsub = monitor.onchange(listener);
71
+ unsub();
72
+ monitor.addEntry('test');
73
+ expect(listener).not.toHaveBeenCalled();
74
+ });
75
+ });
@@ -0,0 +1,80 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { FileWatcher } from '../src/file-watcher.js';
3
+
4
+ describe('FileWatcher', () => {
5
+ it('classifyFile: .pdf returns document', () => {
6
+ const watcher = new FileWatcher({ directories: [] });
7
+ expect(watcher.classifyFile('report.pdf')).toBe('document');
8
+ });
9
+
10
+ it('classifyFile: .jpg returns image', () => {
11
+ const watcher = new FileWatcher({ directories: [] });
12
+ expect(watcher.classifyFile('photo.jpg')).toBe('image');
13
+ });
14
+
15
+ it('classifyFile: .mp4 returns video', () => {
16
+ const watcher = new FileWatcher({ directories: [] });
17
+ expect(watcher.classifyFile('clip.mp4')).toBe('video');
18
+ });
19
+
20
+ it('classifyFile: .mp3 returns audio', () => {
21
+ const watcher = new FileWatcher({ directories: [] });
22
+ expect(watcher.classifyFile('song.mp3')).toBe('audio');
23
+ });
24
+
25
+ it('classifyFile: .ts returns code', () => {
26
+ const watcher = new FileWatcher({ directories: [] });
27
+ expect(watcher.classifyFile('index.ts')).toBe('code');
28
+ });
29
+
30
+ it('classifyFile: .zip returns archive', () => {
31
+ const watcher = new FileWatcher({ directories: [] });
32
+ expect(watcher.classifyFile('backup.zip')).toBe('archive');
33
+ });
34
+
35
+ it('classifyFile: .xlsx returns spreadsheet', () => {
36
+ const watcher = new FileWatcher({ directories: [] });
37
+ expect(watcher.classifyFile('data.xlsx')).toBe('spreadsheet');
38
+ });
39
+
40
+ it('classifyFile: .pptx returns presentation', () => {
41
+ const watcher = new FileWatcher({ directories: [] });
42
+ expect(watcher.classifyFile('slides.pptx')).toBe('presentation');
43
+ });
44
+
45
+ it('classifyFile: unknown ext returns other', () => {
46
+ const watcher = new FileWatcher({ directories: [] });
47
+ expect(watcher.classifyFile('data.xyz')).toBe('other');
48
+ });
49
+
50
+ it('suggestDestination returns correct dir', () => {
51
+ const watcher = new FileWatcher({ directories: [] });
52
+ const result = watcher.suggestDestination('photo.png');
53
+ expect(result.classification).toBe('image');
54
+ expect(result.suggestedDir).toBe('~/Documents/images/');
55
+ });
56
+
57
+ it('suggestDestination uses custom baseDir', () => {
58
+ const watcher = new FileWatcher({ directories: [] });
59
+ const result = watcher.suggestDestination('report.pdf', '/home/user');
60
+ expect(result.suggestedDir).toBe('/home/user/documents/');
61
+ });
62
+
63
+ it('emitEvent notifies listener', () => {
64
+ const watcher = new FileWatcher({ directories: ['/tmp'] });
65
+ const listener = vi.fn();
66
+ watcher.onEvent(listener);
67
+ const event = { type: 'created' as const, path: '/tmp/test.txt', filename: 'test.txt', timestamp: Date.now() };
68
+ watcher.emitEvent(event);
69
+ expect(listener).toHaveBeenCalledWith(event);
70
+ });
71
+
72
+ it('onEvent unsubscribe stops notifications', () => {
73
+ const watcher = new FileWatcher({ directories: [] });
74
+ const listener = vi.fn();
75
+ const unsub = watcher.onEvent(listener);
76
+ unsub();
77
+ watcher.emitEvent({ type: 'created', path: '/tmp/a.txt', filename: 'a.txt', timestamp: Date.now() });
78
+ expect(listener).not.toHaveBeenCalled();
79
+ });
80
+ });
@@ -0,0 +1,59 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { SystemStateMonitor } from '../src/system-state.js';
3
+
4
+ describe('SystemStateMonitor', () => {
5
+ it('getState returns valid platform', () => {
6
+ const monitor = new SystemStateMonitor();
7
+ const state = monitor.getState();
8
+ expect(['darwin', 'linux', 'win32']).toContain(state.platform);
9
+ });
10
+
11
+ it('getState returns hostname', () => {
12
+ const monitor = new SystemStateMonitor();
13
+ const state = monitor.getState();
14
+ expect(typeof state.hostname).toBe('string');
15
+ expect(state.hostname.length).toBeGreaterThan(0);
16
+ });
17
+
18
+ it('getState returns uptime > 0', () => {
19
+ const monitor = new SystemStateMonitor();
20
+ const state = monitor.getState();
21
+ expect(state.uptime).toBeGreaterThan(0);
22
+ });
23
+
24
+ it('getMemoryUsage returns valid percentages', () => {
25
+ const monitor = new SystemStateMonitor();
26
+ const mem = monitor.getMemoryUsage();
27
+ expect(mem.total).toBeGreaterThan(0);
28
+ expect(mem.free).toBeGreaterThan(0);
29
+ expect(mem.usedPercent).toBeGreaterThan(0);
30
+ expect(mem.usedPercent).toBeLessThan(100);
31
+ });
32
+
33
+ it('getCpuInfo returns model and cores', () => {
34
+ const monitor = new SystemStateMonitor();
35
+ const cpu = monitor.getCpuInfo();
36
+ expect(typeof cpu.model).toBe('string');
37
+ expect(cpu.model.length).toBeGreaterThan(0);
38
+ expect(cpu.cores).toBeGreaterThan(0);
39
+ });
40
+
41
+ it('getCpuInfo cores > 0', () => {
42
+ const monitor = new SystemStateMonitor();
43
+ const cpu = monitor.getCpuInfo();
44
+ expect(cpu.cores).toBeGreaterThan(0);
45
+ });
46
+
47
+ it('getCpuInfo loadAvg is an array', () => {
48
+ const monitor = new SystemStateMonitor();
49
+ const cpu = monitor.getCpuInfo();
50
+ expect(Array.isArray(cpu.loadAvg)).toBe(true);
51
+ expect(cpu.loadAvg.length).toBe(3);
52
+ });
53
+
54
+ it('constructor accepts explicit platform', () => {
55
+ const monitor = new SystemStateMonitor('darwin');
56
+ const state = monitor.getState();
57
+ expect(state.platform).toBe('darwin');
58
+ });
59
+ });