diff --git a/pyproject.toml b/pyproject.toml index f920236..fd4b89c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,31 +1,29 @@ -[tool.black] -line-length = 88 -target-version = ['py310'] -include = '\.pyi?$' -extend-exclude = ''' -/( - # directories - \.eggs - | \.git - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | venv - | _build - | buck-out - | build - | dist -)/ -''' +# Project tooling config. Kept minimal on purpose: pytest + coverage settings +# live here so `pytest` and `pytest --cov` behave the same locally and in CI. -[tool.isort] -profile = "black" -multi_line_output = 3 -line_length = 88 -known_first_party = ["app"] +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = "-q" -[tool.pylint] -load-plugins = ["pylint_flask"] -disable = ["C0114", "C0116"] -max-line-length = 88 +[tool.coverage.run] +# Measure only our own package (not the venv or third-party code), and track +# branch coverage (did both sides of each `if` run?) which is more honest than +# plain line coverage. +source = ["kernel_ai"] +branch = true +omit = [ + "*/__main__.py", + "*/tests/*", +] + +[tool.coverage.report] +show_missing = true +skip_empty = true +# Lines we don't expect (or want) to cover — excluded from the % so the number +# reflects real gaps, not boilerplate. +exclude_lines = [ + "pragma: no cover", + "if __name__ == .__main__.:", + "raise NotImplementedError", + "if TYPE_CHECKING:", +] diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..5bcc7da --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,26 @@ +# SonarCloud configuration. +# +# One-time setup on https://sonarcloud.io (free for public repos): +# 1. Log in with GitHub, import this repository. +# 2. Copy your Organization key + Project key into the two fields below. +# 3. In GitHub repo Settings -> Secrets -> Actions, add SONAR_TOKEN +# (SonarCloud -> My Account -> Security -> Generate token). +# The CI workflow (.github/workflows/sonarcloud.yaml) does the rest. + +sonar.organization=REPLACE_WITH_SONARCLOUD_ORG +sonar.projectKey=REPLACE_WITH_PROJECT_KEY +sonar.projectName=kernel-ai + +# What to analyse. +sonar.sources=kernel_ai,static/js +sonar.tests=tests + +# Python coverage report produced by pytest-cov (see the CI workflow). +sonar.python.version=3.10, 3.11 +sonar.python.coverage.reportPaths=coverage.xml + +# Keep the analysis focused on our code. +sonar.exclusions=venv/**,mldata/**,**/*.min.js,static/js/**/vendor/** +sonar.coverage.exclusions=tests/**,static/js/** + +sonar.sourceEncoding=UTF-8 diff --git a/static/js/processes-belt.js b/static/js/processes-belt.js index 0b63dc2..0df9e84 100644 --- a/static/js/processes-belt.js +++ b/static/js/processes-belt.js @@ -1,7 +1,7 @@ // Processes Subsystem Visualization -// Version: 31 — FLOW mode: strand bundle rising into a wireframe cube +// Version: 34 — removed FLOW mode -debugLog('🧠 processes-belt.js v31: Script loading...'); +debugLog('🧠 processes-belt.js v34: Script loading...'); class ProcessesSubsystemVisualization { constructor() { @@ -15,7 +15,7 @@ class ProcessesSubsystemVisualization { this.telemetry = null; this.tick = 0; this.nodeLayout = new Map(); - this.layoutMode = 'flow'; + this.layoutMode = 'microscope'; this.modeButtons = new Map(); this.edgeFilter = 'all'; this.filterButtons = new Map(); @@ -83,7 +83,6 @@ class ProcessesSubsystemVisualization { position:absolute;top:18px;left:18px;display:flex;gap:8px;z-index:1001; `; const modes = [ - { key: 'flow', label: 'FLOW' }, { key: 'microscope', label: 'MICROSCOPE' }, { key: 'temporal', label: '3-LAYER GRAPH' }, { key: 'radial', label: 'RADIAL GRAPH' }, @@ -108,7 +107,7 @@ class ProcessesSubsystemVisualization { } setLayoutMode(modeKey) { - this.layoutMode = ['flow', 'temporal', 'radial', 'microscope', 'wireframe'].includes(modeKey) ? modeKey : 'temporal'; + this.layoutMode = ['temporal', 'radial', 'microscope', 'wireframe'].includes(modeKey) ? modeKey : 'microscope'; this.modeButtons.forEach((btn, key) => { const active = key === this.layoutMode; btn.style.background = active ? 'rgba(32, 52, 81, 0.92)' : 'rgba(8,12,18,0.86)'; @@ -2187,11 +2186,6 @@ class ProcessesSubsystemVisualization { this.ctx.clearRect(0, 0, w, h); this.tick += 1; - if (this.layoutMode === 'flow') { - this.drawFlowScene(w, h); - return; - } - const gap = 16; const top = 58; const statsH = this.layoutMode === 'microscope' ? 74 : 98; @@ -2242,226 +2236,6 @@ class ProcessesSubsystemVisualization { this.drawNeuralGraph(gap, graphY, w - gap * 2, graphH); } - // Map a horizontal position 0..1 to the reference's hue ramp: - // left cyan/teal -> centre amber -> right red/pink (through pure red). - _flowHue(f) { - f = Math.max(0, Math.min(1, f)); - let hue; - if (f <= 0.5) { - hue = 188 + (45 - 188) * (f / 0.5); // cyan -> amber - } else { - hue = 45 + (-70) * ((f - 0.5) / 0.5); // amber -> red -> pink - } - return ((hue % 360) + 360) % 360; - } - - // Rotate a point (Y then X) and apply a simple perspective projection. - _project3d(x, y, z, cx, cy, rotY, rotX, focal) { - const cY = Math.cos(rotY), sY = Math.sin(rotY); - const x1 = x * cY - z * sY; - const z1 = x * sY + z * cY; - const cX = Math.cos(rotX), sX = Math.sin(rotX); - const y2 = y * cX - z1 * sX; - const z2 = y * sX + z1 * cX; - const scale = focal / (focal + z2); - return { x: cx + x1 * scale, y: cy - y2 * scale, scale, z: z2 }; - } - - // FLOW mode: process strands rise from a tight bundle at the bottom and fan - // up into the edges/faces of a slowly-rotating wireframe cube (one-to-one - // with the reference). Visual-first: uses live telemetry when present, - // otherwise a demo field so it always looks alive. Field mapping (width / - // colour <- real metrics) is intentionally coarse for now. - drawFlowScene(w, h) { - const ctx = this.ctx; - const t = this.tick * 0.016; // approx seconds - if (!this.flowStreams) this.flowStreams = new Map(); - this.nodeHitAreas = []; - - // Background: near-black vertical wash + soft vignette. - const bg = ctx.createLinearGradient(0, 0, 0, h); - bg.addColorStop(0, '#05070b'); - bg.addColorStop(0.5, '#070a10'); - bg.addColorStop(1, '#02040a'); - ctx.fillStyle = bg; - ctx.fillRect(0, 0, w, h); - - // Drifting bokeh for depth. - if (!this.flowBokeh) { - this.flowBokeh = Array.from({ length: 46 }, () => ({ - x: Math.random() * w, y: Math.random() * h, - r: 2 + Math.random() * 26, a: 0.04 + Math.random() * 0.10, - vy: 4 + Math.random() * 12, f: Math.random() - })); - } - ctx.globalCompositeOperation = 'lighter'; - this.flowBokeh.forEach((b) => { - b.y -= b.vy * 0.016; - if (b.y < -40) { b.y = h + 40; b.x = Math.random() * w; b.f = Math.random(); } - const hue = this._flowHue(b.f); - const g = ctx.createRadialGradient(b.x, b.y, 0, b.x, b.y, b.r); - g.addColorStop(0, `hsla(${hue},80%,65%,${b.a})`); - g.addColorStop(1, `hsla(${hue},80%,65%,0)`); - ctx.fillStyle = g; - ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2); ctx.fill(); - }); - - // Build per-stream specs from telemetry (or a demo field). - const nodes = Array.isArray(this.telemetry?.neural_graph?.nodes) - ? this.telemetry.neural_graph.nodes : []; - let specs; - if (nodes.length) { - specs = nodes.slice(0, 90).map((n, i) => { - const pid = Number(n.pid || 0) || -(i + 1); - const press = Number(n.syscall_pressure || 0); - const m = press > 0 - ? Math.max(0.18, Math.min(1, press / 100)) - : 0.2 + this.stableUnit(pid) * 0.7; - return { key: pid, m }; - }); - } else { - specs = Array.from({ length: 72 }, (_, i) => ({ - key: -(i + 1), m: 0.3 + this.stableUnit(i + 1) * 0.65 - })); - } - - const count = specs.length; - const seen = new Set(); - - // --- slowly-rotating wireframe cube the strands rise into --- - const cx = w * 0.5; - const cy = h * 0.40; - const s = Math.min(w, h) * 0.24; - const focal = s * 3.6; - this.flowCubeRot = (this.flowCubeRot || 0) + 0.0035; - const rotY = this.flowCubeRot; - const rotX = 0.5; // tilt so we look slightly up into it from the bundle - const project = (p) => this._project3d(p[0] * s, p[1] * s, p[2] * s, cx, cy, rotY, rotX, focal); - - const V = [ - [-1, -1, -1], [1, -1, -1], [1, 1, -1], [-1, 1, -1], - [-1, -1, 1], [1, -1, 1], [1, 1, 1], [-1, 1, 1] - ]; - const E = [ - [0, 1], [1, 2], [2, 3], [3, 0], [4, 5], [5, 6], - [6, 7], [7, 4], [0, 4], [1, 5], [2, 6], [3, 7] - ]; - const projV = V.map(project); - - // Attachment targets: cube vertices + points subdividing each edge, so - // strands land on the edges/faces of the cube. - if (!this.flowCubeTargets) { - const targets = V.map((v) => v.slice()); - E.forEach(([a, b]) => { - for (let k = 1; k <= 3; k++) { - const tk = k / 4; - targets.push([ - V[a][0] + (V[b][0] - V[a][0]) * tk, - V[a][1] + (V[b][1] - V[a][1]) * tk, - V[a][2] + (V[b][2] - V[a][2]) * tk - ]); - } - }); - this.flowCubeTargets = targets; - } - const projTargets = this.flowCubeTargets.map(project); - - // --- bundle of strands: from a tight bottom knot up to cube targets --- - const bundleX = w * 0.5; - const bundleY = h * 0.99; - - specs.forEach((spec) => { - seen.add(spec.key); - const u = this.stableUnit(spec.key < 0 ? (-spec.key + 777) : spec.key); - const targetIdx = Math.floor(u * projTargets.length) % projTargets.length; - let st = this.flowStreams.get(spec.key); - if (!st) { - st = { growth: 0, m: spec.m, phase: u * Math.PI * 2, speed: 0.5 + u * 0.7, targetIdx }; - this.flowStreams.set(spec.key, st); - } - st.m += (spec.m - st.m) * 0.05; // ease metric changes - st.growth += (1 - st.growth) * 0.04; // birth: grow out of the bundle - - const target = projTargets[st.targetIdx]; - const hue = this._flowHue(Math.max(0, Math.min(1, target.x / w))); - const width = 1 + st.m * 2.6; - - // Origin jitter forms the bundle ("пучок") at the bottom centre. - const ox = bundleX + (u - 0.5) * w * 0.045; - const oy = bundleY; - // Growth eases the live endpoint up out of the knot toward the cube. - const ex = ox + (target.x - ox) * st.growth; - const ey = oy + (target.y - oy) * st.growth; - // Control: rise vertically out of the bundle, then curve to the cube. - const c1x = ox + (ex - ox) * 0.2; - const c1y = oy - (oy - ey) * 0.7; - const swayAmp = (5 + u * 12) * (0.5 + st.m * 0.7); - - const N = 26; - const pts = []; - for (let i = 0; i <= N; i++) { - const tt = i / N; - const mt = 1 - tt; - let xx = mt * mt * ox + 2 * mt * tt * c1x + tt * tt * ex; - const yy = mt * mt * oy + 2 * mt * tt * c1y + tt * tt * ey; - xx += Math.sin(t * st.speed + st.phase + tt * Math.PI * 1.4) * swayAmp * tt; - pts.push([xx, yy]); - } - - // Body: gradient dim at the bundle, bright where it meets the cube. - const grad = ctx.createLinearGradient(ox, oy, ex, ey); - grad.addColorStop(0, `hsla(${hue},92%,55%,0)`); - grad.addColorStop(0.3, `hsla(${hue},92%,58%,0.22)`); - grad.addColorStop(1, `hsla(${hue},95%,68%,0.9)`); - ctx.strokeStyle = grad; - ctx.lineWidth = width; - ctx.lineCap = 'round'; - ctx.beginPath(); - ctx.moveTo(pts[0][0], pts[0][1]); - for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i][0], pts[i][1]); - ctx.stroke(); - - // Glow where the strand meets the cube. - const tip = pts[pts.length - 1]; - const tr = 5 + st.m * 7; - const tg = ctx.createRadialGradient(tip[0], tip[1], 0, tip[0], tip[1], tr); - tg.addColorStop(0, `hsla(${hue},95%,82%,0.95)`); - tg.addColorStop(1, `hsla(${hue},95%,70%,0)`); - ctx.fillStyle = tg; - ctx.beginPath(); ctx.arc(tip[0], tip[1], tr, 0, Math.PI * 2); ctx.fill(); - - // Travelling shimmer riding up the strand. - const sh = ((t * 0.35 * st.speed) + u) % 1; - const sp = pts[Math.min(N, Math.max(0, Math.floor(sh * N)))]; - ctx.fillStyle = `hsla(${hue},100%,90%,${0.5 * Math.sin(sh * Math.PI)})`; - ctx.beginPath(); ctx.arc(sp[0], sp[1], width * 0.85, 0, Math.PI * 2); ctx.fill(); - }); - - // Drop streams whose process has gone. - for (const k of this.flowStreams.keys()) { - if (!seen.has(k)) this.flowStreams.delete(k); - } - - // --- wireframe cube on top (subtle, glassy) --- - ctx.lineWidth = 1.2; - ctx.strokeStyle = 'rgba(150, 205, 235, 0.35)'; - E.forEach(([a, b]) => { - ctx.beginPath(); - ctx.moveTo(projV[a].x, projV[a].y); - ctx.lineTo(projV[b].x, projV[b].y); - ctx.stroke(); - }); - projV.forEach((p) => { - ctx.fillStyle = 'rgba(190, 225, 245, 0.7)'; - ctx.beginPath(); ctx.arc(p.x, p.y, 2.2, 0, Math.PI * 2); ctx.fill(); - }); - - ctx.globalCompositeOperation = 'source-over'; - ctx.fillStyle = 'rgba(200,214,235,0.5)'; - ctx.font = '10px "Share Tech Mono", monospace'; - ctx.fillText(`PROCESS FLOW · ${count} streams${nodes.length ? '' : ' · demo'}`, 18, h - 18); - } - animate() { if (!this.isActive) return; this.animationId = requestAnimationFrame(() => this.animate()); diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..d4839a6 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# Tests package diff --git a/tests/test_api_contracts.py b/tests/test_api_contracts.py new file mode 100644 index 0000000..d3b3268 --- /dev/null +++ b/tests/test_api_contracts.py @@ -0,0 +1,88 @@ +"""Integration tests for key API response contracts.""" + +import os + +from kernel_ai.contracts.api_contracts import ( + validate_crypto_realtime_response, + validate_execution_context_response, + validate_filesystem_blocks_response, + validate_isolation_context_response, + validate_kernel_data_response, + validate_kernel_dna_response, + validate_network_stack_realtime_response, + validate_proc_graph_response, + validate_proc_timeline_response, + validate_processes_realtime_response, + validate_security_realtime_response, +) +from kernel_ai.webapp import create_app + + +def _client(): + return create_app().test_client() + + +def test_kernel_data_contract(): + resp = _client().get("/api/kernel-data") + assert resp.status_code == 200 + validate_kernel_data_response(resp.get_json()) + + +def test_network_stack_realtime_contract(): + resp = _client().get("/api/network-stack-realtime") + assert resp.status_code == 200 + validate_network_stack_realtime_response(resp.get_json()) + + +def test_crypto_realtime_contract(): + resp = _client().get("/api/crypto-realtime") + assert resp.status_code == 200 + validate_crypto_realtime_response(resp.get_json()) + + +def test_security_realtime_contract(): + resp = _client().get("/api/security-realtime") + assert resp.status_code == 200 + validate_security_realtime_response(resp.get_json()) + + +def test_filesystem_blocks_contract(): + resp = _client().get("/api/filesystem-blocks") + assert resp.status_code == 200 + validate_filesystem_blocks_response(resp.get_json()) + + +def test_processes_realtime_contract(): + resp = _client().get("/api/processes-realtime") + assert resp.status_code == 200 + validate_processes_realtime_response(resp.get_json()) + + +def test_execution_context_contract(): + resp = _client().get("/api/execution-context") + assert resp.status_code == 200 + validate_execution_context_response(resp.get_json()) + + +def test_kernel_dna_contract(): + resp = _client().get("/api/kernel-dna") + assert resp.status_code == 200 + validate_kernel_dna_response(resp.get_json()) + + +def test_proc_graph_contract(): + resp = _client().get("/api/proc-graph") + assert resp.status_code == 200 + validate_proc_graph_response(resp.get_json()) + + +def test_proc_timeline_contract(): + resp = _client().get(f"/api/proc-timeline?pid={os.getpid()}") + assert resp.status_code == 200 + validate_proc_timeline_response(resp.get_json()) + + +def test_isolation_context_contract(): + resp = _client().get("/api/isolation-context") + assert resp.status_code == 200 + validate_isolation_context_response(resp.get_json()) diff --git a/tests/test_core_observability_service.py b/tests/test_core_observability_service.py new file mode 100644 index 0000000..9dea362 --- /dev/null +++ b/tests/test_core_observability_service.py @@ -0,0 +1,16 @@ +"""Tests for ``kernel_ai.services.core_observability``.""" + +from kernel_ai.services import core_observability as svc + + +def test_get_system_info_has_expected_keys(): + out = svc.get_system_info() + assert "platform" in out + assert "kernel" in out + assert "cpu_count" in out + + +def test_get_process_kernel_map_falls_back_to_mock(): + out = svc.get_process_kernel_map(openai_available=False, openai_module=None) + assert "systemd" in out + assert "nginx" in out diff --git a/tests/test_crypto_entropy_service.py b/tests/test_crypto_entropy_service.py new file mode 100644 index 0000000..980f126 --- /dev/null +++ b/tests/test_crypto_entropy_service.py @@ -0,0 +1,43 @@ +"""Tests for ``kernel_ai.services.crypto.entropy``.""" + +from kernel_ai.services.crypto import entropy as svc + + +def test_read_sysctl_int_missing_returns_default(): + out = svc.read_sysctl_int("/nonexistent/kernel-ai-sysctl", default=123) + assert out == 123 + + +def test_collect_entropy_cloud_status_basic(monkeypatch): + class _Disk: + read_bytes = 1000 + write_bytes = 2000 + + class _Net: + bytes_sent = 3000 + bytes_recv = 4000 + + values = { + "/proc/sys/kernel/random/entropy_avail": 256, + "/proc/sys/kernel/random/poolsize": 256, + "/proc/sys/kernel/random/read_wakeup_threshold": 128, + "/proc/sys/kernel/random/write_wakeup_threshold": 64, + } + + monkeypatch.setattr(svc, "read_sysctl_int", lambda path, default=0: values.get(path, default)) + monkeypatch.setattr(svc.psutil, "disk_io_counters", lambda: _Disk()) + monkeypatch.setattr(svc.psutil, "net_io_counters", lambda: _Net()) + monkeypatch.setattr(svc, "read_proc_interrupt_total", lambda: 100) + + prev = { + "timestamp": None, + "disk_read_bytes": 0, + "disk_write_bytes": 0, + "net_sent_bytes": 0, + "net_recv_bytes": 0, + "interrupt_total": 0, + } + out = svc.collect_entropy_cloud_status(prev) + assert out["entropy_pool_bits"] == 256 + assert out["random_subsystem_state"] in {"stable", "refilling"} + assert len(out["sources"]) == 4 diff --git a/tests/test_crypto_security_service.py b/tests/test_crypto_security_service.py new file mode 100644 index 0000000..da15711 --- /dev/null +++ b/tests/test_crypto_security_service.py @@ -0,0 +1,91 @@ +"""Tests for ``kernel_ai.services.crypto_security``.""" + +from kernel_ai.services import crypto_security as svc + + +class _FakeProc: + def __init__(self, info): + self.info = info + + +def test_collect_crypto_realtime_with_callbacks(monkeypatch): + monkeypatch.setattr(svc.psutil, "net_connections", lambda kind="inet": []) + monkeypatch.setattr( + svc.psutil, + "process_iter", + lambda attrs=None: [_FakeProc({"pid": 101, "name": "nginx"})], + ) + + callbacks = { + "infer_crypto_protocol": lambda _lp, _rp, _name: ("TLS", "AES-GCM/SHA256"), + "is_likely_crypto_actor": lambda *_args: True, + "infer_tls_terminator": lambda _name, _port, _proto, _listeners: "n/a", + "collect_algorithm_competition": lambda algo: {"request": algo.upper(), "selected": {"name": "generic", "source": "kernel"}, "implementations": []}, + "parse_proc_crypto_entries": lambda: [], + "collect_kernel_crypto_clients": lambda _items: [], + "collect_hw_offload_status": lambda _entries, _comps: [], + "collect_sync_async_queue": lambda _items: {"sync": 0, "async": 0}, + "collect_algorithm_requesters": lambda _items, _clients: {"aes": [], "sha": [], "chacha20": []}, + "build_crypto_decision_pipelines": lambda **_kwargs: {"aes": {}}, + "collect_entropy_cloud_status": lambda: {"mode": "test"}, + "collect_crypto_runtime_sources": lambda _items, _entries, _clients, _entropy: [{"id": "tls_sockets", "active": True}], + } + + out = svc.collect_crypto_realtime(callbacks=callbacks, crypto_prev={"timestamp": None, "active_flows": 0}) + assert "items" in out + assert "meta" in out + assert "protected_zones" in out + assert "runtime_sources" in out + assert out["meta"]["runtime_sources"][0]["id"] == "tls_sockets" + assert any(zone["id"] == "tls" for zone in out["protected_zones"]) + assert out["meta"]["active_flows"] >= 1 + + +def test_collect_crypto_runtime_sources_marks_live_tls(): + sources = svc.collect_crypto_runtime_sources( + items=[ + { + "process": "nginx", + "protocol": "TLS", + "status": "ESTABLISHED", + "endpoint": "127.0.0.1:443", + } + ], + proc_crypto_entries=[{"name": "aes", "type": "skcipher"}], + kernel_clients=[{"name": "kTLS", "status": "active", "active_flows": 1}], + entropy_cloud={"crng_state": "ready", "entropy_pool_bits": 256}, + ) + by_id = {source["id"]: source for source in sources} + assert by_id["tls_sockets"]["active"] is True + assert by_id["tls_sockets"]["source"] == "direct" + assert by_id["kernel_registry"]["active"] is True + assert by_id["entropy"]["active"] is True + + +def test_collect_security_realtime_empty_processes(monkeypatch): + monkeypatch.setattr(svc.psutil, "process_iter", lambda _attrs: []) + monkeypatch.setattr(svc.psutil, "net_connections", lambda kind="inet": []) + monkeypatch.setattr(svc.subprocess, "check_output", lambda *args, **kwargs: "0") + monkeypatch.setattr(svc.random, "randint", lambda _a, _b: 10) + + out = svc.collect_security_realtime(security_prev={"timestamp": None, "events": 0}) + assert "pipeline" in out + assert "security_tools" in out + assert out["meta"]["events"] == 0 + + +def test_collect_crypto_realtime_delegates_to_crypto_pipeline(monkeypatch): + called = {} + + def _fake_collect(*, crypto_prev, callbacks, psutil_module, logger): + called["ok"] = True + assert crypto_prev["active_flows"] == 0 + assert callable(callbacks["collect_entropy_cloud_status"]) + assert psutil_module is svc.psutil + assert logger is svc.logger + return {"items": [], "processes": [], "meta": {"active_flows": 0}} + + monkeypatch.setattr(svc._crypto_pipeline, "collect_crypto_realtime", _fake_collect) + out = svc.collect_crypto_realtime(crypto_prev={"timestamp": None, "active_flows": 0}) + assert called.get("ok") is True + assert out["meta"]["active_flows"] == 0 diff --git a/tests/test_devices_service.py b/tests/test_devices_service.py new file mode 100644 index 0000000..218e274 --- /dev/null +++ b/tests/test_devices_service.py @@ -0,0 +1,27 @@ +"""Tests for ``kernel_ai.services.devices``.""" + +from kernel_ai.services import devices as svc + + +def test_detect_bus_uses_category_for_net(): + assert svc._detect_bus("/sys/class/net/eth0", "net") == "net" + + +def test_get_devices_realtime_updates_state(monkeypatch): + monkeypatch.setattr(svc, "_collect_block_devices", lambda _disk, _dt, _prev: [{"name": "sda", "category": "block", "bus": "pcie", "throughput_bps": 100.0, "irq_tokens": ["sda"], "driver": None}]) + monkeypatch.setattr(svc, "_collect_net_devices", lambda _dt, _prev: ([{"name": "eth0", "category": "net", "bus": "net", "throughput_bps": 50.0, "irq_tokens": ["eth0"], "driver": None}], {"eth0": 10})) + monkeypatch.setattr(svc, "_collect_char_devices", lambda: []) + monkeypatch.setattr(svc, "_collect_misc_input_gpu_usb", lambda: []) + monkeypatch.setattr(svc, "_irq_total_for_tokens", lambda _lines, _tokens: 1) + + state = {"timestamp": None, "disk_sectors": {}, "net_bytes": {}, "tty_irq_total": 0, "irq_by_key": {}} + out = svc.get_devices_realtime( + devices_prev=state, + read_diskstats_fn=lambda: {"sda": 20}, + read_interrupt_lines_fn=lambda: [], + read_tty_irq_total_fn=lambda: 0, + ) + + assert "devices" in out + assert out["meta"]["count"] >= 1 + assert state["timestamp"] is not None diff --git a/tests/test_execution_service.py b/tests/test_execution_service.py new file mode 100644 index 0000000..40de22a --- /dev/null +++ b/tests/test_execution_service.py @@ -0,0 +1,43 @@ +"""Tests for ``kernel_ai.services.execution``.""" + +from kernel_ai.services import execution as svc + + +def test_get_execution_context_data_non_linux(monkeypatch): + monkeypatch.setattr(svc.platform, "system", lambda: "Darwin") + out = svc.get_execution_context_data({}, lambda _x: "kernel", {"timestamp": None, "irq_totals": {}, "softirq_totals": {}}) + assert out["mode"] == "kernel" + assert out["preempted"] is False + + +def test_get_kernel_dna_data_uses_callbacks(): + out = svc.get_kernel_dna_data( + get_real_system_calls_fn=lambda: [{"name": "read", "count": 1}], + map_syscall_to_subsystem_fn=lambda _n: "fs", + map_interrupt_to_subsystem_fn=lambda _n: "kernel", + softirq_nucleotides_fn=lambda: [], + ) + assert "nucleotides" in out + assert "genes" in out + + +def test_build_kernel_anomaly_mutations_from_proc_hints(monkeypatch): + monkeypatch.setattr( + svc, + "_read_key_value_proc_file", + lambda _path: {"pgmajfault": 10, "pgscan_direct": 4}, + ) + monkeypatch.setattr(svc, "_read_memory_psi_avg10", lambda: {"some": 1.5, "full": 0.0}) + monkeypatch.setattr(svc, "_read_tcp_retrans_segs", lambda: 12) + monkeypatch.setattr(svc, "_read_softirq_totals", lambda: {"NET_RX": 500, "NET_TX": 100, "TIMER": 20}) + monkeypatch.setattr(svc, "_read_loadavg", lambda: {"load1": 4.0, "runnable": 8, "threads": 120}) + monkeypatch.setattr(svc.psutil, "cpu_count", lambda: 2) + + out = svc.build_kernel_anomaly_mutations() + types = {row["type"] for row in out} + + assert "memory_pressure" in types + assert "page_reclaim_pressure" in types + assert "tcp_retransmission" in types + assert "net_softirq_pressure" in types + assert "scheduler_runqueue_pressure" in types diff --git a/tests/test_frontend_logs_service.py b/tests/test_frontend_logs_service.py new file mode 100644 index 0000000..6e1fff7 --- /dev/null +++ b/tests/test_frontend_logs_service.py @@ -0,0 +1,43 @@ +"""Tests for ``kernel_ai.services.frontend_logs``.""" + +from threading import Lock + +from kernel_ai.services import frontend_logs as svc + + +def test_safe_trim_truncates(): + out = svc.safe_trim("abcdefghij", limit=4) + assert out == "abcd...[truncated]" + + +def test_write_frontend_event_writes_jsonl(tmp_path): + log_file = tmp_path / "frontend-events.jsonl" + svc.write_frontend_event( + event_payload={"level": "INFO", "message": "hello"}, + frontend_log_file=str(log_file), + write_lock=Lock(), + ) + data = log_file.read_text(encoding="utf-8") + assert "hello" in data + assert "kernel-ai-frontend" in data + + +def test_ingest_frontend_logs_payload_rejects_non_json(): + payload, status = svc.ingest_frontend_logs_payload( + payload=None, + write_event_fn=lambda _event: None, + ) + assert status == 400 + assert "Invalid JSON payload" in payload["error"] + + +def test_ingest_frontend_logs_payload_accepts_dict_and_list(): + seen = [] + + payload, status = svc.ingest_frontend_logs_payload( + payload={"events": [{"message": "a"}, {"message": "b"}, "skip-me"]}, + write_event_fn=lambda event: seen.append(event), + ) + assert status == 200 + assert payload["accepted"] == 2 + assert seen == [{"message": "a"}, {"message": "b"}] diff --git a/tests/test_http_routes.py b/tests/test_http_routes.py new file mode 100644 index 0000000..4b85787 --- /dev/null +++ b/tests/test_http_routes.py @@ -0,0 +1,129 @@ +"""Integration-style tests for HTTP route wiring.""" + +import pytest + +from kernel_ai.state import get_state_container +from kernel_ai.webapp import create_app + + +@pytest.fixture +def app(): + return create_app() + + +@pytest.fixture +def client(app): + return app.test_client() + + +def test_health_route_ok(client): + resp = client.get("/health") + assert resp.status_code == 200 + payload = resp.get_json() + assert payload["status"] == "healthy" + assert "timestamp" in payload + assert "system_info" in payload + + +def test_traceroute_requires_ip(client): + resp = client.get("/api/traceroute") + assert resp.status_code == 400 + payload = resp.get_json() + assert "Missing 'ip' query parameter" in payload["error"] + assert payload["code"] == "bad_request" + assert payload.get("request_id") + + +def test_frontend_logs_rejects_non_json(client): + resp = client.post("/api/frontend-logs", data="not-json", content_type="application/text") + assert resp.status_code == 400 + assert "Invalid JSON payload" in resp.get_json()["error"] + + +def test_create_app_can_be_called_multiple_times(): + app1 = create_app() + app2 = create_app() + assert app1 is not app2 + assert app1.test_client().get("/health").status_code == 200 + assert app2.test_client().get("/health").status_code == 200 + + +def test_create_app_attaches_runtime_state_container(): + app = create_app() + state = get_state_container(app) + assert app.extensions["kernel_ai_state"] is state + assert isinstance(state.traceroute_cache, dict) + + +def test_runtime_state_is_isolated_between_apps(): + app1 = create_app() + app2 = create_app() + state1 = get_state_container(app1) + state2 = get_state_container(app2) + state1.crypto_prev["active_flows"] = 999 + state1.traceroute_cache["8.8.8.8"] = {"timestamp": 1, "data": {"ok": True}} + assert state2.crypto_prev["active_flows"] != 999 + assert "8.8.8.8" not in state2.traceroute_cache + + +@pytest.mark.parametrize( + ("path", "expected"), + [ + ("/api/kernel-data", 200), + ("/api/processes", 200), + ("/api/nginx-files", 200), + ("/api/active-connections", 200), + ("/api/network-stack-realtime", 200), + ("/api/devices-realtime", 200), + ("/api/filesystem-blocks", 200), + ], +) +def test_api_smoke_routes(client, path, expected): + resp = client.get(path) + assert resp.status_code == expected + + +def test_metrics_endpoint_available(client): + resp = client.get("/metrics") + assert resp.status_code == 200 + assert "text/plain" in (resp.content_type or "") + body = resp.get_data(as_text=True) + assert "http_requests_total" in body + assert "http_errors_total" in body + + +def test_not_found_handler_returns_json(client): + resp = client.get("/does-not-exist") + assert resp.status_code == 404 + assert resp.is_json + payload = resp.get_json() + assert payload["error"] == "Not found" + assert payload["code"] == "not_found" + assert payload.get("request_id") + + +def test_internal_error_handler_returns_json(app): + @app.route("/__test_500") + def _raise_error(): + raise RuntimeError("boom") + + client = app.test_client() + resp = client.get("/__test_500") + assert resp.status_code == 500 + assert resp.is_json + payload = resp.get_json() + assert payload["error"] == "Internal server error" + assert payload["code"] == "internal_error" + assert payload.get("request_id") + + +def test_request_id_header_present(client): + resp = client.get("/health") + assert resp.status_code == 200 + assert resp.headers.get("X-Request-ID") + + +def test_request_id_header_passthrough(client): + resp = client.get("/health", headers={"X-Request-ID": "req-123"}) + assert resp.status_code == 200 + assert resp.headers.get("X-Request-ID") == "req-123" diff --git a/tests/test_infra_utils_service.py b/tests/test_infra_utils_service.py new file mode 100644 index 0000000..ce307e0 --- /dev/null +++ b/tests/test_infra_utils_service.py @@ -0,0 +1,8 @@ +"""Tests for ``kernel_ai.services.infra_utils``.""" + +from kernel_ai.services import infra_utils as svc + + +def test_resolve_binary_returns_none_for_missing(): + out = svc.resolve_binary("definitely-not-a-real-binary-xyz") + assert out is None diff --git a/tests/test_kernel_maps_service.py b/tests/test_kernel_maps_service.py new file mode 100644 index 0000000..7eecb7a --- /dev/null +++ b/tests/test_kernel_maps_service.py @@ -0,0 +1,19 @@ +"""Tests for ``kernel_ai.services.kernel_maps``.""" + +from kernel_ai.services import kernel_maps as svc + + +def test_syscall_names_contains_common_entries(): + assert svc.SYSCALL_NAMES[0] == "read" + assert svc.SYSCALL_NAMES[41] == "socket" + + +def test_map_syscall_to_subsystem(): + assert svc.map_syscall_to_subsystem("read") == "fs" + assert svc.map_syscall_to_subsystem("socket") == "net" + assert svc.map_syscall_to_subsystem("mmap") == "mm" + + +def test_map_interrupt_to_subsystem(): + assert svc.map_interrupt_to_subsystem("timer0") == "sched" + assert svc.map_interrupt_to_subsystem("eth0") == "net" diff --git a/tests/test_logging_helpers.py b/tests/test_logging_helpers.py new file mode 100644 index 0000000..ef23a5f --- /dev/null +++ b/tests/test_logging_helpers.py @@ -0,0 +1,44 @@ +"""Tests for shared logging helpers.""" + +import logging + +from kernel_ai import logging_helpers as lh + + +def test_sanitize_payload_masks_sensitive_keys(): + payload = { + "Authorization": "Bearer abc", + "nested": {"token": "secret-token", "ok": "x"}, + } + out = lh.sanitize_payload(payload) + assert out["Authorization"] == "***" + assert out["nested"]["token"] == "***" + assert out["nested"]["ok"] == "x" + + +def test_sanitize_payload_truncates_long_strings(): + long_value = "x" * 600 + out = lh.sanitize_payload({"msg": long_value}) + assert out["msg"].endswith("...") + + +def test_log_event_emits_event_data(caplog): + logger = logging.getLogger("tests.logging_helpers") + with caplog.at_level(logging.INFO): + lh.log_event( + logger, + "INFO", + "hello", + event_dataset="kernel_ai.app", + component="tests", + operation="unit", + event_data={"password": "123", "ok": 1}, + ) + assert caplog.records + record = caplog.records[-1] + event_data = getattr(record, "event_data", {}) + assert event_data["event.dataset"] == "kernel_ai.app" + assert event_data["component"] == "tests" + assert event_data["operation"] == "unit" + assert event_data["password"] == "***" + assert event_data["ok"] == 1 diff --git a/tests/test_network_service.py b/tests/test_network_service.py new file mode 100644 index 0000000..368fc7b --- /dev/null +++ b/tests/test_network_service.py @@ -0,0 +1,16 @@ +"""Tests for ``kernel_ai.services.network``.""" + +from kernel_ai.services import network as svc + + +def test_tcp_state_name_mapping(): + assert svc._tcp_state_name("01") == "ESTABLISHED" + assert svc._tcp_state_name("ff") == "FF" + + +def test_traceroute_invalid_ip_raises_value_error(): + try: + svc.get_traceroute_info("not-an-ip") + assert False, "Expected ValueError for invalid IP" + except ValueError: + assert True diff --git a/tests/test_proc_fs.py b/tests/test_proc_fs.py new file mode 100644 index 0000000..ddda7e2 --- /dev/null +++ b/tests/test_proc_fs.py @@ -0,0 +1,54 @@ +"""Unit tests for ``kernel_ai.collectors.proc_fs``.""" + +from pathlib import Path + +import pytest + +from kernel_ai.collectors import proc_fs + + +def test_safe_read_text_reads_file(tmp_path: Path) -> None: + p = tmp_path / "x.txt" + p.write_text("hello\n", encoding="utf-8") + assert proc_fs.safe_read_text(str(p)) == "hello" + + +def test_safe_read_text_missing_returns_none() -> None: + assert proc_fs.safe_read_text("/nonexistent/proc/diskstats-xyz") is None + + +def test_read_diskstats_skips_loop_and_ram() -> None: + sample = ( + " 8 0 loop0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" + " 8 1 sda 100 0 200 0 300 0 400 0 0 0 0 0 0 0 0\n" + ) + + def fake_open(path, *args, **kwargs): + if path == "/proc/diskstats": + from io import StringIO + + return StringIO(sample) + raise AssertionError(f"unexpected open: {path!r}") + + import builtins + + orig_open = builtins.open + builtins.open = fake_open + try: + out = proc_fs.read_diskstats() + finally: + builtins.open = orig_open + + assert "loop0" not in out + assert out.get("sda") == 200 + 400 # sectors read + write + + +def test_monkeypatch_proc_fs_seen_by_handlers(monkeypatch: pytest.MonkeyPatch) -> None: + """Patching ``proc_fs`` updates behavior for network system handlers.""" + from kernel_ai.http.api_handlers import network_system + + def fake_diskstats(): + return {"mockdev": 42} + + monkeypatch.setattr(proc_fs, "read_diskstats", fake_diskstats) + assert network_system._proc_fs.read_diskstats() == {"mockdev": 42} diff --git a/tests/test_process_inspect_service.py b/tests/test_process_inspect_service.py new file mode 100644 index 0000000..7197a7d --- /dev/null +++ b/tests/test_process_inspect_service.py @@ -0,0 +1,64 @@ +"""Tests for ``kernel_ai.services.process_inspect``.""" + +from kernel_ai.services import process_inspect as svc + + +def test_get_ipc_links_summary_empty_proc(monkeypatch): + monkeypatch.setattr(svc.os, "listdir", lambda path: [] if path == "/proc" else []) + monkeypatch.setattr(svc.psutil, "net_connections", lambda kind: []) + out = svc.get_ipc_links_summary(max_pairs=20, max_nodes=8) + assert out["process_nodes"] == [] + assert out["pair_links"] == [] + assert out["stats"]["pair_count"] == 0 + assert out["stats"]["shared_unix_socket_inodes"] == 0 + assert out["stats"]["shared_tcp_socket_inodes"] == 0 + + +def test_get_process_fds_info_basic(monkeypatch): + class _FakeProc: + def num_fds(self): + return 3 + + def open_files(self): + return [] + + def connections(self): + return [] + + monkeypatch.setattr(svc.psutil, "Process", lambda _pid: _FakeProc()) + out = svc.get_process_fds_info(123) + assert out["pid"] == 123 + assert out["num_fds"] == 3 + + +def test_get_process_fds_info_descriptors(monkeypatch): + class _FakeProc: + def num_fds(self): + return 5 + + def open_files(self): + return [] + + def connections(self): + return [] + + targets = { + "/proc/123/fd/0": "/dev/null", + "/proc/123/fd/1": "pipe:[10]", + "/proc/123/fd/2": "pipe:[11]", + "/proc/123/fd/7": "socket:[12]", + "/proc/123/fd/19": "pipe:[13]", + } + + monkeypatch.setattr(svc.psutil, "Process", lambda _pid: _FakeProc()) + monkeypatch.setattr(svc.os.path, "exists", lambda path: path == "/proc/123/fd") + monkeypatch.setattr(svc.os, "listdir", lambda path: ["19", "0", "7", "2", "1"] if path == "/proc/123/fd" else []) + monkeypatch.setattr(svc.os, "readlink", lambda path: targets[path]) + # Isolate the unrelated namespace probe: this test is about fd descriptor + # parsing, and the real fingerprint reads /proc//ns/* (not mocked here). + monkeypatch.setattr(svc, "get_process_namespace_fingerprint", lambda _pid: {}) + + out = svc.get_process_fds_info(123) + descriptors = out["descriptors"] + assert [item["fd"] for item in descriptors] == [0, 1, 2, 7, 19] + assert [item["type"] for item in descriptors] == ["stdin", "stdout", "stderr", "socket", "pipe"] diff --git a/tests/test_process_timeline_service.py b/tests/test_process_timeline_service.py new file mode 100644 index 0000000..8b7e826 --- /dev/null +++ b/tests/test_process_timeline_service.py @@ -0,0 +1,77 @@ +"""Tests for ``kernel_ai.services.process_timeline``.""" + +from kernel_ai.services import process_timeline as svc + + +class _FakeProc: + def as_dict(self, _fields): + return {"pid": 123, "name": "fake", "create_time": 1000.0, "status": "running"} + + +class _FakeIterProc: + def __init__(self, pid, name, cpu, rss): + self.info = { + "pid": pid, + "name": name, + "cpu_percent": cpu, + "memory_info": type("mem", (), {"rss": rss})(), + } + + +def test_get_proc_timeline_data_requires_pid(): + try: + svc.get_proc_timeline_data(None) + assert False, "Expected ValueError" + except ValueError: + assert True + + +def test_get_proc_timeline_data_not_found(monkeypatch): + def _raise(_pid): + raise svc.psutil.NoSuchProcess(pid=999) + + monkeypatch.setattr(svc.psutil, "Process", _raise) + try: + svc.get_proc_timeline_data(999) + assert False, "Expected ProcessLookupError" + except ProcessLookupError: + assert True + + +def test_get_proc_timeline_data_basic(monkeypatch): + monkeypatch.setattr(svc.psutil, "Process", lambda _pid: _FakeProc()) + monkeypatch.setattr(svc, "_safe_read_text", lambda _path: "") + out = svc.get_proc_timeline_data(123, window_s=5) + assert out["pid"] == 123 + assert out["window_s"] == 5 + assert out["timeline"][0]["type"] == "exec" + assert "relative_s" in out["timeline"][0] + + +def test_get_proc_timeline_branches_data_basic(monkeypatch): + monkeypatch.setattr( + svc.psutil, + "process_iter", + lambda _fields: [ + _FakeIterProc(101, "nginx", 12.0, 120 * 1024 * 1024), + _FakeIterProc(202, "python3", 8.0, 220 * 1024 * 1024), + ], + ) + + def _fake_timeline(pid, window_s=30): + return { + "pid": pid, + "name": f"p{pid}", + "window_s": window_s, + "timeline": [ + {"type": "syscall", "name": "exec", "timestamp": "2026-01-01T00:00:00"}, + {"type": "i/o", "name": "read_bytes", "timestamp": "2026-01-01T00:00:01"}, + ], + } + + monkeypatch.setattr(svc, "get_proc_timeline_data", _fake_timeline) + out = svc.get_proc_timeline_branches_data(limit=2, events=4, window_s=120) + assert "branches" in out + assert len(out["branches"]) == 2 + assert out["branches"][0]["event_count"] >= 1 + assert out["meta"]["window_s"] == 120 diff --git a/tests/test_processes_runtime_service.py b/tests/test_processes_runtime_service.py new file mode 100644 index 0000000..b60a139 --- /dev/null +++ b/tests/test_processes_runtime_service.py @@ -0,0 +1,54 @@ +"""Tests for realtime process semantic operation builders.""" + +from kernel_ai.services import processes_runtime as svc + + +def test_build_semantic_ops_uses_procfs_evidence(): + node_pool = { + 100: { + "pid": 100, + "ppid": 1, + "name": "nginx", + "syscall_pressure": 55, + "fd_count": 24, + "num_threads": 4, + "seccomp_mode": "filter", + "connections": 2, + "unique_peers": 1, + "fd_semantics": { + "socket": 2, + "regular": 3, + "eventpoll": 1, + "pipe": 1, + "eventfd": 1, + "sampled": 12, + }, + } + } + network_tracing = [ + { + "pid": 100, + "name": "nginx", + "connections": 2, + "unique_peers": 1, + "peer_sample": ["10.0.0.5"], + "top_state": "ESTABLISHED", + } + ] + security_hooks = [{"name": "LSM stack", "status": "active", "detail": "apparmor"}] + + rows = svc._build_semantic_ops( + node_pool, + network_tracing, + security_hooks, + {"RetransSegs": 7}, + ) + + assert rows[0]["pid"] == 100 + labels = {op["label"] for op in rows[0]["ops"]} + assert "socket()" in labels + assert "epoll_wait()" in labels + assert "nf_conntrack" in labels + assert "tcp retransmission" in labels + assert "sendfile()/splice()" in labels + assert rows[0]["source"] == "procfs+psutil-derived" diff --git a/tests/test_processes_service.py b/tests/test_processes_service.py new file mode 100644 index 0000000..5dd5208 --- /dev/null +++ b/tests/test_processes_service.py @@ -0,0 +1,35 @@ +"""Tests for ``kernel_ai.services.processes``.""" + +from kernel_ai.services import processes as svc + + +class _FakeProc: + def __init__(self, info): + self.info = info + + +def test_get_proc_matrix_data_sorts_by_cpu(monkeypatch): + def fake_iter(_fields): + return [ + _FakeProc({"pid": 10, "name": "a", "cpu_percent": 2.0, "memory_info": None, "io_counters": None, "num_fds": 3}), + _FakeProc({"pid": 20, "name": "b", "cpu_percent": 8.0, "memory_info": None, "io_counters": None, "num_fds": 4}), + ] + + monkeypatch.setattr(svc.psutil, "process_iter", fake_iter) + monkeypatch.setattr(svc.os.path, "exists", lambda _p: False) + + out = svc.get_proc_matrix_data() + assert [row["pid"] for row in out[:2]] == [20, 10] + + +def test_get_proc_graph_data_uses_matrix(monkeypatch): + monkeypatch.setattr( + svc, + "get_proc_matrix_data", + lambda: [{"pid": 1, "name": "systemd"}, {"pid": 2, "name": "kthreadd"}], + ) + + out = svc.get_proc_graph_data() + assert "nodes" in out and "edges" in out + assert out["nodes"][0]["id"] == "kernel" + assert len(out["edges"]) == 2 diff --git a/tests/test_security_pipeline_service.py b/tests/test_security_pipeline_service.py new file mode 100644 index 0000000..8f41591 --- /dev/null +++ b/tests/test_security_pipeline_service.py @@ -0,0 +1,32 @@ +"""Tests for ``kernel_ai.services.crypto.security_pipeline``.""" + +from types import SimpleNamespace + +from kernel_ai.services.crypto import security_pipeline as svc + + +def test_collect_security_realtime_with_empty_sources(): + psutil_module = SimpleNamespace( + Error=Exception, + NoSuchProcess=Exception, + AccessDenied=Exception, + ZombieProcess=Exception, + process_iter=lambda _attrs: [], + net_connections=lambda kind="inet": [], + ) + subprocess_module = SimpleNamespace( + SubprocessError=Exception, + check_output=lambda *args, **kwargs: "0", + ) + random_module = SimpleNamespace(randint=lambda _a, _b: 10) + os_module = SimpleNamespace(path=SimpleNamespace(exists=lambda _p: False)) + + out = svc.collect_security_realtime( + security_prev={"timestamp": None, "events": 0}, + psutil_module=psutil_module, + subprocess_module=subprocess_module, + random_module=random_module, + os_module=os_module, + ) + assert "pipeline" in out + assert "security_tools" in out diff --git a/tests/test_state_container.py b/tests/test_state_container.py new file mode 100644 index 0000000..e51591d --- /dev/null +++ b/tests/test_state_container.py @@ -0,0 +1,18 @@ +"""Tests for runtime state container helpers.""" + +from kernel_ai import state as st + + +def test_create_state_container_isolated_mutables(): + a = st.create_state_container() + b = st.create_state_container() + a.traceroute_cache["1.1.1.1"] = {"timestamp": 1, "data": {}} + a.crypto_prev["active_flows"] = 77 + assert "1.1.1.1" not in b.traceroute_cache + assert b.crypto_prev["active_flows"] != 77 + + +def test_get_state_container_legacy_fallback(): + state = st.get_state_container(None) + assert state.network_stack_prev is st.NETWORK_STACK_PREV + assert state.frontend_log_file == st.FRONTEND_LOG_FILE diff --git a/tests/test_syscalls_service.py b/tests/test_syscalls_service.py new file mode 100644 index 0000000..683b9ec --- /dev/null +++ b/tests/test_syscalls_service.py @@ -0,0 +1,35 @@ +"""Tests for ``kernel_ai.services.syscalls``.""" + +from kernel_ai.services import syscalls as svc + + +def test_get_real_system_calls_non_linux_uses_fallback(monkeypatch): + monkeypatch.setattr(svc.platform, "system", lambda: "Darwin") + out = svc.get_real_system_calls( + syscall_names={}, + map_syscall_to_subsystem_fn=lambda _name: "kernel", + kernel_dna_max_procs=10, + fallback_mock_calls_fn=lambda: [{"name": "mock", "count": 1, "subsystem": "kernel"}], + ) + assert out and out[0]["name"] == "mock" + + +def test_get_real_system_calls_linux_empty_proc(monkeypatch): + monkeypatch.setattr(svc.platform, "system", lambda: "Linux") + monkeypatch.setattr(svc.os, "listdir", lambda _path: []) + out = svc.get_real_system_calls( + syscall_names={}, + map_syscall_to_subsystem_fn=lambda _name: "kernel", + kernel_dna_max_procs=10, + fallback_mock_calls_fn=lambda: [{"name": "mock"}], + ) + assert isinstance(out, list) + + +def test_get_softirq_nucleotides_handles_read_error(monkeypatch): + def _boom(*_args, **_kwargs): + raise OSError("nope") + + monkeypatch.setattr("builtins.open", _boom) + out = svc.get_softirq_nucleotides(map_interrupt_to_subsystem_fn=lambda _n: "kernel") + assert out == [] diff --git a/tests/test_system_view_service.py b/tests/test_system_view_service.py new file mode 100644 index 0000000..86a2142 --- /dev/null +++ b/tests/test_system_view_service.py @@ -0,0 +1,21 @@ +"""Tests for ``kernel_ai.services.system_view``.""" + +from kernel_ai.services import system_view as svc + + +def test_parse_cgroup_path_prefers_unified_entry(monkeypatch): + sample = "9:memory:/foo\n0::/unified/path\n" + monkeypatch.setattr(svc._proc_fs, "safe_read_text", lambda _path: sample) + assert svc._parse_cgroup_path(1234) == "/unified/path" + + +def test_get_isolation_context_handles_empty_process_list(monkeypatch): + monkeypatch.setattr(svc.psutil, "process_iter", lambda _fields: []) + out = svc.get_isolation_context() + assert "namespaces" in out + assert out["processes_scanned"] == 0 + + +def test_read_namespace_inode_parses_inode(monkeypatch): + monkeypatch.setattr(svc.os, "readlink", lambda _path: "net:[4026531993]") + assert svc.read_namespace_inode(123, "net") == "4026531993" diff --git a/tests/test_telemetry_orchestration_service.py b/tests/test_telemetry_orchestration_service.py new file mode 100644 index 0000000..28d0b9f --- /dev/null +++ b/tests/test_telemetry_orchestration_service.py @@ -0,0 +1,29 @@ +"""Tests for ``kernel_ai.services.telemetry_orchestration``.""" + +from kernel_ai.services import telemetry_orchestration as svc + + +def test_get_real_system_calls_delegates(monkeypatch): + called = {} + + def fake_get_real_system_calls(**kwargs): + called.update(kwargs) + return [{"name": "read"}] + + monkeypatch.setattr(svc._syscalls_service, "get_real_system_calls", fake_get_real_system_calls) + out = svc.get_real_system_calls() + + assert out == [{"name": "read"}] + assert called["syscall_names"] is svc.SYSCALL_NAMES + assert callable(called["map_syscall_to_subsystem_fn"]) + assert callable(called["fallback_mock_calls_fn"]) + + +def test_get_execution_context_data_delegates(monkeypatch): + def fake_get_execution_context_data(**kwargs): + return {"ok": True, "exec_context_prev": kwargs["exec_context_prev"]} + + monkeypatch.setattr(svc._execution_service, "get_execution_context_data", fake_get_execution_context_data) + out = svc.get_execution_context_data(exec_context_prev={"x": 1}) + assert out["ok"] is True + assert out["exec_context_prev"] == {"x": 1}