ktalk-cli 3.0.0__tar.gz → 3.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/PKG-INFO +1 -1
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/pyproject.toml +1 -1
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/client.py +11 -1
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/config.py +20 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_cli_host_config_wiring.py +70 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_config.py +107 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_fr44_auth_status_accepted_rejected.py +94 -6
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_fr48_doctor_summary_diagnostics.py +43 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_token_file.py +17 -17
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/.gitignore +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/LICENSE +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/README.md +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/CLAUDE.md +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/__init__.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/auth.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/calendar_reader.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/cli.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/cli_contacts.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/cli_content.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/cli_doctor.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/cli_meeting.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/cli_meeting_args.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/cli_meeting_confirm.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/cli_meetings_read.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/cli_sanction.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/cli_store.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/cli_sync.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/cli_token.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/confirmation.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/contacts.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/contour_diagnostics.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/download.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/endpoints.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/enrichment.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/formatters.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/host_config.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/meeting_body.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/meeting_cancel.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/meeting_scheduling.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/pagination.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/reconciliation.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/registry.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/rooms.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/store.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/store_migration.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/token_file.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/transcript_identity.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/write_journal.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/src/ktalk_cli/write_sanction.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/conftest.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/fixtures/access-info-expired.json +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/fixtures/access-info-full.json +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/fixtures/archive-8col.md +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/fixtures/archive-dup-id.md +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/fixtures/archive-escaped-pipe-7col.md +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/fixtures/archive-page1.json +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/fixtures/archive-page2-empty.json +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/fixtures/archive-pipe-in-name.md +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/fixtures/calendar-item-session.json +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/fixtures/chat-messages-session.json +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/fixtures/conference-history-session-oversized.json +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/fixtures/conference-history-session.json +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/fixtures/error-body-problem-json.json +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/fixtures/error-body-validation.json +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/fixtures/recording-detail-session-full-participants.json +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/fixtures/recording-detail-session-oversized-partial.json +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/fixtures/recording-detail-with-qualities.json +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/fixtures/recording-list-item-session.json +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/fixtures/recordings-list-apikey-page1.json +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/fixtures/recordings-list-apikey-page2.json +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/fixtures/registry-archive-2026-04.md +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/fixtures/registry.md +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/fixtures/room-detail-session.json +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_adr022_ktalk_cli_rename.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_archive.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_auth_modes.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_calendar.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_chat.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_cli.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_cli_config_show.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_cli_content.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_cli_export_store_isolation.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_cli_meeting.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_cli_meeting_sanctioned.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_cli_meetings_surface.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_cli_migrate_to_central_store.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_client.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_concurrency.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_confirmation.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_contour_diagnostics.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_diagnostics.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_download.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_enrichment.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_formatters.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_fr19_auth_status.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_fr21_no_vault_layout.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_fr39_calendar_inclusive_end.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_fr40_timezone_format.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_fr41_last_synced.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_fr42_single_credential_source.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_fr43_legacy_key_removal_warning.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_fr47_missing_fields_completeness.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_host_config.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_issue3_null_id_fallback.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_meeting_body.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_meeting_cancel.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_meeting_scheduling.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_migration.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_nfr17_identity_verification.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_pagination.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_reconciliation.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_registry.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_rooms.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_search_contacts.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_secret_masking.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_store.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_store_migration.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_write_journal.py +0 -0
- {ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/test_write_sanction.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ktalk-cli
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.2
|
|
4
4
|
Summary: CLI for accessing Kontur Talk (KTalk) recordings, transcripts and summaries
|
|
5
5
|
Project-URL: Homepage, https://github.com/mdemyanov/ktalk-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/mdemyanov/ktalk-cli
|
|
@@ -239,7 +239,17 @@ class KTalkClient:
|
|
|
239
239
|
отдельно от `KTalkAuthError` — раньше всплывало сырым текстом до `main()`."""
|
|
240
240
|
try:
|
|
241
241
|
await self.list_recordings(top=1)
|
|
242
|
-
except KTalkAuthError:
|
|
242
|
+
except KTalkAuthError as exc:
|
|
243
|
+
if getattr(exc, "status_code", None) == 403:
|
|
244
|
+
# 403 на пробном запросе — не отказ токена, а нехватка прав на саму
|
|
245
|
+
# пробную операцию (ADR-025 «Разведение 401/403»).
|
|
246
|
+
return AuthStatus(
|
|
247
|
+
alive=True,
|
|
248
|
+
note=(
|
|
249
|
+
"Доступ запрещён: у текущей сессии нет прав на пробную операцию "
|
|
250
|
+
"(список записей). Токен при этом рабочий — обновлять его не нужно."
|
|
251
|
+
),
|
|
252
|
+
)
|
|
243
253
|
return AuthStatus(
|
|
244
254
|
alive=False,
|
|
245
255
|
note="Токен сессии не прошёл проверку (пробный запрос списка записей).",
|
|
@@ -38,6 +38,17 @@ def resolve_db_path(
|
|
|
38
38
|
обязана применяться к итоговому пути независимо от источника (ADR-013-spec
|
|
39
39
|
§«Поток данных» п.4) — до этой правки вызывалась только в ветке машинного
|
|
40
40
|
дефолта; `--db`/`KTALK_REGISTRY_DB`/конфиг хозяина возвращали путь раньше.
|
|
41
|
+
|
|
42
|
+
FR-23 AC-4/AC-5: относительный `registry.db_path` резолвится
|
|
43
|
+
против `host_config.path.parent` (каталог, где найден `.ktalk.toml`), не
|
|
44
|
+
против cwd вызывающего процесса — резолюция от cwd и есть исходный дефект
|
|
45
|
+
(«unable to open database file» при запуске не из корня проекта-хозяина).
|
|
46
|
+
Абсолютный `db_path` используется как есть, каталог `.ktalk.toml` в
|
|
47
|
+
резолюцию не участвует. `host_config.path is None` при относительном
|
|
48
|
+
`db_path` — нарушение инварианта вызывающей стороны (в проде
|
|
49
|
+
`discover_host_config`, SA-003, всегда заполняет `path`) — явное
|
|
50
|
+
исключение, не тихий откат на cwd. `--db`/`KTALK_REGISTRY_DB` вне области
|
|
51
|
+
(BA-031) — резолюция от cwd для них остаётся конвенцией CLI/окружения.
|
|
41
52
|
"""
|
|
42
53
|
from ktalk_cli.store import resolve_store_root, warn_if_sync_dir
|
|
43
54
|
|
|
@@ -54,6 +65,15 @@ def resolve_db_path(
|
|
|
54
65
|
configured = host_config.registry.get("db_path")
|
|
55
66
|
if configured:
|
|
56
67
|
path = Path(configured)
|
|
68
|
+
if not path.is_absolute():
|
|
69
|
+
if host_config.path is None:
|
|
70
|
+
raise KTalkConfigError(
|
|
71
|
+
f"Относительный registry.db_path ({configured!r}) в конфиге "
|
|
72
|
+
"хозяина требует известный каталог .ktalk.toml, а "
|
|
73
|
+
"host_config.path не задан — резолюция от текущего "
|
|
74
|
+
"рабочего каталога воспроизвела бы исходный дефект."
|
|
75
|
+
)
|
|
76
|
+
path = host_config.path.parent / path
|
|
57
77
|
warn_if_sync_dir(path)
|
|
58
78
|
return path
|
|
59
79
|
|
|
@@ -160,6 +160,76 @@ def test_maj02_file_written_after_registry_closes_uses_ambient_umask_not_0o077(
|
|
|
160
160
|
# --- MAJ-03: export пишет зеркало рядом с БД из .ktalk.toml, а не рядом с дефолтом ---
|
|
161
161
|
|
|
162
162
|
|
|
163
|
+
def test_fr23_ac4_bare_cli_relative_db_path_from_subdirectory_reproduces_dashboard_symptom(
|
|
164
|
+
tmp_path, monkeypatch, capsys
|
|
165
|
+
):
|
|
166
|
+
"""Discovery-режим 2 (обход вверх от cwd, SA-003) + FR-23 AC-4 (ktalk-mcp-ds6):
|
|
167
|
+
буквальное воспроизведение отчёта владельца — `cd .../95_TRANSCRIPTS && ktalk
|
|
168
|
+
dashboard` падал `unable to open database file`, потому что относительный
|
|
169
|
+
db_path резолвился от cwd вместо каталога найденного .ktalk.toml (обход
|
|
170
|
+
находит конфиг в родителе, а не в cwd самом). `dashboard` — команда из
|
|
171
|
+
исходного отчёта, не заменена на `list` намеренно."""
|
|
172
|
+
project = tmp_path / "naumen-cto"
|
|
173
|
+
project.mkdir()
|
|
174
|
+
(project / ".git").mkdir()
|
|
175
|
+
db_path = project / "95_TRANSCRIPTS" / ".registry.db"
|
|
176
|
+
db_path.parent.mkdir(parents=True)
|
|
177
|
+
_seed(db_path)
|
|
178
|
+
(project / ".ktalk.toml").write_text(
|
|
179
|
+
'[registry]\ndb_path = "95_TRANSCRIPTS/.registry.db"\n', encoding="utf-8"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
monkeypatch.delenv("KTALK_REGISTRY_DB", raising=False)
|
|
183
|
+
monkeypatch.delenv("CLAUDE_PROJECT_DIR", raising=False)
|
|
184
|
+
monkeypatch.chdir(db_path.parent) # ровно подкаталог хранилища из отчёта
|
|
185
|
+
|
|
186
|
+
from ktalk_cli.cli import main
|
|
187
|
+
|
|
188
|
+
rc = main(["dashboard", "--json"])
|
|
189
|
+
assert rc == 0
|
|
190
|
+
out = json.loads(capsys.readouterr().out)
|
|
191
|
+
ids = {r["recording_id"] for r in out["new"]}
|
|
192
|
+
assert "wired" in ids, (
|
|
193
|
+
"TODO: FR-23 AC-4 — относительный db_path обязан резолвиться от "
|
|
194
|
+
"каталога .ktalk.toml (host_config.path.parent), не от cwd вызова"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_fr23_ac4_explicit_claude_project_dir_relative_db_path_ignores_unrelated_cwd(
|
|
199
|
+
tmp_path, monkeypatch, capsys
|
|
200
|
+
):
|
|
201
|
+
"""Discovery-режим 1 (явный CLAUDE_PROJECT_DIR, SA-003) + FR-23 AC-4: cwd —
|
|
202
|
+
каталог, не являющийся ни проектом, ни его предком/потомком (обход вверх в
|
|
203
|
+
этом режиме не выполняется вовсе, discovery ищет ровно по CLAUDE_PROJECT_DIR).
|
|
204
|
+
Относительный db_path обязан резолвиться от CLAUDE_PROJECT_DIR, не от этого
|
|
205
|
+
произвольного cwd."""
|
|
206
|
+
project = tmp_path / "project"
|
|
207
|
+
project.mkdir()
|
|
208
|
+
db_path = project / "store" / "registry.db"
|
|
209
|
+
db_path.parent.mkdir(parents=True)
|
|
210
|
+
_seed(db_path)
|
|
211
|
+
(project / ".ktalk.toml").write_text(
|
|
212
|
+
'[registry]\ndb_path = "store/registry.db"\n', encoding="utf-8"
|
|
213
|
+
)
|
|
214
|
+
unrelated_cwd = tmp_path / "unrelated"
|
|
215
|
+
unrelated_cwd.mkdir()
|
|
216
|
+
|
|
217
|
+
monkeypatch.delenv("KTALK_REGISTRY_DB", raising=False)
|
|
218
|
+
monkeypatch.setenv("CLAUDE_PROJECT_DIR", str(project))
|
|
219
|
+
monkeypatch.chdir(unrelated_cwd)
|
|
220
|
+
|
|
221
|
+
from ktalk_cli.cli import main
|
|
222
|
+
|
|
223
|
+
rc = main(["list", "--json"])
|
|
224
|
+
assert rc == 0
|
|
225
|
+
out = json.loads(capsys.readouterr().out)
|
|
226
|
+
ids = {r["recording_id"] for r in out["recordings"]}
|
|
227
|
+
assert "wired" in ids, (
|
|
228
|
+
"TODO: FR-23 AC-4 — discovery-режим 1: относительный db_path обязан "
|
|
229
|
+
"резолвиться от CLAUDE_PROJECT_DIR, не от cwd"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
163
233
|
def test_maj03_export_mirror_follows_host_config_db_path(tmp_path, monkeypatch, capsys):
|
|
164
234
|
"""`_cmd_export` резолвил путь БД повторно и БЕЗ host_config: реестр читался из
|
|
165
235
|
`.ktalk.toml`, а `registry.md` уезжал к машинному дефолту (`store.resolve_store_root`).
|
|
@@ -135,3 +135,110 @@ def test_resolve_db_path_none_of_the_four_sources_falls_through_to_machine_defau
|
|
|
135
135
|
assert not resolved.is_relative_to(Path.cwd()), (
|
|
136
136
|
"TODO: FR-22 AC-1 — машинный дефолт вне cwd, не 95_TRANSCRIPTS/.registry.db"
|
|
137
137
|
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# --- FR-23 AC-4/AC-5 (ktalk-mcp-ds6): относительный db_path из конфига хозяина ---
|
|
141
|
+
# джойнится с каталогом .ktalk.toml (host_config.path.parent), не с cwd;
|
|
142
|
+
# абсолютный db_path остаётся как есть, каталог .ktalk.toml не участвует.
|
|
143
|
+
# Spec: openspec/specs/host-project-config-discovery/spec.md, «A relative
|
|
144
|
+
# host-config path resolves against the config file's own directory» /
|
|
145
|
+
# «An absolute host-config path is used unmodified».
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_fr23_ac4_relative_db_path_joins_against_host_config_dir_not_cwd(
|
|
149
|
+
monkeypatch, tmp_path
|
|
150
|
+
):
|
|
151
|
+
"""Unit, ADR-013-spec «Контракт с QA-author»: resolve_db_path вызывается
|
|
152
|
+
напрямую с вручную собранным HostConfig — без discovery, без обхода вверх.
|
|
153
|
+
cwd намеренно ставится в каталог, отличный от каталога .ktalk.toml, чтобы
|
|
154
|
+
join нельзя было случайно спутать со старым (дефектным) резолвом от cwd."""
|
|
155
|
+
monkeypatch.delenv("KTALK_REGISTRY_DB", raising=False)
|
|
156
|
+
host_config_dir = tmp_path / "host-project"
|
|
157
|
+
host_config_dir.mkdir()
|
|
158
|
+
elsewhere_cwd = tmp_path / "elsewhere"
|
|
159
|
+
elsewhere_cwd.mkdir()
|
|
160
|
+
monkeypatch.chdir(elsewhere_cwd)
|
|
161
|
+
|
|
162
|
+
from ktalk_cli.config import resolve_db_path
|
|
163
|
+
from ktalk_cli.host_config import HostConfig
|
|
164
|
+
|
|
165
|
+
host_config = HostConfig(
|
|
166
|
+
registry={"db_path": "custom/registry.db"},
|
|
167
|
+
path=host_config_dir / ".ktalk.toml",
|
|
168
|
+
)
|
|
169
|
+
resolved = resolve_db_path(None, host_config=host_config)
|
|
170
|
+
assert resolved == host_config_dir / "custom/registry.db", (
|
|
171
|
+
"TODO: FR-23 AC-4 — относительный db_path обязан джойниться с "
|
|
172
|
+
"host_config.path.parent, не оставаться голым Path(configured)"
|
|
173
|
+
)
|
|
174
|
+
assert resolved != elsewhere_cwd / "custom/registry.db"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_fr23_ac5_absolute_db_path_ignores_host_config_dir_even_when_path_set(
|
|
178
|
+
monkeypatch, tmp_path
|
|
179
|
+
):
|
|
180
|
+
"""Unit, регрессия FR-23 AC-5: абсолютный db_path применяется как есть, даже
|
|
181
|
+
когда host_config.path указывает на совсем другой каталог — каталог
|
|
182
|
+
.ktalk.toml не должен «подмешиваться» в резолюцию абсолютного пути."""
|
|
183
|
+
monkeypatch.delenv("KTALK_REGISTRY_DB", raising=False)
|
|
184
|
+
absolute_db = tmp_path / "abs-store" / "registry.db"
|
|
185
|
+
host_config_dir = tmp_path / "unrelated-config-dir"
|
|
186
|
+
|
|
187
|
+
from ktalk_cli.config import resolve_db_path
|
|
188
|
+
from ktalk_cli.host_config import HostConfig
|
|
189
|
+
|
|
190
|
+
host_config = HostConfig(
|
|
191
|
+
registry={"db_path": str(absolute_db)},
|
|
192
|
+
path=host_config_dir / ".ktalk.toml",
|
|
193
|
+
)
|
|
194
|
+
resolved = resolve_db_path(None, host_config=host_config)
|
|
195
|
+
assert resolved == absolute_db
|
|
196
|
+
assert not str(resolved).startswith(str(host_config_dir)), (
|
|
197
|
+
"TODO: FR-23 AC-5 — абсолютный db_path не должен резолвиться относительно "
|
|
198
|
+
"каталога .ktalk.toml"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_fr23_ac4_host_config_path_none_with_relative_db_path_raises_not_silent_cwd_fallback(
|
|
203
|
+
monkeypatch, tmp_path
|
|
204
|
+
):
|
|
205
|
+
"""Masked-failure класс (ADR-013-spec, edge case): HostConfig собран вручную
|
|
206
|
+
(не через discover_host_config) с относительным db_path и path=None — базы
|
|
207
|
+
для join нет. Ожидание — явное исключение, НЕ тихий откат на резолюцию от
|
|
208
|
+
cwd (тот самый откат и есть исходный дефект ktalk-mcp-ds6, только замаскированный
|
|
209
|
+
отсутствием ошибки вместо неверного пути)."""
|
|
210
|
+
monkeypatch.delenv("KTALK_REGISTRY_DB", raising=False)
|
|
211
|
+
monkeypatch.chdir(tmp_path)
|
|
212
|
+
|
|
213
|
+
from ktalk_cli.config import resolve_db_path
|
|
214
|
+
from ktalk_cli.host_config import HostConfig
|
|
215
|
+
|
|
216
|
+
host_config = HostConfig(registry={"db_path": "relative/registry.db"}, path=None)
|
|
217
|
+
with pytest.raises(Exception):
|
|
218
|
+
resolve_db_path(None, host_config=host_config)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_fr23_ac4_warn_if_sync_dir_applies_to_final_joined_path_not_bare_relative_value(
|
|
222
|
+
monkeypatch, tmp_path, capsys
|
|
223
|
+
):
|
|
224
|
+
"""ADR-013-spec «Поток данных» п.4: warn_if_sync_dir обязан применяться к
|
|
225
|
+
итоговому (уже джойненному) пути. Маркер `Dropbox` присутствует только в
|
|
226
|
+
каталоге .ktalk.toml, не в самом относительном db_path — до починки
|
|
227
|
+
warn_if_sync_dir вызывался на голом Path("registry.db") и маркер не находил,
|
|
228
|
+
после починки видит его в резолвленном абсолютном пути."""
|
|
229
|
+
monkeypatch.delenv("KTALK_REGISTRY_DB", raising=False)
|
|
230
|
+
host_config_dir = tmp_path / "Dropbox" / "project"
|
|
231
|
+
host_config_dir.mkdir(parents=True)
|
|
232
|
+
|
|
233
|
+
from ktalk_cli.config import resolve_db_path
|
|
234
|
+
from ktalk_cli.host_config import HostConfig
|
|
235
|
+
|
|
236
|
+
host_config = HostConfig(
|
|
237
|
+
registry={"db_path": "registry.db"}, path=host_config_dir / ".ktalk.toml"
|
|
238
|
+
)
|
|
239
|
+
resolve_db_path(None, host_config=host_config)
|
|
240
|
+
captured = capsys.readouterr()
|
|
241
|
+
assert "Dropbox" in captured.err, (
|
|
242
|
+
"TODO: FR-23 AC-4 — warn_if_sync_dir обязан видеть маркер в резолвленном "
|
|
243
|
+
"(джойненном) пути, а не в голом относительном db_path"
|
|
244
|
+
)
|
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
"""AT-design: FR-44 — `auth-status` различает принятое и отвергнутое значение.
|
|
2
2
|
|
|
3
3
|
Покрывает `openspec/specs/talk-api-auth-modes/spec.md`, требование «Auth-status
|
|
4
|
-
diagnosis distinguishes an accepted session token from a rejected one» (
|
|
5
|
-
|
|
6
|
-
`
|
|
7
|
-
`content/30-requirements/single-auth-mode.md`
|
|
4
|
+
diagnosis distinguishes an accepted session token from a rejected one» (4 сценария,
|
|
5
|
+
включая «Probe rejected for permissions, not credential») + новую форму
|
|
6
|
+
`--json`-ответа (`{"alive": bool, "note": str|None}`, без `scopes`/`expired_at`)
|
|
7
|
+
названную ADR-025 «Решение» п.3. Источник — `content/30-requirements/single-auth-mode.md`
|
|
8
|
+
FR-44, issue #11, дефект `ktalk-mcp-qqc` (403 схлопывается в отказ токена).
|
|
8
9
|
|
|
9
10
|
Урок дерева 2026-09-04 (`content/lessons-learned.md`): признак в теле ответа без
|
|
10
11
|
эскалации в код возврата неотличим потребителем, читающим только код, от отсутствия
|
|
11
12
|
отказа — тест обязан утверждать ОБА канала отдельными assert'ами в одном сценарии
|
|
12
|
-
(не полагаться на то, что проверка одного канала подразумевает другой).
|
|
13
|
+
(не полагаться на то, что проверка одного канала подразумевает другой). Урок
|
|
14
|
+
действует в обе стороны (ADR-025-spec «Разведение 401/403…»): эскалация в код
|
|
15
|
+
возврата при живом токене — то же нарушение, что и молчание при настоящем отказе.
|
|
13
16
|
|
|
14
17
|
Красные по замыслу: сегодня `cmd_auth_status` (`cli_sync.py`) возвращает `0`
|
|
15
18
|
безусловно (не пробрасывает `status.alive`), а `--json`-ответ несёт `scopes`/
|
|
16
19
|
`expired_at` (`cli_sync.py`, `AuthStatus`, `auth.py`). `_auth_status_session`
|
|
17
20
|
(`client.py`) не перехватывает непарсящееся тело отдельно от `KTalkAuthError` — тест
|
|
18
21
|
4 падает на утечке сырого текста `json.JSONDecodeError`, не на импорте (имена не
|
|
19
|
-
меняются, `client.py`/`cli_sync.py` уже существуют).
|
|
22
|
+
меняются, `client.py`/`cli_sync.py` уже существуют). `_auth_status_session` также
|
|
23
|
+
не разбирает `exc.status_code` на пойманном `KTalkAuthError` — 403 схлопывается в
|
|
24
|
+
тот же вердикт `alive=False`, что и 401 (тесты `test_fr44_5_*`..`test_fr44_7_*`).
|
|
20
25
|
"""
|
|
21
26
|
|
|
22
27
|
from __future__ import annotations
|
|
@@ -130,3 +135,86 @@ def test_fr44_4_unparsable_probe_body_yields_honest_result_not_raw_decode_error(
|
|
|
130
135
|
assert "Expecting value" not in captured.err
|
|
131
136
|
assert "Traceback" not in captured.err
|
|
132
137
|
assert rc != 0
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
_NOTE_403_VERBATIM = (
|
|
141
|
+
"Доступ запрещён: у текущей сессии нет прав на пробную операцию (список "
|
|
142
|
+
"записей). Токен при этом рабочий — обновлять его не нужно."
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_fr44_5_probe_rejected_403_alive_true_and_exit_zero(
|
|
147
|
+
monkeypatch, httpx_mock: HTTPXMock, capsys
|
|
148
|
+
):
|
|
149
|
+
"""Scenario «Probe rejected for permissions, not credential (fixture, no live
|
|
150
|
+
contour required)» — 403 на пробном запросе не отказ токена: `alive` остаётся
|
|
151
|
+
`true`, код возврата остаётся `0`. Оба канала — отдельным ассертом (урок
|
|
152
|
+
2026-09-04 действует и в эту сторону: эскалация в код возврата при живом
|
|
153
|
+
токене — то же нарушение, что и молчание при настоящем отказе).
|
|
154
|
+
|
|
155
|
+
Красный сегодня (дефект `ktalk-mcp-qqc`): `_auth_status_session` ловит
|
|
156
|
+
`KTalkAuthError` без разбора `exc.status_code`, 403 даёт тот же вердикт, что
|
|
157
|
+
401 — `alive: false`, `rc == 1`.
|
|
158
|
+
"""
|
|
159
|
+
monkeypatch.setenv("KTALK_BASE_URL", "https://test.ktalk.ru")
|
|
160
|
+
monkeypatch.setenv("KTALK_SESSION_TOKEN", "sess-forbidden-0001")
|
|
161
|
+
monkeypatch.delenv("KTALK_PERSONAL_API_KEY", raising=False)
|
|
162
|
+
httpx_mock.add_response(status_code=403)
|
|
163
|
+
|
|
164
|
+
from ktalk_cli.cli import main
|
|
165
|
+
|
|
166
|
+
rc = main(["auth-status", "--json"])
|
|
167
|
+
data = json.loads(capsys.readouterr().out)
|
|
168
|
+
|
|
169
|
+
# Канал 1: поле в теле ответа — токен рабочий.
|
|
170
|
+
assert data["alive"] is True
|
|
171
|
+
# Канал 2: код возврата процесса — независимый ассерт, не производный от канала 1.
|
|
172
|
+
assert rc == 0
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_fr44_6_probe_rejected_403_note_is_verbatim_permission_gap(
|
|
176
|
+
monkeypatch, httpx_mock: HTTPXMock, capsys
|
|
177
|
+
):
|
|
178
|
+
"""Scenario «Probe rejected for permissions, not credential»: `note` называет
|
|
179
|
+
нехватку прав дословно (BA-030) и явно не предлагает обновить токен —
|
|
180
|
+
проверка дословного текста, не только присутствия подстроки (ADR-025-spec
|
|
181
|
+
edge case «403 на пробном запросе внутри `ktalk doctor`»/«разводка 401/403»)."""
|
|
182
|
+
monkeypatch.setenv("KTALK_BASE_URL", "https://test.ktalk.ru")
|
|
183
|
+
monkeypatch.setenv("KTALK_SESSION_TOKEN", "sess-forbidden-0002")
|
|
184
|
+
monkeypatch.delenv("KTALK_PERSONAL_API_KEY", raising=False)
|
|
185
|
+
httpx_mock.add_response(status_code=403)
|
|
186
|
+
|
|
187
|
+
from ktalk_cli.cli import main
|
|
188
|
+
|
|
189
|
+
main(["auth-status", "--json"])
|
|
190
|
+
data = json.loads(capsys.readouterr().out)
|
|
191
|
+
|
|
192
|
+
assert data["note"] == _NOTE_403_VERBATIM
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_fr44_7_probe_rejected_403_note_differs_from_401_note(
|
|
196
|
+
monkeypatch, httpx_mock: HTTPXMock, capsys
|
|
197
|
+
):
|
|
198
|
+
"""Malformed/masked-failure guard: 403 и 401 — разные причины отказа пробного
|
|
199
|
+
запроса, `note` обязан их различать текстом, а не схлопывать в один и тот же
|
|
200
|
+
вердикт (текущий баг `ktalk-mcp-qqc` — оба кода дают идентичный `note`
|
|
201
|
+
«Токен сессии не прошёл проверку...»). Малое, но наблюдаемое поведение: без
|
|
202
|
+
этого различения потребитель, читающий только `note`, не может отличить
|
|
203
|
+
«токен мёртв» от «токену не хватает прав»."""
|
|
204
|
+
monkeypatch.setenv("KTALK_BASE_URL", "https://test.ktalk.ru")
|
|
205
|
+
monkeypatch.setenv("KTALK_SESSION_TOKEN", "sess-forbidden-0003")
|
|
206
|
+
monkeypatch.delenv("KTALK_PERSONAL_API_KEY", raising=False)
|
|
207
|
+
httpx_mock.add_response(status_code=403)
|
|
208
|
+
|
|
209
|
+
from ktalk_cli.cli import main
|
|
210
|
+
|
|
211
|
+
main(["auth-status", "--json"])
|
|
212
|
+
data_403 = json.loads(capsys.readouterr().out)
|
|
213
|
+
|
|
214
|
+
monkeypatch.setenv("KTALK_SESSION_TOKEN", "sess-forbidden-0003b")
|
|
215
|
+
httpx_mock.add_response(status_code=401)
|
|
216
|
+
|
|
217
|
+
main(["auth-status", "--json"])
|
|
218
|
+
data_401 = json.loads(capsys.readouterr().out)
|
|
219
|
+
|
|
220
|
+
assert data_403["note"] != data_401["note"]
|
|
@@ -17,6 +17,12 @@ Dev не зарегистрирует подкоманду. Это стабил
|
|
|
17
17
|
причина падения для КАЖДОГО теста этого файла на сегодняшнем коде — как только
|
|
18
18
|
`doctor` зарегистрирован, тесты начинают падать (или проходить) по своей
|
|
19
19
|
индивидуальной причине.
|
|
20
|
+
|
|
21
|
+
`test_fr48_15_*` — регрессия дефекта `ktalk-mcp-qqc` (403 на пробном запросе
|
|
22
|
+
`auth-status`, схлопнутый в отказ токена, разведён ADR-025-spec «Разведение
|
|
23
|
+
401/403 в пробном запросе `auth-status`»): `auth` не должен попасть в
|
|
24
|
+
`failed_items` доктора, если пробный запрос отклонён по нехватке прав, а не по
|
|
25
|
+
невалидному токену.
|
|
20
26
|
"""
|
|
21
27
|
|
|
22
28
|
from __future__ import annotations
|
|
@@ -390,6 +396,43 @@ def test_fr48_13_human_readable_output_names_failed_item_by_key(
|
|
|
390
396
|
assert "auth" in out
|
|
391
397
|
|
|
392
398
|
|
|
399
|
+
def test_fr48_15_probe_403_does_not_mark_auth_as_failed(
|
|
400
|
+
monkeypatch, tmp_path, httpx_mock: HTTPXMock, capsys
|
|
401
|
+
):
|
|
402
|
+
"""Регрессия дефекта `ktalk-mcp-qqc` внутри `doctor` (ADR-025-spec, «edge
|
|
403
|
+
cases»/companion-спека ADR-026 §2): пробный запрос отклонён `403` — токен
|
|
404
|
+
рабочий, `_auth_item` кладёт пункт `auth` в `failed_items` по `not
|
|
405
|
+
status.alive`, а `alive` обязан остаться `True` на 403 (не отказ токена).
|
|
406
|
+
`auth.alive=True` не создаёт провала сам по себе — тот же приём, что
|
|
407
|
+
`test_fr48_8` фиксирует для `token_file`, здесь наоборот: провал не должен
|
|
408
|
+
возникнуть там, где его не должно быть.
|
|
409
|
+
|
|
410
|
+
Красный сегодня: `_auth_status_session` (`client.py`) не разбирает
|
|
411
|
+
`exc.status_code` — 403 даёт `alive=False`, `_auth_item` кладёт `auth` в
|
|
412
|
+
`failed_items`, `doctor` возвращает ненулевой код по этой причине одной.
|
|
413
|
+
"""
|
|
414
|
+
monkeypatch.chdir(tmp_path)
|
|
415
|
+
monkeypatch.delenv("CLAUDE_PROJECT_DIR", raising=False)
|
|
416
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
417
|
+
monkeypatch.delenv("KTALK_TOKEN_FILE", raising=False)
|
|
418
|
+
monkeypatch.setenv("KTALK_REGISTRY_DB", str(tmp_path / "r.db"))
|
|
419
|
+
monkeypatch.setenv("KTALK_SESSION_TOKEN", "sess-doctor-0015")
|
|
420
|
+
monkeypatch.delenv("KTALK_PERSONAL_API_KEY", raising=False)
|
|
421
|
+
httpx_mock.add_response(status_code=403)
|
|
422
|
+
|
|
423
|
+
from ktalk_cli.cli import main
|
|
424
|
+
from ktalk_cli.token_file import write_token
|
|
425
|
+
|
|
426
|
+
write_token("okDOCTORtoken0123456")
|
|
427
|
+
|
|
428
|
+
rc = main(["doctor", "--json"])
|
|
429
|
+
data = json.loads(capsys.readouterr().out)
|
|
430
|
+
|
|
431
|
+
assert data["auth"]["alive"] is True
|
|
432
|
+
assert "auth" not in data["failed_items"]
|
|
433
|
+
assert rc == 0
|
|
434
|
+
|
|
435
|
+
|
|
393
436
|
def test_fr48_14_found_host_config_path_agrees_with_config_show(
|
|
394
437
|
monkeypatch, tmp_path, httpx_mock: HTTPXMock, capsys
|
|
395
438
|
):
|
|
@@ -50,9 +50,9 @@ def test_write_token_creates_private_file_and_directory(monkeypatch, tmp_path):
|
|
|
50
50
|
monkeypatch.delenv("KTALK_TOKEN_FILE", raising=False)
|
|
51
51
|
from ktalk_cli.token_file import write_token
|
|
52
52
|
|
|
53
|
-
path = write_token("
|
|
53
|
+
path = write_token("FIXTUREnotArealToken01")
|
|
54
54
|
|
|
55
|
-
assert path.read_text(encoding="utf-8") == "
|
|
55
|
+
assert path.read_text(encoding="utf-8") == "FIXTUREnotArealToken01"
|
|
56
56
|
assert _mode(path) == 0o600
|
|
57
57
|
assert _mode(path.parent) == 0o700
|
|
58
58
|
|
|
@@ -63,9 +63,9 @@ def test_write_token_strips_surrounding_whitespace(monkeypatch, tmp_path):
|
|
|
63
63
|
monkeypatch.delenv("KTALK_TOKEN_FILE", raising=False)
|
|
64
64
|
from ktalk_cli.token_file import read_token, write_token
|
|
65
65
|
|
|
66
|
-
write_token("
|
|
66
|
+
write_token(" FIXTUREnotArealToken01\n")
|
|
67
67
|
|
|
68
|
-
assert read_token() == "
|
|
68
|
+
assert read_token() == "FIXTUREnotArealToken01"
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
def test_write_token_rejects_empty_value(monkeypatch, tmp_path):
|
|
@@ -93,7 +93,7 @@ def test_read_token_refuses_world_readable_file(monkeypatch, tmp_path):
|
|
|
93
93
|
monkeypatch.delenv("KTALK_TOKEN_FILE", raising=False)
|
|
94
94
|
from ktalk_cli.token_file import read_token, write_token
|
|
95
95
|
|
|
96
|
-
path = write_token("
|
|
96
|
+
path = write_token("FIXTUREnotArealToken01")
|
|
97
97
|
path.chmod(0o644)
|
|
98
98
|
|
|
99
99
|
assert read_token() is None
|
|
@@ -111,10 +111,10 @@ def test_settings_reads_token_file_when_env_empty(monkeypatch, tmp_path):
|
|
|
111
111
|
from ktalk_cli.config import Settings
|
|
112
112
|
from ktalk_cli.token_file import write_token
|
|
113
113
|
|
|
114
|
-
write_token("
|
|
114
|
+
write_token("FIXTUREnotArealToken01")
|
|
115
115
|
|
|
116
116
|
settings = Settings()
|
|
117
|
-
assert settings.auth_credential == "
|
|
117
|
+
assert settings.auth_credential == "FIXTUREnotArealToken01"
|
|
118
118
|
|
|
119
119
|
|
|
120
120
|
def test_env_session_token_wins_over_file(monkeypatch, tmp_path):
|
|
@@ -141,9 +141,9 @@ def test_token_from_file_is_masked_in_error_text(monkeypatch, tmp_path):
|
|
|
141
141
|
from ktalk_cli.config import redact_secrets
|
|
142
142
|
from ktalk_cli.token_file import write_token
|
|
143
143
|
|
|
144
|
-
write_token("
|
|
144
|
+
write_token("FIXTUREnotArealToken01")
|
|
145
145
|
|
|
146
|
-
assert "
|
|
146
|
+
assert "FIXTUREnotArealToken01" not in redact_secrets("token=FIXTUREnotArealToken01")
|
|
147
147
|
|
|
148
148
|
|
|
149
149
|
# --- CLI: ktalk token set | status ----------------------------------------
|
|
@@ -156,13 +156,13 @@ def test_cli_token_set_reads_stdin_and_writes_private_file(monkeypatch, tmp_path
|
|
|
156
156
|
from ktalk_cli.cli import main
|
|
157
157
|
from ktalk_cli.token_file import token_path
|
|
158
158
|
|
|
159
|
-
monkeypatch.setattr("sys.stdin", _FakeStdin("
|
|
159
|
+
monkeypatch.setattr("sys.stdin", _FakeStdin("FIXTUREnotArealToken01\n"))
|
|
160
160
|
rc = main(["token", "set", "-"])
|
|
161
161
|
|
|
162
162
|
assert rc == 0
|
|
163
|
-
assert token_path().read_text(encoding="utf-8") == "
|
|
163
|
+
assert token_path().read_text(encoding="utf-8") == "FIXTUREnotArealToken01"
|
|
164
164
|
assert _mode(token_path()) == 0o600
|
|
165
|
-
assert "
|
|
165
|
+
assert "FIXTUREnotArealToken01" not in capsys.readouterr().out
|
|
166
166
|
|
|
167
167
|
|
|
168
168
|
def test_cli_token_status_json_reports_present_without_leaking_value(
|
|
@@ -173,7 +173,7 @@ def test_cli_token_status_json_reports_present_without_leaking_value(
|
|
|
173
173
|
from ktalk_cli.cli import main
|
|
174
174
|
from ktalk_cli.token_file import write_token
|
|
175
175
|
|
|
176
|
-
write_token("
|
|
176
|
+
write_token("FIXTUREnotArealToken01")
|
|
177
177
|
rc = main(["token", "status", "--json"])
|
|
178
178
|
|
|
179
179
|
out = capsys.readouterr().out
|
|
@@ -181,7 +181,7 @@ def test_cli_token_status_json_reports_present_without_leaking_value(
|
|
|
181
181
|
payload = json.loads(out)
|
|
182
182
|
assert payload["present"] is True
|
|
183
183
|
assert payload["mode"] == "0600"
|
|
184
|
-
assert "
|
|
184
|
+
assert "FIXTUREnotArealToken01" not in out
|
|
185
185
|
|
|
186
186
|
|
|
187
187
|
def test_cli_token_status_absent_file_is_not_an_error(monkeypatch, tmp_path, capsys):
|
|
@@ -201,7 +201,7 @@ def test_cli_token_status_flags_too_wide_permissions(monkeypatch, tmp_path, caps
|
|
|
201
201
|
from ktalk_cli.cli import main
|
|
202
202
|
from ktalk_cli.token_file import write_token
|
|
203
203
|
|
|
204
|
-
write_token("
|
|
204
|
+
write_token("FIXTUREnotArealToken01").chmod(0o644)
|
|
205
205
|
rc = main(["token", "status", "--json"])
|
|
206
206
|
|
|
207
207
|
payload = json.loads(capsys.readouterr().out)
|
|
@@ -264,9 +264,9 @@ def test_cli_token_set_bad_value_exits_nonzero_and_keeps_old_token(
|
|
|
264
264
|
from ktalk_cli.cli import main
|
|
265
265
|
from ktalk_cli.token_file import read_token, write_token
|
|
266
266
|
|
|
267
|
-
write_token("
|
|
267
|
+
write_token("FIXTUREnotArealToken01")
|
|
268
268
|
rc = main(["token", "set", "/Users/mdemyanov/Devel/ktalk-mcp/README.md"])
|
|
269
269
|
|
|
270
270
|
assert rc != 0
|
|
271
271
|
assert "не похоже на токен" in capsys.readouterr().err
|
|
272
|
-
assert read_token() == "
|
|
272
|
+
assert read_token() == "FIXTUREnotArealToken01"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/fixtures/conference-history-session-oversized.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/fixtures/recording-detail-session-full-participants.json
RENAMED
|
File without changes
|
{ktalk_cli-3.0.0 → ktalk_cli-3.0.2}/tests/fixtures/recording-detail-session-oversized-partial.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|