pocketshell 0.4.13__tar.gz → 0.4.17__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.
- {pocketshell-0.4.13 → pocketshell-0.4.17}/PKG-INFO +1 -1
- {pocketshell-0.4.13 → pocketshell-0.4.17}/pyproject.toml +1 -1
- {pocketshell-0.4.13 → pocketshell-0.4.17}/src/pocketshell/agents.py +25 -5
- {pocketshell-0.4.13 → pocketshell-0.4.17}/src/pocketshell/cards.py +124 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/tests/test_agents.py +108 -17
- {pocketshell-0.4.13 → pocketshell-0.4.17}/tests/test_cards.py +136 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/uv.lock +3 -2
- {pocketshell-0.4.13 → pocketshell-0.4.17}/.gitignore +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/README.md +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/scheduler/README.md +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/scheduler/pocketshell-usage-capture.service +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/scheduler/pocketshell-usage-capture.timer +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/src/pocketshell/__init__.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/src/pocketshell/__main__.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/src/pocketshell/agent_log.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/src/pocketshell/agents_kind.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/src/pocketshell/cgroup_agents.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/src/pocketshell/cli.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/src/pocketshell/daemon.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/src/pocketshell/env.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/src/pocketshell/github.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/src/pocketshell/hooks.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/src/pocketshell/jobs.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/src/pocketshell/logs.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/src/pocketshell/profiles.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/src/pocketshell/prune_attachments.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/src/pocketshell/push.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/src/pocketshell/qr_share.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/src/pocketshell/repos.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/src/pocketshell/resume.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/src/pocketshell/sessions.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/src/pocketshell/tree.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/src/pocketshell/usage.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/src/pocketshell/usage_capture.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/src/pocketshell/usage_reset.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/tests/__init__.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/tests/test_agent_log.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/tests/test_agents_kind.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/tests/test_cgroup_agents.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/tests/test_cli.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/tests/test_daemon.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/tests/test_env.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/tests/test_github.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/tests/test_hooks.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/tests/test_jobs.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/tests/test_logs.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/tests/test_profiles.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/tests/test_prune_attachments.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/tests/test_push.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/tests/test_qr_share.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/tests/test_repos.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/tests/test_resume.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/tests/test_sessions.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/tests/test_tree.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/tests/test_usage.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/tests/test_usage_capture.py +0 -0
- {pocketshell-0.4.13 → pocketshell-0.4.17}/tests/test_usage_reset.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pocketshell
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.17
|
|
4
4
|
Summary: Unified server-side Python utility for the PocketShell Android client.
|
|
5
5
|
Project-URL: Homepage, https://github.com/alexeygrigorev/pocketshell
|
|
6
6
|
Project-URL: Issues, https://github.com/alexeygrigorev/pocketshell/issues
|
|
@@ -8,7 +8,7 @@ name = "pocketshell"
|
|
|
8
8
|
# scripts/check-pypi-version.sh enforces this; .github/workflows/build.yml
|
|
9
9
|
# runs that check before publishing to PyPI. See
|
|
10
10
|
# tools/pocketshell/README.md ("Release flow") for the bump procedure.
|
|
11
|
-
version = "0.4.
|
|
11
|
+
version = "0.4.17"
|
|
12
12
|
description = "Unified server-side Python utility for the PocketShell Android client."
|
|
13
13
|
readme = "README.md"
|
|
14
14
|
requires-python = ">=3.11"
|
|
@@ -379,8 +379,13 @@ def record_agent_kind(
|
|
|
379
379
|
profile name is known here, before ``os.execvpe``; #826 record-at-start
|
|
380
380
|
hard-cut — no detection/parse path). When set, it is written as the
|
|
381
381
|
per-session ``@ps_agent_profile`` user option alongside ``@ps_agent_kind``.
|
|
382
|
-
A default / no-profile launch passes ``None`` and
|
|
383
|
-
|
|
382
|
+
A default / no-profile launch passes ``None`` and the option is
|
|
383
|
+
RECONCILED to the current launch by UNSETTING it
|
|
384
|
+
(``tmux set-option -uq @ps_agent_profile``), so a session previously
|
|
385
|
+
launched with a non-default profile and then relaunched as a default
|
|
386
|
+
agent in the SAME tmux session does not keep the stale profile label
|
|
387
|
+
(issue #889). ``@ps_agent_kind`` is always overwritten on every launch
|
|
388
|
+
so it has no equivalent stale hazard.
|
|
384
389
|
|
|
385
390
|
The options are session-scoped (not global): ``tmux set-option`` without
|
|
386
391
|
``-g`` sets it on the current session, which is the session the agent
|
|
@@ -411,12 +416,26 @@ def record_agent_kind(
|
|
|
411
416
|
check=False,
|
|
412
417
|
)
|
|
413
418
|
if profile:
|
|
414
|
-
#
|
|
415
|
-
# launch writes no option so the tree shows the plain kind.
|
|
419
|
+
# A non-default profile is recorded so the tree shows its label.
|
|
416
420
|
runner(
|
|
417
421
|
["tmux", "set-option", "@ps_agent_profile", profile],
|
|
418
422
|
check=False,
|
|
419
423
|
)
|
|
424
|
+
else:
|
|
425
|
+
# A default / no-profile launch must RECONCILE the option to the
|
|
426
|
+
# current launch by UNSETTING it (issue #889). tmux session
|
|
427
|
+
# options persist for the life of the session, so a session
|
|
428
|
+
# launched once with a non-default profile (e.g. z.ai) and then
|
|
429
|
+
# relaunched as a default agent in the SAME session would keep the
|
|
430
|
+
# stale @ps_agent_profile and be mislabelled in the tree. The
|
|
431
|
+
# ``-u`` unsets the session option; ``-q`` makes unsetting an
|
|
432
|
+
# already-absent option a no-op (a fresh default session stays
|
|
433
|
+
# clean). The kind itself (set above) is always overwritten, so it
|
|
434
|
+
# has no equivalent stale hazard.
|
|
435
|
+
runner(
|
|
436
|
+
["tmux", "set-option", "-uq", "@ps_agent_profile"],
|
|
437
|
+
check=False,
|
|
438
|
+
)
|
|
420
439
|
except Exception:
|
|
421
440
|
# Recording the kind is best-effort; never block the launch on it.
|
|
422
441
|
return False
|
|
@@ -522,7 +541,8 @@ def _resolve_config_dir(
|
|
|
522
541
|
tree can tell a z.ai Claude apart from a default Claude. The engine's
|
|
523
542
|
default profile, ``--config-dir`` (which carries no named profile), and
|
|
524
543
|
omitting both flags all resolve ``profile_label`` to ``None`` — so a
|
|
525
|
-
default
|
|
544
|
+
default launch clears any stale ``@ps_agent_profile`` option (issue
|
|
545
|
+
#889) rather than leaving a profile label behind.
|
|
526
546
|
"""
|
|
527
547
|
if config_dir is not None and profile is not None:
|
|
528
548
|
click.echo(
|
|
@@ -360,6 +360,62 @@ register_card_type(
|
|
|
360
360
|
)
|
|
361
361
|
|
|
362
362
|
|
|
363
|
+
# ---------------------------------------------------------------------------
|
|
364
|
+
# note card type (#859 Slice B — proves the registry is genuinely generic)
|
|
365
|
+
# ---------------------------------------------------------------------------
|
|
366
|
+
#
|
|
367
|
+
# A ``note`` is a non-interactive message the agent hands the human ("deploy
|
|
368
|
+
# finished", "found a flaky test"). Its only interaction is mark-as-read, so the
|
|
369
|
+
# agent can tell from ``push status`` whether the human has seen it. Registering
|
|
370
|
+
# it is exactly one ``register_card_type`` call + a ``push note`` verb — no new
|
|
371
|
+
# transport, no change to the store/read/write/upsert path. That is the whole
|
|
372
|
+
# point of the registry: a second real type is additive.
|
|
373
|
+
|
|
374
|
+
# Default note card id used when ``--id`` is omitted (one default note per
|
|
375
|
+
# session unless the agent names them — mirrors DEFAULT_CHECKLIST_ID).
|
|
376
|
+
DEFAULT_NOTE_ID = "note"
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _note_build_body(*, text: str, **_: Any) -> dict[str, Any]:
|
|
380
|
+
"""Build the note ``body`` — ``{"text": <message>}``."""
|
|
381
|
+
return {"text": text}
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _note_initial_state(_body: dict[str, Any]) -> dict[str, Any]:
|
|
385
|
+
"""A fresh note is unread."""
|
|
386
|
+
return {"read": False, "read_at": None}
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _note_apply_interaction(
|
|
390
|
+
body: dict[str, Any],
|
|
391
|
+
state: dict[str, Any],
|
|
392
|
+
interaction: Mapping[str, Any],
|
|
393
|
+
) -> dict[str, Any]:
|
|
394
|
+
"""Mark a note read/unread.
|
|
395
|
+
|
|
396
|
+
``interaction = {"read": bool}``. ``read`` defaults to True (mark read).
|
|
397
|
+
``read_at`` records when it was first read (cleared on unread) so the agent
|
|
398
|
+
can see acknowledgement timing via ``push status``.
|
|
399
|
+
"""
|
|
400
|
+
read = bool(interaction.get("read", True))
|
|
401
|
+
return {"read": read, "read_at": _now_iso() if read else None}
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _note_summarise(body: dict[str, Any], state: dict[str, Any]) -> str:
|
|
405
|
+
return "note: read" if state.get("read") else "note: unread"
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
register_card_type(
|
|
409
|
+
CardType(
|
|
410
|
+
name="note",
|
|
411
|
+
build_body=_note_build_body,
|
|
412
|
+
initial_state=_note_initial_state,
|
|
413
|
+
apply_interaction=_note_apply_interaction,
|
|
414
|
+
summarise=_note_summarise,
|
|
415
|
+
)
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
|
|
363
419
|
# ---------------------------------------------------------------------------
|
|
364
420
|
# Store read/write
|
|
365
421
|
# ---------------------------------------------------------------------------
|
|
@@ -661,3 +717,71 @@ def register_push_card_commands(push_group: click.Group) -> None:
|
|
|
661
717
|
state = card.get("state", {}) if isinstance(card.get("state"), Mapping) else {}
|
|
662
718
|
summary = handler.summarise(dict(body), dict(state)) if handler else ""
|
|
663
719
|
click.echo(f"{card_id}: {summary}")
|
|
720
|
+
|
|
721
|
+
@push_group.command("note")
|
|
722
|
+
@click.option("--title", "title", default=None, help="Human title for the note card.")
|
|
723
|
+
@click.option(
|
|
724
|
+
"--id",
|
|
725
|
+
"card_id",
|
|
726
|
+
default=DEFAULT_NOTE_ID,
|
|
727
|
+
help=f"Card id (default {DEFAULT_NOTE_ID!r}; one default note per session).",
|
|
728
|
+
)
|
|
729
|
+
@click.option(
|
|
730
|
+
"--text",
|
|
731
|
+
"text",
|
|
732
|
+
default=None,
|
|
733
|
+
help="The note body. Alternative to piping the message on stdin.",
|
|
734
|
+
)
|
|
735
|
+
@click.option("--session", "session", default=None, help="Override the auto-detected tmux session.")
|
|
736
|
+
def push_note(
|
|
737
|
+
title: Optional[str],
|
|
738
|
+
card_id: str,
|
|
739
|
+
text: Optional[str],
|
|
740
|
+
session: Optional[str],
|
|
741
|
+
) -> None:
|
|
742
|
+
"""Create/replace the session's note card from --text or piped stdin.
|
|
743
|
+
|
|
744
|
+
A note is a non-interactive message the human marks read (``push read``).
|
|
745
|
+
The session is auto-detected from ``$TMUX`` (override with ``--session``).
|
|
746
|
+
A re-push fully replaces the card of that id (hard-cut, D22).
|
|
747
|
+
"""
|
|
748
|
+
target = _require_session(session)
|
|
749
|
+
if text is not None and text.strip():
|
|
750
|
+
body_text = text.strip()
|
|
751
|
+
else:
|
|
752
|
+
stdin_text = "" if sys.stdin is None or sys.stdin.isatty() else sys.stdin.read()
|
|
753
|
+
body_text = stdin_text.strip()
|
|
754
|
+
if not body_text:
|
|
755
|
+
raise click.ClickException(
|
|
756
|
+
"empty note: pass --text or pipe the message on stdin."
|
|
757
|
+
)
|
|
758
|
+
card = build_card(
|
|
759
|
+
card_type="note",
|
|
760
|
+
card_id=card_id,
|
|
761
|
+
title=title,
|
|
762
|
+
build_kwargs={"text": body_text},
|
|
763
|
+
)
|
|
764
|
+
path = upsert_card(target, card, paths=resolve_paths())
|
|
765
|
+
click.echo(f"note {card_id!r} -> session {target!r} ({path})")
|
|
766
|
+
|
|
767
|
+
@push_group.command("read")
|
|
768
|
+
@click.option("--id", "card_id", required=True, help="The note card id.")
|
|
769
|
+
@click.option("--read/--unread", "read", default=True, help="Mark read (default) or unread.")
|
|
770
|
+
@click.option("--session", "session", default=None, help="Override the auto-detected tmux session.")
|
|
771
|
+
def push_read(card_id: str, read: bool, session: Optional[str]) -> None:
|
|
772
|
+
"""Set a note's read state (this is what the app's "mark read" calls)."""
|
|
773
|
+
target = _require_session(session)
|
|
774
|
+
try:
|
|
775
|
+
card = apply_interaction(
|
|
776
|
+
target,
|
|
777
|
+
card_id,
|
|
778
|
+
{"read": read},
|
|
779
|
+
paths=resolve_paths(),
|
|
780
|
+
)
|
|
781
|
+
except ValueError as exc:
|
|
782
|
+
raise click.ClickException(str(exc))
|
|
783
|
+
handler = get_card_type(card.get("type", ""))
|
|
784
|
+
body = card.get("body", {}) if isinstance(card.get("body"), Mapping) else {}
|
|
785
|
+
state = card.get("state", {}) if isinstance(card.get("state"), Mapping) else {}
|
|
786
|
+
summary = handler.summarise(dict(body), dict(state)) if handler else ""
|
|
787
|
+
click.echo(f"{card_id}: {summary}")
|
|
@@ -470,11 +470,11 @@ def test_record_agent_kind_sets_session_option_inside_tmux(kind):
|
|
|
470
470
|
runner=lambda argv, **kw: calls.append((argv, kw)),
|
|
471
471
|
)
|
|
472
472
|
assert ok is True
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
assert
|
|
473
|
+
# No profile passed -> the kind is set (session-scoped, no -g), and the
|
|
474
|
+
# @ps_agent_profile option is reconciled away by an unset (issue #889).
|
|
475
|
+
argvs = [argv for argv, _kw in calls]
|
|
476
|
+
assert ["tmux", "set-option", "@ps_agent_kind", kind] in argvs
|
|
477
|
+
assert ["tmux", "set-option", "-uq", "@ps_agent_profile"] in argvs
|
|
478
478
|
|
|
479
479
|
|
|
480
480
|
def test_record_agent_kind_noop_when_not_in_tmux():
|
|
@@ -594,9 +594,12 @@ def test_record_agent_kind_writes_profile_option_when_profile_set():
|
|
|
594
594
|
]
|
|
595
595
|
|
|
596
596
|
|
|
597
|
-
def
|
|
598
|
-
# A default / no-profile launch records the kind
|
|
599
|
-
#
|
|
597
|
+
def test_record_agent_kind_no_profile_clears_profile_option():
|
|
598
|
+
# A default / no-profile launch records the kind AND reconciles the
|
|
599
|
+
# @ps_agent_profile option by UNSETTING it (issue #889), so a session
|
|
600
|
+
# previously launched with a non-default profile cannot keep a stale
|
|
601
|
+
# label. ``-uq`` makes unsetting an already-absent option a no-op, so a
|
|
602
|
+
# fresh default session stays clean.
|
|
600
603
|
calls = []
|
|
601
604
|
ok = agents.record_agent_kind(
|
|
602
605
|
"claude",
|
|
@@ -605,11 +608,15 @@ def test_record_agent_kind_no_profile_writes_only_kind():
|
|
|
605
608
|
profile=None,
|
|
606
609
|
)
|
|
607
610
|
assert ok is True
|
|
608
|
-
assert calls == [
|
|
611
|
+
assert calls == [
|
|
612
|
+
["tmux", "set-option", "@ps_agent_kind", "claude"],
|
|
613
|
+
["tmux", "set-option", "-uq", "@ps_agent_profile"],
|
|
614
|
+
]
|
|
609
615
|
|
|
610
616
|
|
|
611
|
-
def
|
|
612
|
-
# An empty-string profile is treated like no profile
|
|
617
|
+
def test_record_agent_kind_empty_profile_clears_profile_option():
|
|
618
|
+
# An empty-string profile is treated like no profile: kind recorded, the
|
|
619
|
+
# @ps_agent_profile option unset (issue #889).
|
|
613
620
|
calls = []
|
|
614
621
|
agents.record_agent_kind(
|
|
615
622
|
"codex",
|
|
@@ -617,7 +624,45 @@ def test_record_agent_kind_empty_profile_writes_only_kind():
|
|
|
617
624
|
runner=lambda argv, **kw: calls.append(argv),
|
|
618
625
|
profile="",
|
|
619
626
|
)
|
|
620
|
-
assert calls == [
|
|
627
|
+
assert calls == [
|
|
628
|
+
["tmux", "set-option", "@ps_agent_kind", "codex"],
|
|
629
|
+
["tmux", "set-option", "-uq", "@ps_agent_profile"],
|
|
630
|
+
]
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def test_record_agent_kind_default_relaunch_clears_stale_profile():
|
|
634
|
+
# REPRODUCE-FIRST (issue #889): the reported false-z.ai-label bug. A tmux
|
|
635
|
+
# session is launched once with the z.ai profile (sets @ps_agent_profile),
|
|
636
|
+
# then the agent is killed and RELAUNCHED as a regular default Claude in
|
|
637
|
+
# the SAME session. tmux session options persist, so without the #889 fix
|
|
638
|
+
# the stale "Claude (Z.AI)" lingers and the tree mislabels the session.
|
|
639
|
+
# The default relaunch must emit the unset that clears it.
|
|
640
|
+
#
|
|
641
|
+
# Launch 1: z.ai profile.
|
|
642
|
+
zai_calls = []
|
|
643
|
+
agents.record_agent_kind(
|
|
644
|
+
"claude",
|
|
645
|
+
env={"TMUX": "/tmp/tmux-1000/default,1234,0"},
|
|
646
|
+
runner=lambda argv, **kw: zai_calls.append(argv),
|
|
647
|
+
profile="Claude (Z.AI)",
|
|
648
|
+
)
|
|
649
|
+
assert ["tmux", "set-option", "@ps_agent_profile", "Claude (Z.AI)"] in zai_calls
|
|
650
|
+
|
|
651
|
+
# Launch 2: default relaunch in the same session. It MUST issue the unset
|
|
652
|
+
# so the previously-set @ps_agent_profile is reconciled away.
|
|
653
|
+
default_calls = []
|
|
654
|
+
agents.record_agent_kind(
|
|
655
|
+
"claude",
|
|
656
|
+
env={"TMUX": "/tmp/tmux-1000/default,1234,0"},
|
|
657
|
+
runner=lambda argv, **kw: default_calls.append(argv),
|
|
658
|
+
profile=None,
|
|
659
|
+
)
|
|
660
|
+
assert ["tmux", "set-option", "-uq", "@ps_agent_profile"] in default_calls
|
|
661
|
+
# And it must NOT re-set a profile value.
|
|
662
|
+
assert not any(
|
|
663
|
+
argv[:3] == ["tmux", "set-option", "@ps_agent_profile"]
|
|
664
|
+
for argv in default_calls
|
|
665
|
+
)
|
|
621
666
|
|
|
622
667
|
|
|
623
668
|
def test_launch_agent_records_profile_before_exec(tmp_path, monkeypatch):
|
|
@@ -669,9 +714,10 @@ def test_cli_agent_named_profile_records_profile_option(tmp_path, monkeypatch):
|
|
|
669
714
|
assert ["tmux", "set-option", "@ps_agent_profile", "Claude (Z.AI)"] in calls
|
|
670
715
|
|
|
671
716
|
|
|
672
|
-
def
|
|
673
|
-
"""A default Claude profile records the kind
|
|
674
|
-
|
|
717
|
+
def test_cli_agent_default_profile_clears_profile_option(tmp_path, monkeypatch):
|
|
718
|
+
"""A default Claude profile records the kind and reconciles the profile
|
|
719
|
+
option by UNSETTING it (issue #889), so a default launch never carries a
|
|
720
|
+
stale label and never sets a spurious one."""
|
|
675
721
|
_seed_zlaude_home(tmp_path, monkeypatch)
|
|
676
722
|
workdir = tmp_path / "work"
|
|
677
723
|
workdir.mkdir()
|
|
@@ -684,13 +730,15 @@ def test_cli_agent_default_profile_records_no_profile_option(tmp_path, monkeypat
|
|
|
684
730
|
)
|
|
685
731
|
assert rc == 0
|
|
686
732
|
assert ["tmux", "set-option", "@ps_agent_kind", "claude"] in calls
|
|
733
|
+
assert ["tmux", "set-option", "-uq", "@ps_agent_profile"] in calls
|
|
687
734
|
assert not any(
|
|
688
735
|
argv[:3] == ["tmux", "set-option", "@ps_agent_profile"] for argv in calls
|
|
689
736
|
)
|
|
690
737
|
|
|
691
738
|
|
|
692
|
-
def
|
|
693
|
-
"""A bare launch (no --profile/--config-dir) records the kind
|
|
739
|
+
def test_cli_agent_no_profile_clears_profile_option(tmp_path, monkeypatch):
|
|
740
|
+
"""A bare launch (no --profile/--config-dir) records the kind and unsets
|
|
741
|
+
@ps_agent_profile (issue #889)."""
|
|
694
742
|
workdir = tmp_path / "work"
|
|
695
743
|
workdir.mkdir()
|
|
696
744
|
monkeypatch.setenv("HOME", str(tmp_path))
|
|
@@ -701,11 +749,54 @@ def test_cli_agent_no_profile_records_no_profile_option(tmp_path, monkeypatch):
|
|
|
701
749
|
rc = main(["agent", "codex", "--dir", str(workdir)])
|
|
702
750
|
assert rc == 0
|
|
703
751
|
assert ["tmux", "set-option", "@ps_agent_kind", "codex"] in calls
|
|
752
|
+
assert ["tmux", "set-option", "-uq", "@ps_agent_profile"] in calls
|
|
704
753
|
assert not any(
|
|
705
754
|
argv[:3] == ["tmux", "set-option", "@ps_agent_profile"] for argv in calls
|
|
706
755
|
)
|
|
707
756
|
|
|
708
757
|
|
|
758
|
+
def test_cli_agent_default_then_zai_relaunch_sets_profile(tmp_path, monkeypatch):
|
|
759
|
+
"""Class coverage (issue #889): default-then-relaunched-z.ai. A default
|
|
760
|
+
launch in the session unsets the profile; relaunching the SAME session as
|
|
761
|
+
z.ai sets the correct label (the chip appears)."""
|
|
762
|
+
_seed_zlaude_home(tmp_path, monkeypatch)
|
|
763
|
+
workdir = tmp_path / "work"
|
|
764
|
+
workdir.mkdir()
|
|
765
|
+
monkeypatch.setenv("TMUX", "/tmp/tmux-1000/default,1234,0")
|
|
766
|
+
monkeypatch.setattr(agents.os, "execvpe", lambda f, a, e: None)
|
|
767
|
+
|
|
768
|
+
# Launch 1: default -> unset.
|
|
769
|
+
default_calls = []
|
|
770
|
+
monkeypatch.setattr(
|
|
771
|
+
agents.subprocess, "run", lambda argv, **kw: default_calls.append(argv)
|
|
772
|
+
)
|
|
773
|
+
assert main(["agent", "claude", "--dir", str(workdir), "--profile", "Claude"]) == 0
|
|
774
|
+
assert ["tmux", "set-option", "-uq", "@ps_agent_profile"] in default_calls
|
|
775
|
+
|
|
776
|
+
# Launch 2: z.ai relaunch in the same session -> sets the label.
|
|
777
|
+
zai_calls = []
|
|
778
|
+
monkeypatch.setattr(
|
|
779
|
+
agents.subprocess, "run", lambda argv, **kw: zai_calls.append(argv)
|
|
780
|
+
)
|
|
781
|
+
assert (
|
|
782
|
+
main(
|
|
783
|
+
[
|
|
784
|
+
"agent",
|
|
785
|
+
"claude",
|
|
786
|
+
"--dir",
|
|
787
|
+
str(workdir),
|
|
788
|
+
"--profile",
|
|
789
|
+
"Claude (Z.AI)",
|
|
790
|
+
]
|
|
791
|
+
)
|
|
792
|
+
== 0
|
|
793
|
+
)
|
|
794
|
+
assert ["tmux", "set-option", "@ps_agent_profile", "Claude (Z.AI)"] in zai_calls
|
|
795
|
+
assert not any(
|
|
796
|
+
argv[:3] == ["tmux", "set-option", "-uq"] for argv in zai_calls
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
|
|
709
800
|
def test_resolve_config_dir_default_profile_label_is_none(tmp_path, monkeypatch):
|
|
710
801
|
"""The default Claude profile resolves to a None profile label."""
|
|
711
802
|
_seed_zlaude_home(tmp_path, monkeypatch)
|
|
@@ -389,3 +389,139 @@ def test_cli_check_unknown_item_errors(tmp_path: Path) -> None:
|
|
|
389
389
|
)
|
|
390
390
|
assert res.exit_code != 0
|
|
391
391
|
assert "unknown item" in res.output
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
# ----- note card type (#859 Slice B — registry is genuinely generic) ----
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def test_note_card_type_is_registered() -> None:
|
|
398
|
+
"""AC: the host registers a REAL `note` CardType (not just the stub test)."""
|
|
399
|
+
handler = cards_mod.get_card_type("note")
|
|
400
|
+
assert handler is not None
|
|
401
|
+
assert handler.name == "note"
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def test_build_upsert_read_note(tmp_path: Path) -> None:
|
|
405
|
+
"""AC: note `body={text}`, `state={read:false, read_at:None}` round-trips."""
|
|
406
|
+
paths = _paths(tmp_path)
|
|
407
|
+
card = cards_mod.build_card(
|
|
408
|
+
card_type="note",
|
|
409
|
+
card_id="note",
|
|
410
|
+
title="Heads up",
|
|
411
|
+
build_kwargs={"text": "deploy finished"},
|
|
412
|
+
)
|
|
413
|
+
cards_mod.upsert_card("demo", card, paths=paths)
|
|
414
|
+
got = cards_mod.read_cards("demo", paths=paths)
|
|
415
|
+
assert len(got) == 1
|
|
416
|
+
assert got[0]["type"] == "note"
|
|
417
|
+
assert got[0]["title"] == "Heads up"
|
|
418
|
+
assert got[0]["body"] == {"text": "deploy finished"}
|
|
419
|
+
assert got[0]["state"] == {"read": False, "read_at": None}
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def test_note_apply_interaction_marks_read_and_unread(tmp_path: Path) -> None:
|
|
423
|
+
"""AC: mark-as-read interaction sets read + read_at; unread clears read_at."""
|
|
424
|
+
paths = _paths(tmp_path)
|
|
425
|
+
card = cards_mod.build_card(
|
|
426
|
+
card_type="note", card_id="note", title=None,
|
|
427
|
+
build_kwargs={"text": "hi"},
|
|
428
|
+
)
|
|
429
|
+
cards_mod.upsert_card("demo", card, paths=paths)
|
|
430
|
+
|
|
431
|
+
read = cards_mod.apply_interaction("demo", "note", {"read": True}, paths=paths)
|
|
432
|
+
assert read["state"]["read"] is True
|
|
433
|
+
assert read["state"]["read_at"] # a timestamp, not None
|
|
434
|
+
|
|
435
|
+
unread = cards_mod.apply_interaction("demo", "note", {"read": False}, paths=paths)
|
|
436
|
+
assert unread["state"]["read"] is False
|
|
437
|
+
assert unread["state"]["read_at"] is None
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def test_note_and_checklist_coexist_in_one_session(tmp_path: Path) -> None:
|
|
441
|
+
"""The store is type-agnostic: a note + a checklist live side by side."""
|
|
442
|
+
paths = _paths(tmp_path)
|
|
443
|
+
checklist = cards_mod.build_card(
|
|
444
|
+
card_type="checklist", card_id="checklist", title="Deploy",
|
|
445
|
+
build_kwargs={"items": [{"id": "build-0", "text": "build"}]},
|
|
446
|
+
)
|
|
447
|
+
note = cards_mod.build_card(
|
|
448
|
+
card_type="note", card_id="note", title=None,
|
|
449
|
+
build_kwargs={"text": "fyi"},
|
|
450
|
+
)
|
|
451
|
+
cards_mod.upsert_card("demo", checklist, paths=paths)
|
|
452
|
+
cards_mod.upsert_card("demo", note, paths=paths)
|
|
453
|
+
got = cards_mod.read_cards("demo", paths=paths)
|
|
454
|
+
assert {c["type"] for c in got} == {"checklist", "note"}
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def test_cli_note_set_get_read_status_flow(tmp_path: Path) -> None:
|
|
458
|
+
"""AC: push note (stdin) → get --json → read → status reflects read."""
|
|
459
|
+
runner = CliRunner()
|
|
460
|
+
env = _env(tmp_path)
|
|
461
|
+
|
|
462
|
+
res = runner.invoke(
|
|
463
|
+
cli,
|
|
464
|
+
["push", "note", "--title", "Heads up", "--session", "demo"],
|
|
465
|
+
input="deploy finished\n",
|
|
466
|
+
env=env,
|
|
467
|
+
)
|
|
468
|
+
assert res.exit_code == 0, res.output
|
|
469
|
+
assert "note 'note'" in res.output
|
|
470
|
+
|
|
471
|
+
res = runner.invoke(cli, ["push", "get", "--json", "--session", "demo"], env=env)
|
|
472
|
+
assert res.exit_code == 0, res.output
|
|
473
|
+
payload = json.loads(res.output)
|
|
474
|
+
card = payload["cards"][0]
|
|
475
|
+
assert card["type"] == "note"
|
|
476
|
+
assert card["title"] == "Heads up"
|
|
477
|
+
assert card["body"]["text"] == "deploy finished"
|
|
478
|
+
assert card["state"]["read"] is False
|
|
479
|
+
|
|
480
|
+
# Human marks it read (this is what the app's "mark read" calls).
|
|
481
|
+
res = runner.invoke(
|
|
482
|
+
cli, ["push", "read", "--id", "note", "--session", "demo"], env=env
|
|
483
|
+
)
|
|
484
|
+
assert res.exit_code == 0, res.output
|
|
485
|
+
assert "note: read" in res.output
|
|
486
|
+
|
|
487
|
+
# Agent reads the acknowledgement via status.
|
|
488
|
+
res = runner.invoke(cli, ["push", "status", "--json", "--session", "demo"], env=env)
|
|
489
|
+
assert res.exit_code == 0, res.output
|
|
490
|
+
status = json.loads(res.output)
|
|
491
|
+
assert status["status"][0]["state"]["read"] is True
|
|
492
|
+
|
|
493
|
+
res = runner.invoke(cli, ["push", "status", "--session", "demo"], env=env)
|
|
494
|
+
assert res.exit_code == 0, res.output
|
|
495
|
+
assert "note: read" in res.output
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def test_cli_note_text_via_flag(tmp_path: Path) -> None:
|
|
499
|
+
runner = CliRunner()
|
|
500
|
+
env = _env(tmp_path)
|
|
501
|
+
res = runner.invoke(
|
|
502
|
+
cli, ["push", "note", "--text", "found a flaky test", "--session", "demo"],
|
|
503
|
+
env=env,
|
|
504
|
+
)
|
|
505
|
+
assert res.exit_code == 0, res.output
|
|
506
|
+
got = cards_mod.read_cards("demo", paths=_paths(tmp_path))
|
|
507
|
+
assert got[0]["body"]["text"] == "found a flaky test"
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def test_cli_note_empty_errors(tmp_path: Path) -> None:
|
|
511
|
+
runner = CliRunner()
|
|
512
|
+
env = _env(tmp_path)
|
|
513
|
+
res = runner.invoke(
|
|
514
|
+
cli, ["push", "note", "--session", "demo"], input="", env=env
|
|
515
|
+
)
|
|
516
|
+
assert res.exit_code != 0
|
|
517
|
+
assert "empty note" in res.output
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def test_cli_read_unknown_card_errors(tmp_path: Path) -> None:
|
|
521
|
+
runner = CliRunner()
|
|
522
|
+
env = _env(tmp_path)
|
|
523
|
+
res = runner.invoke(
|
|
524
|
+
cli, ["push", "read", "--id", "ghost", "--session", "demo"], env=env
|
|
525
|
+
)
|
|
526
|
+
assert res.exit_code != 0
|
|
527
|
+
assert "no card" in res.output
|
|
@@ -3,7 +3,8 @@ revision = 3
|
|
|
3
3
|
requires-python = ">=3.11"
|
|
4
4
|
|
|
5
5
|
[options]
|
|
6
|
-
exclude-newer = "2026-
|
|
6
|
+
exclude-newer = "2026-06-18T14:08:51.166593044Z"
|
|
7
|
+
exclude-newer-span = "P7D"
|
|
7
8
|
|
|
8
9
|
[[package]]
|
|
9
10
|
name = "annotated-doc"
|
|
@@ -314,7 +315,7 @@ wheels = [
|
|
|
314
315
|
|
|
315
316
|
[[package]]
|
|
316
317
|
name = "pocketshell"
|
|
317
|
-
version = "0.4.
|
|
318
|
+
version = "0.4.16"
|
|
318
319
|
source = { editable = "." }
|
|
319
320
|
dependencies = [
|
|
320
321
|
{ name = "click" },
|
|
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
|