Files
ad_toolbox/vscode-appdaemon/tests/logExplorer.test.js
T
2026-09-27 22:54:08 +02:00

63 lines
3.8 KiB
JavaScript

const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const os = require('os');
const path = require('path');
const Module = require('module');
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ad-log-test-'));
const originalLoad = Module._load;
let messages = [];
const vscode = {workspace:{workspaceFolders:[{uri:{fsPath:root}}], getConfiguration:()=>({get:(key, fallback)=>fallback})}};
Module._load = function(id, ...args) { return id === 'vscode' ? vscode : originalLoad.call(this, id, ...args); };
const {LogExplorer} = require('../out/logExplorer');
Module._load = originalLoad;
function explorer() { const e = new LogExplorer({}); const webview={postMessage:m=>messages.push(m)}; e.panel={webview}; e.activeWebview=webview; return e; }
function latest() { return messages.at(-1)?.entries || []; }
fs.mkdirSync(path.join(root,'logs'));
const file=path.join(root,'logs/appdaemon.log');
test('follows incomplete UTF-8 lines and multiline exceptions',()=>{
const e=explorer(); messages=[];
const first='27-09-26 10:12:13 ERROR app: Échec';
const bytes=Buffer.from(first+'\n'); const split=bytes.indexOf(Buffer.from('É'))+1;
fs.writeFileSync(file,bytes.subarray(0,split));e.poll();assert.equal(latest().length,0);
fs.appendFileSync(file,Buffer.concat([bytes.subarray(split),Buffer.from(' File "app.py", line 42\nRuntimeError: failed\n')]));e.poll();
assert.equal(latest().length,1);assert.match(latest()[0].message,/Échec\n File/);assert.match(latest()[0].message,/RuntimeError/);
});
test('detects same-sized inode replacement and truncation',()=>{
const e=explorer();messages=[];
fs.writeFileSync(file,'27-09-26 10:12:13 INFO app: before\n');e.poll();
fs.renameSync(file,file+'.old');fs.writeFileSync(file,'27-09-26 10:12:14 INFO app: after!\n');e.poll();
assert.ok(latest().some(x=>x.message==='after!'));
fs.writeFileSync(file,'27-09-26 10:12:15 INFO a: x\n');e.poll();assert.ok(latest().some(x=>x.message==='x'));
});
test('reads base text logs and ignores structured events by default',()=>{
const e=explorer();messages=[];
fs.writeFileSync(file,'27-09-26 10:12:13 ERROR app: failed\n27-09-26 10:12:14 INFO AppDaemon: AppDaemon is ready\n');
fs.writeFileSync(path.join(root,'logs/error.log'),'27-09-26 10:12:13 ERROR app: failed\n');
fs.writeFileSync(path.join(root,'logs/events.jsonl'),JSON.stringify({version:1,event_id:'trace-only',timestamp:'2026-09-27T08:12:14Z',level:'INFO',kind:'evaluation',message:'not a raw log',app:'app',cause_ids:[]})+'\n');e.poll();
assert.equal(latest().filter(e=>e.message==='failed').length,1);
assert.ok(latest().some(e=>e.app==='AppDaemon' && e.message==='AppDaemon is ready'));
assert.equal(latest().some(e=>e.event_id==='trace-only'),false);
});
test('bounds retained history',()=>{
const e=explorer();messages=[];
fs.writeFileSync(file,Array.from({length:10500},(_,i)=>`27-09-26 10:12:13 INFO app: message ${i}\n`).join(''));
e.poll();e.poll();assert.ok(e.entries.size<=10000);
});
process.on('exit',()=>fs.rmSync(root,{recursive:true,force:true}));
test('groups plain app.log lines between explicit initialization markers',()=>{
const e=explorer();messages=[];
fs.writeFileSync(file,[
'27-09-26 10:12:13 INFO kitchen: Initialization started [group=initialization:0123456789ab]',
'27-09-26 10:12:13 INFO kitchen: Setting up an output through app.log()',
'27-09-26 10:12:14 INFO kitchen: Configured helper [group=initialization:0123456789ab]',
'27-09-26 10:12:14 INFO kitchen: Initialization completed [group=initialization:0123456789ab]'
].join('\n')+'\n');e.poll();
const grouped=latest().filter(entry=>entry.app==='kitchen');
assert.equal(grouped.length,4);
assert.deepEqual(grouped.map(entry=>entry.initialization_id),Array(4).fill('0123456789ab'));
assert.equal(grouped[1].group_inferred,true);
assert.equal(grouped[2].group_inferred,undefined);
});