Files
PacketStation/tests/server/test_policy_shop.py
T
spencermandClaude Fable 5.1 5a4174e0c2
ci / frontend (push) Successful in 34s
ci / server (push) Successful in 7m15s
ci / image (push) Successful in 39s
ci / e2e (push) Successful in 2m4s
Phase 5: Supervisor and Stall screens, weld map, heat table, clock and CO verification
The shop at parity with the old build. Supervisor screen (department chips, CO headline, numbers, Change orders
on hold, station grid with clocks and CO tags, boards) and Stall screen (station picker, hero, My packets,
Submitted). Shop steps on the packet page: assign with station, welder picker and stamp; pull back; Start, Pause,
Resume; Submit with a PIN signature and the missing steps as links; review with Send to Q.C., Return to station
and Skip Q.C. route; a lead's read-only review. Weld map (pdf.js viewer with pan, pinch, zoom, full screen; marks
with leader lines; weld sheet; weld record with field CAS; carried marks). Heat map (heat table placed, dragged,
sized, removed; legacy amber tags; borrowed heat map read-only). Checklist tab with templates from settings.
VOID stamps on voided pages. Change orders: CO-n labels, supervisor verify with signature, approval copies the
heat table to the new sheet and the BOM merge keeps the shop's values. Server clock of record with VISUAL ticket
hours beside it. Presence per packet over SSE. Migration 0006 (bfx, bfy, carried_from, checklist_templates).
New endpoints /api/shop/board, /api/shop/stations, /api/shop/stall, /api/departments/{code}/people.

Tests: policy oracle for every shop action, role and status; clock arithmetic; concurrent inserts; CO apply;
frontend geometry, heat table, clock, checklist, weld sheet, screens; seven Playwright scenarios. Docs updated.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-16 22:46:56 -06:00

575 lines
22 KiB
Python

