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
4 changes: 3 additions & 1 deletion e2e/code_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@

from free_claude_code.application.code_sessions import CodeService
from free_claude_code.runtime.code_sessions_sqlite import SQLiteCodeStore
from free_claude_code.runtime.sqlite_database import SQLiteDatabase
from tests.code_sessions_support import FakeHarness


class CodeControl:
def __init__(self, directory):
self.harness = FakeHarness()
self.folder_picker = FolderPickerControl()
self.database = SQLiteDatabase(directory / "fcc.db", directory / "code.lock")
self.service = CodeService(
SQLiteCodeStore(directory / "code.db", directory / "code.lock"),
SQLiteCodeStore(self.database),
self.harness,
)
self.loop: asyncio.AbstractEventLoop | None = None
Expand Down
1 change: 1 addition & 0 deletions e2e/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ async def fixture_provider(provider_id: str, _settings: Settings) -> BaseProvide
configuration=ConfigurationService(ManagedConfigStore()),
transcriber=None,
code_service=code_control.service,
database=code_control.database,
)
monkeypatch.setattr(
NativeFolderPicker,
Expand Down
17 changes: 12 additions & 5 deletions e2e/test_claude_desktop_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,26 @@ def fail(path, *args, **kwargs):
),
)
elif status_failure == "startup":
marker = tmp_path / ".fcc/claude-desktop-disconnect.json"
failed = {"state": "failed", "message": "Startup check failed"}

def failed_update(route):
result = route.fetch()
body = result.json()
if body["disconnect_pending"]:
if marker.exists():
body["connected"] = None
body["update"] = {
"state": "failed",
"message": "Startup check failed",
}
body["update"] = failed
route.fulfill(response=result, json=body)

def failed_startup(route):
result = route.fetch()
body = result.json()
if marker.exists():
body["startup"]["integrations"]["claude-desktop"] = failed
route.fulfill(response=result, json=body)

