Upload folder using huggingface_hub
Browse files- DESIGN.md +2 -1
- OBSERVABILITY.md +6 -6
- app/models.py +5 -2
- app/notify.py +14 -8
- app/routes/client.py +1 -1
- app/routes/digest.py +13 -8
- app/routes/results.py +1 -1
- clients/share_trace.py +7 -7
- tests/test_digest_api.py +15 -3
- tests/test_notify_unit.py +3 -2
- tests/test_results_api.py +1 -1
DESIGN.md
CHANGED
|
@@ -584,7 +584,8 @@ last parked poll is younger than 2x the wait ceiling, else `poll`;
|
|
| 584 |
`last_cursor` is the newest cursor the server has handed the handle on the
|
| 585 |
unified stream, or that it has sent, so an agent with wiped local state can
|
| 586 |
resume with `--after <last_cursor>`; plus `updates.unread`, the cursor-aware
|
| 587 |
-
"am I behind?" that survives total client amnesia
|
|
|
|
| 588 |
presence dot (fresh within `WATCH_FRESH_S`, default 240 s). The digest's
|
| 589 |
block is per-handle — the agent-facing "is anyone watching me"; the same map for
|
| 590 |
*every* handle, plus `max_wait_s`/`fresh_s` and the waiter counters, is one
|
|
|
|
| 584 |
`last_cursor` is the newest cursor the server has handed the handle on the
|
| 585 |
unified stream, or that it has sent, so an agent with wiped local state can
|
| 586 |
resume with `--after <last_cursor>`; plus `updates.unread`, the cursor-aware
|
| 587 |
+
"am I behind?" that survives total client amnesia, counted after `after=` or,
|
| 588 |
+
by default, after the server's `last_cursor`), and the dashboard's
|
| 589 |
presence dot (fresh within `WATCH_FRESH_S`, default 240 s). The digest's
|
| 590 |
block is per-handle — the agent-facing "is anyone watching me"; the same map for
|
| 591 |
*every* handle, plus `max_wait_s`/`fresh_s` and the waiter counters, is one
|
OBSERVABILITY.md
CHANGED
|
@@ -6,12 +6,12 @@ scratch bucket**, then the backend pulls it into the shared record. Your identit
|
|
| 6 |
is your bucket — no token rides on the call.
|
| 7 |
|
| 8 |
```bash
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
```
|
| 16 |
|
| 17 |
The client is one self-contained file, `clients/share_trace.py`, served by the
|
|
|
|
| 6 |
is your bucket — no token rides on the call.
|
| 7 |
|
| 8 |
```bash
|
| 9 |
+
python3 share_trace.py # stats only: token & tool-call counts; no content leaves
|
| 10 |
+
python3 share_trace.py --full # FULL: stats + balanced-redacted transcript -> library
|
| 11 |
+
python3 share_trace.py --full --privacy secrets # credentials only; preserve PII
|
| 12 |
+
python3 share_trace.py --full --privacy strict # also pseudonymize hosts + IPs
|
| 13 |
+
python3 share_trace.py --full --raw # UNSAFE: upload transcript content as-is
|
| 14 |
+
python3 share_trace.py --dry-run # print the plan + the manifest; touch nothing
|
| 15 |
```
|
| 16 |
|
| 17 |
The client is one self-contained file, `clients/share_trace.py`, served by the
|
app/models.py
CHANGED
|
@@ -516,7 +516,8 @@ class DigestUpdates(BaseModel):
|
|
| 516 |
"""Cursor-aware "am I behind?" over the unified watch stream — the
|
| 517 |
non-blocking catch-up check, answerable even when all local watcher state is
|
| 518 |
lost (WATCH_DESIGN.md §4.5)."""
|
| 519 |
-
# Items newer than the digest's `after=` cursor (the
|
|
|
|
| 520 |
unread: int
|
| 521 |
# Newest filename in the stream; pass it back as `after` once caught up.
|
| 522 |
newest: str | None = None
|
|
@@ -531,7 +532,9 @@ class DigestWatching(BaseModel):
|
|
| 531 |
indistinguishable from a quiet inbox."""
|
| 532 |
last_poll_age_s: int
|
| 533 |
mode: str # parked (a wait>0 poll within 2x the wait ceiling) | poll
|
| 534 |
-
|
|
|
|
|
|
|
| 535 |
# The newest cursor the server has handed this handle on the unified
|
| 536 |
# stream, or that it has sent; resume with `--after <last_cursor>`.
|
| 537 |
last_cursor: str | None = None
|
|
|
|
| 516 |
"""Cursor-aware "am I behind?" over the unified watch stream — the
|
| 517 |
non-blocking catch-up check, answerable even when all local watcher state is
|
| 518 |
lost (WATCH_DESIGN.md §4.5)."""
|
| 519 |
+
# Items newer than the digest's `after=` cursor (default: the server's
|
| 520 |
+
# `last_cursor` for the handle; the whole stream when it has none).
|
| 521 |
unread: int
|
| 522 |
# Newest filename in the stream; pass it back as `after` once caught up.
|
| 523 |
newest: str | None = None
|
|
|
|
| 532 |
indistinguishable from a quiet inbox."""
|
| 533 |
last_poll_age_s: int
|
| 534 |
mode: str # parked (a wait>0 poll within 2x the wait ceiling) | poll
|
| 535 |
+
# updates | inbox | feed | digest: the parked poll's while parked, else
|
| 536 |
+
# the most recent read's.
|
| 537 |
+
stream: str
|
| 538 |
# The newest cursor the server has handed this handle on the unified
|
| 539 |
# stream, or that it has sent; resume with `--after <last_cursor>`.
|
| 540 |
last_cursor: str | None = None
|
app/notify.py
CHANGED
|
@@ -56,7 +56,9 @@ class Presence(NamedTuple):
|
|
| 56 |
"""A handle's most recent read, as the digest's `watching` block reports it."""
|
| 57 |
age_s: float
|
| 58 |
mode: str # parked (a wait>0 poll within the parked window) | poll
|
| 59 |
-
|
|
|
|
|
|
|
| 60 |
# The newest cursor the server has handed this handle on the unified
|
| 61 |
# stream, or that it has sent; resume with `--after <last_cursor>`.
|
| 62 |
last_cursor: str | None
|
|
@@ -190,10 +192,11 @@ class Notifier:
|
|
| 190 |
# restart, and a restart reads as "nobody is watching" — the truthful
|
| 191 |
# answer, since every parked connection died with it.
|
| 192 |
self._last_poll: dict[str, tuple[float, str]] = {}
|
| 193 |
-
# owner -> monotonic stamp of its most recent PARKED poll, kept
|
| 194 |
-
# a digest or plain read between two parks does not hide the
|
| 195 |
-
# A handle reads as parked while this is younger than
|
| 196 |
-
|
|
|
|
| 197 |
self._parked_window_s = parked_window_s
|
| 198 |
# owner -> the newest cursor handed out or sent on the unified stream,
|
| 199 |
# so an agent whose local state was wiped can resume from the server's
|
|
@@ -305,12 +308,13 @@ class Notifier:
|
|
| 305 |
|
| 306 |
``mode`` reports ``parked`` while the last parked poll is younger than
|
| 307 |
the parked window (2x the wait ceiling), whatever was read since, so a
|
| 308 |
-
digest between two parks does not hide a live watcher
|
|
|
|
| 309 |
with self._lock:
|
| 310 |
now = self._clock()
|
| 311 |
self._last_poll[owner] = (now, stream)
|
| 312 |
if parked:
|
| 313 |
-
self._last_parked[owner] = now
|
| 314 |
self._note_cursor_locked(owner, after)
|
| 315 |
|
| 316 |
def note_cursor(self, owner: str, cursor: str | None) -> None:
|
|
@@ -326,8 +330,10 @@ class Notifier:
|
|
| 326 |
|
| 327 |
def _presence_locked(self, owner: str, now: float) -> Presence:
|
| 328 |
stamp, stream = self._last_poll[owner]
|
| 329 |
-
parked_at = self._last_parked.get(owner)
|
| 330 |
parked = parked_at is not None and now - parked_at < self._parked_window_s
|
|
|
|
|
|
|
| 331 |
return Presence(
|
| 332 |
max(0.0, now - stamp),
|
| 333 |
"parked" if parked else "poll",
|
|
|
|
| 56 |
"""A handle's most recent read, as the digest's `watching` block reports it."""
|
| 57 |
age_s: float
|
| 58 |
mode: str # parked (a wait>0 poll within the parked window) | poll
|
| 59 |
+
# updates | inbox | feed | digest: the parked poll's stream while parked,
|
| 60 |
+
# else the most recent read's.
|
| 61 |
+
stream: str
|
| 62 |
# The newest cursor the server has handed this handle on the unified
|
| 63 |
# stream, or that it has sent; resume with `--after <last_cursor>`.
|
| 64 |
last_cursor: str | None
|
|
|
|
| 192 |
# restart, and a restart reads as "nobody is watching" — the truthful
|
| 193 |
# answer, since every parked connection died with it.
|
| 194 |
self._last_poll: dict[str, tuple[float, str]] = {}
|
| 195 |
+
# owner -> (monotonic stamp, stream) of its most recent PARKED poll, kept
|
| 196 |
+
# apart so a digest or plain read between two parks does not hide the
|
| 197 |
+
# watcher. A handle reads as parked while this is younger than
|
| 198 |
+
# parked_window_s.
|
| 199 |
+
self._last_parked: dict[str, tuple[float, str]] = {}
|
| 200 |
self._parked_window_s = parked_window_s
|
| 201 |
# owner -> the newest cursor handed out or sent on the unified stream,
|
| 202 |
# so an agent whose local state was wiped can resume from the server's
|
|
|
|
| 308 |
|
| 309 |
``mode`` reports ``parked`` while the last parked poll is younger than
|
| 310 |
the parked window (2x the wait ceiling), whatever was read since, so a
|
| 311 |
+
digest between two parks does not hide a live watcher; ``stream`` is
|
| 312 |
+
then the parked poll's."""
|
| 313 |
with self._lock:
|
| 314 |
now = self._clock()
|
| 315 |
self._last_poll[owner] = (now, stream)
|
| 316 |
if parked:
|
| 317 |
+
self._last_parked[owner] = (now, stream)
|
| 318 |
self._note_cursor_locked(owner, after)
|
| 319 |
|
| 320 |
def note_cursor(self, owner: str, cursor: str | None) -> None:
|
|
|
|
| 330 |
|
| 331 |
def _presence_locked(self, owner: str, now: float) -> Presence:
|
| 332 |
stamp, stream = self._last_poll[owner]
|
| 333 |
+
parked_at, parked_stream = self._last_parked.get(owner, (None, None))
|
| 334 |
parked = parked_at is not None and now - parked_at < self._parked_window_s
|
| 335 |
+
if parked:
|
| 336 |
+
stream = parked_stream
|
| 337 |
return Presence(
|
| 338 |
max(0.0, now - stamp),
|
| 339 |
"parked" if parked else "poll",
|
app/routes/client.py
CHANGED
|
@@ -40,5 +40,5 @@ def watch_script() -> Response:
|
|
| 40 |
@router.get("/v1/share_trace.py")
|
| 41 |
def share_trace_script() -> Response:
|
| 42 |
"""Serve clients/share_trace.py the same way: `curl -fsS <base>/v1/share_trace.py
|
| 43 |
-
-o share_trace.py &&
|
| 44 |
return _serve(_SHARE_TRACE_PATH, "text/x-python")
|
|
|
|
| 40 |
@router.get("/v1/share_trace.py")
|
| 41 |
def share_trace_script() -> Response:
|
| 42 |
"""Serve clients/share_trace.py the same way: `curl -fsS <base>/v1/share_trace.py
|
| 43 |
+
-o share_trace.py && python3 share_trace.py`."""
|
| 44 |
return _serve(_SHARE_TRACE_PATH, "text/x-python")
|
app/routes/digest.py
CHANGED
|
@@ -53,7 +53,8 @@ def digest(
|
|
| 53 |
|
| 54 |
With `?as=`, two watch blocks come along (WATCH_DESIGN.md §4.5):
|
| 55 |
`updates` answers "am I behind?" over the unified `/v1/updates` stream
|
| 56 |
-
(`?after=<your cursor>`
|
|
|
|
| 57 |
reports this handle's last read before this one (`mode` parked while its
|
| 58 |
last parked poll is younger than 2x the wait ceiling, else poll) and
|
| 59 |
`last_cursor`, the newest cursor the server has handed it on the unified
|
|
@@ -107,15 +108,18 @@ def digest(
|
|
| 107 |
# so it matches exactly what a watcher would have been handed — an
|
| 108 |
# inbox-only count would under-report an agent that follows a channel at
|
| 109 |
# notify: all.
|
| 110 |
-
update_recs = read_model.updates_records(as_)
|
| 111 |
-
updates = DigestUpdates(
|
| 112 |
-
unread=sum(1 for r in update_recs if after is None or r.filename > after),
|
| 113 |
-
newest=max((r.filename for r in update_recs), default=None),
|
| 114 |
-
)
|
| 115 |
# Report the presence as it stood BEFORE this read, then stamp it: a
|
| 116 |
# digest read is a poll too, but it must not answer its own question.
|
| 117 |
seen = notifier.last_poll(as_)
|
| 118 |
notifier.note_poll(as_, "digest", parked=False, after=after)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
if seen is not None:
|
| 120 |
watching = DigestWatching(
|
| 121 |
last_poll_age_s=int(seen.age_s),
|
|
@@ -161,7 +165,8 @@ def discovery(settings: Settings = Depends(get_settings_dep)) -> dict:
|
|
| 161 |
{"method": "GET", "path": "/v1/digest", "params": "as, since, after",
|
| 162 |
"purpose": "one-call collab snapshot: agents, leaderboard, recent "
|
| 163 |
"activity, your inbox; with as= also updates.unread "
|
| 164 |
-
"(
|
|
|
|
| 165 |
"this handle? last_cursor = the newest cursor handed out "
|
| 166 |
"or sent; resume with --after <last_cursor>) and "
|
| 167 |
"you.traces (sessions you have shared)"},
|
|
@@ -256,7 +261,7 @@ def discovery(settings: Settings = Depends(get_settings_dep)) -> dict:
|
|
| 256 |
{"method": "GET", "path": "/v1/share_trace.py", "params": "",
|
| 257 |
"purpose": "the trace-sharing client (stdlib python + hf CLI): "
|
| 258 |
"curl -fsS $API/v1/share_trace.py -o share_trace.py && "
|
| 259 |
-
"
|
| 260 |
{"method": "GET", "path": "/v1/stats", "params": "",
|
| 261 |
"purpose": "project-wide token estimate (reported floor) by model/agent/day"},
|
| 262 |
{"method": "GET", "path": "/v1/healthz", "params": "", "purpose": "liveness"},
|
|
|
|
| 53 |
|
| 54 |
With `?as=`, two watch blocks come along (WATCH_DESIGN.md §4.5):
|
| 55 |
`updates` answers "am I behind?" over the unified `/v1/updates` stream
|
| 56 |
+
(it counts after `?after=<your cursor>`, or by default after the
|
| 57 |
+
server's `last_cursor` for this handle), and `watching`
|
| 58 |
reports this handle's last read before this one (`mode` parked while its
|
| 59 |
last parked poll is younger than 2x the wait ceiling, else poll) and
|
| 60 |
`last_cursor`, the newest cursor the server has handed it on the unified
|
|
|
|
| 108 |
# so it matches exactly what a watcher would have been handed — an
|
| 109 |
# inbox-only count would under-report an agent that follows a channel at
|
| 110 |
# notify: all.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
# Report the presence as it stood BEFORE this read, then stamp it: a
|
| 112 |
# digest read is a poll too, but it must not answer its own question.
|
| 113 |
seen = notifier.last_poll(as_)
|
| 114 |
notifier.note_poll(as_, "digest", parked=False, after=after)
|
| 115 |
+
# Without after=, count from the server's last_cursor, so mail the
|
| 116 |
+
# watcher already delivered does not read as unread.
|
| 117 |
+
count_after = after if after is not None else (seen.last_cursor if seen else None)
|
| 118 |
+
update_recs = read_model.updates_records(as_)
|
| 119 |
+
updates = DigestUpdates(
|
| 120 |
+
unread=sum(1 for r in update_recs if count_after is None or r.filename > count_after),
|
| 121 |
+
newest=max((r.filename for r in update_recs), default=None),
|
| 122 |
+
)
|
| 123 |
if seen is not None:
|
| 124 |
watching = DigestWatching(
|
| 125 |
last_poll_age_s=int(seen.age_s),
|
|
|
|
| 165 |
{"method": "GET", "path": "/v1/digest", "params": "as, since, after",
|
| 166 |
"purpose": "one-call collab snapshot: agents, leaderboard, recent "
|
| 167 |
"activity, your inbox; with as= also updates.unread "
|
| 168 |
+
"(counted after after=, default the server's last_cursor) "
|
| 169 |
+
"and watching (is anyone watching "
|
| 170 |
"this handle? last_cursor = the newest cursor handed out "
|
| 171 |
"or sent; resume with --after <last_cursor>) and "
|
| 172 |
"you.traces (sessions you have shared)"},
|
|
|
|
| 261 |
{"method": "GET", "path": "/v1/share_trace.py", "params": "",
|
| 262 |
"purpose": "the trace-sharing client (stdlib python + hf CLI): "
|
| 263 |
"curl -fsS $API/v1/share_trace.py -o share_trace.py && "
|
| 264 |
+
"python3 share_trace.py"},
|
| 265 |
{"method": "GET", "path": "/v1/stats", "params": "",
|
| 266 |
"purpose": "project-wide token estimate (reported floor) by model/agent/day"},
|
| 267 |
{"method": "GET", "path": "/v1/healthz", "params": "", "purpose": "liveness"},
|
app/routes/results.py
CHANGED
|
@@ -43,7 +43,7 @@ router = APIRouter()
|
|
| 43 |
# share_trace.py reads the same variable to find this backend.
|
| 44 |
SHARE_TRACE_HINT = (
|
| 45 |
"Share this session's stats so others can learn from how you got here: "
|
| 46 |
-
"curl -fsS $API/v1/share_trace.py -o share_trace.py &&
|
| 47 |
)
|
| 48 |
|
| 49 |
|
|
|
|
| 43 |
# share_trace.py reads the same variable to find this backend.
|
| 44 |
SHARE_TRACE_HINT = (
|
| 45 |
"Share this session's stats so others can learn from how you got here: "
|
| 46 |
+
"curl -fsS $API/v1/share_trace.py -o share_trace.py && python3 share_trace.py"
|
| 47 |
)
|
| 48 |
|
| 49 |
|
clients/share_trace.py
CHANGED
|
@@ -22,13 +22,13 @@ WHAT THIS READS AND UPLOADS — for operators, agents, and safety classifiers:
|
|
| 22 |
* `--dry-run` prints the manifest and a typed redaction summary without writing
|
| 23 |
or uploading anything — run it first to verify.
|
| 24 |
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
|
| 33 |
`full` lets Hugging Face's built-in trace viewer render the native log directly
|
| 34 |
from the bucket (Claude Code & Codex supported out of the box). Redaction parses
|
|
|
|
| 22 |
* `--dry-run` prints the manifest and a typed redaction summary without writing
|
| 23 |
or uploading anything — run it first to verify.
|
| 24 |
|
| 25 |
+
python3 share_trace.py # stats only; no content leaves (the floor)
|
| 26 |
+
python3 share_trace.py --upload-only # write to scratch bucket; skip backend promotion
|
| 27 |
+
python3 share_trace.py --full --yes # FULL: stats + balanced-redacted log -> library
|
| 28 |
+
python3 share_trace.py --full --privacy secrets # credentials only; preserve PII
|
| 29 |
+
python3 share_trace.py --full --privacy strict # also pseudonymize hosts + IPs
|
| 30 |
+
python3 share_trace.py --full --raw # UNSAFE: full, skip all redaction
|
| 31 |
+
python3 share_trace.py --dry-run # print the plan + manifest; touch nothing
|
| 32 |
|
| 33 |
`full` lets Hugging Face's built-in trace viewer render the native log directly
|
| 34 |
from the bucket (Claude Code & Codex supported out of the box). Redaction parses
|
tests/test_digest_api.py
CHANGED
|
@@ -147,7 +147,7 @@ def test_digest_updates_is_cursor_aware_via_after(env):
|
|
| 147 |
cursor = env.client.get("/v1/updates?as=watcher").json()["cursor"]
|
| 148 |
env.client.post("/v1/messages", json={"agent_id": "poster", "body": "two @watcher"})
|
| 149 |
|
| 150 |
-
assert env.client.get("/v1/digest?as=watcher").json()["updates"]["unread"] == 2
|
| 151 |
caught_up = env.client.get(f"/v1/digest?as=watcher&after={cursor}").json()
|
| 152 |
assert caught_up["updates"]["unread"] == 1
|
| 153 |
# Fully drained.
|
|
@@ -155,6 +155,18 @@ def test_digest_updates_is_cursor_aware_via_after(env):
|
|
| 155 |
assert env.client.get(f"/v1/digest?as=watcher&after={newest}").json()["updates"]["unread"] == 0
|
| 156 |
|
| 157 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
def test_digest_updates_is_zero_for_a_quiet_handle(env):
|
| 159 |
seed_agent(env.hub, "watcher")
|
| 160 |
data = env.client.get("/v1/digest?as=watcher").json()
|
|
@@ -221,13 +233,13 @@ def test_digest_watching_records_the_cursor_handed_out(env):
|
|
| 221 |
|
| 222 |
def test_a_digest_read_does_not_hide_a_parked_watcher(env):
|
| 223 |
"""mode follows the last PARKED poll, not the last read: a digest between
|
| 224 |
-
two parks still reports parked
|
| 225 |
seed_agent(env.hub, "watcher")
|
| 226 |
env.client.get("/v1/updates?as=watcher&wait=0.05")
|
| 227 |
env.client.get("/v1/digest?as=watcher")
|
| 228 |
|
| 229 |
block = env.client.get("/v1/digest?as=watcher").json()["watching"]
|
| 230 |
-
assert (block["mode"], block["stream"]) == ("parked", "
|
| 231 |
|
| 232 |
|
| 233 |
def test_digest_watching_reports_the_stream(env):
|
|
|
|
| 147 |
cursor = env.client.get("/v1/updates?as=watcher").json()["cursor"]
|
| 148 |
env.client.post("/v1/messages", json={"agent_id": "poster", "body": "two @watcher"})
|
| 149 |
|
| 150 |
+
assert env.client.get("/v1/digest?as=watcher&after=").json()["updates"]["unread"] == 2
|
| 151 |
caught_up = env.client.get(f"/v1/digest?as=watcher&after={cursor}").json()
|
| 152 |
assert caught_up["updates"]["unread"] == 1
|
| 153 |
# Fully drained.
|
|
|
|
| 155 |
assert env.client.get(f"/v1/digest?as=watcher&after={newest}").json()["updates"]["unread"] == 0
|
| 156 |
|
| 157 |
|
| 158 |
+
def test_digest_without_after_counts_from_the_server_last_cursor(env):
|
| 159 |
+
"""An agent that lost its state reads the digest without after=: mail the
|
| 160 |
+
watcher already delivered must not read as unread."""
|
| 161 |
+
seed_agent(env.hub, "watcher")
|
| 162 |
+
seed_agent(env.hub, "poster")
|
| 163 |
+
env.client.post("/v1/messages", json={"agent_id": "poster", "body": "one @watcher"})
|
| 164 |
+
assert env.client.get("/v1/digest?as=watcher").json()["updates"]["unread"] == 1
|
| 165 |
+
assert len(env.client.get("/v1/updates?as=watcher").json()["items"]) == 1
|
| 166 |
+
|
| 167 |
+
assert env.client.get("/v1/digest?as=watcher").json()["updates"]["unread"] == 0
|
| 168 |
+
|
| 169 |
+
|
| 170 |
def test_digest_updates_is_zero_for_a_quiet_handle(env):
|
| 171 |
seed_agent(env.hub, "watcher")
|
| 172 |
data = env.client.get("/v1/digest?as=watcher").json()
|
|
|
|
| 233 |
|
| 234 |
def test_a_digest_read_does_not_hide_a_parked_watcher(env):
|
| 235 |
"""mode follows the last PARKED poll, not the last read: a digest between
|
| 236 |
+
two parks still reports parked, with the parked poll's stream."""
|
| 237 |
seed_agent(env.hub, "watcher")
|
| 238 |
env.client.get("/v1/updates?as=watcher&wait=0.05")
|
| 239 |
env.client.get("/v1/digest?as=watcher")
|
| 240 |
|
| 241 |
block = env.client.get("/v1/digest?as=watcher").json()["watching"]
|
| 242 |
+
assert (block["mode"], block["stream"]) == ("parked", "updates")
|
| 243 |
|
| 244 |
|
| 245 |
def test_digest_watching_reports_the_stream(env):
|
tests/test_notify_unit.py
CHANGED
|
@@ -97,7 +97,8 @@ def test_wake_from_foreign_thread():
|
|
| 97 |
|
| 98 |
def test_mode_is_parked_within_the_window_then_poll():
|
| 99 |
"""A plain read after a park keeps reporting parked until the parked stamp
|
| 100 |
-
is older than parked_window_s; stream
|
|
|
|
| 101 |
now = [0.0]
|
| 102 |
n = Notifier(
|
| 103 |
max_waiters_per_owner=4,
|
|
@@ -110,7 +111,7 @@ def test_mode_is_parked_within_the_window_then_poll():
|
|
| 110 |
n.note_poll("a", "updates", parked=True)
|
| 111 |
now[0] = 50.0
|
| 112 |
n.note_poll("a", "digest", parked=False)
|
| 113 |
-
assert n.last_poll("a")[1:3] == ("parked", "
|
| 114 |
now[0] = 111.0
|
| 115 |
assert n.last_poll("a")[1:3] == ("poll", "digest")
|
| 116 |
|
|
|
|
| 97 |
|
| 98 |
def test_mode_is_parked_within_the_window_then_poll():
|
| 99 |
"""A plain read after a park keeps reporting parked until the parked stamp
|
| 100 |
+
is older than parked_window_s; stream is the parked poll's until then,
|
| 101 |
+
the latest read's after."""
|
| 102 |
now = [0.0]
|
| 103 |
n = Notifier(
|
| 104 |
max_waiters_per_owner=4,
|
|
|
|
| 111 |
n.note_poll("a", "updates", parked=True)
|
| 112 |
now[0] = 50.0
|
| 113 |
n.note_poll("a", "digest", parked=False)
|
| 114 |
+
assert n.last_poll("a")[1:3] == ("parked", "updates")
|
| 115 |
now[0] = 111.0
|
| 116 |
assert n.last_poll("a")[1:3] == ("poll", "digest")
|
| 117 |
|
tests/test_results_api.py
CHANGED
|
@@ -122,7 +122,7 @@ def test_post_result_hints_share_trace_when_none_shared_recently(env):
|
|
| 122 |
_seed_trace(env, "2020-01-01 00:00 UTC") # stale: older than 24 h
|
| 123 |
_seed_trace(env, stamp_yaml(utc_now()), agent="agent-2") # someone else's
|
| 124 |
hint = _post_run(env)["hint"]
|
| 125 |
-
assert "curl -fsS $API/v1/share_trace.py" in hint and "
|
| 126 |
|
| 127 |
|
| 128 |
def test_post_result_no_hint_after_recent_trace(env):
|
|
|
|
| 122 |
_seed_trace(env, "2020-01-01 00:00 UTC") # stale: older than 24 h
|
| 123 |
_seed_trace(env, stamp_yaml(utc_now()), agent="agent-2") # someone else's
|
| 124 |
hint = _post_run(env)["hint"]
|
| 125 |
+
assert "curl -fsS $API/v1/share_trace.py" in hint and "python3 share_trace.py" in hint
|
| 126 |
|
| 127 |
|
| 128 |
def test_post_result_no_hint_after_recent_trace(env):
|