diff --git a/devtools/e2e_live/scenarios.py b/devtools/e2e_live/scenarios.py index 99d8db69d..32a7f5c99 100644 --- a/devtools/e2e_live/scenarios.py +++ b/devtools/e2e_live/scenarios.py @@ -476,7 +476,7 @@ def worktree_after_commit(clone: pathlib.Path) -> tuple[bool, str, list[str]]: def _git_show(clone: pathlib.Path, rev: str, path: str) -> str: """The exact text of ``path`` at ``rev`` ('' when absent there).""" proc = subprocess.run(["git", "show", f"{rev}:{path}"], cwd=str(clone), check=False, capture_output=True) - return proc.stdout.decode("utf-8") if proc.returncode == 0 else "" + return proc.stdout.decode("utf-8").replace("\r\n", "\n").replace("\r", "\n") if proc.returncode == 0 else "" def release_carriers_desync_at(clone: pathlib.Path, rev: str) -> str: diff --git a/docs/architecture/03-web-ui-pages-and-buttons.md b/docs/architecture/03-web-ui-pages-and-buttons.md index 5c8a4188f..5a4f2ba84 100644 --- a/docs/architecture/03-web-ui-pages-and-buttons.md +++ b/docs/architecture/03-web-ui-pages-and-buttons.md @@ -281,7 +281,7 @@ The synchronous lock-owning apply executor publishes process-local stage observa Settings has Accounts, Secrets, Models, Agents, Behavior, Appearance, Advanced and About tabs — a sequence from connections to runtime detail. Accounts: managed subscriptions and their shared service banner, API providers, custom compatible endpoints, local runtime entry points, and the optional non-loopback network gate. Secrets: known provider/integration secrets, skill-requested keys and owner-defined custom keys, without returning stored values. Models: compact source/model/account role rows, ordered fallbacks, context assertions and effort lanes. Agents: task actors and review lanes, with delegation permissions, per-root and depth limits and subagent path roots; their accounts are managed in Accounts. Behavior: context, safety-supervisor coverage, task acceptance, self-evolution, prompt-cache posture. Appearance: the client-local theme choice described above, never a runtime-settings value. Advanced: process, timeout, local-model, integration, source-control and cleanup controls (worker count is process capacity, so it lives here). About reports application/runtime identity. Keys, defaults and per-key semantics are the §7 Default settings table, not this chapter. Models, Available subagents and Review lanes share ONE grouped source select owned by `web/modules/route_editor_primitives.js` (`routeChoiceGroups`, `configuredApiProviders`): the owner picks a source and the editor composes the stored id, so the provider prefixes (`provider::model`, `claudexor::source=model`, `harness=model`) are serialization only, never owner input. -`settings_catalog.js` owns model-catalog reads and the Accounts subscription: a changed confirmed account or recovery from a failed read refreshes discovery; identical settled status stays quiet, and `settings.js` arms/disposes the subscription with the page. Catalog updates preserve drafts. Per-button request ownership releases busy state independently of global response freshness; a newer background read cannot strand a manual button. +`settings_catalog.js` owns model-catalog reads and the Accounts subscription: a changed confirmed account or recovery from a failed read refreshes discovery; identical settled status stays quiet, and `settings.js` arms/disposes the subscription with the page. Catalog updates preserve drafts. Per-button ownership releases busy state independently of data freshness: background reads cannot strand a manual button. The Settings client validates the whole current draft before Save; a local error keeps every value available for correction and sends no partial save (`settings_controls.js` keeps custom-key collection pure and dirty reads passive). Ordinary refresh and failed writes preserve current edits, leaving or explicitly reloading a dirty draft asks first, the write response distinguishes saved, unsaved and unknown outcomes, and there is no durable cross-page draft store or secret persistence. diff --git a/tests/test_release_metadata_diagnostics.py b/tests/test_release_metadata_diagnostics.py index c80e96235..e48d14fc1 100644 --- a/tests/test_release_metadata_diagnostics.py +++ b/tests/test_release_metadata_diagnostics.py @@ -85,6 +85,18 @@ def test_crlf_carriers_have_same_text_semantics_in_index_and_worktree(candidate) assert result["status"] == "clean", result +def test_sm1_committed_crlf_export_preserves_carrier_lines(candidate): + from devtools.e2e_live.scenarios import _git_show, release_carriers_desync_at + repo = candidate.repo_dir + _git(repo, "config", "core.autocrlf", "false") + for name, text in _release().items(): + (repo / name).write_bytes(text.replace("\n", "\r\n").encode("utf-8")) + _git(repo, "add", ".") + _git(repo, "commit", "-qm", "CRLF carriers") + assert _git_show(repo, "HEAD", "uv.lock") == _release()["uv.lock"] + assert release_carriers_desync_at(repo, "HEAD") == "" + + def test_unicode_worktree_discovery_is_not_locale_decoded(candidate): repo = candidate.repo_dir _git(repo, "config", "core.quotepath", "false") @@ -239,7 +251,7 @@ def test_git_discovery_failure_is_never_clean(candidate, monkeypatch, source): def fail_discovery(argv, **kwargs): if "status" in argv or "diff" in argv: - return subprocess.CompletedProcess(argv, 128, stdout="", stderr="fixture failed") + return subprocess.CompletedProcess(argv, 128, stdout=b"", stderr=b"fixture failed") return real(argv, **kwargs) # The real run(check=True) raises for index discovery; emulate that contract. diff --git a/tests/test_ui_smoke_status_attention.py b/tests/test_ui_smoke_status_attention.py index e0e12e662..8a27ad9ca 100644 --- a/tests/test_ui_smoke_status_attention.py +++ b/tests/test_ui_smoke_status_attention.py @@ -327,7 +327,10 @@ def test_task_status_stays_factual_in_main_and_project_chat( # This synthetic code has no producer phrase. Unknown reasons remain # visible verbatim; hiding them would discard the only available cause. assert "provider_route_failed" in failed.locator( - ":scope > [data-live-summary-button]" + ":scope > [data-live-summary-button] [data-live-activity]" + ).inner_text() + assert "provider_route_failed" not in failed.locator( + ":scope > [data-live-summary-button] [data-live-title]" ).inner_text() status = scope.locator(status_selector)