deepsieve-cli 0.2.0__tar.gz → 0.3.0__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.
- {deepsieve_cli-0.2.0 → deepsieve_cli-0.3.0}/.gitignore +7 -0
- {deepsieve_cli-0.2.0 → deepsieve_cli-0.3.0}/PKG-INFO +2 -1
- {deepsieve_cli-0.2.0 → deepsieve_cli-0.3.0}/README.md +1 -0
- {deepsieve_cli-0.2.0 → deepsieve_cli-0.3.0}/pyproject.toml +1 -1
- {deepsieve_cli-0.2.0 → deepsieve_cli-0.3.0}/src/deepsieve_cli/main.py +111 -3
- {deepsieve_cli-0.2.0 → deepsieve_cli-0.3.0}/tests/test_cli.py +163 -1
- {deepsieve_cli-0.2.0 → deepsieve_cli-0.3.0}/src/deepsieve_cli/__init__.py +0 -0
- {deepsieve_cli-0.2.0 → deepsieve_cli-0.3.0}/src/deepsieve_cli/__main__.py +0 -0
- {deepsieve_cli-0.2.0 → deepsieve_cli-0.3.0}/src/deepsieve_cli/client.py +0 -0
- {deepsieve_cli-0.2.0 → deepsieve_cli-0.3.0}/src/deepsieve_cli/config.py +0 -0
|
@@ -132,3 +132,10 @@ page-*.yml
|
|
|
132
132
|
|
|
133
133
|
# Antigravity (Gemini) agent local config — per-machine, like .claude/settings.local.json
|
|
134
134
|
.agents/
|
|
135
|
+
|
|
136
|
+
# --- Review-seat scratch (`.review-<ticket>/`) ---
|
|
137
|
+
# Reviewer briefs and seat reports written into a worktree for a review round.
|
|
138
|
+
# `.review-1539/` reached `development` in #1561 through a `git add -A` — the
|
|
139
|
+
# same mechanism as the node_modules note above, a second time. Reports belong
|
|
140
|
+
# in the session scratchpad; a copy in the tree is only ever a transient.
|
|
141
|
+
.review-*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: deepsieve-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: DeepSieve CLI — run cited deep research from your terminal
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: httpx>=0.27
|
|
@@ -26,6 +26,7 @@ revoked there at any time.
|
|
|
26
26
|
deepsieve whoami # identity, workspace, scopes
|
|
27
27
|
deepsieve runs create --query "..." --dry-run # free simulated run (~15s)
|
|
28
28
|
deepsieve runs create --query "..." --wait # real run: spends credits
|
|
29
|
+
deepsieve runs create --seeds "..." --allowed-source "*.gov" # only cite these
|
|
29
30
|
deepsieve runs list
|
|
30
31
|
deepsieve runs get <id> # exit 4 while still running
|
|
31
32
|
deepsieve runs cancel <id>
|
|
@@ -18,6 +18,7 @@ revoked there at any time.
|
|
|
18
18
|
deepsieve whoami # identity, workspace, scopes
|
|
19
19
|
deepsieve runs create --query "..." --dry-run # free simulated run (~15s)
|
|
20
20
|
deepsieve runs create --query "..." --wait # real run: spends credits
|
|
21
|
+
deepsieve runs create --seeds "..." --allowed-source "*.gov" # only cite these
|
|
21
22
|
deepsieve runs list
|
|
22
23
|
deepsieve runs get <id> # exit 4 while still running
|
|
23
24
|
deepsieve runs cancel <id>
|
|
@@ -250,6 +250,11 @@ def cmd_runs_create(args) -> int:
|
|
|
250
250
|
body["query"] = args.query
|
|
251
251
|
if args.seeds:
|
|
252
252
|
body["seeds"] = [s.strip() for s in args.seeds.split(",") if s.strip()]
|
|
253
|
+
allowed = [s.strip() for s in (getattr(args, "allowed_source", None) or []) if s.strip()]
|
|
254
|
+
if allowed:
|
|
255
|
+
# #1802 — repeatable; the server resolves and refuses, the CLI only carries.
|
|
256
|
+
# Only blanks given → no list at all, never an empty one (the server 400s on []).
|
|
257
|
+
body["allowed_sources"] = allowed
|
|
253
258
|
|
|
254
259
|
# A real run spends money. Confirm interactively unless told not to — and
|
|
255
260
|
# never silently in a pipe, where nobody is watching.
|
|
@@ -451,21 +456,120 @@ def _remote_server_name(prof) -> str:
|
|
|
451
456
|
return "deepsieve"
|
|
452
457
|
|
|
453
458
|
|
|
459
|
+
def _remote_oauth_enabled(prof) -> bool | None:
|
|
460
|
+
"""Whether this deployment serves remote MCP browser-OAuth.
|
|
461
|
+
|
|
462
|
+
Probes the RFC 9728 discovery document, which ``api/routes/mcp.py`` serves
|
|
463
|
+
with a 200 only when the deployment's ``oauth_enabled()`` (``authkit_domain``
|
|
464
|
+
+ ``mcp_base_url``) is true, and a 404 (``MCP OAuth not configured``)
|
|
465
|
+
otherwise. Unauthenticated, so it works before anyone has signed in — the
|
|
466
|
+
situation this command exists for.
|
|
467
|
+
|
|
468
|
+
Three states, not two — the distinction is load-bearing (#1798):
|
|
469
|
+
|
|
470
|
+
- ``True`` — a 200: OAuth is live, emit the HTTP/browser registration.
|
|
471
|
+
- ``False`` — the documented **404** (``MCP OAuth not configured``): the one
|
|
472
|
+
definitive "OAuth is off" signal → fall back to the local stdio server.
|
|
473
|
+
- ``None`` — could not determine: a network error, OR any other non-2xx the
|
|
474
|
+
app itself never emits (a 5xx/504 from the ALB, a 429, a WAF 403 — a 503
|
|
475
|
+
deploy blip is the live example). We must NOT read those as "OAuth off":
|
|
476
|
+
that would relocate the found-nothing/could-not-look collapse this ticket
|
|
477
|
+
fixes to the 5xx boundary and render a transient error as the categorical
|
|
478
|
+
"no browser login". The caller keeps the HTTP default on ``None`` (a
|
|
479
|
+
normally-reachable origin usually has whatever it has) but says the answer
|
|
480
|
+
is unverified; only a definitive ``False`` flips to stdio.
|
|
481
|
+
"""
|
|
482
|
+
try:
|
|
483
|
+
Client(prof.origin).request("GET", "/.well-known/oauth-protected-resource")
|
|
484
|
+
return True
|
|
485
|
+
except ApiError as exc:
|
|
486
|
+
# Only the 404 is definitive. `http_404` is what `client.request` derives
|
|
487
|
+
# for the route's `{"detail": ...}` body (no `error` envelope → the
|
|
488
|
+
# status-code default). Everything else is "could not determine".
|
|
489
|
+
return False if exc.code == "http_404" else None
|
|
490
|
+
|
|
491
|
+
|
|
454
492
|
def cmd_setup_mcp(args) -> int:
|
|
455
|
-
"""Print (or install) the MCP registration for this profile's origin.
|
|
493
|
+
"""Print (or install) the MCP registration for this profile's origin.
|
|
494
|
+
|
|
495
|
+
Emits the transport the deployment actually supports: remote HTTP + browser
|
|
496
|
+
OAuth when it is configured, else the local stdio server keyed off
|
|
497
|
+
``DEEPSIEVE_API_KEY``. Both the human and ``--json`` output derive that
|
|
498
|
+
choice from the SAME live probe (`_remote_oauth_enabled`), so the two can
|
|
499
|
+
never advertise different transports — #1798, where the served ``setup.md``
|
|
500
|
+
and this command both hardcoded HTTP and dead-ended every agent on a
|
|
501
|
+
deployment without OAuth.
|
|
502
|
+
"""
|
|
456
503
|
_, prof = _client(args)
|
|
457
|
-
url = f"{prof.origin}/mcp"
|
|
458
504
|
# Ask the deployment what it calls itself rather than assuming "deepsieve":
|
|
459
505
|
# hardcoding it here would print a staging registration that overwrites the
|
|
460
506
|
# production entry — the exact collision the env-aware naming fixed.
|
|
461
507
|
name = args.name or _remote_server_name(prof)
|
|
508
|
+
|
|
509
|
+
oauth = _remote_oauth_enabled(prof)
|
|
510
|
+
# Three probe outcomes get three distinct renderings — "could not look" must
|
|
511
|
+
# never be byte-identical to "confirmed OAuth" (#1798). The `oauth_discovery`
|
|
512
|
+
# marker rides every --json payload; the human output states an unverified
|
|
513
|
+
# default in words. `unconfirmed` covers both an unreachable origin and any
|
|
514
|
+
# non-404 status (e.g. a 5xx) — neither is a definitive "OAuth off". Only a
|
|
515
|
+
# definitive `False` (the 404) flips us off the HTTP path.
|
|
516
|
+
discovery = {True: "confirmed", False: "absent", None: "unconfirmed"}[oauth]
|
|
517
|
+
# The stdio registration line — the primary path when OAuth is absent, and
|
|
518
|
+
# the fallback hint when discovery was unreachable. One definition, so the
|
|
519
|
+
# two spots cannot drift.
|
|
520
|
+
stdio_add = f"claude mcp add {name} --env DEEPSIEVE_API_URL={prof.origin} -- uvx deepsieve-mcp"
|
|
521
|
+
|
|
522
|
+
if oauth is False:
|
|
523
|
+
# No remote OAuth on this deployment → local stdio server. The key is
|
|
524
|
+
# never inlined: the server inherits DEEPSIEVE_API_KEY from the
|
|
525
|
+
# environment, the rule setup.md enforces (a key in a config file gets
|
|
526
|
+
# committed/logged).
|
|
527
|
+
cfg = {
|
|
528
|
+
"command": "uvx",
|
|
529
|
+
"args": ["deepsieve-mcp"],
|
|
530
|
+
"env": {"DEEPSIEVE_API_URL": prof.origin},
|
|
531
|
+
}
|
|
532
|
+
if args.json:
|
|
533
|
+
out({"mcpServers": {name: cfg}, "oauth_discovery": discovery}, True)
|
|
534
|
+
return EXIT_OK
|
|
535
|
+
print(f"This deployment has no browser login — register the local {name} MCP server.\n")
|
|
536
|
+
print(
|
|
537
|
+
f"Set DEEPSIEVE_API_KEY in your environment (an Agent key from "
|
|
538
|
+
f"{prof.origin}/settings/api-keys), then:\n"
|
|
539
|
+
)
|
|
540
|
+
print(f" {stdio_add}\n")
|
|
541
|
+
print(
|
|
542
|
+
"Or add to your agent's MCP config (the server inherits DEEPSIEVE_API_KEY "
|
|
543
|
+
"from the environment — do not inline the key):\n"
|
|
544
|
+
)
|
|
545
|
+
print(json.dumps({"mcpServers": {name: cfg}}, indent=2))
|
|
546
|
+
print(f"\nFull setup, including skills and rules: {prof.origin}/setup.md")
|
|
547
|
+
return EXIT_OK
|
|
548
|
+
|
|
549
|
+
# OAuth confirmed (200) OR unreachable (None): both register the HTTP
|
|
550
|
+
# endpoint, but the unreachable case says so in words — an unverified default
|
|
551
|
+
# dressed as a verified one walks the agent into the exact dead Authenticate
|
|
552
|
+
# button this ticket exists to remove.
|
|
553
|
+
url = f"{prof.origin}/mcp"
|
|
462
554
|
if args.json:
|
|
463
|
-
out({"mcpServers": {name: {"url": url}}}, True)
|
|
555
|
+
out({"mcpServers": {name: {"url": url}}, "oauth_discovery": discovery}, True)
|
|
464
556
|
return EXIT_OK
|
|
557
|
+
if oauth is None:
|
|
558
|
+
print(
|
|
559
|
+
f"Could not confirm {prof.origin} offers browser login (no response, or an "
|
|
560
|
+
"unexpected status); defaulting to the HTTP endpoint.\n"
|
|
561
|
+
)
|
|
465
562
|
print(f"Register the {name} MCP server (browser login, no key needed):\n")
|
|
466
563
|
print(f" claude mcp add --transport http {name} {url}\n")
|
|
467
564
|
print("Or add to your agent's MCP config:\n")
|
|
468
565
|
print(json.dumps({"mcpServers": {name: {"url": url}}}, indent=2))
|
|
566
|
+
if oauth is None:
|
|
567
|
+
print(
|
|
568
|
+
"\nIf Authenticate does nothing, this deployment has no remote OAuth — "
|
|
569
|
+
"re-run when it is reachable, or use the local stdio server:\n"
|
|
570
|
+
f" {stdio_add}\n"
|
|
571
|
+
"(the stdio server needs DEEPSIEVE_API_KEY in the environment)."
|
|
572
|
+
)
|
|
469
573
|
print(f"\nFull setup, including skills and rules: {prof.origin}/setup.md")
|
|
470
574
|
return EXIT_OK
|
|
471
575
|
|
|
@@ -531,6 +635,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
531
635
|
rc = runs.add_parser("create", help="start a run", parents=[common])
|
|
532
636
|
rc.add_argument("--query")
|
|
533
637
|
rc.add_argument("--seeds", help="comma-separated names or URLs")
|
|
638
|
+
rc.add_argument("--allowed-source", action="append", metavar="PATTERN",
|
|
639
|
+
help="restrict evidence to this source (repeatable): a host, *.gov, "
|
|
640
|
+
"https://host/path/*, or blueprint:primary; a cell cited only "
|
|
641
|
+
"elsewhere is not written")
|
|
534
642
|
rc.add_argument("--depth", default="standard", choices=["standard", "max"])
|
|
535
643
|
rc.add_argument("--dry-run", action="store_true", help="free simulated run (~15s)")
|
|
536
644
|
rc.add_argument("--no-monitor", dest="monitored", action="store_false",
|
|
@@ -386,7 +386,14 @@ def test_setup_mcp_reads_the_name_from_the_deployment(monkeypatch, capsys):
|
|
|
386
386
|
cfg.save_profile(cfg.Profile(name="staging", origin="https://staging.test", api_key="k"))
|
|
387
387
|
|
|
388
388
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
389
|
-
|
|
389
|
+
# Both endpoints the command touches (the name lookup and the OAuth
|
|
390
|
+
# discovery probe) are unauthenticated; neither may be /api/system/status.
|
|
391
|
+
assert request.url.path in (
|
|
392
|
+
"/setup.md",
|
|
393
|
+
"/.well-known/oauth-protected-resource",
|
|
394
|
+
), "must not need an authenticated endpoint"
|
|
395
|
+
if request.url.path == "/.well-known/oauth-protected-resource":
|
|
396
|
+
return httpx.Response(200, json={"resource": "https://staging.test/mcp"})
|
|
390
397
|
return httpx.Response(
|
|
391
398
|
200,
|
|
392
399
|
text=(
|
|
@@ -420,6 +427,140 @@ def test_setup_mcp_falls_back_to_the_brand_not_the_hostname(monkeypatch, capsys)
|
|
|
420
427
|
assert " staging " not in printed, f"leaked a hostname label as the name: {printed}"
|
|
421
428
|
|
|
422
429
|
|
|
430
|
+
# ── #1798: the transport must match what the deployment actually serves ───────
|
|
431
|
+
#
|
|
432
|
+
# setup.md and `deepsieve setup mcp` used to hardcode the remote HTTP + browser
|
|
433
|
+
# OAuth path. On a deployment where OAuth is not configured, the discovery doc
|
|
434
|
+
# 404s and no /mcp transport is mounted, so an agent following the snippet
|
|
435
|
+
# dead-ends at an inoperative Authenticate button. Both tests assert the PROBE
|
|
436
|
+
# FIRED and the BRANCH TAKEN — a snippet that renders correctly while the probe
|
|
437
|
+
# never ran is the exact failure we are fixing.
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _setup_mcp_handler(seen, *, oauth_status):
|
|
441
|
+
"""A handler that records paths and answers the OAuth discovery probe with
|
|
442
|
+
`oauth_status`. /setup.md carries the canonical name line."""
|
|
443
|
+
|
|
444
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
445
|
+
seen.append(request.url.path)
|
|
446
|
+
if request.url.path == "/.well-known/oauth-protected-resource":
|
|
447
|
+
if oauth_status == 200:
|
|
448
|
+
return httpx.Response(200, json={"resource": "https://x.test/mcp"})
|
|
449
|
+
return httpx.Response(oauth_status, json={"detail": "MCP OAuth not configured"})
|
|
450
|
+
if request.url.path == "/setup.md":
|
|
451
|
+
return httpx.Response(
|
|
452
|
+
200,
|
|
453
|
+
text="```bash\nclaude mcp add --transport http deepsieve https://x.test/mcp\n```\n",
|
|
454
|
+
)
|
|
455
|
+
return httpx.Response(404, json={"detail": "Not Found"})
|
|
456
|
+
|
|
457
|
+
return handler
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def test_setup_mcp_emits_http_when_oauth_is_configured(monkeypatch, capsys):
|
|
461
|
+
"""A 200 on the discovery doc → the browser-OAuth HTTP registration."""
|
|
462
|
+
cfg.save_profile(cfg.Profile(name="default", origin="https://x.test", api_key="ds_live_SECRET"))
|
|
463
|
+
seen: list[str] = []
|
|
464
|
+
monkeypatch.setattr(httpx, "Client", _mock(_setup_mcp_handler(seen, oauth_status=200)))
|
|
465
|
+
|
|
466
|
+
assert main(["setup", "mcp", "--json"]) == 0
|
|
467
|
+
out = capsys.readouterr().out
|
|
468
|
+
body = json.loads(out)
|
|
469
|
+
|
|
470
|
+
assert "/.well-known/oauth-protected-resource" in seen, "OAuth probe never fired"
|
|
471
|
+
server = body["mcpServers"]["deepsieve"]
|
|
472
|
+
assert server == {"url": "https://x.test/mcp"}, server
|
|
473
|
+
assert "uvx" not in out and "command" not in out, "took the stdio branch on a 200"
|
|
474
|
+
assert body["oauth_discovery"] == "confirmed", body
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def test_setup_mcp_falls_back_to_stdio_when_oauth_unconfigured(monkeypatch, capsys):
|
|
478
|
+
"""A 404 on the discovery doc → the local stdio server, key from the env.
|
|
479
|
+
|
|
480
|
+
Reds against the pre-fix command, which emitted the HTTP `/mcp` URL
|
|
481
|
+
unconditionally regardless of what the deployment serves.
|
|
482
|
+
"""
|
|
483
|
+
cfg.save_profile(cfg.Profile(name="default", origin="https://x.test", api_key="ds_live_SECRET"))
|
|
484
|
+
seen: list[str] = []
|
|
485
|
+
monkeypatch.setattr(httpx, "Client", _mock(_setup_mcp_handler(seen, oauth_status=404)))
|
|
486
|
+
|
|
487
|
+
assert main(["setup", "mcp", "--json"]) == 0
|
|
488
|
+
out = capsys.readouterr().out
|
|
489
|
+
body = json.loads(out)
|
|
490
|
+
|
|
491
|
+
assert "/.well-known/oauth-protected-resource" in seen, "OAuth probe never fired"
|
|
492
|
+
server = body["mcpServers"]["deepsieve"]
|
|
493
|
+
assert server["command"] == "uvx", server
|
|
494
|
+
assert server["args"] == ["deepsieve-mcp"], server
|
|
495
|
+
assert server["env"]["DEEPSIEVE_API_URL"] == "https://x.test", server
|
|
496
|
+
assert "url" not in server, "emitted the HTTP transport on a 404"
|
|
497
|
+
assert "ds_live_SECRET" not in out, "inlined the API key into the config snippet"
|
|
498
|
+
assert body["oauth_discovery"] == "absent", body
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def test_setup_mcp_marks_http_as_unverified_when_discovery_unreachable(monkeypatch, capsys):
|
|
502
|
+
"""Unreachable discovery keeps the HTTP default (a network blip must not read
|
|
503
|
+
as "OAuth off") but MUST say so — "could not look" and a confirmed 200 must
|
|
504
|
+
never render identically (#1798). Asserts the uncertainty marker is present,
|
|
505
|
+
not merely that the HTTP snippet renders.
|
|
506
|
+
"""
|
|
507
|
+
cfg.save_profile(cfg.Profile(name="default", origin="https://x.test", api_key="ds_live_SECRET"))
|
|
508
|
+
seen: list[str] = []
|
|
509
|
+
|
|
510
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
511
|
+
seen.append(request.url.path)
|
|
512
|
+
raise httpx.ConnectError("boom") # the whole deployment is unreachable
|
|
513
|
+
|
|
514
|
+
monkeypatch.setattr(httpx, "Client", _mock(handler))
|
|
515
|
+
|
|
516
|
+
# --json: distinct marker, HTTP kept as the fallback (not stdio)
|
|
517
|
+
assert main(["setup", "mcp", "--json"]) == 0
|
|
518
|
+
body = json.loads(capsys.readouterr().out)
|
|
519
|
+
assert "/.well-known/oauth-protected-resource" in seen, "OAuth probe never fired"
|
|
520
|
+
assert body["mcpServers"]["deepsieve"] == {"url": "https://x.test/mcp"}, body
|
|
521
|
+
assert body["oauth_discovery"] == "unconfirmed", body
|
|
522
|
+
|
|
523
|
+
# human: HTTP snippet AND an explicit uncertainty line — not identical to a 200
|
|
524
|
+
assert main(["setup", "mcp"]) == 0
|
|
525
|
+
human = capsys.readouterr().out
|
|
526
|
+
assert "--transport http" in human, human
|
|
527
|
+
assert "Could not confirm" in human, "unverified default rendered as if confirmed"
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def test_setup_mcp_5xx_is_unconfirmed_not_absent(monkeypatch, capsys):
|
|
531
|
+
"""A 5xx (e.g. a 503 deploy blip) is "could not determine", NOT "OAuth off".
|
|
532
|
+
Only the documented 404 is the definitive off-signal. A 5xx must render as
|
|
533
|
+
`unconfirmed`/HTTP, never as the categorical stdio "no browser login" — that
|
|
534
|
+
would relocate the found-nothing/could-not-look collapse to the 5xx boundary
|
|
535
|
+
(Seat A finding). Reds against a version that mapped every non-2xx to stdio.
|
|
536
|
+
"""
|
|
537
|
+
cfg.save_profile(cfg.Profile(name="default", origin="https://x.test", api_key="ds_live_SECRET"))
|
|
538
|
+
seen: list[str] = []
|
|
539
|
+
monkeypatch.setattr(httpx, "Client", _mock(_setup_mcp_handler(seen, oauth_status=503)))
|
|
540
|
+
|
|
541
|
+
assert main(["setup", "mcp", "--json"]) == 0
|
|
542
|
+
body = json.loads(capsys.readouterr().out)
|
|
543
|
+
assert "/.well-known/oauth-protected-resource" in seen, "OAuth probe never fired"
|
|
544
|
+
assert body["oauth_discovery"] == "unconfirmed", body
|
|
545
|
+
server = body["mcpServers"]["deepsieve"]
|
|
546
|
+
assert server == {"url": "https://x.test/mcp"}, "a 5xx wrongly routed to stdio"
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def test_setup_mcp_human_output_matches_the_probe(monkeypatch, capsys):
|
|
550
|
+
"""The human (non --json) output derives from the SAME probe, so the two
|
|
551
|
+
output modes can never advertise different transports (#1798 anti-drift)."""
|
|
552
|
+
cfg.save_profile(cfg.Profile(name="default", origin="https://x.test", api_key="ds_live_SECRET"))
|
|
553
|
+
seen: list[str] = []
|
|
554
|
+
monkeypatch.setattr(httpx, "Client", _mock(_setup_mcp_handler(seen, oauth_status=404)))
|
|
555
|
+
|
|
556
|
+
assert main(["setup", "mcp"]) == 0
|
|
557
|
+
out = capsys.readouterr().out
|
|
558
|
+
assert "/.well-known/oauth-protected-resource" in seen, "OAuth probe never fired"
|
|
559
|
+
assert "uvx deepsieve-mcp" in out, out
|
|
560
|
+
assert "--transport http" not in out, "advertised HTTP in human output on a 404"
|
|
561
|
+
assert "ds_live_SECRET" not in out, "printed the API key"
|
|
562
|
+
|
|
563
|
+
|
|
423
564
|
def test_failed_run_exits_failure_not_success(monkeypatch):
|
|
424
565
|
"""README's table says 1 = failure; `runs get` consulted only `done`."""
|
|
425
566
|
import httpx
|
|
@@ -505,3 +646,24 @@ def test_members_list_surfaces_a_scope_refusal_as_a_typed_error(monkeypatch, cap
|
|
|
505
646
|
captured = capsys.readouterr()
|
|
506
647
|
assert "insufficient_scope" in captured.err or "members:read" in captured.err
|
|
507
648
|
assert "owner@example.com" not in captured.out
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def test_run_create_forwards_each_allowed_source(monkeypatch):
|
|
652
|
+
"""#1802 — `--allowed-source` is repeatable and reaches the body as a list;
|
|
653
|
+
absent, the key is absent (the server applies the workspace default)."""
|
|
654
|
+
cfg.save_profile(cfg.Profile(name="default", origin="https://x.test", api_key="k"))
|
|
655
|
+
seen = {}
|
|
656
|
+
|
|
657
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
658
|
+
seen["body"] = json.loads(request.content)
|
|
659
|
+
return httpx.Response(202, json={"id": "run_1", "status": "queued"})
|
|
660
|
+
|
|
661
|
+
monkeypatch.setattr(httpx, "Client", _mock(handler))
|
|
662
|
+
assert main(["runs", "create", "--seeds", "Acme", "--dry-run",
|
|
663
|
+
"--allowed-source", "*.gov", "--allowed-source", " dot.state.al.us "]) == 0
|
|
664
|
+
assert seen["body"]["allowed_sources"] == ["*.gov", "dot.state.al.us"]
|
|
665
|
+
assert main(["runs", "create", "--seeds", "Acme", "--dry-run"]) == 0
|
|
666
|
+
assert "allowed_sources" not in seen["body"]
|
|
667
|
+
# Only blanks → no key at all, never `[]` (the server refuses an empty list).
|
|
668
|
+
assert main(["runs", "create", "--seeds", "Acme", "--dry-run", "--allowed-source", " "]) == 0
|
|
669
|
+
assert "allowed_sources" not in seen["body"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|