merge issue-89-metadata-lock → main (Create-string model, #89/#90)
Drop the metadata_locked column + "Save and lock" UI end-to-end. The review endpoint now builds the deterministic title/short_description on every call; the owner's Create title / Create short desc buttons fire the build and write the result (force-overwrite contract). commitReview auto-fills only empty fields (no owner-clobber) and folds the deterministic copy into its single PUT (kills the client_rev 409 cascade). Taken wholesale from the branch: photo strip, wood primary/secondary, server-side thickness band (hidden from UI, passed to Woo on publish), and fixes #80/#82/#83/#84. - backend: remove metadata_locked from schema.sql, db sentinels, slabs.py client fields/exposure, review.py lock action; review always computes title/short_desc - frontend: slabs.ts + Review.svelte drop the lock model, add Create-string workflow + wood primary + photo strip - e2e: m3-create + review-photo-strip rewritten for the Create-string contract; thickness-band test asserts absence (per #90) - deploy: recreate LAN binds (override-m3.yml was absent); verify metadata_locked gone, review builds title, PUT persists Gates: backend 117/117, unit 88/88, tsc clean, e2e 6/6 (m3-create + photo-strip). Live UAT verified at 192.168.1.202:10881.
This commit is contained in:
@@ -55,19 +55,14 @@ def test_title_live_edge_no_dims(env):
|
||||
conn.execute("INSERT INTO slabs(id,sku,length_in,thickness_in,sqft,bdft,width_min_in,width_max_in,width_avg_in,species_id,wood_category_ids,figure_term_ids,price,price_source,title,short_description,description,content_source,status,server_rev,client_rev,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
("s1","SLAB-01",114.0,1.75,30.0,18.4,18.0,24.3,24.3,1,json.dumps([2]),json.dumps([1]),None,None,None,None,"A slab","manual","calibrated",1,1,"t","t"))
|
||||
conn.commit()
|
||||
# #89: title/short_description are built AND persisted only after an explicit lock.
|
||||
r = env[0].post("/api/v1/slabs/s1/review", json={"action": "lock"})
|
||||
# #89: the review always returns the deterministic title/short_description
|
||||
# built from the current metadata (the Create button's source of truth).
|
||||
r = env[0].post("/api/v1/slabs/s1/review", json={})
|
||||
assert r.status_code == 200, r.text
|
||||
body = r.json()
|
||||
assert body["locked"] is True
|
||||
assert body["title"] == "Figured Black Walnut Live Edge Slab"
|
||||
assert body["short_description"].startswith("Figured Black Walnut Live Edge Slab, 9ft 6in x 24.3in x 1.75in")
|
||||
assert "18.4 bdft" in body["short_description"]
|
||||
# GET shows the stored (persisted) copy — it matches the deterministic pattern.
|
||||
got = env[0].get("/api/v1/slabs/s1").json()
|
||||
assert got["title"] == "Figured Black Walnut Live Edge Slab"
|
||||
assert got["short_description"].startswith("Figured Black Walnut Live Edge Slab,")
|
||||
assert got["metadata_locked"] is True
|
||||
|
||||
|
||||
def test_ready_requires_full_set(env):
|
||||
@@ -141,103 +136,54 @@ def test_knife_blank_carries_dims(env):
|
||||
conn.execute("INSERT INTO slabs(id,sku,length_in,thickness_in,sqft,bdft,width_min_in,width_max_in,width_avg_in,species_id,wood_category_ids,figure_term_ids,price,price_source,title,short_description,description,content_source,status,server_rev,client_rev,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
("s1","SLAB-01",12.0,1.5,6.0,0.7,6.0,6.5,6.5,1,json.dumps([3]),json.dumps([1]),None,None,None,None,"A slab","manual","calibrated",1,1,"t","t"))
|
||||
conn.commit()
|
||||
# #89: build happens on lock.
|
||||
r = client.post("/api/v1/slabs/s1/review", json={"action": "lock"})
|
||||
# #89: the deterministic build runs on every review.
|
||||
r = client.post("/api/v1/slabs/s1/review", json={})
|
||||
assert r.json()["title"] == "Figured Black Walnut Knife Blank (12 x 6.5 x 1.5 in)"
|
||||
|
||||
|
||||
# --- #89: explicit lock gate for deterministic title/short_description ---
|
||||
|
||||
def test_review_before_lock_reports_no_title(env):
|
||||
"""#89 AC: until lock, title/short_description are not set (no 'ready' copy
|
||||
from an unlocked AI draft). A plain review reports the stored (empty) values."""
|
||||
def test_review_always_reports_deterministic_title(env):
|
||||
"""#89: the review always returns the deterministic title/short_description
|
||||
built from the current metadata. The client's Create button writes these into
|
||||
the fields; the server does not persist them from the review payload."""
|
||||
client, conn = env
|
||||
conn.execute("INSERT INTO slabs(id,sku,length_in,thickness_in,sqft,bdft,width_min_in,width_max_in,width_avg_in,species_id,wood_category_ids,figure_term_ids,price,price_source,title,short_description,description,content_source,status,server_rev,client_rev,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
("s1","SLAB-01",114.0,1.75,30.0,18.4,18.0,24.3,24.3,1,json.dumps([2]),json.dumps([1]),None,None,None,None,"A slab","manual","calibrated",1,1,"t","t"))
|
||||
conn.commit()
|
||||
r = client.post("/api/v1/slabs/s1/review", json={})
|
||||
body = r.json()
|
||||
assert body["locked"] is False
|
||||
assert body["title"] is None
|
||||
assert body["short_description"] is None
|
||||
# title/short_description are in the missing set (not ready until locked).
|
||||
assert "title" in body["missing"] and "short_description" in body["missing"]
|
||||
# Nothing was persisted: GET still shows empty copy.
|
||||
assert client.get("/api/v1/slabs/s1").json()["title"] is None
|
||||
|
||||
|
||||
def test_lock_persists_title_and_get_matches(env):
|
||||
"""#89 AC: after lock with species + primary wood category, title/short_desc
|
||||
are populated deterministically, stored, and GET reflects them."""
|
||||
client, conn = env
|
||||
conn.execute("INSERT INTO slabs(id,sku,length_in,thickness_in,sqft,bdft,width_min_in,width_max_in,width_avg_in,species_id,wood_category_ids,figure_term_ids,price,price_source,title,short_description,description,content_source,status,server_rev,client_rev,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
("s1","SLAB-01",114.0,1.75,30.0,18.4,18.0,24.3,24.3,1,json.dumps([2]),json.dumps([1]),None,None,None,None,"A slab","manual","calibrated",1,1,"t","t"))
|
||||
conn.commit()
|
||||
r = client.post("/api/v1/slabs/s1/review", json={"action": "lock"})
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body["locked"] is True
|
||||
assert body["title"] == "Figured Black Walnut Live Edge Slab"
|
||||
assert body["short_description"].startswith("Figured Black Walnut Live Edge Slab,")
|
||||
# The review does NOT persist title/short_description from the payload.
|
||||
got = client.get("/api/v1/slabs/s1").json()
|
||||
assert got["title"] == body["title"]
|
||||
assert got["short_description"] == body["short_description"]
|
||||
assert got["metadata_locked"] is True
|
||||
assert got["title"] is None
|
||||
assert got["short_description"] is None
|
||||
|
||||
|
||||
def test_lock_requires_species(env):
|
||||
def test_review_no_species_reports_stored_title(env):
|
||||
"""With no species, there is no deterministic pattern; the review reports the
|
||||
stored title (None here) and lists title as missing."""
|
||||
client, conn = env
|
||||
# No species_id -> lock is rejected with a plain message.
|
||||
conn.execute("INSERT INTO slabs(id,sku,length_in,thickness_in,sqft,bdft,width_min_in,width_max_in,width_avg_in,species_id,wood_category_ids,price,title,short_description,status,server_rev,client_rev,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
("s1","SLAB-01",114.0,1.75,30.0,18.4,18.0,24.3,24.3,None,json.dumps([2]),None,None,None,"calibrated",1,1,"t","t"))
|
||||
conn.commit()
|
||||
r = client.post("/api/v1/slabs/s1/review", json={"action": "lock"})
|
||||
assert r.status_code == 422
|
||||
assert r.json()["error"]["code"] == "not_ready"
|
||||
|
||||
|
||||
def test_lock_requires_wood_category(env):
|
||||
client, conn = env
|
||||
# species present but no wood category -> rejected.
|
||||
conn.execute("INSERT INTO slabs(id,sku,length_in,thickness_in,sqft,bdft,width_min_in,width_max_in,width_avg_in,species_id,wood_category_ids,price,title,short_description,status,server_rev,client_rev,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
("s1","SLAB-01",114.0,1.75,30.0,18.4,18.0,24.3,24.3,1,json.dumps([]),None,None,None,"calibrated",1,1,"t","t"))
|
||||
conn.commit()
|
||||
r = client.post("/api/v1/slabs/s1/review", json={"action": "lock"})
|
||||
assert r.status_code == 422
|
||||
assert r.json()["error"]["code"] == "not_ready"
|
||||
|
||||
|
||||
def test_lock_owner_prose_wins(env):
|
||||
# METADATA-50: typed owner title/short_description win over the pattern.
|
||||
client, conn = env
|
||||
conn.execute("INSERT INTO slabs(id,sku,length_in,thickness_in,sqft,bdft,width_min_in,width_max_in,width_avg_in,species_id,wood_category_ids,figure_term_ids,price,price_source,title,short_description,description,content_source,status,server_rev,client_rev,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
("s1","SLAB-01",114.0,1.75,30.0,18.4,18.0,24.3,24.3,1,json.dumps([2]),json.dumps([1]),None,None,None,None,"A slab","manual","calibrated",1,1,"t","t"))
|
||||
conn.commit()
|
||||
r = client.post("/api/v1/slabs/s1/review", json={
|
||||
"action": "lock",
|
||||
"title": "My Handcrafted Walnut Slab",
|
||||
"short_description": "Owner-authored short description.",
|
||||
})
|
||||
r = client.post("/api/v1/slabs/s1/review", json={})
|
||||
body = r.json()
|
||||
assert body["title"] == "My Handcrafted Walnut Slab"
|
||||
assert body["short_description"] == "Owner-authored short description."
|
||||
got = client.get("/api/v1/slabs/s1").json()
|
||||
assert got["title"] == "My Handcrafted Walnut Slab"
|
||||
assert body["title"] is None
|
||||
assert "title" in body["missing"]
|
||||
|
||||
|
||||
def test_review_persists_owner_overrides(env):
|
||||
"""#80: regular review (no lock) persists price/sku/title/short_description
|
||||
overrides from the payload to the DB, so the client doesn't need a separate
|
||||
PUT for each override."""
|
||||
"""#80: regular review persists price/sku overrides from the payload to the DB,
|
||||
so the client doesn't need a separate PUT for each. (title/short_description
|
||||
are NOT persisted here — they come from the deterministic build, not the payload.)"""
|
||||
client, conn = env
|
||||
conn.execute("INSERT INTO slabs(id,sku,length_in,thickness_in,sqft,bdft,width_min_in,width_max_in,width_avg_in,species_id,wood_category_ids,figure_term_ids,price,price_source,title,short_description,description,content_source,status,server_rev,client_rev,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
("s1","SLAB-01",114.0,1.75,30.0,18.4,18.0,24.3,24.3,1,json.dumps([2]),json.dumps([1]),None,None,None,None,"A slab","manual","calibrated",1,1,"t","t"))
|
||||
conn.commit()
|
||||
# Review with owner overrides for price + sku + title + short_description.
|
||||
# Review with owner overrides for price + sku.
|
||||
r = client.post("/api/v1/slabs/s1/review", json={
|
||||
"price": 250.0,
|
||||
"sku": "SLAB-99",
|
||||
"title": "Owner Title",
|
||||
"short_description": "Owner Short Desc",
|
||||
})
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
@@ -246,7 +192,5 @@ def test_review_persists_owner_overrides(env):
|
||||
got = client.get("/api/v1/slabs/s1").json()
|
||||
assert got["price"] == 250.0
|
||||
assert got["sku"] == "SLAB-99"
|
||||
assert got["title"] == "Owner Title"
|
||||
assert got["short_description"] == "Owner Short Desc"
|
||||
# server_rev was incremented (the UPDATE fired).
|
||||
assert got["server_rev"] == 2
|
||||
|
||||
Reference in New Issue
Block a user