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:
sol
2026-03-07 20:31:32 +00:00
parent cc485f0009
commit 868574d939
31 changed files with 3596 additions and 68 deletions

View 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);
});
});

View File

@@ -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) {

View File

@@ -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));

View File

@@ -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'));

View File

@@ -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', () => {