deepdebug-local-agent 1.7.0 → 1.8.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.
@@ -0,0 +1,165 @@
1
+ // F3-11b: the detector wired on the REAL v2 session (buildV2Link) against the in-memory reference control plane, with real files.
2
+ // What the CP receives is checked on the wire: 9 metadata fields, and never a word of the error text.
3
+ import { test } from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import fs from "node:fs";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+ import { ReferenceCp, TENANT, AGENT, waitFor } from "../la-protocol-v2/helpers/reference-cp.js";
9
+ import { signingFor } from "../la-protocol-v2/helpers/la-rig.js";
10
+ import { buildV2Link } from "../../src/la-protocol-v2/runtime.js";
11
+ import { createIncidentDetector } from "../../src/incident-detector/detector.js";
12
+ import { resolveIncidentDetectorConfig } from "../../src/incident-detector/config.js";
13
+
14
+ const VECTORS = JSON.parse(fs.readFileSync(path.join(path.dirname(new URL(import.meta.url).pathname), "parity.vectors.json"), "utf8"));
15
+ const tmp = () => fs.mkdtempSync(path.join(os.tmpdir(), "la-incw-"));
16
+ const GH = "ghp_" + "A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6Q7r8";
17
+ const CANARIES = ["CANARY-MSG-7731", "CanaryVault", "/home/customer/secret-app", "order-9001-detail", GH];
18
+
19
+ async function linkFor(cp, env, root = tmp()) {
20
+ const logs = [];
21
+ const log = { info: (m) => logs.push(m), log: (m) => logs.push(m), warn: (m) => logs.push(m), error: (m) => logs.push(m) };
22
+ const wsManager = { list: () => [{ id: "ws-1", root, tenantId: TENANT }] };
23
+ const link = await buildV2Link({ tenantId: TENANT, agentId: AGENT, signing: signingFor({ keyId: cp.keyId, publicKeyRaw: cp.publicKeyRaw }, "enforce"), wsManager, getWorkspaceRoot: () => root, log, env });
24
+ const transport = { send: (t) => cp.feed(t), close() {}, bufferedAmount: () => 0 };
25
+ cp.attach((t) => queueMicrotask(() => link.session.handleText(t, transport)), () => {});
26
+ link.session.attach(transport);
27
+ await waitFor(() => link.session.state === "ready");
28
+ return { link, logs };
29
+ }
30
+ const candidates = (cp) => cp.received.filter((f) => f.type === "incident.candidate");
31
+ const envFor = (dir, extra = {}) => ({
32
+ LOCAL_AGENT_INCIDENT_DETECTOR: "true", LOCAL_AGENT_INCIDENT_LOG_FILES: path.join(dir, "billing-api.log"), LOCAL_AGENT_INCIDENT_STORE_DIR: path.join(dir, "store"),
33
+ LOCAL_AGENT_INCIDENT_FILE_POLL_MS: "50", LOCAL_AGENT_INCIDENT_FLUSH_MS: "300", LOCAL_AGENT_DLP_MODE: "enforce", ...extra,
34
+ });
35
+
36
+ test("flag off (unset or any non-true value): no capability, nothing started, the log file is not read, no store dir; flag on without a source warns and starts nothing", async () => {
37
+ for (const v of [undefined, "false", "maybe"]) {
38
+ const dir = tmp();
39
+ fs.writeFileSync(path.join(dir, "billing-api.log"), "");
40
+ const cp = new ReferenceCp();
41
+ const { link } = await linkFor(cp, { LOCAL_AGENT_INCIDENT_DETECTOR: v, LOCAL_AGENT_INCIDENT_LOG_FILES: path.join(dir, "billing-api.log"), LOCAL_AGENT_INCIDENT_STORE_DIR: path.join(dir, "store") });
42
+ fs.appendFileSync(path.join(dir, "billing-api.log"), "2026-10-29 10:00:00 ERROR java.lang.IllegalStateException: boom\n");
43
+ await new Promise((r) => setTimeout(r, 150));
44
+ assert.equal(link.incidents, undefined);
45
+ assert.ok(!cp.registers[0].capabilities.includes("incident.candidate"));
46
+ assert.equal(candidates(cp).length, 0);
47
+ assert.ok(!fs.existsSync(path.join(dir, "store")), "no store dir");
48
+ link.close();
49
+ }
50
+ const cp = new ReferenceCp();
51
+ const { link, logs } = await linkFor(cp, { LOCAL_AGENT_INCIDENT_DETECTOR: "true" });
52
+ assert.equal(link.incidents, undefined);
53
+ assert.ok(logs.some((l) => /no source is configured/.test(l)));
54
+ assert.ok(!cp.registers[0].capabilities.includes("incident.candidate"));
55
+ link.close();
56
+ });
57
+
58
+ test("flag on + file source: candidates carry the 9 metadata fields and nothing of the text; the detail is in the local store; the capability is announced", async () => {
59
+ const dir = tmp(); const file = path.join(dir, "billing-api.log");
60
+ fs.writeFileSync(file, "");
61
+ const cp = new ReferenceCp();
62
+ const { link } = await linkFor(cp, envFor(dir));
63
+ assert.ok(cp.registers[0].capabilities.includes("incident.candidate"));
64
+ await new Promise((r) => setTimeout(r, 120)); // the tailer has taken its offset
65
+ const stack = `\tat com.acme.CanaryVault.open(CanaryVault.java:44)\n\tat com.acme.Main.run(Main.java:9)\n`;
66
+ fs.appendFileSync(file,
67
+ `2026-10-29 10:00:00 ERROR c.a.Order : java.lang.IllegalStateException: CANARY-MSG-7731 order 9001 not found\n`
68
+ + `java.lang.IllegalStateException: CANARY-MSG-7731 order-9001-detail at /home/customer/secret-app token ${GH}\n` + stack
69
+ + "2026-10-29 10:00:01 INFO nothing to see\n");
70
+ await waitFor(() => candidates(cp).length >= 1, 4000);
71
+ const c = candidates(cp)[0];
72
+ assert.deepEqual(Object.keys(c.payload).sort(), ["count", "environment", "errorHash", "fingerprint", "firstSeen", "lastSeen", "logCategory", "service", "severity"]);
73
+ assert.equal(c.payload.service, "billing-api");
74
+ assert.equal(c.payload.environment, "production");
75
+ assert.equal(c.payload.severity, "high");
76
+ assert.equal(c.payload.logCategory, "exception");
77
+ assert.equal(c.payload.count, 1);
78
+ assert.match(c.payload.errorHash, /^[0-9a-f]{32}$/);
79
+ assert.match(c.payload.fingerprint, /^[0-9a-f]{64}$/);
80
+ assert.equal(c.workspaceId, "");
81
+ // NOTHING of the text on the wire (every raw frame of the session, both the handshake and the data)
82
+ const wire = cp.rawIn.join("\n");
83
+ for (const w of CANARIES) assert.ok(!wire.includes(w), `the wire must not carry ${w.slice(0, 12)}...`);
84
+ // the detail stayed local, masked
85
+ const detail = await link.incidents.getDetail(c.payload.errorHash);
86
+ assert.equal(detail.length, 1);
87
+ assert.ok(detail[0].message.includes("CANARY-MSG-7731"));
88
+ assert.ok(!JSON.stringify(detail).includes(GH), "the DLP masked the token before it was stored");
89
+ assert.ok(detail[0].stack.includes("CanaryVault"));
90
+ assert.equal(detail[0].stackBytes, Buffer.byteLength(`java.lang.IllegalStateException: CANARY-MSG-7731 order-9001-detail at /home/customer/secret-app token ${GH}\n` + stack.replace(/\n$/, "")), "the size of the stack is local metadata");
91
+ assert.equal(fs.statSync(path.join(dir, "store", "incident-detail.jsonl")).mode & 0o777, 0o600);
92
+ // the same error with other ids: same errorHash, ONE window: the count grows in a batched update
93
+ for (let i = 0; i < 5; i++) fs.appendFileSync(file, `2026-10-29 10:00:0${2 + i} ERROR c.a.Order : java.lang.IllegalStateException: CANARY-MSG-7731 order ${9100 + i} not found\n`);
94
+ await waitFor(() => candidates(cp).length >= 2, 4000);
95
+ const u = candidates(cp).at(-1).payload;
96
+ assert.equal(u.firstSeen, c.payload.firstSeen, "same window");
97
+ assert.ok(u.count >= 2);
98
+ const wire2 = cp.rawIn.join("\n");
99
+ for (const w of CANARIES) assert.ok(!wire2.includes(w));
100
+ // the audit has no text either
101
+ assert.ok(!JSON.stringify(link.incidents.audit.tail(100)).includes("CANARY"));
102
+ link.close();
103
+ });
104
+
105
+ test("flag on: noise, INFO/WARN, duplicates and an entry without a message produce no candidate", async () => {
106
+ const dir = tmp(); const file = path.join(dir, "billing-api.log");
107
+ fs.writeFileSync(file, "");
108
+ const cp = new ReferenceCp();
109
+ const { link } = await linkFor(cp, envFor(dir));
110
+ await new Promise((r) => setTimeout(r, 120));
111
+ fs.appendFileSync(file, [
112
+ '2026-10-29 10:00:00 ERROR {code=9, message=Ready condition status changed to False}',
113
+ "2026-10-29 10:00:01 WARN java.lang.RuntimeException: not an error level",
114
+ "2026-10-29 10:00:02 ERROR GET /actuator/health Exception",
115
+ "2026-10-29 10:00:03 ERROR plain failure without any marker",
116
+ "2026-10-29 10:00:04 ERROR org.apache.maven.lifecycle.LifecycleExecutionException: build",
117
+ "",
118
+ ].join("\n"));
119
+ await new Promise((r) => setTimeout(r, 500));
120
+ assert.equal(candidates(cp).length, 0);
121
+ const s = link.incidents.detector.stats;
122
+ assert.equal(s.noise, 1, "the Maven build failure is significant to ErrorDetector but infra/build noise to LogNoiseClassifier");
123
+ assert.equal(s.insignificant, 4, JSON.stringify(s));
124
+ assert.equal(s.detected, 0);
125
+ link.close();
126
+ });
127
+
128
+ // ------------------------------------------------------------------------------------------ the Java vectors through the whole pipeline
129
+
130
+ test("parity end to end: every significant vector goes through detector -> aggregator -> emitter and the candidate carries the Java errorHash and fingerprint", async () => {
131
+ const sent = [];
132
+ const session = { state: "ready", on() {}, off() {}, async send(type, payload) { sent.push(payload); } };
133
+ const { createCandidateEmitter } = await import("../../src/incident-detector/emitter.js");
134
+ const { createDetailStore } = await import("../../src/incident-detector/store.js");
135
+ const config = resolveIncidentDetectorConfig({ LOCAL_AGENT_INCIDENT_DETECTOR: "1", LOCAL_AGENT_INCIDENT_STORE_DIR: path.join(tmp(), "s"), LOCAL_AGENT_INCIDENT_MAX_PENDING: "5000", LOCAL_AGENT_INCIDENT_MAX_EMITS_PER_MINUTE: "600", LOCAL_AGENT_INCIDENT_MAX_TRACKED: "5000" }, () => {});
136
+ config.emitBurst = 100_000;
137
+ const emitter = createCandidateEmitter({ session, config, env: {}, log: { warn() {}, info() {}, log() {} } });
138
+ const store = createDetailStore({ dir: config.storeDir, maxBytes: 8 << 20, maxQueue: 100, messageChars: 1000, stackChars: 1000, samplesPerWindow: 1, env: {} });
139
+ const det = createIncidentDetector({ config, emitter, store, log: console });
140
+ let expected = 0;
141
+ const want = new Map();
142
+ for (const c of VECTORS.cases) {
143
+ const e = c.expect;
144
+ if (!e.significant || e.noiseClass !== "app_error" || e.fingerprint == null) continue;
145
+ const svc = `svc-${expected}`;
146
+ await det.ingest({ severity: c.severity, message: c.message, stacktrace: c.stack, service: svc, tsMs: 1_700_000_000_000 + expected });
147
+ want.set(svc, e);
148
+ expected++;
149
+ }
150
+ await emitter.flush();
151
+ assert.ok(expected >= 40, `${expected} significant vectors`);
152
+ // errorHash is shared by cases with the same normalised first line: one window per errorHash, so compare per hash
153
+ const byHash = new Map(sent.map((p) => [p.errorHash, p]));
154
+ const hashes = new Set([...want.values()].map((e) => e.groupHash));
155
+ assert.equal(byHash.size, hashes.size, "one window per Java errorHash");
156
+ for (const e of want.values()) {
157
+ const p = byHash.get(e.groupHash);
158
+ assert.ok(p, "candidate for " + e.groupHash);
159
+ }
160
+ // the first occurrence of each hash carries ITS fingerprint (first one wins, as in incident_dedup_log)
161
+ const firstOfHash = new Map();
162
+ for (const e of want.values()) if (!firstOfHash.has(e.groupHash)) firstOfHash.set(e.groupHash, e);
163
+ for (const [h, e] of firstOfHash) assert.equal(byHash.get(h).fingerprint, e.fingerprint, "Java fingerprint of the first occurrence of " + h);
164
+ emitter.close();
165
+ });