Files
Project-SDE-WP-Suite/tests/kitting_check.py
n.siegfried 96387105f4 T8.2 - CR-010: the warehouse owner is an account, on the package
A named person owns fulfillment of the kit - today that is Paul Coonrod,
informally, and everything bottlenecks through him. The package now records
it explicitly:

- The Warehouse owner control is a dropdown of project members (the same
  roster the Owner picker reads). Picking someone stores BOTH the display
  name (kitOwner - exports and old renderers keep working) and the account id
  (kitOwnerId - the routing CR-011's notifications will read at T8.3).
- Confirmed Aug 18: the field lives ON the work package, not the project - a
  package retargeted to a different warehouse notifies the right person
  without touching the project. The wizard gets no field.
- A stored name with no matching account - typed before the field was
  account-backed, or someone since removed from the project - is KEPT as a
  selected "(no account)" option and round-trips unchanged. Removing someone
  from the project breaks nothing.
- The dashboard filters by warehouse owner, options drawn from the owners
  actually present in the data - a filter offering people with nothing to
  fulfill is noise.

Verification (each probe run alone): kitting_check.py extended to 21/21
(T8.2 section: picker, id+name persistence, orphan survival, board filter).
Regression: generalinfo_check 49/49.

Items: CR-010

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 12:00:14 -07:00

214 lines
9.8 KiB
Python

#!/usr/bin/env python3
"""Kitting: statuses, the owner, the filter — CR-009 (T8.1) + CR-010 (T8.2).
The statuses become an explicit five (Not Started / Picking / Staged /
In Transit / Delivered) - fulfillment states, not free text. A value stored
before the set existed is kept and shown as a legacy option: renamed sets must
not orphan recorded data. The Micron EUV configuration (the sample SOP, per
CR-016/T5.7) turns the section OFF through the CR-006 toggle; any other SOP
can turn it on and everything works; nothing is deleted either way.
Self-contained: throwaway SQLite, its own uvicorn, headless Edge or Chrome.
Exit 0 all passed, 1 a failure, 2 could not run.
"""
import json
import os
import re
import sys
import tempfile
import time
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import cdp # noqa: E402
from browser_check import seed, start_server, chk, _PASS, _FAIL # noqa: E402
from sections_check import set_sop # noqa: E402
from stepper_check import dismiss_dialogs # noqa: E402
KIT = ["Not Started", "Picking", "Staged", "In Transit", "Delivered"]
def ascii_(v, n=280):
return re.sub(r"\s+", " ", str(v)).encode("ascii", "replace").decode()[:n]
def settle(seconds=0.5):
time.sleep(seconds)
def wait_creator(page, tries=40):
for _ in range(tries):
if page.eval("!!window.wpCreatorReady"):
return True
time.sleep(0.3)
return False
def kit_options(page):
return json.loads(page.eval(
"JSON.stringify([...document.getElementById('wp_kit_status').options]"
".map(o=>o.textContent))"))
def main():
exe = cdp.find_browser()
if not exe:
print("no headless-capable browser found; set WP_BROWSER.")
return 2
tmpdir = tempfile.mkdtemp(prefix="wpsuite-kit-")
db_path = os.path.join(tmpdir, "check.db")
server = None
browser = None
try:
tok = seed(db_path)
set_sop(db_path, {}) # sopA: every section ON, kitting included
port = cdp.free_port()
base = "http://127.0.0.1:%d" % port
server = start_server(port, db_path)
browser = cdp.Browser(exe)
page = browser.page()
page.clear_cookies()
page.set_cookie("wp_session", tok["root"])
page.viewport(1440, 900)
page.goto(base + "/wp-creation-index.html?project=projA")
dismiss_dialogs(page)
chk("the creator boots", wait_creator(page))
settle(1.6)
page.eval("window.alert=()=>{}; window.confirm=()=>false; window.prompt=()=>null;")
# ── 1. the set ────────────────────────────────────────────────────────
print("\n1. an explicit set, not free text")
opts = kit_options(page)
chk("the kitting status control is a select offering exactly the five states",
opts == [""] + KIT, ascii_(opts))
chk("...and the set is one named constant",
json.loads(page.eval("JSON.stringify(KIT_STATUSES)")) == KIT)
# a pre-CR-009 value survives as a legacy option, selected
page.eval("""loadPackageIntoForm({subject:'legacy kit', type:'Conduit Install',
kitStatus:'Kitted', kitOwner:'Paul Coonrod', constraints:[], holds:[]})""")
settle(0.8)
chk("a stored pre-CR-009 value is kept, selected, and marked legacy",
page.eval("document.getElementById('wp_kit_status').value") == "Kitted"
and any("legacy" in o for o in kit_options(page)), ascii_(kit_options(page)))
chk("...and it round-trips through collect unchanged - nothing re-written",
page.eval("collectPackage().kitStatus") == "Kitted")
page.eval("document.getElementById('wp_kit_status').value='Staged'")
chk("...and picking a new value works from the same control",
page.eval("collectPackage().kitStatus") == "Staged")
# ── 2. this SOP (kitting ON) has the section, working ─────────────────
print("\n2. an SOP with kitting on")
vis = page.eval("""(() => { const c=document.getElementById('mimo-card');
return c && !c.hidden && getComputedStyle(c).display !== 'none'; })()""")
chk("the Kitting & MIMO section is present and rendered", bool(vis))
chk("...and appears in the section rail", page.eval(
"!!document.querySelector(%s)" % json.dumps('.sec-rail-item[data-sec="mimo-card"]')))
# ── 3. Micron EUV (the sample): kitting OFF ───────────────────────────
print("\n3. Micron EUV: off, hidden, not deleted")
page.eval("loadSampleSOP()")
settle(1.0)
chk("the sample IS the Micron configuration and turns kitting off",
page.eval("SOP.sections && SOP.sections.kitting === false"))
hidden = page.eval("""(() => { const c=document.getElementById('mimo-card');
return c && (c.hidden || getComputedStyle(c).display === 'none'); })()""")
chk("the section is gone from the form", bool(hidden))
chk("...and from the rail", page.eval(
"!document.querySelector(%s)" % json.dumps('.sec-rail-item[data-sec="mimo-card"]')))
page.eval("loadExample()")
settle(1.0)
doc = page.eval("(() => { renderPackage(collectPackage()); "
"return document.getElementById('pkg-doc').innerHTML; })()")
chk("...and from the export", "Kitting & MIMO" not in doc)
chk("the example package's kitting DATA is still on the record - hidden, "
"never deleted",
page.eval("collectPackage().kitStatus") != ""
and bool(page.eval("collectPackage().kitOwner")), ascii_(
(page.eval("collectPackage().kitStatus"),
page.eval("collectPackage().kitOwner"))))
# ── 4. CR-010: the warehouse owner (T8.2) ────────────────────────────
print(chr(10) + "4. CR-010: the warehouse owner")
page.eval("set_sop_reload = 0") # marker only
page.goto(base + "/wp-creation-index.html?project=projA")
dismiss_dialogs(page)
wait_creator(page)
settle(1.6)
page.eval("window.alert=()=>{}; window.confirm=()=>false; window.prompt=()=>null;")
chk("the warehouse owner is a dropdown of project members, not free text",
page.eval("document.getElementById('wp_kit_owner_sel').tagName") == "SELECT"
and page.eval("document.getElementById('wp_kit_owner_sel').options.length") > 1,
page.eval("document.getElementById('wp_kit_owner_sel').options.length"))
page.eval("""(() => {
const sel=document.getElementById('wp_kit_owner_sel');
sel.value=[...sel.options].find(o=>o.value==='user_sue').value;
sel.onchange.call(sel);
})()""")
got = json.loads(page.eval(
"JSON.stringify({name: collectPackage().kitOwner, id: collectPackage().kitOwnerId})"))
chk("picking a member persists BOTH the display name and the account id "
"(the id is what CR-011 notifications will read)",
got["id"] == "user_sue" and got["name"], ascii_(got))
page.eval("""loadPackageIntoForm({subject:'legacy owner', type:'Conduit Install',
kitOwner:'Paul Coonrod', constraints:[], holds:[]})""")
settle(0.8)
chk("a stored name with NO account renders as a kept '(no account)' option "
"- removing someone from the project breaks nothing",
page.eval("document.getElementById('wp_kit_owner_sel').value") == "__orphan__"
and "no account" in page.eval(
"document.getElementById('wp_kit_owner_sel').selectedOptions[0].textContent"))
chk("...and the stored name still round-trips",
page.eval("collectPackage().kitOwner") == "Paul Coonrod")
# the dashboard filter
page.eval("""(() => {
const mk=(id,num,owner)=>({id, number:num, subject:'kit '+num,
type:'Conduit Install', status:'Draft', kitOwner:owner,
constraints:[], holds:[], projectId:activeProjectId});
savedPackages.push(mk('wpKA','WP70-KA','Paul Coonrod'));
savedPackages.push(mk('wpKB','WP71-KB','Sue'));
saveStore(); renderSavedList();
})()""")
page.eval("showDashboard()")
settle(1.2)
page.eval("dashFilter.kitOwner='Paul Coonrod'; dashPage=0; renderDashboard()")
settle(0.8)
board = page.eval("(document.getElementById('dash-body')||{textContent:''}).textContent")
chk("the dashboard filters by warehouse owner - their package shows",
"WP70-KA" in board, ascii_(board, 120))
chk("...and the other owner's does not", "WP71-KB" not in board)
chk("...through a labelled control on the board",
page.eval("!!document.querySelector('[aria-label=%s]')"
% json.dumps("Filter by warehouse owner")))
js_errors = [e for e in page.js_errors() if "beforeunload" not in e]
chk("no JavaScript errors anywhere in this run", not js_errors,
ascii_(js_errors[:2]))
finally:
if browser is not None:
try:
browser.close()
except Exception:
pass
if server is not None:
try:
server.terminate()
except Exception:
pass
print("\n" + "-" * 54)
print("%d/%d checks passed." % (len(_PASS), len(_PASS) + len(_FAIL)))
for f in _FAIL:
print(" - " + f)
return 1 if _FAIL else 0
if __name__ == "__main__":
sys.exit(main())