Files
cs249r_book/tinytorch/tests/cli/test_progress_sync.py
Vijay Janapa Reddi f0de9f970e fix(tito): reliably sync CLI progress to the community dashboard (#1849)
The dashboard did not reflect CLI progress for users across Windows and
Ubuntu. Three independent client defects, now fixed:

1. Automatic sync was silently skipped on any non-TTY shell. _trigger_submission
   gated on `not sys.stdin.isatty()`, which is False on Windows Git Bash/MinTTY
   and many IDE terminals even when interactive, so `tito module complete`
   updated local progress.json but never uploaded, with no message. Decouple
   "should we sync" (logged-in and not CI) from "should we prompt" (needs a
   TTY): non-interactive real users now sync without a prompt instead of being
   skipped.

2. A 2xx response with synced_modules null/0 was reported as success using the
   local count, hiding backend failures. sync_progress now returns a SyncResult
   and reports an honest accepted-but-unconfirmed warning when the server does
   not confirm persistence.

3. No standalone resync path and login did not sync, so modules completed before
   logging in never reached the dashboard. Add `tito community sync` and an
   offer to sync existing progress after login.

The sync trigger decision now lives in one shared helper
(auto_sync_after_completion) used by the module, milestone, and login paths,
plus a small core/runtime.py that owns is_ci()/is_interactive(). Adds
tests/cli/test_progress_sync.py (15 tests) covering the non-TTY regression, the
CI/not-logged-in branches, honest 2xx interpretation, and the new command.
2026-06-15 13:57:06 -04:00

226 lines
7.9 KiB
Python

"""
Regression tests for community progress sync (issue #1849).
These guard the bug where the TinyTorch dashboard did not reflect CLI progress:
1. Automatic sync was silently skipped on any non-TTY shell (Git Bash / MinTTY
on Windows, IDE terminals), because the trigger gated on
``sys.stdin.isatty()``. The fix decouples "should we sync" from "should we
prompt": a logged-in, non-CI user always syncs; the prompt is only skipped
when there is no usable TTY.
2. A 2xx response with ``synced_modules`` null/0 was reported as success using
the local count, hiding backend failures. The fix reports an honest
accepted-but-unconfirmed warning instead.
3. There was no standalone resync command and login did not sync, so progress
completed before logging in never reached the dashboard.
"""
import sys
from pathlib import Path
import pytest
# Add tito to path (CI exports the package first; these tests only need tito).
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from rich.console import Console
from tito.core import runtime
from tito.core import submission
from tito.core.submission import (
SyncResult,
SubmissionHandler,
auto_sync_after_completion,
)
from tito.core.config import CLIConfig
@pytest.fixture
def handler():
config = CLIConfig.from_project_root(Path.cwd())
return SubmissionHandler(config, Console())
# ---------------------------------------------------------------------------
# runtime: CI vs interactivity are independent
# ---------------------------------------------------------------------------
def test_is_ci_detects_env(monkeypatch):
monkeypatch.delenv("CI", raising=False)
monkeypatch.delenv("GITHUB_ACTIONS", raising=False)
assert runtime.is_ci() is False
monkeypatch.setenv("CI", "true")
assert runtime.is_ci() is True
def test_is_interactive_false_in_ci(monkeypatch):
monkeypatch.setenv("CI", "true")
assert runtime.is_interactive() is False
def test_is_interactive_false_without_tty(monkeypatch):
monkeypatch.delenv("CI", raising=False)
monkeypatch.delenv("GITHUB_ACTIONS", raising=False)
class _NoTTY:
def isatty(self):
return False
monkeypatch.setattr(sys, "stdin", _NoTTY())
monkeypatch.setattr(sys, "stdout", _NoTTY())
assert runtime.is_interactive() is False
# ---------------------------------------------------------------------------
# auto_sync_after_completion: the trigger decision
# ---------------------------------------------------------------------------
def test_auto_sync_runs_when_non_interactive_but_logged_in(monkeypatch):
"""THE core regression: a logged-in user on a non-TTY shell must still sync.
Previously this path returned early on ``not sys.stdin.isatty()`` and never
uploaded -- the silent skip behind the dashboard desync.
"""
monkeypatch.setattr(submission.runtime, "is_ci", lambda: False)
monkeypatch.setattr(submission.runtime, "is_interactive", lambda: False)
monkeypatch.setattr(submission.auth, "is_logged_in", lambda: True)
calls = {}
def fake_sync(self, total_modules=20, is_retry=False):
calls["ran"] = True
return SyncResult(ok=True, accepted=True, synced_modules=2, sent_modules=2)
monkeypatch.setattr(SubmissionHandler, "sync_progress", fake_sync)
result = auto_sync_after_completion(CLIConfig.from_project_root(Path.cwd()), Console())
assert calls.get("ran") is True
assert result is not None and result.ok
def test_auto_sync_prompts_when_interactive(monkeypatch):
monkeypatch.setattr(submission.runtime, "is_ci", lambda: False)
monkeypatch.setattr(submission.runtime, "is_interactive", lambda: True)
monkeypatch.setattr(submission.auth, "is_logged_in", lambda: True)
asked = {}
monkeypatch.setattr(submission.Confirm, "ask", lambda *a, **k: asked.setdefault("asked", True) or True)
ran = {}
monkeypatch.setattr(
SubmissionHandler, "sync_progress",
lambda self, total_modules=20, is_retry=False: ran.setdefault("ran", True) or SyncResult(ok=True),
)
auto_sync_after_completion(CLIConfig.from_project_root(Path.cwd()), Console())
assert asked.get("asked") is True
assert ran.get("ran") is True
def test_auto_sync_interactive_decline_does_not_sync(monkeypatch):
monkeypatch.setattr(submission.runtime, "is_ci", lambda: False)
monkeypatch.setattr(submission.runtime, "is_interactive", lambda: True)
monkeypatch.setattr(submission.auth, "is_logged_in", lambda: True)
monkeypatch.setattr(submission.Confirm, "ask", lambda *a, **k: False)
ran = {}
monkeypatch.setattr(
SubmissionHandler, "sync_progress",
lambda self, total_modules=20, is_retry=False: ran.setdefault("ran", True),
)
result = auto_sync_after_completion(CLIConfig.from_project_root(Path.cwd()), Console())
assert "ran" not in ran
assert result is None
def test_auto_sync_skipped_in_ci(monkeypatch):
monkeypatch.setattr(submission.runtime, "is_ci", lambda: True)
monkeypatch.setattr(submission.auth, "is_logged_in", lambda: True)
ran = {}
monkeypatch.setattr(
SubmissionHandler, "sync_progress",
lambda self, total_modules=20, is_retry=False: ran.setdefault("ran", True),
)
result = auto_sync_after_completion(CLIConfig.from_project_root(Path.cwd()), Console())
assert "ran" not in ran
assert result is None
def test_auto_sync_hint_when_not_logged_in(monkeypatch):
monkeypatch.setattr(submission.runtime, "is_ci", lambda: False)
monkeypatch.setattr(submission.auth, "is_logged_in", lambda: False)
ran = {}
monkeypatch.setattr(
SubmissionHandler, "sync_progress",
lambda self, total_modules=20, is_retry=False: ran.setdefault("ran", True),
)
result = auto_sync_after_completion(CLIConfig.from_project_root(Path.cwd()), Console())
assert "ran" not in ran
assert result is None
# ---------------------------------------------------------------------------
# honest 2xx interpretation
# ---------------------------------------------------------------------------
def test_2xx_confirmed_is_ok(handler):
result = handler._interpret_2xx(synced=2, sent=2)
assert result.ok is True
assert result.accepted is True
assert result.synced_modules == 2
def test_2xx_null_synced_is_unconfirmed_warning(handler):
"""2xx but server reports nothing persisted while we sent modules -> NOT ok."""
result = handler._interpret_2xx(synced=None, sent=2)
assert result.ok is False
assert result.accepted is True
assert result.sent_modules == 2
def test_2xx_zero_synced_is_unconfirmed_warning(handler):
result = handler._interpret_2xx(synced=0, sent=3)
assert result.ok is False
assert result.accepted is True
def test_2xx_nothing_to_sync_is_ok(handler):
result = handler._interpret_2xx(synced=None, sent=0)
assert result.ok is True
assert result.accepted is True
def test_sync_result_truthiness():
assert bool(SyncResult(ok=True)) is True
assert bool(SyncResult(ok=False, accepted=True)) is False
# ---------------------------------------------------------------------------
# the recovery command exists and is wired
# ---------------------------------------------------------------------------
def test_community_sync_subcommand_registered():
import argparse
from tito.commands.community import CommunityCommand
cmd = CommunityCommand(CLIConfig.from_project_root(Path.cwd()))
parser = argparse.ArgumentParser()
cmd.add_arguments(parser)
args = parser.parse_args(["sync"])
assert args.community_command == "sync"
def test_community_sync_requires_login(monkeypatch):
from tito.commands.community import CommunityCommand
import argparse
monkeypatch.setattr("tito.commands.community.auth.is_logged_in", lambda: False)
cmd = CommunityCommand(CLIConfig.from_project_root(Path.cwd()))
ns = argparse.Namespace(community_command="sync")
assert cmd.run(ns) == 1 # not logged in -> nonzero exit, no upload attempted