Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .commit-dod.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# leftover from blocked commit attempt — ignore
48 changes: 0 additions & 48 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions run-session-bound-tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail
cd /work/OpenSwarm/worktree/11481ea4-83b4-46f7-accc-043cf5fcefb0
if [ ! -f node_modules/vitest/vitest.mjs ]; then
npm install
fi
node --experimental-vm-modules node_modules/vitest/vitest.mjs run \
src/support/promptHelper.test.ts \
src/support/timeWindow.test.ts \
src/support/workSessionRoutes.test.ts \
src/tui/chatModel.test.ts \
src/tui/components/ChatInput.test.tsx \
src/tui/panels/ChatPanel.history.test.tsx \
src/tui/panels/ChatPanel.history.test.ts \
--reporter=verbose
30 changes: 29 additions & 1 deletion src/support/promptHelper.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, it, expect } from 'vitest';
import { resolveChoice, resolveConfirm, type ChoiceOption } from './promptHelper.js';
import { PassThrough } from 'node:stream';
import { createPrompter, MAX_LINE_QUEUE, resolveChoice, resolveConfirm, type ChoiceOption } from './promptHelper.js';

const opts: ChoiceOption<string>[] = [
{ label: 'local', value: 'L' },
Expand Down Expand Up @@ -37,3 +38,30 @@ describe('resolveConfirm', () => {
expect(resolveConfirm('maybe', false)).toBe(false);
});
});

describe('createPrompter stdin queue bound', () => {
it(`retains at most ${MAX_LINE_QUEUE} queued lines and drops the oldest`, async () => {
const input = new PassThrough();
const output = new PassThrough();
const prompter = createPrompter(input, output);

const total = MAX_LINE_QUEUE + 25;
for (let i = 0; i < total; i++) {
input.write(`line-${i}\n`);
}

// Let readline drain line events into the bounded queue before asking.
await new Promise((r) => setImmediate(r));

const first = await prompter.ask('q');
// Oldest 25 were shifted out; first retained is line-25.
expect(first).toBe('line-25');

for (let i = 26; i < total; i++) {
expect(await prompter.ask('q')).toBe(`line-${i}`);
}

prompter.close();
input.end();
});
});
32 changes: 20 additions & 12 deletions src/support/promptHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,24 @@ export function resolveConfirm(raw: string, def: boolean): boolean {
}

export interface Prompter {
/** Free-text question; returns the trimmed answer or `def` if blank. */
ask(question: string, def?: string): Promise<string>;
/** Numbered menu; re-prompts until a valid option is chosen. */
choose<T>(question: string, options: ChoiceOption<T>[]): Promise<T>;
/** Yes/no; blank answer takes `def`. */
confirm(question: string, def?: boolean): Promise<boolean>;
ask(q: string, def?: string): Promise<string>;
choose<T>(q: string, options: ChoiceOption<T>[]): Promise<T>;
confirm(q: string, def?: boolean): Promise<boolean>;
close(): void;
}

/** Maximum number of queued stdin lines to prevent unbounded memory growth. */
export const MAX_LINE_QUEUE = 100;

/**
* Create an interactive prompter backed by readline.
*
* Drains readline's `line` events into a bounded queue and hands them out one
* at a time. rl.question (both callback and promises forms) drops lines when a
* pipe delivers several at once and then EOFs; queueing the line events is
* robust for both piped stdin and a live TTY.
*/
export function createPrompter(input: Readable = processStdin, output: Writable = processStdout): Prompter {
// Drain readline's `line` events into a queue and hand them out one at a time.
// rl.question (both callback and promises forms) drops lines when a pipe
// delivers several at once and then EOFs; queueing the line events is robust
// for both piped stdin and a live TTY.
const rl = createInterface({ input, output });
const lineQueue: string[] = [];
const waiters: Array<{ resolve: (l: string) => void; reject: (e: Error) => void }> = [];
Expand All @@ -61,7 +65,11 @@ export function createPrompter(input: Readable = processStdin, output: Writable
rl.on('line', (line: string) => {
const w = waiters.shift();
if (w) w.resolve(line);
else lineQueue.push(line);
else {
lineQueue.push(line);
// Bound queue to prevent unbounded memory growth
if (lineQueue.length > MAX_LINE_QUEUE) lineQueue.shift();
}
});
rl.on('close', () => {
closed = true;
Expand Down Expand Up @@ -103,4 +111,4 @@ export function createPrompter(input: Readable = processStdin, output: Writable
rl.close();
},
};
}
}
24 changes: 23 additions & 1 deletion src/support/timeWindow.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { DEFAULT_TIME_WINDOW, getMarketStatus, isWorkAllowed } from './timeWindow.js';
import {
DEFAULT_TIME_WINDOW,
getMarketStatus,
getTimeWindowSummary,
isWorkAllowed,
setTimeWindowConfig,
} from './timeWindow.js';

describe('timeWindow', () => {
afterEach(() => {
vi.useRealTimers();
setTimeWindowConfig({ ...DEFAULT_TIME_WINDOW });
});

it('reports the next allowed window start while inside a blocked window', () => {
Expand Down Expand Up @@ -36,4 +43,19 @@ describe('timeWindow', () => {
expect(status.status).toBe('regular');
expect(status.canWork).toBe(false);
});

it('getTimeWindowSummary uses the active configuration for market status', () => {
vi.useFakeTimers();
// Monday 10:00 KST — default config blocks this window.
vi.setSystemTime(new Date('2026-07-06T01:00:00.000Z'));

const blockedSummary = getTimeWindowSummary();
expect(blockedSummary).toContain('🔴');

// Clear blocked windows so market/work status follows the active override.
setTimeWindowConfig({ blockedWindows: [], allowedWindows: [{ start: '00:00', end: '23:59' }] });
const openSummary = getTimeWindowSummary();
expect(openSummary).toContain('🟢');
expect(openSummary).not.toEqual(blockedSummary);
});
});
5 changes: 3 additions & 2 deletions src/support/timeWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,8 +296,9 @@ export function assertWorkAllowed(taskName?: string): void {
* Time window status summary (for Discord reporting)
*/
export function getTimeWindowSummary(): string {
const work = isWorkAllowed();
const market = getMarketStatus();
const config = getTimeWindowConfig();
const work = isWorkAllowed(config);
const market = getMarketStatus(config);

const icon = work.allowed ? '🟢' : '🔴';
const status = work.allowed ? t('timeWindow.workAllowed') : t('timeWindow.workBlocked');
Expand Down
Loading
Loading