Files
wehub-resource-sync 09e9f3545f
Test / Code Quality (push) Has been cancelled
Test / Test (macos-latest, Python 3.10) (push) Has been cancelled
Test / Test (macos-latest, Python 3.11) (push) Has been cancelled
Test / Test (macos-latest, Python 3.12) (push) Has been cancelled
Test / Test (macos-latest, Python 3.13) (push) Has been cancelled
Test / Test (macos-latest, Python 3.14) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.10) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.11) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.12) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.13) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.14) (push) Has been cancelled
Test / Test (windows-latest, Python 3.10) (push) Has been cancelled
Test / Test (windows-latest, Python 3.11) (push) Has been cancelled
Test / Test (windows-latest, Python 3.12) (push) Has been cancelled
Test / Test (windows-latest, Python 3.13) (push) Has been cancelled
Test / Test (windows-latest, Python 3.14) (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
dependency-audit / pip-audit (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:30:13 +08:00

195 lines
7.1 KiB
Python

"""Focused exit-code tests for the ``source stale`` freshness predicate.
The ``source stale`` command previously used an inverted-predicate exit
convention (0 = stale, 1 = fresh) so the shell idiom
``if notebooklm source stale ID; then refresh; fi`` worked naturally.
That convention has been flipped: the default now follows the canonical
CLI exit code policy (0 = success, 1 = error). The inverted behavior is
preserved as an opt-in via ``--exit-on-stale`` for callers that depended
on the prior semantics.
This file pins the new default + the back-compat flag across both text
and JSON output modes. The companion documentation lives in
``docs/cli-exit-codes.md`` under the ``source stale`` section and the
``Exit code semantics`` summary.
"""
from __future__ import annotations
import json
from contextlib import contextmanager
from unittest.mock import AsyncMock, patch
import pytest
from click.testing import CliRunner
import notebooklm.auth as auth_module
from notebooklm.notebooklm_cli import cli
from notebooklm.types import Source
from .conftest import create_mock_client, inject_client
@pytest.fixture
def runner():
return CliRunner()
@contextmanager
def _stale_command_environment(*, is_fresh: bool):
"""Build the injected client env for ``source stale`` tests.
Yields the ``ctx.obj`` payload that injects the mock client; callers thread
it into ``runner.invoke(cli, args, obj=obj)``. The ``fetch_tokens`` auth
seam is still patched here.
"""
with patch.object(
auth_module,
"fetch_tokens_with_domains",
new_callable=AsyncMock,
return_value=("csrf", "session"),
):
mock_client = create_mock_client()
mock_client.sources.list = AsyncMock(
return_value=[Source(id="src_123", title="Test Source")]
)
mock_client.sources.check_freshness = AsyncMock(return_value=is_fresh)
yield inject_client(mock_client)
# ---------------------------------------------------------------------------
# Default (standard) exit-code semantics: 0 on success regardless of freshness
# ---------------------------------------------------------------------------
class TestSourceStaleDefaultExitCodes:
"""Default behavior: exit 0 when the check completes, 1 only on error."""
@pytest.mark.parametrize(
("is_fresh", "expected_verdict_text"),
[
(False, "stale"),
(True, "fresh"),
],
)
def test_default_text_mode_exits_zero_regardless_of_verdict(
self, runner, mock_auth, is_fresh, expected_verdict_text
):
"""Default policy: exit 0 once the check succeeds, verdict on stdout."""
with _stale_command_environment(is_fresh=is_fresh) as obj:
result = runner.invoke(cli, ["source", "stale", "src_123", "-n", "nb_123"], obj=obj)
assert result.exit_code == 0, result.output
assert expected_verdict_text in result.output.lower()
def test_default_text_mode_stale_branch_mentions_refresh(self, runner, mock_auth):
"""The stale branch should point callers at ``source refresh`` on stdout."""
with _stale_command_environment(is_fresh=False) as obj:
result = runner.invoke(cli, ["source", "stale", "src_123", "-n", "nb_123"], obj=obj)
assert result.exit_code == 0, result.output
assert "refresh" in result.output.lower()
@pytest.mark.parametrize(
("is_fresh", "expected_stale"),
[
(False, True),
(True, False),
],
)
def test_default_json_mode_exits_zero_with_verdict_in_payload(
self, runner, mock_auth, is_fresh, expected_stale
):
"""JSON mode mirrors text mode: exit 0, verdict in payload fields."""
with _stale_command_environment(is_fresh=is_fresh) as obj:
result = runner.invoke(
cli, ["source", "stale", "src_123", "-n", "nb_123", "--json"], obj=obj
)
assert result.exit_code == 0, result.output
data = json.loads(result.output)
assert data["stale"] is expected_stale
assert data["fresh"] is is_fresh
assert data["source_id"] == "src_123"
# ---------------------------------------------------------------------------
# --exit-on-stale: back-compat inverted-predicate semantics
# ---------------------------------------------------------------------------
class TestSourceStaleExitOnStaleFlag:
"""With --exit-on-stale, the legacy inverted predicate is preserved."""
@pytest.mark.parametrize(
("is_fresh", "expected_exit", "expected_verdict_text"),
[
(False, 0, "stale"), # predicate true → exit 0
(True, 1, "fresh"), # predicate false → exit 1
],
)
def test_exit_on_stale_text_mode_inverts_exit_codes(
self, runner, mock_auth, is_fresh, expected_exit, expected_verdict_text
):
with _stale_command_environment(is_fresh=is_fresh) as obj:
result = runner.invoke(
cli,
["source", "stale", "src_123", "-n", "nb_123", "--exit-on-stale"],
obj=obj,
)
assert result.exit_code == expected_exit, result.output
assert expected_verdict_text in result.output.lower()
@pytest.mark.parametrize(
("is_fresh", "expected_exit", "expected_stale"),
[
(False, 0, True), # predicate true → exit 0
(True, 1, False), # predicate false → exit 1
],
)
def test_exit_on_stale_json_mode_inverts_exit_codes_with_payload_intact(
self, runner, mock_auth, is_fresh, expected_exit, expected_stale
):
with _stale_command_environment(is_fresh=is_fresh) as obj:
result = runner.invoke(
cli,
[
"source",
"stale",
"src_123",
"-n",
"nb_123",
"--exit-on-stale",
"--json",
],
obj=obj,
)
assert result.exit_code == expected_exit, result.output
data = json.loads(result.output)
assert data["stale"] is expected_stale
assert data["fresh"] is is_fresh
# ---------------------------------------------------------------------------
# Help text surface — the flag is discoverable
# ---------------------------------------------------------------------------
class TestSourceStaleHelp:
"""The ``--exit-on-stale`` flag must be discoverable through ``--help``."""
def test_help_advertises_exit_on_stale_flag(self, runner):
result = runner.invoke(cli, ["source", "stale", "--help"])
assert result.exit_code == 0
assert "--exit-on-stale" in result.output
def test_help_advertises_default_standard_convention(self, runner):
result = runner.invoke(cli, ["source", "stale", "--help"])
assert result.exit_code == 0
# The help must signal that the default follows the standard
# convention; phrasing may evolve, but the canonical doc link must
# remain reachable.
assert "cli-exit-codes.md" in result.output or "exit-code" in result.output.lower()