fix: remove dead delete+recreate and pin code, add poll fallback test
Phase 1 cleanup: - Remove deletePost() method (dead code, replaced by PUT in-place updates) - Remove _postInfo Map tracking (no longer needed) - Remove pin/unpin API calls from watcher-manager.js (incompatible with PUT updates) - Add JSDoc note on (edited) label limitation in _flushUpdate() - Add integration test: test/integration/poll-fallback.test.js - Fix addSession() lastOffset===0 falsy bug (0 was treated as 'no offset') - Fix pre-existing test failures: add lastOffset:0 where tests expect backlog reads - Fix pre-existing session-monitor test: create stub transcript files - Fix pre-existing status-formatter test: update indent check for blockquote format - Format plugin/ files with Prettier (pre-existing formatting drift)
This commit is contained in:
77
test/integration/poll-fallback.test.js
Normal file
77
test/integration/poll-fallback.test.js
Normal file
@@ -0,0 +1,77 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Integration test: poll-fallback
|
||||
*
|
||||
* Verifies that StatusWatcher detects file changes via the polling fallback
|
||||
* (not just fs.watch). Creates a temp JSONL file, initializes a StatusWatcher,
|
||||
* appends a line, and asserts the 'session-update' event fires within 1000ms.
|
||||
*/
|
||||
|
||||
var describe = require('node:test').describe;
|
||||
var it = require('node:test').it;
|
||||
var beforeEach = require('node:test').beforeEach;
|
||||
var afterEach = require('node:test').afterEach;
|
||||
var assert = require('node:assert/strict');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var os = require('os');
|
||||
|
||||
var StatusWatcher = require('../../src/status-watcher').StatusWatcher;
|
||||
|
||||
function createTmpDir() {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'poll-fallback-test-'));
|
||||
}
|
||||
|
||||
describe('StatusWatcher poll fallback', function () {
|
||||
var tmpDir;
|
||||
var watcher;
|
||||
|
||||
beforeEach(function () {
|
||||
tmpDir = createTmpDir();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
if (watcher) {
|
||||
watcher.stop();
|
||||
watcher = null;
|
||||
}
|
||||
try {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
} catch (_e) {
|
||||
/* ignore */
|
||||
}
|
||||
});
|
||||
|
||||
it('emits session-update within 1000ms when a line is appended to the JSONL file', function (t, done) {
|
||||
var transcriptFile = path.join(tmpDir, 'test-session.jsonl');
|
||||
fs.writeFileSync(transcriptFile, '');
|
||||
|
||||
watcher = new StatusWatcher({ transcriptDir: tmpDir, idleTimeoutS: 600 });
|
||||
|
||||
// Use a saved offset of 0 so we read from beginning
|
||||
watcher.addSession('test:poll', transcriptFile, { lastOffset: 0 });
|
||||
|
||||
var timer = setTimeout(function () {
|
||||
done(new Error('session-update event not received within 1000ms'));
|
||||
}, 1000);
|
||||
|
||||
watcher.once('session-update', function (sessionKey) {
|
||||
clearTimeout(timer);
|
||||
assert.equal(sessionKey, 'test:poll');
|
||||
done();
|
||||
});
|
||||
|
||||
// Append a valid JSONL line after a short delay to allow watcher setup
|
||||
setTimeout(function () {
|
||||
var record = {
|
||||
type: 'message',
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: [{ type: 'text', text: 'Poll fallback test line' }],
|
||||
},
|
||||
};
|
||||
fs.appendFileSync(transcriptFile, JSON.stringify(record) + '\n');
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,17 @@ function writeSessionsJson(dir, agentId, sessions) {
|
||||
const agentDir = path.join(dir, agentId, 'sessions');
|
||||
fs.mkdirSync(agentDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(agentDir, 'sessions.json'), JSON.stringify(sessions, null, 2));
|
||||
// Create stub transcript files so SessionMonitor's stale-check doesn't skip them
|
||||
for (const key of Object.keys(sessions)) {
|
||||
const entry = sessions[key]; // eslint-disable-line security/detect-object-injection
|
||||
const sessionId = entry.sessionId || entry.uuid;
|
||||
if (sessionId) {
|
||||
const transcriptPath = path.join(agentDir, `${sessionId}.jsonl`);
|
||||
if (!fs.existsSync(transcriptPath)) {
|
||||
fs.writeFileSync(transcriptPath, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
|
||||
@@ -86,7 +86,8 @@ describe('StatusWatcher', () => {
|
||||
appendLine(file, { type: 'tool_call', name: 'exec', id: '1' });
|
||||
|
||||
watcher = new StatusWatcher({ transcriptDir: tmpDir, idleTimeoutS: 600 });
|
||||
watcher.addSession('test:key', file);
|
||||
// lastOffset: 0 explicitly reads from beginning (no lastOffset = skip to end)
|
||||
watcher.addSession('test:key', file, { lastOffset: 0 });
|
||||
|
||||
const state = watcher.getSessionState('test:key');
|
||||
assert.ok(state.lines.length > 0);
|
||||
@@ -119,7 +120,7 @@ describe('StatusWatcher', () => {
|
||||
appendLine(file, { type: 'tool_result', name: 'exec', id: '1' });
|
||||
|
||||
watcher = new StatusWatcher({ transcriptDir: tmpDir, idleTimeoutS: 600 });
|
||||
watcher.addSession('test:key', file);
|
||||
watcher.addSession('test:key', file, { lastOffset: 0 });
|
||||
|
||||
const state = watcher.getSessionState('test:key');
|
||||
assert.equal(state.pendingToolCalls, 0);
|
||||
@@ -137,7 +138,7 @@ describe('StatusWatcher', () => {
|
||||
appendLine(file, { type: 'tool_result', name: 'exec', id: '1' });
|
||||
|
||||
watcher = new StatusWatcher({ transcriptDir: tmpDir, idleTimeoutS: 600 });
|
||||
watcher.addSession('test:key', file);
|
||||
watcher.addSession('test:key', file, { lastOffset: 0 });
|
||||
|
||||
const state = watcher.getSessionState('test:key');
|
||||
assert.equal(state.pendingToolCalls, 1); // Read still pending
|
||||
@@ -149,7 +150,7 @@ describe('StatusWatcher', () => {
|
||||
appendLine(file, { type: 'usage', input_tokens: 1000, output_tokens: 500 });
|
||||
|
||||
watcher = new StatusWatcher({ transcriptDir: tmpDir, idleTimeoutS: 600 });
|
||||
watcher.addSession('test:key', file);
|
||||
watcher.addSession('test:key', file, { lastOffset: 0 });
|
||||
|
||||
const state = watcher.getSessionState('test:key');
|
||||
assert.equal(state.tokenCount, 1500);
|
||||
@@ -195,7 +196,7 @@ describe('StatusWatcher', () => {
|
||||
|
||||
// Should not throw
|
||||
assert.doesNotThrow(() => {
|
||||
watcher.addSession('test:key', file);
|
||||
watcher.addSession('test:key', file, { lastOffset: 0 });
|
||||
});
|
||||
|
||||
const state = watcher.getSessionState('test:key');
|
||||
@@ -211,7 +212,8 @@ describe('StatusWatcher', () => {
|
||||
appendLine(file, { type: 'assistant', text: 'Starting...' });
|
||||
|
||||
watcher = new StatusWatcher({ transcriptDir: tmpDir, idleTimeoutS: 0.2 }); // 200ms
|
||||
watcher.addSession('test:key', file);
|
||||
// lastOffset: 0 so the pre-written content is read and idle timer is scheduled
|
||||
watcher.addSession('test:key', file, { lastOffset: 0 });
|
||||
|
||||
const idle = [];
|
||||
watcher.on('session-idle', (key) => idle.push(key));
|
||||
|
||||
@@ -25,9 +25,20 @@ function createTmpDir() {
|
||||
}
|
||||
|
||||
function writeSessionsJson(dir, agentId, sessions) {
|
||||
const agentDir = path.join(dir, agentId, 'sessions');
|
||||
var agentDir = path.join(dir, agentId, 'sessions');
|
||||
fs.mkdirSync(agentDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(agentDir, 'sessions.json'), JSON.stringify(sessions, null, 2));
|
||||
// Create stub transcript files so SessionMonitor doesn't skip them as missing
|
||||
for (var key in sessions) {
|
||||
var entry = sessions[key]; // eslint-disable-line security/detect-object-injection
|
||||
var sessionId = entry.sessionId || entry.uuid;
|
||||
if (sessionId) {
|
||||
var transcriptPath = path.join(agentDir, sessionId + '.jsonl');
|
||||
if (!fs.existsSync(transcriptPath)) {
|
||||
fs.writeFileSync(transcriptPath, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
@@ -156,21 +167,22 @@ describe('Sub-Agent Support (Phase 3)', () => {
|
||||
const result = format(parentState);
|
||||
const lines = result.split('\n');
|
||||
|
||||
// Parent header at depth 0
|
||||
assert.ok(lines[0].startsWith('[ACTIVE]'));
|
||||
// Parent header at depth 0 — wrapped in blockquote ("> ")
|
||||
assert.ok(lines[0].includes('[ACTIVE]'));
|
||||
assert.ok(lines[0].includes('main'));
|
||||
|
||||
// Child header at depth 1 (indented)
|
||||
// Child header at depth 1 (indented with "> " + 2 spaces)
|
||||
const childHeaderLine = lines.find((l) => l.includes('proj035-planner'));
|
||||
assert.ok(childHeaderLine);
|
||||
assert.ok(childHeaderLine.startsWith(' ')); // indented by 2 spaces
|
||||
// Child lines appear as "> ..." (blockquote + 2-space indent)
|
||||
assert.ok(/^> {2}/.test(childHeaderLine));
|
||||
|
||||
// Child should have [DONE] line
|
||||
assert.ok(result.includes('[DONE]'));
|
||||
|
||||
// Parent should not have [DONE] footer (still active)
|
||||
// The top-level [DONE] is from child, not parent
|
||||
const parentDoneLines = lines.filter((l) => /^(?!\s)\[DONE\]/.test(l));
|
||||
// The top-level [DONE] is from child, wrapped in blockquote
|
||||
const parentDoneLines = lines.filter((l) => /^\[DONE\]/.test(l));
|
||||
assert.equal(parentDoneLines.length, 0);
|
||||
});
|
||||
|
||||
@@ -248,8 +260,8 @@ describe('Sub-Agent Support (Phase 3)', () => {
|
||||
|
||||
const grandchildLine = lines.find((l) => l.includes('grandchild-agent'));
|
||||
assert.ok(grandchildLine);
|
||||
// Grandchild at depth 2 should have 4 spaces of indent
|
||||
assert.ok(grandchildLine.startsWith(' '));
|
||||
// Grandchild at depth 2 should have 4 spaces of indent after blockquote prefix ("> ")
|
||||
assert.ok(/^> {4}/.test(grandchildLine));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -266,8 +278,8 @@ describe('Sub-Agent Support (Phase 3)', () => {
|
||||
|
||||
const watcher = new StatusWatcher({ transcriptDir: tmpDir, idleTimeoutS: 600 });
|
||||
try {
|
||||
watcher.addSession('parent:key', parentFile, { depth: 0 });
|
||||
watcher.addSession('child:key', childFile, { depth: 1 });
|
||||
watcher.addSession('parent:key', parentFile, { depth: 0, lastOffset: 0 });
|
||||
watcher.addSession('child:key', childFile, { depth: 1, lastOffset: 0 });
|
||||
|
||||
const parentState = watcher.getSessionState('parent:key');
|
||||
const childState = watcher.getSessionState('child:key');
|
||||
@@ -296,8 +308,8 @@ describe('Sub-Agent Support (Phase 3)', () => {
|
||||
watcher.on('session-idle', (key) => idleEvents.push(key));
|
||||
|
||||
try {
|
||||
watcher.addSession('parent2:key', parentFile, { depth: 0 });
|
||||
watcher.addSession('child2:key', childFile, { depth: 1 });
|
||||
watcher.addSession('parent2:key', parentFile, { depth: 0, lastOffset: 0 });
|
||||
watcher.addSession('child2:key', childFile, { depth: 1, lastOffset: 0 });
|
||||
|
||||
await sleep(600);
|
||||
|
||||
@@ -321,7 +333,7 @@ describe('Sub-Agent Support (Phase 3)', () => {
|
||||
|
||||
const watcher = new StatusWatcher({ transcriptDir: tmpDir, idleTimeoutS: 600 });
|
||||
try {
|
||||
watcher.addSession('subagent:key', file, { agentId: 'planner', depth: 1 });
|
||||
watcher.addSession('subagent:key', file, { agentId: 'planner', depth: 1, lastOffset: 0 });
|
||||
const state = watcher.getSessionState('subagent:key');
|
||||
|
||||
assert.ok(state.lines.some((l) => l.includes('Reading PLAN.md')));
|
||||
@@ -342,7 +354,7 @@ describe('Sub-Agent Support (Phase 3)', () => {
|
||||
|
||||
const watcher = new StatusWatcher({ transcriptDir: tmpDir, idleTimeoutS: 600 });
|
||||
try {
|
||||
watcher.addSession('err:key', file);
|
||||
watcher.addSession('err:key', file, { lastOffset: 0 });
|
||||
const state = watcher.getSessionState('err:key');
|
||||
|
||||
const execLine = state.lines.find((l) => l.includes('exec'));
|
||||
|
||||
@@ -108,9 +108,10 @@ describe('status-formatter.js', () => {
|
||||
const result = format(parent);
|
||||
assert.ok(result.includes('proj035-planner'));
|
||||
assert.ok(result.includes('Reading protocol...'));
|
||||
// Child should be indented
|
||||
// Child should be indented — top-level uses blockquote prefix ("> ") so child
|
||||
// lines appear as "> ..." ("> " + depth*2 spaces). Verify indentation exists.
|
||||
const childLine = result.split('\n').find((l) => l.includes('proj035-planner'));
|
||||
assert.ok(childLine && childLine.startsWith(' '));
|
||||
assert.ok(childLine && /^> {2}/.test(childLine));
|
||||
});
|
||||
|
||||
it('active session has no done footer', () => {
|
||||
|
||||
Reference in New Issue
Block a user