Files
PacketStation/tests/server/test_phase8_server_smoke.py
T
spencermandClaude Opus 5.5 70a97398f1
ci / server (pull_request) Successful in 12m48s
ci / frontend (pull_request) Successful in 54s
ci / image (pull_request) Skipped
ci / e2e (pull_request) Successful in 4m38s
Build Seth's changes C1 to C11 (Gitea issues 1, 8, 18, 23, 24, 25, 27, 31, 32, 33, 36)
- C1 (issue 1): over target follows VISUAL ticket hours, not the timer.
- C2 (issue 24): a Q.C. reject always needs a reason; accept needs an NCR number after a reject without one.
- C3 (issue 25): an incomplete unit ITP blocks filing.
- C4 (issue 27): prints open the print dialog on the page, no new tab.
- C5 (issue 31): filing is blocked while VISUAL shows a work order open; the feed sets vclosed on VISUAL packets.
- C6 (issue 8): the data book leaves out the history pages.
- C7 (issue 18): the supervisor approves each received short; accept and filing wait for it.
- C8 (issue 23): per-department checklists, edited by the department supervisor or Admin, frozen at START.
- C9 (issue 32): Q.C. and Clerical may run the stamp check.
- C10 (issue 33): the PM screen and reports are for PS-ProjectMgmt and Admin only.
- C11 (issue 36): the Stall screen lists the packets assigned to the welder, at the supervisor's stall.

Migration 0011. Docs: PLAN (also issue 9 confirmed, issue 4 asked), POLICY, SECURITY, SCHEMA, FEED, README, CHANGELOG.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 16:36:49 -06:00

141 lines
7.6 KiB
Python