page.route(endpoint, failed_update)
page.route("**/admin/api/status", failed_startup)
dialog.get_by_role("button", name="Disconnect", exact=True).click()
expect(
dialog.get_by_role(
Expand Down
13 changes: 11 additions & 2 deletions e2e/test_connected_accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ def take_pending_status(self, page: Page) -> Route:
page.wait_for_timeout(10)
return self.pending_status.pop(0)

def take_pending_login(self, page: Page) -> Route:
deadline = time.monotonic() + 5
while not self.pending_login:
if time.monotonic() >= deadline:
pytest.fail("Account login request did not reach the route handler")
page.wait_for_timeout(10)
return self.pending_login.pop(0)

def config(self, route: Route) -> None:
response = route.fetch()
config = response.json()
Expand Down Expand Up @@ -247,9 +255,10 @@ def test_openai_connect_uses_browser_login(
try:
assert popup.url == "about:blank"
assert popup.evaluate("window.opener === null") is True
login = accounts.take_pending_login(page)
assert accounts.login_requests == [("openai", "browser")]
assert len(accounts.pending_login) == 1
accounts.pending_login.pop().fulfill(json=accounts.statuses["openai"])
assert not accounts.pending_login
login.fulfill(json=accounts.statuses["openai"])
popup.wait_for_url(f"{admin_base_url}/account-test-sign-in")
finally:
popup.close()
Expand Down
27 changes: 27 additions & 0 deletions e2e/test_messaging_storage_recovery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import pytest
from playwright.sync_api import expect


@pytest.fixture
def admin_client_files(tmp_path):
source = tmp_path / ".fcc/agent_workspace/sessions.json"
source.parent.mkdir(parents=True)
source.write_text('{"conversation":')
return source


def test_damaged_legacy_history_warns_without_blocking_settings(
page, admin_base_url, admin_client_files
):
page.goto(f"{admin_base_url}/admin")
page.get_by_role("button", name="Messaging", exact=True).click()
notice = page.locator("#startupMessage")
expect(notice).to_be_visible()
expect(notice).to_contain_text("Messaging can still be used")
expect(page.locator("#field-TELEGRAM_BOT_TOKEN")).to_be_enabled()
assert admin_client_files.read_text() == '{"conversation":'
# A preserved source is not a second writer, and the server stays usable.
response = page.request.get(f"{admin_base_url}/health")
assert response.ok
page.get_by_role("button", name="Providers", exact=True).click()
expect(notice).to_be_hidden()
10 changes: 5 additions & 5 deletions smoke/capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ class CapabilityContract:
"persistence",
"restart_restore",
"restart_restore",
"free_claude_code.messaging.session.SessionStore",
"free_claude_code.runtime.messaging_sqlite.SQLiteMessagingStore",
"stored tree JSON",
"restored scoped reply lookup",
"stale pending work marked lost",
Expand All @@ -501,11 +501,11 @@ class CapabilityContract:
"persistence",
"session_store",
"session_persistence",
"free_claude_code.messaging.session.SessionStore",
"free_claude_code.runtime.messaging_sqlite.SQLiteMessagingStore",
"scoped tree data and message log",
"JSON persistence compatible with existing files",
"best-effort flush error logging",
("tests/messaging/test_session_store_edge_cases.py",),
"transactional SQLite persistence with one-time legacy JSON import",
"storage failures propagate to the owning messaging operation",
("tests/runtime/test_messaging_sqlite.py",),
),
CapabilityContract(
"voice",
Expand Down
4 changes: 2 additions & 2 deletions smoke/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -638,8 +638,8 @@ def has_pytest_coverage(self) -> bool:
),
FeatureCoverage(
"session_persistence",
"Session JSON preserves scoped trees and message logs",
("tests/messaging/test_session_store_edge_cases.py",),
"SQLite persistence preserves scoped trees and message logs",
("tests/runtime/test_messaging_sqlite.py",),
(),
("test_restart_restore_and_session_persistence_e2e",),
("messaging",),
Expand Down
22 changes: 17 additions & 5 deletions smoke/lib/e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@
)
from free_claude_code.harnesses.claude import build_claude_proxy_env
from free_claude_code.messaging.models import IncomingMessage, MessageScope
from free_claude_code.messaging.session import SessionStore
from free_claude_code.messaging.voice import VoiceCancellationResult
from free_claude_code.messaging.workflow import MessagingWorkflow
from free_claude_code.runtime.messaging_sqlite import SQLiteMessagingStore
from free_claude_code.runtime.sqlite_database import SQLiteDatabase
from smoke.lib.child_process import run_captured_text
from smoke.lib.config import ProviderModel, SmokeConfig, auth_headers
from smoke.lib.http import conversation_headers
Expand Down Expand Up @@ -568,15 +569,18 @@ class FakePlatformDriver:
event_batches: list[list[dict[str, Any]]] | None = None
platform: FakePlatform = field(init=False)
cli_manager: FakeCLIManager = field(init=False)
session_store: SessionStore = field(init=False)
session_store: SQLiteMessagingStore = field(init=False)
workflow: MessagingWorkflow = field(init=False)
database: SQLiteDatabase = field(init=False)

def __post_init__(self) -> None:
self.platform = FakePlatform(self.platform_name)
self.cli_manager = FakeCLIManager(self.event_batches)
self.session_store = SessionStore(
storage_path=str(self.tmp_path / f"{self.platform_name}-sessions.json")
self.database = SQLiteDatabase(
self.tmp_path / f"{self.platform_name}-fcc.db",
self.tmp_path / f"{self.platform_name}-fcc.lock",
)
self.session_store = SQLiteMessagingStore(self.database)
self.workflow = MessagingWorkflow(
self.platform,
self.cli_manager,
Expand All @@ -586,6 +590,14 @@ def __post_init__(self) -> None:
)
self.platform.on_message(self.workflow.handle_message)

async def start(self) -> None:
await self.database.start()
await self.workflow.restore()

async def close(self) -> None:
await self.workflow.close()
await self.database.close()

async def send(
self,
text: str,
Expand Down Expand Up @@ -628,7 +640,7 @@ async def wait_for_idle(self, *, timeout_s: float = 5.0) -> None:
while time.monotonic() < deadline:
pending = [task for task in self.platform._tasks if not task.done()]
if not pending and await self._all_tree_nodes_terminal():
self.session_store.flush_pending_save()
await self.workflow.tree_queue.wait_idle()
return
await asyncio.sleep(0.02)
raise AssertionError("fake platform did not become idle")
Expand Down
Loading
Loading