From 53aa68ac64b6947640fc5459c8748fecece85686 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 17:31:52 +0000 Subject: [PATCH] Refactor memory telemetry to use util.debuglog Replaced `console.log` with `util.debuglog('torch-memory')` in `src/services/memory/index.js` to prevent stdout pollution. Telemetry and metrics are now logged to stderr only when `NODE_DEBUG=torch-memory` is set, improving library behavior and observability control. Added regression test `test/memory-telemetry.test.mjs`. Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- src/services/memory/index.js | 6 +- test/memory-telemetry.test.mjs | 102 +++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 test/memory-telemetry.test.mjs diff --git a/src/services/memory/index.js b/src/services/memory/index.js index 494dc1d..a6f1321 100644 --- a/src/services/memory/index.js +++ b/src/services/memory/index.js @@ -6,8 +6,10 @@ import { startScheduler } from './scheduler.js'; import { getMemoryPruneMode, isMemoryIngestEnabled } from './feature-flags.js'; import fs from 'node:fs'; import path from 'node:path'; +import util from 'node:util'; const MEMORY_FILE_PATH = path.join(process.cwd(), '.scheduler-memory', 'memory-store.json'); +const debug = util.debuglog('torch-memory'); function loadMemoryStore() { try { @@ -113,7 +115,7 @@ function buildTelemetryEmitter(options = {}) { } return (event, payload) => { - console.log('memory_telemetry', { + debug('memory_telemetry', { event, payload: toSafeTelemetryPayload(payload), ts: Date.now(), @@ -132,7 +134,7 @@ function emitMetric(options = {}, metric, payload) { return; } - console.log('memory_metric', { metric, payload, ts: Date.now() }); + debug('memory_metric', { metric, payload, ts: Date.now() }); } function applyMemoryFilters(memories, filters = {}) { diff --git a/test/memory-telemetry.test.mjs b/test/memory-telemetry.test.mjs new file mode 100644 index 0000000..d233415 --- /dev/null +++ b/test/memory-telemetry.test.mjs @@ -0,0 +1,102 @@ + +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { spawn } from 'node:child_process'; +import path from 'node:path'; +import fs from 'node:fs'; + +const FIXTURE_PATH = path.join(process.cwd(), 'test', 'memory-telemetry-check.mjs'); + +const FIXTURE_CONTENT = ` +import { ingestEvents } from '../src/services/memory/index.js'; + +// Mock process.stdout.write to capture output +const originalWrite = process.stdout.write; +let output = ''; +process.stdout.write = (chunk, encoding, callback) => { + output += chunk.toString(); + return originalWrite.call(process.stdout, chunk, encoding, callback); +}; + +// Also mock process.env to ensure ingest is DISABLED to trigger skip telemetry +process.env.TORCH_MEMORY_ENABLED = 'true'; +process.env.TORCH_MEMORY_INGEST_ENABLED = 'off'; + +async function run() { + console.log('--- Start Test ---'); + + // Call ingestEvents which should trigger telemetry + await ingestEvents([], { agent_id: 'test-agent' }); + + console.log('--- End Test ---'); + + if (output.includes('memory_telemetry')) { + console.log('\\n[FAIL] Found memory_telemetry in stdout (Pollution detected)'); + process.exit(1); + } else { + console.log('\\n[PASS] Did not find memory_telemetry in stdout (Clean)'); + } +} + +run().catch(console.error); +`; + +test.before(() => { + fs.writeFileSync(FIXTURE_PATH, FIXTURE_CONTENT); +}); + +test.after(() => { + if (fs.existsSync(FIXTURE_PATH)) { + fs.unlinkSync(FIXTURE_PATH); + } +}); + +// Helper to run the script and capture output +function runScript(env = {}) { + return new Promise((resolve) => { + const child = spawn(process.execPath, [FIXTURE_PATH], { + env: { ...process.env, ...env }, + stdio: ['ignore', 'pipe', 'pipe'] + }); + + let stdout = ''; + let stderr = ''; + + child.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + child.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + child.on('close', (code) => { + // The script exits with 0 on pass, 1 on fail + resolve({ code, stdout, stderr }); + }); + }); +} + +test('telemetry should not log to stdout or stderr by default', async () => { + const { code, stdout, stderr } = await runScript(); + + assert.equal(code, 0, 'Script should pass'); + // The pass message contains "memory_telemetry", so we check for the FAIL condition specifically + assert.doesNotMatch(stdout, /Pollution detected/, 'stdout should not indicate pollution'); + assert.match(stdout, /\[PASS\]/, 'stdout should indicate pass'); + + // stderr should be empty or minimal (no debug logs) + assert.doesNotMatch(stderr, /memory_telemetry/, 'stderr should not contain telemetry payload'); +}); + +test('telemetry should log to stderr when NODE_DEBUG=torch-memory is set', async () => { + const { code, stdout, stderr } = await runScript({ NODE_DEBUG: 'torch-memory' }); + + assert.equal(code, 0, 'Script should pass (stdout is clean)'); + assert.doesNotMatch(stdout, /Pollution detected/, 'stdout should not indicate pollution'); + + // stderr should contain the debug log + // util.debuglog format: "TORCH-MEMORY : " + assert.match(stderr, /TORCH-MEMORY/, 'stderr should contain debug tag'); + assert.match(stderr, /memory_telemetry/, 'stderr should contain telemetry event name'); +});