basant307/AI_Governance_Project
048
1/**2 * @license3 * Copyright 2025 Qwen Team4 * SPDX-License-Identifier: Apache-2.05 */6import * as os from 'node:os';7import * as path from 'node:path';8import { mkdtempSync, readFileSync, existsSync, writeFileSync, rmSync, realpathSync, lstatSync, } from 'node:fs';9import { describe, it, expect, afterEach, beforeEach } from 'vitest';10import { buildDaemonLogLine, initDaemonLogger } from './daemon-logger.js';11describe('buildDaemonLogLine', () => {12 const FIXED = new Date('2026-05-26T03:14:15.926Z');13 it('formats INFO with no ctx', () => {14 expect(buildDaemonLogLine({15 level: 'INFO',16 message: 'daemon started',17 now: FIXED,18 })).toBe('2026-05-26T03:14:15.926Z [INFO] [DAEMON] daemon started\n');19 });20 it('renders ctx fields in fixed order', () => {21 const line = buildDaemonLogLine({22 level: 'ERROR',23 message: 'route failed',24 now: FIXED,25 ctx: {26 sessionId: 'sess-1',27 route: 'POST /session/:id/prompt',28 clientId: 'client-x',29 childPid: 4242,30 channelId: 'ch-9',31 },32 });33 expect(line).toBe('2026-05-26T03:14:15.926Z [ERROR] [DAEMON] ' +34 'route=POST /session/:id/prompt sessionId=sess-1 clientId=client-x ' +35 'childPid=4242 channelId=ch-9 route failed\n');36 });37 it('appends extra ctx keys sorted lexicographically after fixed keys', () => {38 const line = buildDaemonLogLine({39 level: 'WARN',40 message: 'note',41 now: FIXED,42 ctx: { zeta: 1, alpha: 'a', sessionId: 's' },43 });44 expect(line).toBe('2026-05-26T03:14:15.926Z [WARN] [DAEMON] sessionId=s alpha=a zeta=1 note\n');45 });46 it('JSON.stringify-quotes values that contain spaces or =', () => {47 const line = buildDaemonLogLine({48 level: 'INFO',49 message: 'hi',50 now: FIXED,51 ctx: { weird: 'has space', eq: 'a=b' },52 });53 expect(line).toBe('2026-05-26T03:14:15.926Z [INFO] [DAEMON] eq="a=b" weird="has space" hi\n');54 });55 it('appends error stack as indented continuation lines', () => {56 const err = new Error('boom');57 err.stack =58 'Error: boom\n at fn (file.ts:1:1)\n at main (file.ts:2:2)';59 const line = buildDaemonLogLine({60 level: 'ERROR',61 message: 'failed',62 now: FIXED,63 err,64 });65 expect(line).toBe('2026-05-26T03:14:15.926Z [ERROR] [DAEMON] failed\n' +66 ' Error: boom\n' +67 ' at fn (file.ts:1:1)\n' +68 ' at main (file.ts:2:2)\n');69 });70 it('falls back to err.message when stack missing', () => {71 const err = { name: 'Plain', message: 'no stack' };72 const line = buildDaemonLogLine({73 level: 'ERROR',74 message: 'failed',75 now: FIXED,76 err,77 });78 expect(line).toBe('2026-05-26T03:14:15.926Z [ERROR] [DAEMON] failed\n' +79 ' Plain: no stack\n');80 });81});82describe('initDaemonLogger opt-out', () => {83 const originalEnv = process.env['QWEN_DAEMON_LOG_FILE'];84 afterEach(() => {85 if (originalEnv === undefined)86 delete process.env['QWEN_DAEMON_LOG_FILE'];87 else88 process.env['QWEN_DAEMON_LOG_FILE'] = originalEnv;89 });90 for (const val of ['0', 'false', 'off', 'no', 'False', ' OFF ']) {91 it(`returns no-op logger when QWEN_DAEMON_LOG_FILE=${JSON.stringify(val)}`, () => {92 process.env['QWEN_DAEMON_LOG_FILE'] = val;93 const stderr = [];94 const logger = initDaemonLogger({95 boundWorkspace: '/tmp/ws',96 baseDir: '/tmp/nonexistent-should-not-touch',97 stderr: (s) => stderr.push(s),98 });99 logger.info('hello');100 logger.warn('there');101 logger.error('boom');102 logger.raw('raw');103 expect(stderr).toEqual([]); // no-op = nothing104 expect(logger.getLogPath()).toBe('');105 expect(logger.getDaemonId()).toBe('');106 });107 }108});109describe('initDaemonLogger file init', () => {110 let tmp;111 beforeEach(() => {112 tmp = mkdtempSync(path.join(os.tmpdir(), 'daemon-log-'));113 });114 afterEach(() => {115 try {116 rmSync(tmp, { recursive: true, force: true });117 }118 catch {119 // cleanup best-effort120 }121 });122 it('derives daemon-scoped daemon-id and creates log file', () => {123 const logger = initDaemonLogger({124 boundWorkspace: '/workspace/foo',125 pid: 1234,126 baseDir: tmp,127 });128 expect(logger.getDaemonId()).toBe('daemon:1234');129 expect(logger.getLogPath()).toBe(path.join(tmp, 'daemon', 'serve-1234.log'));130 expect(existsSync(logger.getLogPath())).toBe(true);131 expect(readFileSync(logger.getLogPath(), 'utf8')).toMatch(/\[INFO\] \[DAEMON\] workspace=\/workspace\/foo workspaceHash=[0-9a-f]{8} daemon started pid=1234/);132 });133 it('falls back to no-op when mkdir fails', () => {134 const stderr = [];135 // Create a file where the directory should be -> mkdir EEXIST/ENOTDIR136 const blockingFile = path.join(tmp, 'daemon');137 writeFileSync(blockingFile, 'blocker');138 const logger = initDaemonLogger({139 boundWorkspace: '/w',140 pid: 1,141 baseDir: tmp,142 stderr: (s) => stderr.push(s),143 });144 expect(logger.getLogPath()).toBe('');145 expect(stderr.join('\n')).toMatch(/daemon log disabled/);146 expect(() => logger.info('after')).not.toThrow();147 });148});149describe('initDaemonLogger raw', () => {150 let tmp;151 beforeEach(() => {152 tmp = mkdtempSync(path.join(os.tmpdir(), 'daemon-log-'));153 });154 afterEach(() => {155 try {156 rmSync(tmp, { recursive: true, force: true });157 }158 catch {159 // cleanup best-effort160 }161 });162 it('appends prefixed line, no stderr tee', async () => {163 const stderr = [];164 const logger = initDaemonLogger({165 boundWorkspace: '/w',166 pid: 1,167 baseDir: tmp,168 stderr: (s) => stderr.push(s),169 });170 const stderrBefore = stderr.length;171 logger.raw('[serve pid=123 cwd=/x] child crashed', 'warn');172 logger.raw('[serve pid=123 cwd=/x] another');173 await logger.flush();174 const content = readFileSync(logger.getLogPath(), 'utf8');175 expect(content).toContain('[WARN] [DAEMON] [serve pid=123 cwd=/x] child crashed\n');176 expect(content).toContain('[INFO] [DAEMON] [serve pid=123 cwd=/x] another\n');177 // No new stderr lines from raw()178 expect(stderr.length).toBe(stderrBefore);179 });180});181describe('initDaemonLogger info/warn/error', () => {182 let tmp;183 beforeEach(() => {184 tmp = mkdtempSync(path.join(os.tmpdir(), 'daemon-log-'));185 });186 afterEach(() => {187 try {188 rmSync(tmp, { recursive: true, force: true });189 }190 catch {191 // cleanup best-effort192 }193 });194 it('info appends to file and tees to stderr', async () => {195 const stderr = [];196 const fixed = new Date('2026-05-26T03:14:15.926Z');197 const logger = initDaemonLogger({198 boundWorkspace: '/w',199 pid: 1,200 baseDir: tmp,201 stderr: (s) => stderr.push(s),202 now: () => fixed,203 });204 logger.info('hello', { route: 'GET /' });205 await logger.flush();206 const content = readFileSync(logger.getLogPath(), 'utf8');207 expect(content).toContain('[INFO] [DAEMON] route=GET / hello\n');208 // Stderr saw the same line (after boot banner, which isn't teed here).209 const teedLines = stderr.filter((s) => s.includes('[INFO] [DAEMON]'));210 expect(teedLines).toHaveLength(1);211 });212 it('error appends err.stack as continuation', async () => {213 const logger = initDaemonLogger({214 boundWorkspace: '/w',215 pid: 1,216 baseDir: tmp,217 });218 const err = new Error('boom');219 logger.error('route failed', err, { route: 'POST /x' });220 await logger.flush();221 const content = readFileSync(logger.getLogPath(), 'utf8');222 expect(content).toMatch(/\[ERROR\] \[DAEMON\] route=POST \/x route failed\n {2}Error: boom/);223 });224 it('flush awaits all pending appends', async () => {225 const logger = initDaemonLogger({226 boundWorkspace: '/w',227 pid: 1,228 baseDir: tmp,229 });230 for (let i = 0; i < 50; i++)231 logger.info(`msg-${i}`);232 await logger.flush();233 const lines = readFileSync(logger.getLogPath(), 'utf8').split('\n');234 const msgLines = lines.filter((l) => /msg-\d+$/.test(l));235 expect(msgLines).toHaveLength(50);236 for (let i = 0; i < 50; i++) {237 expect(msgLines[i]).toContain(`msg-${i}`);238 }239 });240 it('warns once on append failure and keeps trying', async () => {241 const logger = initDaemonLogger({242 boundWorkspace: '/w',243 pid: 1,244 baseDir: tmp,245 stderr: () => { },246 });247 // Sabotage by removing the parent directory so subsequent appendFile fails with ENOENT.248 rmSync(path.dirname(logger.getLogPath()), { recursive: true, force: true });249 logger.info('after-rm-1');250 logger.info('after-rm-2');251 await logger.flush();252 // No throw — degraded path swallows.253 });254});255describe('initDaemonLogger latest symlink', () => {256 let tmp;257 beforeEach(() => {258 tmp = mkdtempSync(path.join(os.tmpdir(), 'daemon-log-'));259 });260 afterEach(() => {261 try {262 rmSync(tmp, { recursive: true, force: true });263 }264 catch {265 // cleanup best-effort266 }267 });268 it('creates daemon/latest pointing to the current log', async () => {269 const logger = initDaemonLogger({270 boundWorkspace: '/w',271 pid: 42,272 baseDir: tmp,273 });274 // Allow the async symlink to settle.275 await new Promise((r) => setTimeout(r, 50));276 const linkPath = path.join(tmp, 'daemon', 'latest');277 expect(lstatSync(linkPath).isSymbolicLink()).toBe(true);278 expect(realpathSync(linkPath)).toBe(realpathSync(logger.getLogPath()));279 });280 it('updates latest on subsequent init in same dir', async () => {281 const a = initDaemonLogger({ boundWorkspace: '/w', pid: 1, baseDir: tmp });282 await new Promise((r) => setTimeout(r, 50));283 const b = initDaemonLogger({ boundWorkspace: '/w', pid: 2, baseDir: tmp });284 await new Promise((r) => setTimeout(r, 50));285 expect(realpathSync(path.join(tmp, 'daemon', 'latest'))).toBe(realpathSync(b.getLogPath()));286 expect(realpathSync(a.getLogPath())).not.toBe(realpathSync(b.getLogPath()));287 });288});289//# sourceMappingURL=daemon-logger.test.js.map