fullstack-critic 1.0.0 → 1.0.3
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/README.md +9 -12
- package/bin/fullstack-critic.js +10 -2
- package/docs/GETTING_STARTED.md +11 -19
- package/package.json +1 -1
- package/src/analyzer.js +66 -1
- package/src/cli.js +66 -2
- package/src/observatory.js +204 -0
- package/src/report.js +35 -4
- package/src/rules.js +115 -0
- package/src/sysinfo.js +395 -0
- package/src/ui.js +443 -0
- package/src/util.js +11 -1
- package/src/watcher.js +12 -2
- package/tests/observatory.test.js +111 -0
- package/tests/sysinfo.test.js +121 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* sysinfo unit tests — deliberately exercise the PURE, deterministic parts of the
|
|
4
|
+
* OS telemetry (grouping, CPU-delta, categorisation, ETA math) so they pass on
|
|
5
|
+
* any machine, plus a bounded check that the live process probe never throws.
|
|
6
|
+
*/
|
|
7
|
+
const test = require('node:test');
|
|
8
|
+
const assert = require('node:assert');
|
|
9
|
+
|
|
10
|
+
const sys = require('../src/sysinfo');
|
|
11
|
+
|
|
12
|
+
test('classify() folds process names into structured categories', () => {
|
|
13
|
+
assert.strictEqual(sys.classify('node'), 'runtime');
|
|
14
|
+
assert.strictEqual(sys.classify('chrome'), 'browser');
|
|
15
|
+
assert.strictEqual(sys.classify('MicrosoftEdge'), 'browser');
|
|
16
|
+
assert.strictEqual(sys.classify('powershell'), 'terminal');
|
|
17
|
+
assert.strictEqual(sys.classify('Code'), 'editor');
|
|
18
|
+
assert.strictEqual(sys.classify('postgres'), 'database');
|
|
19
|
+
assert.strictEqual(sys.classify('com.docker.backend'), 'container');
|
|
20
|
+
assert.strictEqual(sys.classify('git'), 'vcs');
|
|
21
|
+
assert.strictEqual(sys.classify('svchost'), 'system');
|
|
22
|
+
assert.strictEqual(sys.classify('mystery-thing'), 'other');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('toAppRows() computes real per-app CPU% from a sample delta', () => {
|
|
26
|
+
const procs = [
|
|
27
|
+
{ pid: 1, name: 'node', cpuSec: 2, memBytes: 1000 },
|
|
28
|
+
{ pid: 2, name: 'node', cpuSec: 1, memBytes: 500 },
|
|
29
|
+
{ pid: 3, name: 'chrome', cpuSec: 0.5, memBytes: 4000 },
|
|
30
|
+
];
|
|
31
|
+
// One second earlier: node pid1 used 1s CPU, pid2 1s, chrome 0.5s (no change).
|
|
32
|
+
const prev = new Map([
|
|
33
|
+
[1, { cpuSec: 1, ts: 0 }],
|
|
34
|
+
[2, { cpuSec: 1, ts: 0 }],
|
|
35
|
+
[3, { cpuSec: 0.5, ts: 0 }],
|
|
36
|
+
]);
|
|
37
|
+
const { rows } = sys.toAppRows(procs, prev, 1000, /* cores */ 8);
|
|
38
|
+
const node = rows.find((r) => r.app === 'node');
|
|
39
|
+
const chrome = rows.find((r) => r.app === 'chrome');
|
|
40
|
+
assert.strictEqual(node.count, 2, 'two node processes folded into one app');
|
|
41
|
+
assert.ok(Math.abs(node.cpuPct - 100) < 1, 'pid1 grew 1 CPU-sec in 1 wall-sec = ~100% (pid2 flat)');
|
|
42
|
+
assert.strictEqual(chrome.cpuPct, 0, 'no CPU-sec delta → 0%');
|
|
43
|
+
assert.strictEqual(node.category, 'runtime');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('toAppRows() uses ps instantaneous % on POSIX-style rows and clamps to cores', () => {
|
|
47
|
+
const procs = [{ pid: 9, name: 'python', memBytes: 10, cpuPctRaw: 1600 }]; // absurd value
|
|
48
|
+
const { rows } = sys.toAppRows(procs, null, 1000, /* cores */ 8);
|
|
49
|
+
assert.strictEqual(rows[0].cpuPct, 800, 'clamped to cores*100');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('byCategory() aggregates counts and memory per category', () => {
|
|
53
|
+
const rows = [
|
|
54
|
+
{ app: 'node', category: 'runtime', count: 2, cpuPct: 10, memBytes: 1000 },
|
|
55
|
+
{ app: 'deno', category: 'runtime', count: 1, cpuPct: 5, memBytes: 1000 },
|
|
56
|
+
{ app: 'chrome', category: 'browser', count: 4, cpuPct: 20, memBytes: 9999 },
|
|
57
|
+
];
|
|
58
|
+
const by = sys.byCategory(rows);
|
|
59
|
+
assert.strictEqual(by.runtime.count, 3);
|
|
60
|
+
assert.strictEqual(by.runtime.apps, 2);
|
|
61
|
+
assert.strictEqual(by.browser.memBytes, 9999);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('cpuPctFrom() turns two os.cpus() readings into a 0-100 percentage', () => {
|
|
65
|
+
const prev = [{ times: { user: 0, nice: 0, sys: 0, idle: 100, irq: 0 } }];
|
|
66
|
+
const cur = [{ times: { user: 50, nice: 0, sys: 0, idle: 150, irq: 0 } }];
|
|
67
|
+
assert.strictEqual(sys.cpuPctFrom(prev, cur), 50);
|
|
68
|
+
assert.strictEqual(sys.cpuPctFrom(null, cur), null, 'no previous reading → null (caller degrades)');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('computeEstimate(): a finished review pass reports throughput + a completed ETA', () => {
|
|
72
|
+
const e = sys.computeEstimate({ mode: 'review', passNo: 1, lastPassMs: 420, filesTotal: 30, totalLines: 1500, cores: 8 });
|
|
73
|
+
assert.strictEqual(e.complete, true);
|
|
74
|
+
assert.match(e.etaLabel, /completed in/);
|
|
75
|
+
assert.ok(e.scanRateFps > 70 && e.scanRateFps < 72, 'files/sec from measured duration (' + e.scanRateFps + ')');
|
|
76
|
+
assert.strictEqual(e.lastPassMs, 420);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('computeEstimate(): watch mode never claims completion and stretches under load', () => {
|
|
80
|
+
const calm = sys.computeEstimate({ mode: 'watch', passNo: 3, durationsMs: [200, 300, 400], filesTotal: 50, cores: 4, cpuPct: 20 });
|
|
81
|
+
assert.strictEqual(calm.complete, false);
|
|
82
|
+
assert.match(calm.etaLabel, /re-scan/);
|
|
83
|
+
assert.strictEqual(calm.avgPassMs, 300, 'rolling average of prior passes');
|
|
84
|
+
assert.strictEqual(calm.loadFactor, 1, 'no load penalty below 60% CPU');
|
|
85
|
+
|
|
86
|
+
const hot = sys.computeEstimate({ mode: 'watch', passNo: 3, durationsMs: [300], filesTotal: 50, cores: 4, cpuPct: 100 });
|
|
87
|
+
assert.strictEqual(hot.loadFactor, 1.4, 'ETA scales up under heavy system load');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('systemStatic() / memInfo() always return sane, bounded numbers', () => {
|
|
91
|
+
const s = sys.systemStatic();
|
|
92
|
+
assert.ok(s.cores >= 1);
|
|
93
|
+
assert.ok(typeof s.platform === 'string' && s.platform.length);
|
|
94
|
+
const m = sys.memInfo();
|
|
95
|
+
assert.ok(m.memPct >= 0 && m.memPct <= 100, 'memory % bounded');
|
|
96
|
+
assert.ok(m.memUsed >= 0 && m.memUsed <= m.memTotal);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('createMonitor().sample() advances a bounded rolling history', () => {
|
|
100
|
+
const mon = sys.createMonitor('.');
|
|
101
|
+
const a = mon.sample();
|
|
102
|
+
const b = mon.sample();
|
|
103
|
+
assert.ok(a.system.cpuPct >= 0 && a.system.cpuPct <= 100);
|
|
104
|
+
assert.ok(b.history.length >= a.history.length, 'each sample appends a point');
|
|
105
|
+
assert.ok(mon.historyLen <= 60, 'history capped');
|
|
106
|
+
assert.ok(a.self && typeof a.self.rss === 'number' && a.self.rss > 0, 'critic self memory is real');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('processesAsync() resolves with an array and never throws (bounded, offline-safe)', (_t, done) => {
|
|
110
|
+
const to = setTimeout(() => assert.fail('processesAsync did not call back in 12s'), 12000);
|
|
111
|
+
sys.processesAsync((err, rows) => {
|
|
112
|
+
clearTimeout(to);
|
|
113
|
+
assert.ok(Array.isArray(rows) || err, 'either an array (possibly empty on a locked-down box) or an err');
|
|
114
|
+
if (Array.isArray(rows) && rows.length) {
|
|
115
|
+
const p = rows[0];
|
|
116
|
+
assert.ok(Number.isInteger(p.pid) && p.pid > 0, 'a process row has an integer pid');
|
|
117
|
+
assert.ok(typeof p.name === 'string' && p.name.length, 'and a name');
|
|
118
|
+
}
|
|
119
|
+
done();
|
|
120
|
+
});
|
|
121
|
+
});
|