"""Phase 5 shop policy: every shop step as every test user from every status the helpers reach, with the expected
code (200, 403 forbidden, 403 wrong_status, 403 precondition, 400 it_reason_required, 400 reason_required, 400
signature_required); the assigned-welder and station-limit rules; lead versus supervisor; RETURN_TO_CLERICAL only
when wh_skipped; the change-order hold set; and the read model's `can` and `blocked` agreeing with the writes.
The expectations come from an oracle written from the contract (who, from-status, preconditions), not from the
engine, so the engine and the oracle check each other. Part one runs the pure policy; part two runs the API."""
from __future__ import annotations
import dataclasses
import itertools
from collections.abc import Iterator
from typing import Any
import pytest
from fakedir import FakeDirectory
from pkthelpers import step
from pshelpers import app_conn, needs_pg
from psycopg.types.json import Jsonb
from shophelpers import (
IT,
MSG_CHECKLIST,
SIGN,
USERS,
Shop,
draft,
finish_stage,
fresh,
in_progress,
in_review,
issue_co,
ok,
open_shop,
ready_packet,
ready_skipped,
refused,
run,
set_limits,
start,
submit,
to_station,
to_warehouse,
verify,
)
from packetstation.policy import Action, PacketState, Principal, can, check, principal_from_roles
from packetstation.settings import Settings
A = Action
ANDY = "00000000-0000-0000-0000-00000000a001"
STALL1, STALL2 = 101, 102
SHOP_ACTIONS = (
"assign",
"unassign",
"pull_back",
"start",
"submit",
"return_to_station",
"send_to_qc",
"route_from_review",
"send_back_to_warehouse",
"return_to_clerical",
)
FROM: dict[str, frozenset[str]] = {
"assign": frozenset({"ready"}),
"unassign": frozenset({"assigned"}),
"pull_back": frozenset({"in_progress"}),
"start": frozenset({"assigned"}),
"submit": frozenset({"assigned", "in_progress"}),
"return_to_station": frozenset({"review"}),
"send_to_qc": frozenset({"review"}),
"route_from_review": frozenset({"review"}),
"send_back_to_warehouse": frozenset({"ready"}),
"return_to_clerical": frozenset({"ready"}),
}
WHO = {
"assign": "lead",
"unassign": "lead",
"pull_back": "lead",
"start": "works",
"submit": "works",
"return_to_station": "sup",
"send_to_qc": "sup",
"route_from_review": "sup",
"send_back_to_warehouse": "lead",
"return_to_clerical": "lead",
}
CO_HELD = frozenset({"submit", "send_to_qc", "route_from_review"})
REASON = "Weld 2 needs a repair pass"
# Shop levels in 03 per test user (multi.mia leads 06 only; both.bo is worker and supervisor, so supervisor).
LEVEL_03 = {"weld.andy": 1, "dup.dave": 1, "lead.lou": 2, "sup.wes": 3, "both.bo": 3}
ROLES = {
"admin.ann": ["admin"],
"clerk.clara": ["clerical"],
"clerk.leo": ["clerical_supervisor"],
"wh.wade": ["warehouse"],
"qc.quinn": ["qc"],
"pm.paula": ["pm"],
"weld.andy": ["shop:03:worker"],
"lead.lou": ["shop:03:lead"],
"sup.wes": ["shop:03:supervisor"],
"multi.mia": ["clerical", "shop:06:supervisor"],
"both.bo": ["shop:03:worker", "shop:03:supervisor"],
"dup.dave": ["shop:03:worker"],
}
@dataclasses.dataclass(frozen=True)
class Case:
"""A status the helpers reach, and what matters about it for the shop steps."""
name: str
status: str
skipped: bool = False # wh_skipped
finished: bool = False # checklist, a weld and the filler done
CASES = {
"warehouse": Case("warehouse", "warehouse"),
"ready": Case("ready", "ready"),
"ready_skipped": Case("ready_skipped", "ready", skipped=True),
"assigned": Case("assigned", "assigned"),
"in_progress": Case("in_progress", "in_progress"),
"in_progress_done": Case("in_progress_done", "in_progress", finished=True),
"review": Case("review", "review", finished=True),
"qc": Case("qc", "qc", finished=True),
}
def who_ok(user: str, action: str, *, welder: str | None = "weld.andy", limited_out: bool = False) -> bool:
level = LEVEL_03.get(user, 0)
kind = WHO[action]
if kind == "lead":
return level >= 2
if kind == "sup":
return level >= 3
return level >= 2 or (level == 1 and user == welder and not limited_out)
def expected(
user: str,
case: Case,
action: str,
*,
it_reason: bool = False,
co_hold: bool = False,
welder: str | None = "weld.andy",
limited_out: bool = False,
) -> tuple[int, str]:
"""The answer to the step with every request input given (station, welder, reason, book, spool, location,
dept, signature). The order is the engine's: status, CO hold, admin bypass, who, preconditions."""
if case.status not in FROM[action]:
return 403, "wrong_status"
if action == "return_to_clerical" and not case.skipped:
return 403, "wrong_status"
if co_hold and action in CO_HELD:
return 403, "co_hold"
allowed = who_ok(user, action, welder=welder, limited_out=limited_out)
if not allowed and user == "admin.ann":
if not it_reason:
return 400, "it_reason_required"
allowed = True
if not allowed:
return 403, "forbidden"
if action == "submit" and not case.finished:
return 403, "precondition"
return 200, ""
def in_can(user: str, case: Case, action: str, **kw: Any) -> bool:
"""`can` assumes the admin's IT reason and skips request inputs."""
return expected(user, case, action, it_reason=True, **kw)[0] == 200
def in_blocked(user: str, case: Case, action: str, **kw: Any) -> bool:
"""`blocked`: who and custody pass (or the admin bypass) but a state precondition or the CO hold fails."""
code = expected(user, case, action, it_reason=True, **kw)[1]
if code == "co_hold":
# The engine defers the CO hold in read mode and lists it only once who passes.
return who_ok(user, action, welder=kw.get("welder", "weld.andy")) or user == "admin.ann"
return code == "precondition"
# ================================================================ part one: the pure policy
def P(user: str, limits: tuple[int, ...] = ()) -> Principal:
uid = ANDY if user == "weld.andy" else f"00000000-0000-0000-0000-{abs(hash(user)) % 10**12:012d}"
return principal_from_roles(uid, ROLES[user], station_limits=limits)
def state(case: Case, **kw: Any) -> PacketState:
active = case.status in ("assigned", "in_progress", "review")
base = PacketState(
id=1,
status=case.status,
dept="03",
station_id=STALL1 if active else None,
welder_id=ANDY if active else None,
wh_skipped=case.skipped,
routed_to=None,
vclosed=False,
removed=False,
source_kind="hand",
weld_dept=True,
co_hold=False,
drawing_attached=True,
heat_map=True,
bom_total=3,
bom_ready=3,
welds=1 if case.finished else 0,
checklist_complete=case.finished,
filler_set=case.finished,
)
return dataclasses.replace(base, **kw)
EXTRA = {
"assign": {"station_id": STALL1, "welder_id": ANDY},
"return_to_station": {"reason": REASON},
"send_back_to_warehouse": {"reason": REASON},
"return_to_clerical": {"reason": REASON},
"send_to_qc": {"book": True, "spool": True, "location": "Bay 2"},
"route_from_review": {"dept": "09"},
}
@pytest.mark.parametrize(("user", "case", "action"), list(itertools.product(ROLES, CASES, SHOP_ACTIONS)))
def test_policy_matrix(user: str, case: str, action: str) -> None:
c = CASES[case]
s = state(c)
d = check(P(user), s, A(action), extra=EXTRA.get(action, {}))
want = expected(user, c, action)
code = "" if d.ok else d.code
assert code == want[1], (user, case, action, d)
allowed, blocked = can(P(user), s)
assert (A(action) in allowed) == in_can(user, c, action), (user, case, action, allowed)
assert (A(action) in blocked) == in_blocked(user, c, action), (user, case, action, blocked)
@pytest.mark.parametrize("user", list(ROLES))
@pytest.mark.parametrize("case", ["ready", "assigned", "in_progress_done", "review"])
def test_policy_co_hold_set(user: str, case: str) -> None:
"""The CO hold blocks SUBMIT, SEND_TO_QC and ROUTE_FROM_REVIEW; never ASSIGN, START or the others."""
c = CASES[case]
why = "Change order CO-2 must be verified first by: 03 Weld Parts."
s = state(c, co_hold=True, co_hold_why=why)
allowed, blocked = can(P(user), s)
for action in SHOP_ACTIONS:
d = check(P(user), s, A(action), extra=EXTRA.get(action, {}))
want = expected(user, c, action, co_hold=True)
assert ("" if d.ok else d.code) == want[1], (user, case, action, d)
if want[1] == "co_hold":
assert d.why == why
if in_blocked(user, c, action, co_hold=True):
assert blocked[A(action)][0] == why
assert (A(action) in allowed) == in_can(user, c, action, co_hold=True), (user, case, action)
@pytest.mark.parametrize("action", ["start", "submit"])
def test_policy_assigned_welder_rules(action: str) -> None:
c = CASES["in_progress_done"] if action == "submit" else CASES["assigned"]
s = state(c)
assert check(P("weld.andy"), s, A(action)).ok
# another worker in the department is not the assigned welder
assert check(P("dup.dave"), s, A(action)).code == "forbidden"
# the assigned welder limited to another station
assert check(P("weld.andy", (STALL2,)), s, A(action)).code == "forbidden"
assert check(P("weld.andy", (STALL1, STALL2)), s, A(action)).ok
# no welder recorded: a worker cannot, a lead can
nobody = dataclasses.replace(s, welder_id=None)
assert check(P("weld.andy"), nobody, A(action)).code == "forbidden"
assert check(P("lead.lou"), nobody, A(action)).ok
# lead and above pass without being the welder
for user in ("lead.lou", "sup.wes", "both.bo"):
assert check(P(user), s, A(action)).ok, user
def test_policy_lead_versus_supervisor() -> None:
s = state(CASES["review"])
for action in ("send_to_qc", "route_from_review", "return_to_station"):
d = check(P("lead.lou"), s, A(action), extra=EXTRA[action])
assert d.code == "forbidden", action
assert check(P("sup.wes"), s, A(action), extra=EXTRA[action]).ok, action
allowed, blocked = can(P("lead.lou"), s)
assert not {A.SEND_TO_QC, A.ROUTE_FROM_REVIEW, A.RETURN_TO_STATION} & set(allowed)
assert not {A.SEND_TO_QC, A.ROUTE_FROM_REVIEW, A.RETURN_TO_STATION} & set(blocked)
def test_policy_return_to_clerical_only_when_skipped() -> None:
body = EXTRA["return_to_clerical"]
d = check(P("lead.lou"), state(CASES["ready"]), A.RETURN_TO_CLERICAL, extra=body)
assert d.code == "wrong_status"
assert d.why == "This packet came through the warehouse. Send it back to the warehouse instead."
assert check(P("lead.lou"), state(CASES["ready_skipped"]), A.RETURN_TO_CLERICAL, extra=body).ok
d = check(P("lead.lou"), state(CASES["ready_skipped"]), A.RETURN_TO_CLERICAL, extra={})
assert d.code == "reason_required"
# ================================================================ part two: the API
@pytest.fixture
def shop(pg_settings: Settings, fake_dir: FakeDirectory, dev_visual: None) -> Iterator[Shop]:
yield from open_shop(pg_settings, fake_dir)
def body_for(shop: Shop, action: str) -> dict[str, Any]:
"""Every request input the step can need."""
return {
"assign": {"station_id": shop.stall(), "welder_id": shop.uid("weld.andy")},
"submit": {"sign": SIGN},
"return_to_station": {"reason": REASON},
"send_to_qc": {"book": True, "spool": True, "location": "Bay 2", "sign": SIGN},
"route_from_review": {"dept": "09", "sign": SIGN},
"send_back_to_warehouse": {"reason": REASON},
"return_to_clerical": {"reason": REASON},
}.get(action, {})
def build(shop: Shop, case: str) -> dict[str, Any]:
"""A packet in the case's status, made through the API."""
if case == "warehouse":
return to_warehouse(shop, draft(shop))
if case == "ready":
return ready_packet(shop)
if case == "ready_skipped":
return ready_skipped(shop)
if case == "assigned":
return to_station(shop, ready_packet(shop))
if case == "in_progress":
return in_progress(shop)
if case == "in_progress_done":
return finish_stage(shop, in_progress(shop))
if case == "review":
return in_review(shop)
if case == "qc":
pkt = in_review(shop)
return ok(step(shop("sup.wes"), pkt, "send_to_qc", book=True, spool=True, location="Bay 2", sign=SIGN))
raise AssertionError(case)
@needs_pg
@pytest.mark.parametrize("case", list(CASES))
def test_api_matrix(shop: Shop, case: str) -> None:
"""Every refused step answers the oracle's code and changes nothing; `can` and `blocked` agree for every user.
Steps the oracle allows are not sent here (they would move the packet); test_api_success sends each one."""
c = CASES[case]
pkt = build(shop, case)
assert pkt["status"] == c.status and pkt["wh_skipped"] == c.skipped
for user in USERS:
mine = fresh(shop(user), pkt["id"])
for action in SHOP_ACTIONS:
want = expected(user, c, action)
assert (action in mine["can"]) == in_can(user, c, action), (user, case, action, mine["can"])
assert (action in mine["blocked"]) == in_blocked(user, c, action), (user, case, action, mine["blocked"])
if want[0] == 200:
continue
r = step(shop(user), pkt, action, **body_for(shop, action))
assert (r.status_code, r.json().get("code")) == want, (user, case, action, r.text)
if want[1] == "precondition" and action == "submit":
assert r.json()["detail"] == MSG_CHECKLIST
assert mine["blocked"]["submit"][0] == MSG_CHECKLIST
after = fresh(shop("admin.ann"), pkt["id"])
assert (after["status"], after["version"]) == (pkt["status"], pkt["version"])
SUCCESS = [
(case, action, user)
for case, action in (
("ready", "assign"),
("ready", "send_back_to_warehouse"),
("ready_skipped", "return_to_clerical"),
("assigned", "unassign"),
("assigned", "start"),
("assigned", "submit"),
("in_progress", "pull_back"),
("in_progress_done", "submit"),
("review", "return_to_station"),
("review", "send_to_qc"),
("review", "route_from_review"),
)
for user in USERS
if expected(user, CASES[case], action, it_reason=True)[0] == 200 and not (case == "assigned" and action == "submit")
]
TO_STATUS = {
"assign": "assigned",
"send_back_to_warehouse": "warehouse",
"return_to_clerical": "draft",
"unassign": "ready",
"start": "in_progress",
"submit": "review",
"pull_back": "ready",
"return_to_station": "assigned",
"send_to_qc": "qc",
"route_from_review": "ready",
}
@needs_pg
@pytest.mark.parametrize(("case", "action", "user"), SUCCESS)
def test_api_success(shop: Shop, case: str, action: str, user: str) -> None:
"""Each allowed user does each step; the admin only with an IT reason."""
pkt = build(shop, case)
assert action in fresh(shop(user), pkt["id"])["can"]
body = body_for(shop, action)
if user == "admin.ann":
refused(step(shop(user), pkt, action, **body), 400, "it_reason_required")
body["it_reason"] = IT
out = ok(step(shop(user), pkt, action, **body))
assert out["status"] == TO_STATUS[action]
if action == "route_from_review":
assert out["dept"]["code"] == "09"
@needs_pg
@pytest.mark.parametrize(
("case", "action"),
[
("ready", "send_back_to_warehouse"),
("ready_skipped", "return_to_clerical"),
("review", "return_to_station"),
],
)
def test_api_reason_required(shop: Shop, case: str, action: str) -> None:
pkt = build(shop, case)
for reason in (None, "", "short"):
body = {} if reason is None else {"reason": reason}
refused(step(shop("sup.wes"), pkt, action, **body), 400, "reason_required")
assert fresh(shop("sup.wes"), pkt["id"])["status"] == pkt["status"]
@needs_pg
@pytest.mark.parametrize(
("case", "action", "user"),
[
("in_progress_done", "submit", "weld.andy"),
("review", "send_to_qc", "sup.wes"),
("review", "route_from_review", "sup.wes"),
],
)
def test_api_signature_required(shop: Shop, case: str, action: str, user: str) -> None:
pkt = build(shop, case)
body = body_for(shop, action)
body.pop("sign")
r = refused(step(shop(user), pkt, action, **body), 400, "signature_required")
assert r.get("meaning")
assert fresh(shop(user), pkt["id"])["status"] == pkt["status"]
@needs_pg
def test_api_assigned_welder_and_station_limits(shop: Shop) -> None:
pkt = to_station(shop, ready_packet(shop))
# dup.dave works in 03 but is not the welder
refused(step(shop("dup.dave"), pkt, "start"), 403, "forbidden")
assert "start" not in fresh(shop("dup.dave"), pkt["id"])["can"]
# weld.andy limited to Stall 2 while the packet is at Stall 1
set_limits(shop.settings, "weld.andy", [shop.stall("03", "Stall 2")])
refused(step(shop("weld.andy"), pkt, "start"), 403, "forbidden")
mine = fresh(shop("weld.andy"), pkt["id"])
assert "start" not in mine["can"] and "start" not in mine["blocked"]
# the lead starts it (lead and above pass)
pkt = start(shop, pkt, by="lead.lou")
refused(step(shop("weld.andy"), pkt, "submit", sign=SIGN), 403, "forbidden")
# limits that include Stall 1: weld.andy works on it again
set_limits(shop.settings, "weld.andy", [shop.stall("03", "Stall 1"), shop.stall("03", "Stall 2")])
pkt = finish_stage(shop, pkt)
pkt = submit(shop, pkt, by="weld.andy")
# the lead may submit too: a second packet
other = finish_stage(shop, start(shop, to_station(shop, ready_packet(shop)), by="weld.andy"), by="lead.lou")
submit(shop, other, by="lead.lou")
@needs_pg
def test_api_lead_review_read_only(shop: Shop) -> None:
pkt = in_review(shop)
lou = fresh(shop("lead.lou"), pkt["id"])
for action in ("send_to_qc", "route_from_review", "return_to_station"):
assert action not in lou["can"] and action not in lou["blocked"], action
refused(step(shop("lead.lou"), pkt, action, **body_for(shop, action)), 403, "forbidden")
wes = fresh(shop("sup.wes"), pkt["id"])
assert {"send_to_qc", "route_from_review", "return_to_station"} <= set(wes["can"])
@needs_pg
def test_api_return_to_clerical_only_when_skipped(shop: Shop) -> None:
pkt = ready_packet(shop)
r = refused(step(shop("lead.lou"), pkt, "return_to_clerical", reason=REASON), 403, "wrong_status")
assert r["detail"] == "This packet came through the warehouse. Send it back to the warehouse instead."
skipped = ready_skipped(shop)
out = ok(step(shop("lead.lou"), skipped, "return_to_clerical", reason=REASON))
assert out["status"] == "draft" and out["return_note"] == REASON
def held_text(no: int, depts: str = "03 Weld Parts") -> str:
return f"Change order CO-{no} must be verified first by: {depts}."
@needs_pg
def test_api_co_hold_set(shop: Shop) -> None:
"""A CO requiring 03 holds SUBMIT, SEND_TO_QC and ROUTE_FROM_REVIEW, never ASSIGN or START."""
pkt = ready_packet(shop)
pkt = issue_co(shop, pkt, ["03"])
assert pkt["open_cos"] == [1]
wes = fresh(shop("sup.wes"), pkt["id"])
assert "assign" in wes["can"] and "assign" not in wes["blocked"]
pkt = to_station(shop, pkt)
andy = fresh(shop("weld.andy"), pkt["id"])
assert "start" in andy["can"]
pkt = finish_stage(shop, start(shop, pkt))
andy = fresh(shop("weld.andy"), pkt["id"])
assert "submit" not in andy["can"] and andy["blocked"]["submit"] == [held_text(1)]
refused(step(shop("weld.andy"), pkt, "submit", sign=SIGN), 403, "co_hold", held_text(1))
refused(step(shop("admin.ann"), pkt, "submit", sign=SIGN, it_reason=IT), 403, "co_hold", held_text(1))
ok(verify(shop, pkt, 1, "03", "sup.wes"))
pkt = submit(shop, fresh(shop("weld.andy"), pkt["id"]))
pkt = issue_co(shop, pkt, ["03"])
wes = fresh(shop("sup.wes"), pkt["id"])
for action in ("send_to_qc", "route_from_review"):
assert action not in wes["can"] and wes["blocked"][action] == [held_text(2)], action
refused(step(shop("sup.wes"), pkt, action, **body_for(shop, action)), 403, "co_hold", held_text(2))
assert "return_to_station" in wes["can"]
# a pending VISUAL change order with hold: both sentences, joined by a space
with_pending = _pending_feed_co(shop, pkt["id"], hold=True)
pending = (
f"CO-{with_pending}: Clerical is reviewing a change from VISUAL. Held until Clerical approves or rejects it."
)
wes = fresh(shop("sup.wes"), pkt["id"])
assert wes["blocked"]["send_to_qc"] == [f"{held_text(2)} {pending}"] or wes["blocked"]["send_to_qc"] == [
f"{pending} {held_text(2)}"
]
ok(verify(shop, pkt, 2, "03", "sup.wes"))
wes = fresh(shop("sup.wes"), pkt["id"])
assert wes["blocked"]["send_to_qc"] == [pending]
run(
shop.settings,
"UPDATE change_orders SET hold = false WHERE packet_id = %s AND co_no = %s",
(pkt["id"], with_pending),
)
wes = fresh(shop("sup.wes"), pkt["id"])
assert "send_to_qc" in wes["can"] and "send_to_qc" not in wes["blocked"]
ok(step(shop("sup.wes"), wes, "send_to_qc", book=True, spool=True, location="Bay 2", sign=SIGN))
def _pending_feed_co(shop: Shop, pid: int, *, hold: bool) -> int:
"""A pending VISUAL change order by SQL, as the feed opens one. Returns its number."""
with app_conn(shop.settings) as conn:
no = conn.execute(
"UPDATE packets SET next_co_no = next_co_no + 1 WHERE id = %s RETURNING next_co_no - 1", (pid,)
).fetchone()[0]
conn.execute(
"INSERT INTO change_orders (packet_id, co_no, origin, review, minor, hold, held_at, ecn, reason, vis)"
" VALUES (%s, %s, 'visual', 'pending', false, %s, CASE WHEN %s THEN now() END, 'VISUAL',"
" 'VISUAL changed the packet: qty 2 -> 3', %s)",
(pid, no, hold, hold, Jsonb({"dwg_change": False, "changes": ["qty 2 -> 3"]})),
)
return int(no)
@needs_pg
def test_api_stale_expect_after_pull_back(shop: Shop) -> None:
pkt = finish_stage(shop, in_progress(shop))
ok(step(shop("sup.wes"), pkt, "pull_back"))
r = refused(
step(shop("weld.andy"), pkt, "submit", sign=SIGN), 409, "stale_step", "Someone else already took this step."
)
assert r["current"]["status"] == "ready"