Spaces:
Running
Running
| """Deleting constructs, not just conversations. | |
| The reviewer's words were "no catalog of conversations/chats or way to delete | |
| 'constructs' and conversations". The catalog shipped with rename and delete for | |
| conversations and neither for constructs — the cards had one action, "open". | |
| Half of that gap is a server gap. Plasmids and primer analyses have | |
| owner-checked DELETE routes; saved LIBRARIES and CRISPR DESIGNS have no route | |
| in server.py and no function in auth.py, and a library is the primary artifact | |
| this product makes. So catalog.js drives a table, CX_DELETE, and renders a | |
| Delete button only for the kinds that have somewhere to send it — a delete | |
| control that 404s is worse than an honest absence. | |
| These tests keep the two halves in step: every endpoint the client is prepared | |
| to call has to exist on the server, and the kinds that cannot be deleted have | |
| to stay silent rather than grow a button that fails. | |
| """ | |
| import re | |
| from dee import server | |
| _CAT = "dee/static/catalog.js" | |
| def _read(path): | |
| with open(path, encoding="utf-8") as fh: | |
| return fh.read() | |
| def _cx_delete_table(): | |
| """The live CX_DELETE map, parsed out of catalog.js (commented-out entries | |
| are deliberately NOT picked up — they are the documented server gap).""" | |
| src = _read(_CAT) | |
| block = src[src.index("var CX_DELETE = {"):] | |
| block = block[:block.index("};")] | |
| out = {} | |
| for line in block.split("\n"): | |
| stripped = line.strip() | |
| if stripped.startswith("//"): | |
| continue | |
| m = re.search(r'(\w+)\s*:\s*\{\s*path:\s*"([^"]+)"', stripped) | |
| if m: | |
| out[m.group(1)] = m.group(2) | |
| return out | |
| def _delete_routes(): | |
| app = server.create_app() | |
| return {str(r.rule) for r in app.url_map.iter_rules() if "DELETE" in (r.methods or set())} | |
| def test_every_endpoint_the_catalog_will_call_actually_exists(): | |
| """The failure this prevents: shipping a Delete button whose fetch 404s, | |
| which looks to the user exactly like the bug they reported.""" | |
| routes = _delete_routes() | |
| for kind, path in _cx_delete_table().items(): | |
| # "/api/plasmid/library/" in the client -> "/api/plasmid/library/<id>" | |
| # on the server. Match on the prefix, since the client appends the id. | |
| assert any(rule.startswith(path) for rule in routes), ( | |
| f"catalog.js will DELETE {path}<id> for a {kind} construct, but no " | |
| f"DELETE route starts with that. Routes: {sorted(routes)}") | |
| def test_the_two_kinds_with_no_route_render_no_delete_button(): | |
| """Libraries and CRISPR designs genuinely cannot be deleted yet. The | |
| catalog must not pretend otherwise — and when the routes land, uncommenting | |
| two lines in CX_DELETE is the whole client change.""" | |
| table = _cx_delete_table() | |
| routes = _delete_routes() | |
| for kind, prefix in (("library", "/api/library/"), ("crispr", "/api/crispr/designs/")): | |
| if any(r.startswith(prefix) for r in routes): | |
| # The server gap is closed — then the client must offer the verb. | |
| assert kind in table, ( | |
| f"{prefix}<id> now exists server-side; uncomment the {kind} entry " | |
| "in CX_DELETE so the catalog offers it") | |
| else: | |
| assert kind not in table, ( | |
| f"catalog.js offers to delete a {kind} construct but there is no " | |
| f"DELETE route under {prefix}") | |
| def test_the_construct_card_only_grows_actions_it_can_perform(): | |
| src = _read(_CAT) | |
| card = src[src.index("function constructCard"):] | |
| card = card[:card.index("\n }")] | |
| assert "CX_DELETE[c.kind]" in card, ( | |
| "the Delete button must be gated on the endpoint table, not rendered " | |
| "unconditionally") | |
| assert 'data-act="delcx"' in card | |
| def test_deleting_a_construct_confirms_first_and_names_it(): | |
| """Same rule as conversations: there is no undo behind any of this, so the | |
| confirm says which thing is going rather than asking 'are you sure?'.""" | |
| src = _read(_CAT) | |
| fn = src[src.index("function confirmDeleteConstruct"):] | |
| fn = fn[:fn.index("\n }")] | |
| assert "confirmDelete(" in fn | |
| assert "cx.name" in fn, "the confirm must name the construct" | |
| assert "del.noun" in fn, "the confirm must say what kind of thing it is" | |
| def test_deleting_a_construct_refreshes_mission_control(): | |
| """Mission Control lists the same constructs from the same endpoint. | |
| Leaving it showing a card whose record is gone is the silted-up-dropdown | |
| problem in a second place.""" | |
| src = _read(_CAT) | |
| fn = src[src.index("function doDeleteConstruct"):] | |
| fn = fn[:fn.index("\n }\n")] | |
| assert "TDMission" in fn and "reload" in fn | |