Skip to content
Merged
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
8 changes: 8 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ fallback_version = "0.0.0"
[project.scripts]
ddss = "ddss.main:cli"

[project.optional-dependencies]
dev = [
"pytest~=9.0.2",
"pytest-asyncio~=1.3.0",
"pytest-cov~=7.0.0",
"ruff~=0.14.10",
]
Comment on lines +34 to +40

Copilot AI Dec 22, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing pytest-asyncio configuration in pyproject.toml. pytest-asyncio requires explicit configuration to handle async fixtures and tests properly. Without configuration, you may encounter warnings or unexpected behavior.

Add a [tool.pytest.ini_options] section with asyncio_mode configuration, for example:

[tool.pytest.ini_options]
asyncio_mode = "auto"

or

asyncio_default_fixture_loop_scope = "function"

This ensures pytest-asyncio operates in the expected mode.

Copilot uses AI. Check for mistakes.

[tool.ruff]
line-length = 120

Expand Down
113 changes: 113 additions & 0 deletions tests/test_output.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import asyncio
import tempfile
import pathlib
import pytest
import pytest_asyncio
from ddss.orm import initialize_database, Facts, Ideas
from ddss.output import main


@pytest_asyncio.fixture
async def temp_db():
"""Fixture to create a temporary database."""
with tempfile.TemporaryDirectory() as tmpdir:
db_path = pathlib.Path(tmpdir) / "test.db"
addr = f"sqlite+aiosqlite:///{db_path.as_posix()}"
engine, session = await initialize_database(addr)
yield addr, engine, session
await engine.dispose()

Copilot AI Dec 22, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test fixture disposes the engine after yielding, but the main() function being tested also disposes the engine in its finally block (line 37 of output.py). This could lead to a "double dispose" situation where the engine is disposed twice, potentially causing warnings or errors.

Consider either:

  1. Not disposing the engine in the fixture since main() will handle it
  2. Adding a flag to main() to skip engine disposal when running in test mode
  3. Catching and ignoring any disposal errors in the fixture cleanup
Suggested change
await engine.dispose()
try:
await engine.dispose()
except Exception:
# Ignore errors if the engine was already disposed (e.g., by main()).
pass

Copilot uses AI. Check for mistakes.


@pytest.mark.asyncio
async def test_output_formats_facts_correctly(temp_db, capsys):
"""Test that Facts data is formatted correctly using unparse() ('a\\n----\\nb' becomes 'a => b')."""
addr, engine, session = temp_db

# Add test data
async with session() as sess:
sess.add(Facts(data="a\n----\nb"))
await sess.commit()

# Run the main function with a timeout to avoid infinite loop
task = asyncio.create_task(main(addr, engine, session))
await asyncio.sleep(0.2) # Give it time to process

Copilot AI Dec 22, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests rely on timing with asyncio.sleep(0.2) to allow the output loop to process entries. This makes tests non-deterministic and potentially flaky, especially on slower systems or under load. The tests could fail if the system is too slow to process within 200ms, or could waste time if processing completes faster.

Consider using a more reliable synchronization mechanism, such as:

  1. Adding a callback or event mechanism to main() for testing
  2. Using a mock/spy on the print function to detect when output occurs
  3. Polling the output buffer until expected content appears (with a timeout)

Copilot uses AI. Check for mistakes.
task.cancel()
try:
await task
except asyncio.CancelledError:

Copilot AI Dec 22, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'except' clause does nothing but pass and there is no explanatory comment.

Copilot uses AI. Check for mistakes.
pass

# Check output
captured = capsys.readouterr()
assert "fact: a => b" in captured.out


@pytest.mark.asyncio
async def test_output_formats_ideas_correctly(temp_db, capsys):
"""Test that Ideas data is formatted correctly using unparse() ('x\\n----\\ny' becomes 'x => y')."""
addr, engine, session = temp_db

# Add test data
async with session() as sess:
sess.add(Ideas(data="x\n----\ny"))
await sess.commit()

# Run the main function with a timeout to avoid infinite loop
task = asyncio.create_task(main(addr, engine, session))
await asyncio.sleep(0.2) # Give it time to process
task.cancel()
try:
await task
except asyncio.CancelledError:

Copilot AI Dec 22, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'except' clause does nothing but pass and there is no explanatory comment.

Copilot uses AI. Check for mistakes.
pass

# Check output
captured = capsys.readouterr()
assert "idea: x => y" in captured.out


@pytest.mark.asyncio
async def test_output_multiple_entries(temp_db, capsys):
"""Test that output handles multiple Facts and Ideas correctly."""
addr, engine, session = temp_db

# Add test data
async with session() as sess:
sess.add(Facts(data="a\n----\nb"))
sess.add(Facts(data="c\n----\nd"))
sess.add(Ideas(data="x\n----\ny"))
sess.add(Ideas(data="p\n----\nq"))
await sess.commit()

# Run the main function with a timeout to avoid infinite loop
task = asyncio.create_task(main(addr, engine, session))
await asyncio.sleep(0.2) # Give it time to process
task.cancel()
try:
await task
except asyncio.CancelledError:

Copilot AI Dec 22, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'except' clause does nothing but pass and there is no explanatory comment.

Copilot uses AI. Check for mistakes.
pass

# Check output
captured = capsys.readouterr()
assert "idea: x => y" in captured.out
assert "idea: p => q" in captured.out
assert "fact: a => b" in captured.out
assert "fact: c => d" in captured.out


@pytest.mark.asyncio
async def test_output_cancellation(temp_db):
"""Test that the output main function can be cancelled without hanging."""
addr, engine, session = temp_db

# Run the main function and cancel it
task = asyncio.create_task(main(addr, engine, session))
await asyncio.sleep(0.1) # Let it start
task.cancel()

# Should complete without hanging
try:
await task
except asyncio.CancelledError:
pass # Expected - cancellation worked
Loading