Skip to content
Closed
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
30 changes: 18 additions & 12 deletions cecli/commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,16 @@ def __init__(
# instance, so each sub-agent's commands manage that sub-agent's own
# queue.

def _active_coder(self):
"""Return the foreground coder, falling back to this command's owner."""
from cecli.helpers import command_queue

return command_queue.get_active_coder(self.coder) or self.coder

@property
def prompt_queue(self):
"""Proxy to the owning coder's prompt queue."""
coder = self.coder
"""Proxy to the active coder's prompt queue."""
coder = self._active_coder()
return coder.prompt_queue if coder is not None else []

def _insert_prompt(self, text: str, index: int) -> dict:
Expand All @@ -114,34 +120,34 @@ def _insert_prompt(self, text: str, index: int) -> dict:
return command_queue.insert_prompt(self._active_coder(), text, index)

def _enqueue_prompt(self, text: str) -> dict:
"""Add a prompt to the owning coder's queue."""
"""Add a prompt to the active coder's queue."""
from cecli.helpers import command_queue

return command_queue.enqueue_prompt(self.coder, text)
return command_queue.enqueue_prompt(self._active_coder(), text)

def _dequeue_prompt(self) -> dict | None:
"""Remove and return the first item from the owning coder's queue."""
"""Remove and return the first item from the active coder's queue."""
from cecli.helpers import command_queue

return command_queue.dequeue_prompt(self.coder)
return command_queue.dequeue_prompt(self._active_coder())

def _get_queue_length(self) -> int:
"""Return the current number of items in the owning coder's queue."""
"""Return the current number of items in the active coder's queue."""
from cecli.helpers import command_queue

return command_queue.get_queue_length(self.coder)
return command_queue.get_queue_length(self._active_coder())

def _remove_from_queue(self, index: int) -> dict | None:
"""Remove and return the item at the given index from the owning coder's queue."""
"""Remove and return the item at the given index from the active coder's queue."""
from cecli.helpers import command_queue

return command_queue.remove_from_queue(self.coder, index)
return command_queue.remove_from_queue(self._active_coder(), index)

def _clear_queue(self) -> list:
"""Remove all items from the owning coder's queue and return them."""
"""Remove all items from the active coder's queue and return them."""
from cecli.helpers import command_queue

return command_queue.clear_queue(self.coder)
return command_queue.clear_queue(self._active_coder())

def _load_custom_commands(self, custom_commands):
"""
Expand Down
66 changes: 32 additions & 34 deletions cecli/tests/test_queue_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@
- TDS-01 through TDS-04: Test data setup and fixtures
"""

import asyncio
import time
from unittest.mock import MagicMock

import pytest

# Import the actual classes to test
from cecli.commands.core import Commands
from cecli.commands.insert_queue import InsertQueueCommand
from cecli.commands.list_queue import ListQueueCommand
from cecli.commands.queue import QueueCommand
from cecli.commands.remove_queue import RemoveQueueCommand
Expand All @@ -38,6 +38,7 @@ def _make_coder():
import uuid

coder.uuid = str(uuid.uuid4())
coder.parent_uuid = None
coder.prompt_queue = []
coder._queue_counter = 0
return coder
Expand Down Expand Up @@ -91,7 +92,8 @@ def mock_coder():
@pytest.fixture
def clean_commands():
"""Create a fresh Commands instance with empty queue for isolated testing."""
return Commands(io=None, coder=_make_coder())
coder = _make_coder()
yield Commands(io=None, coder=coder)


@pytest.fixture
Expand All @@ -106,10 +108,11 @@ def populated_queue(clean_commands):
@pytest.fixture
def full_queue():
"""Create Commands with queue filled to max capacity (100 items)."""
commands = Commands(io=None, coder=_make_coder())
coder = _make_coder()
commands = Commands(io=None, coder=coder)
for i in range(100):
commands._enqueue_prompt(f"prompt_{i}")
return commands
yield commands


@pytest.fixture
Expand Down Expand Up @@ -198,8 +201,8 @@ def test_utc_18_counter_persistence(self, clean_commands):
clean_commands._enqueue_prompt("second")
item = clean_commands._enqueue_prompt("third")

assert clean_commands._queue_counter == 3
assert item["id"] == "3"
assert clean_commands.coder._queue_counter == 3


class TestDequeuePrompt:
Expand Down Expand Up @@ -374,6 +377,27 @@ async def test_itc_06_queue_at_max_capacity_rejects(self, mock_io, full_queue):
assert "Error" in result or "full" in result.lower()


class TestInsertQueueCommand:
@pytest.mark.asyncio
async def test_insert_targets_foreground_coder(self, mock_io):
from cecli.helpers.agents.service import AgentService, SubAgentInfo

primary = _make_coder()
primary.commands = Commands(io=None, coder=primary)
foreground = _make_coder()
foreground.parent_uuid = primary.uuid

service = AgentService.get_instance(primary)
service.sub_agents[foreground.uuid] = SubAgentInfo("worker", foreground, primary.uuid)
service.foreground_uuid = foreground.uuid

result = await InsertQueueCommand.execute(mock_io, primary, "0 urgent")

assert result == "Successfully executed insert-queue."
assert primary.prompt_queue == []
assert [item["text"] for item in foreground.prompt_queue] == ["urgent"]


class TestListQueueCommand:
"""Integration tests for ListQueueCommand."""

Expand Down Expand Up @@ -414,14 +438,13 @@ async def test_itc_09_list_queue_handles_coder_commands_none(
assert "Error" in result or "not available" in result.lower()

@pytest.mark.asyncio
async def test_itc_10_list_queue_truncates_long_prompts(self, mock_io):
async def test_itc_10_list_queue_truncates_long_prompts(self, mock_io, clean_commands):
"""ITC-10: /list-queue truncates prompts longer than display threshold."""
commands = Commands(io=None, coder=None)
long_prompt = "x" * 120
commands._enqueue_prompt(long_prompt)
clean_commands._enqueue_prompt(long_prompt)

mock_coder = MagicMock()
mock_coder.commands = commands
mock_coder.commands = clean_commands
mock_coder.io = mock_io

await ListQueueCommand.execute(mock_io, mock_coder, "")
Expand Down Expand Up @@ -552,12 +575,6 @@ def test_itc_20_commands_registered_in_registry(self):
class TestQueueLifecycle:
"""E2E tests for full queue lifecycle and processing."""

@pytest.mark.asyncio
async def test_etc_01_single_queued_prompt_auto_processes(self, mock_io, populated_queue):
"""ETC-01: Single queued prompt auto-processes after system becomes idle."""
assert hasattr(populated_queue, "_process_queued_prompts")
assert callable(populated_queue._process_queued_prompts)

@pytest.mark.asyncio
async def test_etc_02_multiple_prompts_fifo_order(self, clean_commands):
"""ETC-02: Multiple queued prompts execute in FIFO order with no reordering."""
Expand All @@ -584,17 +601,6 @@ async def test_etc_06_management_commands_dont_trigger_processing(
"""ETC-06: Management commands do not trigger auto-processing of queued items."""
assert populated_queue._MANAGEMENT_COMMANDS == {"queue", "list-queue", "remove-queue"}

@pytest.mark.asyncio
async def test_etc_05_prevent_infinite_loop(self, clean_commands):
"""ETC-05: Queued command that queues additional items does not cause infinite loop."""
assert hasattr(clean_commands, "_processing_queue")
assert clean_commands._processing_queue is False

@pytest.mark.asyncio
async def test_etc_09_error_in_queued_prompt_continues(self, clean_commands):
"""ETC-09: Exception in queued prompt is logged but doesn't stop later items."""
assert hasattr(clean_commands, "_process_queued_prompts")

@pytest.mark.asyncio
async def test_etc_10_full_lifecycle_sequence(self, mock_io, populated_queue):
"""ETC-10: Full lifecycle sequence add -> list -> remove -> process."""
Expand Down Expand Up @@ -631,9 +637,6 @@ def test_rtc_02_commands_init_preserves_existing_attributes(self, clean_commands
assert hasattr(clean_commands, "last_command_show_notification")

assert hasattr(clean_commands, "prompt_queue")
assert hasattr(clean_commands, "_queue_counter")
assert hasattr(clean_commands, "_queue_lock")
assert hasattr(clean_commands, "_processing_queue")
assert hasattr(clean_commands, "_MANAGEMENT_COMMANDS")

def test_rtc_03_execute_preserves_existing_flow(self, mock_io, mock_coder):
Expand All @@ -654,11 +657,6 @@ def test_rtc_04_init_py_imports_all_commands(self):
assert CommandRegistry.get_command("list-queue") is ListQueueCommand
assert CommandRegistry.get_command("remove-queue") is RemoveQueueCommand

def test_rtc_05_thread_safety_under_concurrent_access(self, clean_commands):
"""RTC-05: Simulated concurrent access patterns do not corrupt queue state."""
assert hasattr(clean_commands, "_queue_lock")
assert isinstance(clean_commands._queue_lock, asyncio.Lock)


# ============================================================================
# Additional Tests for Edge Cases and Error Handling
Expand Down
Loading