"""Phase 8 server smoke: one small world through every new read (the find bar on the list and the boards, the PM
reports, the exports) so the server agent's shapes are exercised end to end. The detailed checks live in
test_search.py, test_filters.py, test_pm_api.py, test_pm_export.py and test_presence.py."""
from __future__ import annotations
import io
from collections.abc import Iterator
import pytest
from fakedir import FakeDirectory
from pshelpers import app_conn, needs_pg
from pypdf import PdfReader
from shophelpers import Shop, draft, make_unit, ok, open_shop, ready_packet, run, set_running_since, start, to_station
from shophelpers import to_warehouse as send_to_wh
from packetstation.settings import Settings
pytestmark = needs_pg
@pytest.fixture
def shop(pg_settings: Settings, fake_dir: FakeDirectory, dev_visual: None) -> Iterator[Shop]:
yield from open_shop(pg_settings, fake_dir)
def test_find_bar_boards_and_pm_reports(shop: Shop) -> None:
settings = shop.settings
project_id, u1 = make_unit(settings, "L670", "052")
with app_conn(settings) as conn:
u2 = conn.execute(
"INSERT INTO units (project_id, lot, name, status, due) VALUES (%s, '053', 'Unit two', 'open',"
" current_date - 2) RETURNING id",
(project_id,),
).fetchone()[0]
conn.execute("UPDATE projects SET due = current_date + 30 WHERE id = %s", (project_id,))
p_draft = draft(shop, u1, title="Inlet nozzle =SUM(A1)")
p_wh = send_to_wh(shop, draft(shop, u1, drawing="D-7000"))
ok(
shop("wh.wade").post(
f"/api/packets/{p_wh['id']}/wh",
json={"stage": "hold", "reason": "Waiting on purchasing / PO", "expect_stage": "incoming"},
)
)
p_st = start(shop, to_station(shop, ready_packet(shop, u2)))
set_running_since(settings, p_st["id"], 6 * 3600)
with app_conn(settings) as conn:
line = conn.execute("SELECT id FROM bom_lines WHERE packet_id = %s AND item = 2", (p_wh["id"],)).fetchone()[0]
conn.execute(
"INSERT INTO bom_shorts (packet_id, bom_line_id, qty, qty_open, reason, ref, eta)"
" VALUES (%s, %s, 2, 2, 'Not in stock', 'PO-881', current_date - 1)",
(p_wh["id"], line),
)
conn.execute(
"INSERT INTO qc_history (packet_id, at, result, inspector, note, dept_code, snapshot)"
" VALUES (%s, now() - interval '3 days', 'rejected', 'Quinn QC', 'NCR-12 · undercut', '03', '{}')",
(p_st["id"],),
)
co = conn.execute(
"INSERT INTO change_orders (packet_id, co_no, origin, review, ecn, reason, issued_at)"
" VALUES (%s, 1, 'manual', 'approved', 'ECN-77', 'Rev C', now() - interval '4 days') RETURNING id",
(p_st["id"],),
).fetchone()[0]
conn.execute("INSERT INTO co_required_depts (co_id, dept_code) VALUES (%s, '03')", (co,))
run(settings, "UPDATE packets SET wps = 'WPS-99' WHERE id = %s", (p_draft["id"],))
sup = shop("sup.wes")
# The find bar on the list
got = ok(sup.get("/api/packets", params={"q": "lot 52", "project_id": project_id, "facets": 1}))
ids = {c["id"] for c in got["packets"]}
assert p_draft["id"] in ids and p_st["id"] not in ids, ids
assert len(got["facets"]["workers"]) == 1 and len(got["facets"]["stations"]) == 1
assert ok(sup.get("/api/packets", params={"q": "HT-20"}))["packets"][0]["id"] == p_st["id"]
assert [c["id"] for c in ok(sup.get("/api/packets", params={"q": "ECN-77"}))["packets"]] == [p_st["id"]]
assert [c["id"] for c in ok(sup.get("/api/packets", params={"status": "station"}))["packets"]] == [p_st["id"]]
shorts = ok(sup.get("/api/packets", params={"flags": "short"}))["packets"]
assert [c["id"] for c in shorts] == [p_wh["id"]]
mine = ok(sup.get("/api/packets", params={"flags": "mine", "mine_screen": "supervisor"}))["packets"]
assert {c["id"] for c in mine} == {p_draft["id"], p_wh["id"], p_st["id"]}
over = ok(sup.get("/api/packets", params={"flags": "overdue"}))["packets"]
assert [c["id"] for c in over] == [p_st["id"]]
for sort in ("updated", "project", "due", "status", "dept"):
assert len(ok(sup.get("/api/packets", params={"sort": sort, "project_id": project_id}))["packets"]) == 3
hand = ok(sup.get("/api/packets", params={"flag": "hand"}))["packets"]
assert {c["id"] for c in hand} == {p_draft["id"]}
# The boards keep their KPIs on the project scope
b = ok(sup.get("/api/shop/board", params={"q": "nothing-matches-this"}))
assert b["kpis"]["at_stations"] == 1 and not b["depts"][0]["stations"][0]["packets"]
assert "facets" in b
wh = ok(shop("wh.wade").get("/api/warehouse/board", params={"flags": "short"}))
assert [c["id"] for c in wh["columns"]["hold"]] == [p_wh["id"]] and "facets" in wh
qc = ok(shop("qc.quinn").get("/api/qc/home", params={"q": "zzz"}))
assert qc["kpis"]["waiting"] == 0 and "facets" in qc
stall = ok(shop("weld.andy").get("/api/shop/stall", params={"station_id": shop.stall(), "flags": "mine"}))
assert [c["id"] for c in stall["mine"]] == [p_st["id"]]
mine = ok(shop("weld.andy").get("/api/stall/mine")) # change C11: no station picked
assert [c["id"] for c in mine["mine"]] == [p_st["id"]] and mine["mine"][0]["station_id"] == shop.stall()
# Project Management
pmc = shop("pm.paula")
ov = ok(pmc.get("/api/pm/overview", params={"period": "30"}))
k = {x["key"]: x["n"] for x in ov["kpis"]}
assert k["open"] == 3 and k["holds"] == 1 and k["shorts"] == 1 and k["cos"] == 1 and k["rejects"] == 1, k
assert k["late_units"] == 1, k
assert ov["scope"]["label"] == "All projects" and ov["summary"][0].startswith("3 open packets")
assert sum(s["count"] for s in ov["pipeline"]) == 3
assert [(r["cells"][0], r["health"]) for r in ov["attention"]["rows"]] == [("L670052", "risk"), ("L670053", "late")]
pr = ok(pmc.get("/api/pm/projects", params={"expand": "all"}))
assert [r["lvl"] for r in pr["tables"][0]["rows"]][:3] == [0, 1, 1]
ag = ok(pmc.get("/api/pm/aging"))
assert [t["key"] for t in ag["tables"]] == ["aging_steps", "aging_open", "aging_history"]
hr = ok(pmc.get("/api/pm/hours"))
over_rows = hr["tables"][3]["rows"]
# change C1: the timer is over its target but there are no ticket hours, so it is not over target
assert over_rows == [], hr
ql = ok(pmc.get("/api/pm/quality"))
assert [len(t["rows"]) for t in ql["tables"]][:4] == [1, 1, 1, 1], ql
fd = ok(pmc.get("/api/pm/find", params={"sort": "project", "project_id": project_id}))
assert fd["scope"]["label"] == "Project L670" and len(fd["packets"]) == 3
r = pmc.get("/api/pm/export.csv", params={"table": "all"})
assert r.status_code == 200, r.text
text = r.content.decode("utf-8")
assert text.startswith("\ufeffPESCO Packet Station — All reports\r\n")
assert "attachment" in r.headers["content-disposition"]
assert pmc.get("/api/pm/export.csv", params={"table": "nope"}).status_code == 404
r = pmc.get("/api/pm/report.pdf")
assert r.status_code == 200 and r.content.startswith(b"%PDF")
reader = PdfReader(io.BytesIO(r.content))
assert len(reader.pages) > 5
assert "PRODUCTION STATUS REPORT" in reader.pages[0].extract_text()
assert all("Controlled copy" in pg.extract_text() for pg in reader.pages)
with app_conn(settings) as conn:
kinds = [x[0] for x in conn.execute("SELECT kind FROM print_log ORDER BY id")]
acts = [x[0] for x in conn.execute("SELECT action FROM audit_events WHERE action = 'report exported'")]
assert kinds == ["report-csv", "report-pdf"] and len(acts) == 2
assert shop("clerk.clara").get("/api/pm/overview").status_code == 403 # change C10: PM and Admin only