diff --git a/use-cases/priyansh-0304/apparel-techpack-generator/.env.example b/use-cases/priyansh-0304/apparel-techpack-generator/.env.example new file mode 100644 index 0000000..a10939d --- /dev/null +++ b/use-cases/priyansh-0304/apparel-techpack-generator/.env.example @@ -0,0 +1 @@ +SUPERDOCS_API_KEY=your_key_here diff --git a/use-cases/priyansh-0304/apparel-techpack-generator/.gitignore b/use-cases/priyansh-0304/apparel-techpack-generator/.gitignore new file mode 100644 index 0000000..cdce095 --- /dev/null +++ b/use-cases/priyansh-0304/apparel-techpack-generator/.gitignore @@ -0,0 +1,14 @@ +# Environment +.env +venv/ + +# Python +__pycache__/ +*.pyc +.pytest_cache/ + +# Output +output/ + +# OS +.DS_Store \ No newline at end of file diff --git a/use-cases/priyansh-0304/apparel-techpack-generator/README.md b/use-cases/priyansh-0304/apparel-techpack-generator/README.md new file mode 100644 index 0000000..673b46a --- /dev/null +++ b/use-cases/priyansh-0304/apparel-techpack-generator/README.md @@ -0,0 +1,88 @@ +# Apparel Technical Pack Generator + +Generates a factory-ready technical pack (graded measurement spec, bill of +materials, construction notes, flat sketches) for an apparel style, built on +the SuperDocs API. + +## What it does + +1. Loads a style definition (`styles/*.json`) containing points of measure, + grade rules, materials, and construction notes. +2. **Grades every measurement point deterministically in code** — the AI + never computes sizing math. `techpack/grading.py` applies + `value = base_value + grade_rule × steps_from_base_size` per point, so + correctness across sizes is guaranteed by arithmetic, not by trusting a + model. +3. Builds an initial HTML document with the graded table already baked in, + sends it to SuperDocs for formatting polish and AI-generated flat + sketches, and **re-verifies the measurement table against the source + data after every edit** (`techpack/verification.py`) — the pipeline + never trusts a chat response's own claim that numbers are unchanged. +4. Exports the final result as a `.docx` via SuperDocs' export endpoint. + +## Setup + + python3 -m venv venv + source venv/bin/activate + pip install -r requirements.txt -r requirements-dev.txt + cp .env.example .env # add your SUPERDOCS_API_KEY + +## Run + + python3 main.py + +Output lands in `output/_techpack.docx`. + +## Tests + + python3 -m pytest tests/ -v + +17 tests, all mocked — grading correctness, JSON-string-in-JSON response +parsing, and table-verification logic, none requiring a live API key. + +## Design decisions & assumptions (logged as I went) + +- **Grading is code, not AI.** The task brief notes that where a build + handles figures it can't independently verify, it's graded on whether it + *detects and surfaces* problems, not whether numbers happen to be right. + Rather than ask the model to grade sizes, the math is deterministic and + the AI is only used for formatting and imagery — removing the risk + entirely rather than trying to catch it after the fact. +- **`export` endpoint path was undocumented for the Bearer-key API.** + Found the real payload shape (`POST /v1/documents/export`, full HTML in + the body, raw file bytes back) by inspecting the web app's own network + traffic in DevTools. Confirmed working from `api.superdocs.app` with a + Bearer key in this build. +- **`approve`/`pending_changes` never triggered in testing.** Every edit + sent during development — including a full 6-section rewrite — came back + `auto_approved`. `approve_changes()` is implemented and wired into + `main.py`'s flow but is unverified against a live response, since no + instruction sent during this task ever produced `requires_approval: true`. + Logged as an open gap rather than assumed correct. +- **Sessions are stateful and persist edits.** Reusing a `session_id` + continues editing the same in-progress document; a repeated, + already-satisfied instruction correctly returns a no-op + (`document_changes: null`) rather than reapplying itself. + +## Known bugs found while building + +- A tested edit's `response` field claimed a formatting-only change while + the returned HTML contained an entirely fabricated, unrelated + "Operational Correspondence" letter with invented reference numbers and + unfilled placeholder fields — reported as success despite being wrong. +- Requesting a single front/back flat sketch pair sometimes inserted two + duplicate pairs into the document instead of one. +- An early response's `response` field returned a generic "wasn't able to + put together a safe reply" fallback message despite the underlying edit + succeeding correctly. + +## Project structure + + styles/ style definitions (POMs, materials, construction) + techpack/ + grading.py deterministic size grading + generator.py builds initial document HTML + superdocs_client.py API wrapper (chat, approve, export) + verification.py post-edit correctness checks + tests/ pytest suite, no live key required + main.py orchestrates the full pipeline \ No newline at end of file diff --git a/use-cases/priyansh-0304/apparel-techpack-generator/main.py b/use-cases/priyansh-0304/apparel-techpack-generator/main.py new file mode 100644 index 0000000..7476817 --- /dev/null +++ b/use-cases/priyansh-0304/apparel-techpack-generator/main.py @@ -0,0 +1,93 @@ +import json +import time +from pathlib import Path + +from dotenv import load_dotenv +from techpack.generator import generate_tech_pack_html +from techpack.superdocs_client import send_edit, approve_changes, export_document, SuperDocsError +from techpack.verification import verify_measurement_table + +load_dotenv() + +STYLE_PATH = Path("styles/classic_crew_tee.json") +OUTPUT_DIR = Path("output") + + +def load_style(path: Path) -> dict: + with open(path) as f: + return json.load(f) + + +def get_updated_html(result: dict, fallback_html: str) -> str: + """Returns the new HTML if the edit produced changes, otherwise falls + back to whatever HTML we already had — a no-op is not an error.""" + changes = result.get("document_changes") or {} + updated = changes.get("updated_html") + if updated: + return updated + print("ℹ️ No changes returned (edit was a no-op) — keeping prior HTML state.") + return fallback_html + + +def main(): + style = load_style(STYLE_PATH) + session_id = f"techpack-{style['style_id']}-{int(time.time())}" + + html = generate_tech_pack_html(style) + print(f"Generated HTML for {style['style_name']} ({len(html)} chars)") + + result = send_edit( + session_id=session_id, + message="Format this document cleanly with a professional letterhead style. Do not change any numbers in the measurement table.", + document_html=html, + ) + print("Chat response:", result.get("response")) + + latest_html = get_updated_html(result, html) + is_valid, mismatches = verify_measurement_table(latest_html, style) + print("✅ Table verified after formatting" if is_valid else f"❌ Table mismatch after formatting: {mismatches}") + + result = send_edit( + session_id=session_id, + message=( + "Generate a front-view and back-view black and white technical flat " + "sketch of a classic crew neck t-shirt and insert them at the top of " + "the document, above the measurement table." + ), + ) + print("Chat response:", result.get("response")) + + latest_html = get_updated_html(result, latest_html) + is_valid, mismatches = verify_measurement_table(latest_html, style) + print("✅ Table still verified after image insertion" if is_valid else f"❌ Table mismatch after images: {mismatches}") + + if " tag found in returned HTML — images may not have actually been inserted despite the success message.") + + changes = result.get("document_changes") or {} + version_id = changes.get("version_id") + + if changes.get("requires_approval") or changes.get("pending_changes"): + if not version_id: + print("⚠️ Approval required but no version_id was returned — cannot approve. Skipping approval step.") + else: + try: + approve_changes(version_id=version_id, approve_all=True) + print("Changes approved.") + except SuperDocsError as e: + print(f"Approve step failed: {e}") + else: + print("No approval required — changes were auto-approved.") + + OUTPUT_DIR.mkdir(exist_ok=True) + try: + file_bytes = export_document(html=latest_html, filename=style["style_id"]) + out_path = OUTPUT_DIR / f"{style['style_id']}_techpack.docx" + out_path.write_bytes(file_bytes) + print(f"Exported to {out_path}") + except SuperDocsError as e: + print(f"Export step failed: {e}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/use-cases/priyansh-0304/apparel-techpack-generator/requirements-dev.txt b/use-cases/priyansh-0304/apparel-techpack-generator/requirements-dev.txt new file mode 100644 index 0000000..d9c92db --- /dev/null +++ b/use-cases/priyansh-0304/apparel-techpack-generator/requirements-dev.txt @@ -0,0 +1 @@ +pytest>=8.0.0 \ No newline at end of file diff --git a/use-cases/priyansh-0304/apparel-techpack-generator/requirements.txt b/use-cases/priyansh-0304/apparel-techpack-generator/requirements.txt new file mode 100644 index 0000000..dff5783 --- /dev/null +++ b/use-cases/priyansh-0304/apparel-techpack-generator/requirements.txt @@ -0,0 +1,2 @@ +requests>=2.31.0 +python-dotenv>=1.0.0 \ No newline at end of file diff --git a/use-cases/priyansh-0304/apparel-techpack-generator/styles/classic_crew_tee.json b/use-cases/priyansh-0304/apparel-techpack-generator/styles/classic_crew_tee.json new file mode 100644 index 0000000..09785e7 --- /dev/null +++ b/use-cases/priyansh-0304/apparel-techpack-generator/styles/classic_crew_tee.json @@ -0,0 +1,30 @@ +{ + "style_id": "TEE-001", + "style_name": "Classic Crew Neck Tee", + "base_size": "M", + "size_range": ["XS", "S", "M", "L", "XL", "XXL"], + "unit": "inches", + "measurement_points": [ + {"point_name": "Chest Width", "measured_at": "1 in below armhole, laid flat", "base_value": 20.0, "grade_rule": 1.0, "tolerance": 0.5}, + {"point_name": "Body Length", "measured_at": "HPS to hem", "base_value": 28.0, "grade_rule": 0.75, "tolerance": 0.5}, + {"point_name": "Shoulder Width", "measured_at": "seam to seam", "base_value": 18.0, "grade_rule": 0.5, "tolerance": 0.25}, + {"point_name": "Sleeve Length", "measured_at": "shoulder seam to cuff", "base_value": 8.5, "grade_rule": 0.375, "tolerance": 0.25}, + {"point_name": "Armhole Depth", "measured_at": "straight, shoulder to underarm", "base_value": 9.5, "grade_rule": 0.375, "tolerance": 0.25}, + {"point_name": "Neck Width", "measured_at": "seam to seam", "base_value": 7.0, "grade_rule": 0.125, "tolerance": 0.125}, + {"point_name": "Sleeve Opening", "measured_at": "flat, hemmed edge", "base_value": 7.0, "grade_rule": 0.25, "tolerance": 0.125}, + {"point_name": "Hem Width", "measured_at": "flat, bottom edge", "base_value": 20.0, "grade_rule": 1.0, "tolerance": 0.5} + ], + "materials": [ + {"component": "Main fabric", "spec": "100% combed cotton, single jersey knit, 180 GSM", "placement": "Body panels, sleeves"}, + {"component": "Neck ribbing", "spec": "1x1 ribbed cotton, self-fabric, 2cm finished height", "placement": "Neckline"}, + {"component": "Main label", "spec": "Woven polyester, 3x4cm", "placement": "Center back neck"}, + {"component": "Thread", "spec": "100% polyester, tex 27, color-matched", "placement": "All seams"} + ], + "construction_notes": [ + "Side-seam construction, not tubular", + "Shoulder seams taped to prevent stretch-out", + "Neckband: 1x1 self-fabric rib, 3-thread overlock attach, 2-needle coverstitch topstitch", + "Hem: double-needle coverstitch, 1cm width", + "Stitch density: 10-12 SPI on main seams" + ] +} \ No newline at end of file diff --git a/use-cases/priyansh-0304/apparel-techpack-generator/techpack/__init__.py b/use-cases/priyansh-0304/apparel-techpack-generator/techpack/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/use-cases/priyansh-0304/apparel-techpack-generator/techpack/generator.py b/use-cases/priyansh-0304/apparel-techpack-generator/techpack/generator.py new file mode 100644 index 0000000..9281e02 --- /dev/null +++ b/use-cases/priyansh-0304/apparel-techpack-generator/techpack/generator.py @@ -0,0 +1,41 @@ +"""Builds the tech pack HTML from a style dict. All measurement values here +are already-graded, code-computed numbers — no AI involvement in the math.""" + +from techpack.grading import build_graded_spec + + +def generate_tech_pack_html(style: dict) -> str: + graded = build_graded_spec(style) + sizes = style["size_range"] + + row_parts = [] + for point in style["measurement_points"]: + cells = "".join(f"{graded[point['point_name']][s]}\"" for s in sizes) + row_parts.append(f"{point['point_name']}{cells}±{point['tolerance']}\"") + rows = "".join(row_parts) + + bom_rows = "".join( + f"{m['component']}{m['spec']}{m['placement']}" + for m in style["materials"] + ) + construction_list = "".join(f"
  • {note}
  • " for note in style["construction_notes"]) + + return f""" +

    {style['style_name']} — Technical Pack

    +

    Style ID: {style['style_id']} | Base Size: {style['base_size']}

    + +

    Graded Measurement Specification

    + + {"".join(f"" for s in sizes)} + {rows} +
    Point of Measure{s}Tolerance
    + +

    Bill of Materials

    + + + {bom_rows} +
    ComponentSpecPlacement
    + +

    Construction Notes

    + + """ \ No newline at end of file diff --git a/use-cases/priyansh-0304/apparel-techpack-generator/techpack/grading.py b/use-cases/priyansh-0304/apparel-techpack-generator/techpack/grading.py new file mode 100644 index 0000000..70dcfd0 --- /dev/null +++ b/use-cases/priyansh-0304/apparel-techpack-generator/techpack/grading.py @@ -0,0 +1,28 @@ +"""Deterministic size grading — the AI never touches these numbers.""" + +SIZE_ORDER = ["XS", "S", "M", "L", "XL", "XXL"] + + +def grade_measurement(base_value: float, grade_rule: float, base_size: str, target_size: str) -> float: + try: + base_idx = SIZE_ORDER.index(base_size) + target_idx = SIZE_ORDER.index(target_size) + except ValueError as e: + raise ValueError( + f"Unsupported size in grading: {e}. Supported sizes are: {SIZE_ORDER}. " + f"Got base_size={base_size!r}, target_size={target_size!r}." + ) from e + steps = target_idx - base_idx + return round(base_value + (grade_rule * steps), 3) + + +def build_graded_spec(style: dict) -> dict: + """Returns {point_name: {size: value}} for every measurement point in the style.""" + base_size = style["base_size"] + table = {} + for point in style["measurement_points"]: + table[point["point_name"]] = { + size: grade_measurement(point["base_value"], point["grade_rule"], base_size, size) + for size in style["size_range"] + } + return table \ No newline at end of file diff --git a/use-cases/priyansh-0304/apparel-techpack-generator/techpack/superdocs_client.py b/use-cases/priyansh-0304/apparel-techpack-generator/techpack/superdocs_client.py new file mode 100644 index 0000000..9151def --- /dev/null +++ b/use-cases/priyansh-0304/apparel-techpack-generator/techpack/superdocs_client.py @@ -0,0 +1,118 @@ +import os +import json +import requests + +BASE_URL = "https://api.superdocs.app/v1" +REQUEST_TIMEOUT = 30 # seconds -- prevents an indefinite hang on network issues + + +class SuperDocsError(Exception): + """Raised when the SuperDocs API returns an error or unexpected shape.""" + pass + + +def _get_headers(): + api_key = os.environ.get("SUPERDOCS_API_KEY") + if not api_key: + raise SuperDocsError( + "SUPERDOCS_API_KEY not set. Run: export SUPERDOCS_API_KEY='sk_...' " + "or load it from a .env file before calling the client." + ) + return { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + } + + +def send_edit(session_id: str, message: str, document_html: str = None) -> dict: + """ + Send an edit instruction. If document_html is provided, it (re)initializes + the document for this session. Omit it on follow-up turns in the same + session to keep editing what's already there. + + Returns the parsed response dict. Raises SuperDocsError on failure. + """ + payload = {"session_id": session_id, "message": message} + if document_html is not None: + payload["document_html"] = document_html + + resp = requests.post(f"{BASE_URL}/chat", headers=_get_headers(), json=payload, timeout=REQUEST_TIMEOUT) + + if resp.status_code != 200: + raise SuperDocsError(f"chat call failed ({resp.status_code}): {resp.text}") + + data = resp.json() + + # Debug visibility gated behind an explicit env var -- printing full raw + # API responses by default risks leaking document contents into logs. + if data.get("document_changes") is None and os.environ.get("SUPERDOCS_DEBUG"): + print("⚠️ document_changes was null. Full raw response:") + print(json.dumps(data, indent=2)) + + changes = data.get("document_changes") or {} + for change in changes.get("changes") or []: + for key in ("old_html", "new_html"): + val = change.get(key) + if isinstance(val, str) and val.strip().startswith("{"): + try: + change[key] = json.loads(val) + except json.JSONDecodeError: + pass # it was just HTML that happened to start with '{', leave it + + return data + + +def approve_changes(version_id: str, change_ids: list[str] = None, approve_all: bool = True) -> dict: + """ + NOT YET CONFIRMED against live docs — path/shape is inferred from the task + doc's naming, not verified. Test this before depending on it. + """ + payload = {"version_id": version_id} + if approve_all: + payload["approve_all"] = True + else: + payload["change_ids"] = change_ids or [] + + resp = requests.post(f"{BASE_URL}/approve", headers=_get_headers(), json=payload, timeout=REQUEST_TIMEOUT) + + if resp.status_code == 404: + raise SuperDocsError( + "POST /v1/approve returned 404 — endpoint path is unconfirmed, " + "check docs.superdocs.app for the real name before proceeding." + ) + if resp.status_code != 200: + raise SuperDocsError(f"approve call failed ({resp.status_code}): {resp.text}") + + return resp.json() + + +def export_document(html: str, filename: str = "document", format: str = "docx") -> bytes: + """ + Confirmed shape (captured from the web app's own network traffic): + the export call takes the full current document HTML directly, not a + session_id. + """ + payload = { + "format": format, + "html": html, + "options": { + "paper_size": "Letter", + "orientation": "portrait", + "margins": "custom", + "custom_margins_inches": {"top": 1, "right": 1, "bottom": 1, "left": 1}, + "source_filename": filename, + }, + } + + resp = requests.post(f"{BASE_URL}/documents/export", headers=_get_headers(), json=payload, timeout=REQUEST_TIMEOUT) + + if resp.status_code == 404: + raise SuperDocsError( + "POST /v1/documents/export returned 404 on api.superdocs.app — " + "this path was captured from the web app's session-authenticated " + "internal API, may not exist identically on the Bearer-key API." + ) + if resp.status_code != 200: + raise SuperDocsError(f"export call failed ({resp.status_code}): {resp.text}") + + return resp.content \ No newline at end of file diff --git a/use-cases/priyansh-0304/apparel-techpack-generator/techpack/verification.py b/use-cases/priyansh-0304/apparel-techpack-generator/techpack/verification.py new file mode 100644 index 0000000..98458c1 --- /dev/null +++ b/use-cases/priyansh-0304/apparel-techpack-generator/techpack/verification.py @@ -0,0 +1,77 @@ +"""Verifies that measurement numbers survived an AI edit unchanged. +Never trust the model's own claim ('wording unchanged') — check the actual HTML.""" + +import re +from techpack.grading import build_graded_spec + + +def extract_measurement_table(html: str) -> list[list[str]]: + """Pulls raw cell text out of the first in the HTML (the measurement table).""" + table_match = re.search(r"(.*?)
    ", html, re.DOTALL) + if not table_match: + return [] + + table_html = table_match.group(1) + rows = re.findall(r"(.*?)", table_html, re.DOTALL) + + parsed_rows = [] + for row in rows: + cells = re.findall(r"(.*?)", row, re.DOTALL) + cleaned = [re.sub(r"<.*?>", "", c).strip() for c in cells] + parsed_rows.append(cleaned) + + return parsed_rows + + +def verify_measurement_table(html: str, style: dict) -> tuple[bool, list[str]]: + """ + Compares the measurement values actually present in the HTML against + the deterministically graded values. Returns (is_valid, list_of_mismatches). + """ + graded = build_graded_spec(style) + sizes = style["size_range"] + rows = extract_measurement_table(html) + + if not rows or len(rows) < 2: + return False, ["No measurement table found in HTML — cannot verify."] + + data_rows = rows[1:] # skip header row + expected_points = [p["point_name"] for p in style["measurement_points"]] + + mismatches = [] + found_points = {row[0] for row in data_rows if row} + + for point in expected_points: + if point not in found_points: + mismatches.append(f"MISSING: '{point}' row not found in table at all.") + + for row in data_rows: + if not row: + continue + point_name = row[0] + if point_name not in expected_points: + continue # some other row, e.g. from a merged/extra table — not our concern here + + for i, size in enumerate(sizes): + cell_index = i + 1 + if cell_index >= len(row): + mismatches.append(f"MISSING VALUE: '{point_name}' has no cell for size {size}") + continue + + expected_val = graded[point_name][size] + actual_cell = row[cell_index].replace('"', "").strip() + + try: + actual_val = float(actual_cell) + except ValueError: + mismatches.append( + f"UNPARSEABLE: '{point_name}' at {size} — got '{actual_cell}', expected {expected_val}" + ) + continue + + if abs(actual_val - expected_val) > 0.001: + mismatches.append( + f"MISMATCH: '{point_name}' at {size} — expected {expected_val}, got {actual_val}" + ) + + return len(mismatches) == 0, mismatches \ No newline at end of file diff --git a/use-cases/priyansh-0304/apparel-techpack-generator/tests/test_grading.py b/use-cases/priyansh-0304/apparel-techpack-generator/tests/test_grading.py new file mode 100644 index 0000000..aaf0faa --- /dev/null +++ b/use-cases/priyansh-0304/apparel-techpack-generator/tests/test_grading.py @@ -0,0 +1,72 @@ +"""Tests for deterministic size grading — the core correctness guarantee +of this build. No API involved; pure arithmetic.""" + +import pytest +from techpack.grading import grade_measurement, build_graded_spec, SIZE_ORDER + + +SAMPLE_STYLE = { + "base_size": "M", + "size_range": ["XS", "S", "M", "L", "XL", "XXL"], + "measurement_points": [ + {"point_name": "Chest Width", "base_value": 20.0, "grade_rule": 1.0, "tolerance": 0.5}, + {"point_name": "Neck Width", "base_value": 7.0, "grade_rule": 0.125, "tolerance": 0.125}, + ], +} + + +def test_base_size_returns_exact_base_value(): + # Grading at the base size should never drift from the source value + assert grade_measurement(20.0, 1.0, "M", "M") == 20.0 + + +def test_grades_up_correctly(): + # L is one step above base M -> +1 grade rule + assert grade_measurement(20.0, 1.0, "M", "L") == 21.0 + # XXL is three steps above M -> +3 grade rules + assert grade_measurement(20.0, 1.0, "M", "XXL") == 23.0 + + +def test_grades_down_correctly(): + # XS is two steps below base M -> -2 grade rules + assert grade_measurement(20.0, 1.0, "M", "XS") == 18.0 + + +def test_small_grade_rule_precision(): + # Neck width grades slowly (0.125"/size) — checks fractional grading works + assert grade_measurement(7.0, 0.125, "M", "L") == 7.125 + assert grade_measurement(7.0, 0.125, "M", "XS") == 6.75 + + +def test_zero_grade_rule_never_changes_across_sizes(): + # A point that shouldn't grade at all (grade_rule=0) must stay constant + for size in SIZE_ORDER: + assert grade_measurement(5.0, 0.0, "M", size) == 5.0 + + +def test_build_graded_spec_covers_every_point_and_size(): + table = build_graded_spec(SAMPLE_STYLE) + + assert set(table.keys()) == {"Chest Width", "Neck Width"} + for point_name in table: + assert set(table[point_name].keys()) == set(SAMPLE_STYLE["size_range"]) + + +def test_build_graded_spec_matches_manual_calculation(): + table = build_graded_spec(SAMPLE_STYLE) + + # Manually verify a few values independently of the grading function itself + assert table["Chest Width"]["M"] == 20.0 + assert table["Chest Width"]["XXL"] == 23.0 + assert table["Chest Width"]["XS"] == 18.0 + assert table["Neck Width"]["L"] == 7.125 + + +def test_different_base_size_still_grades_correctly(): + # Sanity check: grading logic isn't hardcoded to assume M is always base + style = {**SAMPLE_STYLE, "base_size": "S"} + table = build_graded_spec(style) + + # Now M is one step above the S base + assert table["Chest Width"]["S"] == 20.0 + assert table["Chest Width"]["M"] == 21.0 \ No newline at end of file diff --git a/use-cases/priyansh-0304/apparel-techpack-generator/tests/test_superdocs_client.py b/use-cases/priyansh-0304/apparel-techpack-generator/tests/test_superdocs_client.py new file mode 100644 index 0000000..fd75c1e --- /dev/null +++ b/use-cases/priyansh-0304/apparel-techpack-generator/tests/test_superdocs_client.py @@ -0,0 +1,88 @@ +"""Tests for the SuperDocs client wrapper — specifically the documented +JSON-string-in-JSON gotcha, using a mocked response so no live key or +network call is required.""" + +import json +from unittest.mock import patch, MagicMock + +import pytest +from techpack.superdocs_client import send_edit, SuperDocsError + + +def _mock_response(status_code=200, json_body=None): + mock = MagicMock() + mock.status_code = status_code + mock.json.return_value = json_body or {} + mock.text = json.dumps(json_body or {}) + return mock + + +@patch.dict("os.environ", {"SUPERDOCS_API_KEY": "sk_test_fake_key"}) +@patch("techpack.superdocs_client.requests.post") +def test_send_edit_parses_json_encoded_string_field(mock_post): + # Simulates the documented gotcha: old_html/new_html arriving as a + # JSON-encoded string that needs a second parse + nested_as_string = json.dumps({"text": "some nested content"}) + + mock_post.return_value = _mock_response(200, { + "response": "ok", + "session_id": "test", + "document_changes": { + "changes": [ + {"old_html": nested_as_string, "new_html": "

    plain html, not json

    "} + ] + }, + }) + + result = send_edit(session_id="test", message="do something") + parsed_old = result["document_changes"]["changes"][0]["old_html"] + + # It should have been auto-parsed from a string into a real dict + assert isinstance(parsed_old, dict) + assert parsed_old["text"] == "some nested content" + + +@patch.dict("os.environ", {"SUPERDOCS_API_KEY": "sk_test_fake_key"}) +@patch("techpack.superdocs_client.requests.post") +def test_send_edit_leaves_plain_html_untouched(mock_post): + # Plain HTML starting with '<' should never be mistakenly parsed as JSON + mock_post.return_value = _mock_response(200, { + "response": "ok", + "session_id": "test", + "document_changes": { + "changes": [{"old_html": "

    Hello

    ", "new_html": "

    Hi

    "}] + }, + }) + + result = send_edit(session_id="test", message="do something") + assert result["document_changes"]["changes"][0]["old_html"] == "

    Hello

    " + + +@patch.dict("os.environ", {"SUPERDOCS_API_KEY": "sk_test_fake_key"}) +@patch("techpack.superdocs_client.requests.post") +def test_send_edit_handles_null_document_changes(mock_post): + # Real behavior observed during development: a repeated, already-satisfied + # instruction returns document_changes: null rather than an error + mock_post.return_value = _mock_response(200, { + "response": "already done", + "session_id": "test", + "document_changes": None, + }) + + result = send_edit(session_id="test", message="do something") + assert result["document_changes"] is None # send_edit itself doesn't need to fix this — callers (get_updated_html) do + + +def test_send_edit_raises_without_api_key(): + with patch.dict("os.environ", {}, clear=True): + with pytest.raises(SuperDocsError, match="SUPERDOCS_API_KEY"): + send_edit(session_id="test", message="anything") + + +@patch.dict("os.environ", {"SUPERDOCS_API_KEY": "sk_test_fake_key"}) +@patch("techpack.superdocs_client.requests.post") +def test_send_edit_raises_on_error_status(mock_post): + mock_post.return_value = _mock_response(500, {"detail": "server error"}) + + with pytest.raises(SuperDocsError, match="500"): + send_edit(session_id="test", message="anything") \ No newline at end of file diff --git a/use-cases/priyansh-0304/apparel-techpack-generator/tests/test_verification.py b/use-cases/priyansh-0304/apparel-techpack-generator/tests/test_verification.py new file mode 100644 index 0000000..098d612 --- /dev/null +++ b/use-cases/priyansh-0304/apparel-techpack-generator/tests/test_verification.py @@ -0,0 +1,55 @@ +"""Tests for the post-edit verification step — confirms the pipeline can +actually detect a mismatch, not just report success optimistically.""" + +from techpack.verification import extract_measurement_table, verify_measurement_table + + +SAMPLE_STYLE = { + "base_size": "M", + "size_range": ["XS", "S", "M"], + "measurement_points": [ + {"point_name": "Chest Width", "base_value": 20.0, "grade_rule": 1.0, "tolerance": 0.5}, + ], +} + +CORRECT_HTML = """ + + + +
    Point of MeasureXSSMTolerance
    Chest Width18.0"19.0"20.0"±0.5"
    +""" + +WRONG_HTML = """ + + + +
    Point of MeasureXSSMTolerance
    Chest Width17.0"19.0"20.0"±0.5"
    +""" + +NO_TABLE_HTML = "

    Just a heading, no table at all

    " + + +def test_extract_measurement_table_parses_rows(): + rows = extract_measurement_table(CORRECT_HTML) + assert rows[0] == ["Point of Measure", "XS", "S", "M", "Tolerance"] + assert rows[1][0] == "Chest Width" + + +def test_verify_passes_on_correct_values(): + is_valid, mismatches = verify_measurement_table(CORRECT_HTML, SAMPLE_STYLE) + assert is_valid is True + assert mismatches == [] + + +def test_verify_catches_a_single_wrong_value(): + # This is the case that matters most: one AI-mangled number in an + # otherwise-correct table must not slip through silently + is_valid, mismatches = verify_measurement_table(WRONG_HTML, SAMPLE_STYLE) + assert is_valid is False + assert any("Chest Width" in m and "XS" in m for m in mismatches) + + +def test_verify_fails_gracefully_when_table_missing(): + is_valid, mismatches = verify_measurement_table(NO_TABLE_HTML, SAMPLE_STYLE) + assert is_valid is False + assert len(mismatches) > 0 \ No newline at end of file