From 4e61f0f2ba2f9863763dc0ee264861865a8d6dd2 Mon Sep 17 00:00:00 2001 From: JesseLeeStringer Date: Sun, 3 May 2026 13:53:54 +1000 Subject: [PATCH] Fix YAML ScannerError on Windows credentials_path `adloop init` writes Windows paths like `c:\Users\user\.adloop\credentials.json` inside double-quoted YAML, which the parser treats as escape sequences (`\U` begins a Unicode escape). Loading the resulting config raises ScannerError and the MCP server fails to start. Switch to single-quoted YAML for credentials_path; single-quoted strings are literal so backslashes are safe. Embedded single quotes are escaped by doubling per the YAML 1.1 spec. Adds tests/test_cli.py covering the Windows path, POSIX path, embedded-apostrophe, and no-credentials-path cases. --- src/adloop/cli.py | 7 +++++- tests/test_cli.py | 56 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 tests/test_cli.py diff --git a/src/adloop/cli.py b/src/adloop/cli.py index 9d2e234..914de9a 100644 --- a/src/adloop/cli.py +++ b/src/adloop/cli.py @@ -160,7 +160,12 @@ def _generate_config_yaml( if project_id: lines.append(f' project_id: "{project_id}"') if credentials_path: - lines.append(f' credentials_path: "{credentials_path}"') + # YAML treats `\` inside double-quoted strings as escape sequences (e.g. + # `\U` begins a Unicode escape, breaking Windows paths like `c:\Users\...`). + # Single-quoted strings are literal — backslashes are safe. Embedded + # single quotes (rare in paths) are escaped per YAML spec by doubling them. + escaped = credentials_path.replace("'", "''") + lines.append(f" credentials_path: '{escaped}'") else: lines.append(" # Using built-in credentials (no credentials_path needed)") lines.append(' token_path: "~/.adloop/token.json"') diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..8728f0a --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,56 @@ +"""Tests for the `adloop init` wizard helpers in `adloop.cli`.""" + +from __future__ import annotations + +import yaml + +from adloop.cli import _generate_config_yaml + + +class TestGenerateConfigYaml: + """YAML generation in the init wizard must produce parseable output.""" + + def _generate(self, **overrides): + defaults = { + "project_id": "", + "credentials_path": "", + "property_id": "123456789", + "developer_token": "abc123", + "customer_id": "123-456-7890", + "login_customer_id": "987-654-3210", + "max_daily_budget": 50.0, + "require_dry_run": True, + } + defaults.update(overrides) + return _generate_config_yaml(**defaults) + + def test_windows_credentials_path_parses(self): + """Regression for Windows backslash paths breaking YAML parsing. + + Previously the wizard wrote `credentials_path: "c:\\Users\\..."` which + YAML interpreted as a `\\U` Unicode escape sequence and raised + ScannerError. Single quotes treat backslashes literally. + """ + win_path = r"c:\Users\user\.adloop\credentials.json" + text = self._generate(credentials_path=win_path) + parsed = yaml.safe_load(text) + assert parsed["google"]["credentials_path"] == win_path + + def test_posix_credentials_path_parses(self): + posix_path = "/home/user/.adloop/credentials.json" + text = self._generate(credentials_path=posix_path) + parsed = yaml.safe_load(text) + assert parsed["google"]["credentials_path"] == posix_path + + def test_path_with_embedded_apostrophe_parses(self): + """YAML single-quoted strings escape `'` by doubling — make sure we do.""" + weird_path = r"c:\Users\o'brien\.adloop\credentials.json" + text = self._generate(credentials_path=weird_path) + parsed = yaml.safe_load(text) + assert parsed["google"]["credentials_path"] == weird_path + + def test_no_credentials_path_uses_bundled_comment(self): + text = self._generate(credentials_path="") + assert "Using built-in credentials" in text + parsed = yaml.safe_load(text) + assert "credentials_path" not in parsed.get("google", {}) \ No newline at end of file