From bb71b156020810d876d3604c7c414bc7b6570d12 Mon Sep 17 00:00:00 2001 From: quantamixsol Date: Sun, 2 Aug 2026 22:56:28 +0200 Subject: [PATCH 1/2] CR-DIST-06: guard that every referenced listing asset actually exists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit plugin.json declares a logo and screenshots by relative path, and nothing verified those paths resolved. A typo — or a screenshot referenced before the file was added — would ship a manifest pointing at nothing. A directory reviewer fetching a 404 image is a rejection, and it is invisible until somebody else looks. The guard allows `screenshots` to be EMPTY (assets not supplied yet) but requires every entry that IS listed to resolve. That distinction matters: it lets paths be wired ahead of the images without a false failure, while still catching the case the guard exists for. Proven by doing exactly that: I wired the three screenshot paths first, the guard failed on all three missing files, and I removed the entries again rather than ship a red test. The paths go back in when the images land. Also checks PNG magic bytes — a .png that is not a PNG fails silently in a listing. Mutation-tested: pointing a screenshot at a non-existent file makes the guard fail; restoring makes it pass. tests/test_packaging 61 passed, 4 skipped. assets/README.md records the exact three screenshots needed, their specs, and a pre-commit checklist (no keys, no client code, nothing from the CrawlQ side, no trade-secret thresholds) since these ship publicly and permanently. Co-Authored-By: Claude Opus 5 (1M context) --- plugins/codex/graqle/assets/README.md | 41 +++++++++ .../test_codex_plugin_assets.py | 90 +++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 plugins/codex/graqle/assets/README.md create mode 100644 tests/test_packaging/test_codex_plugin_assets.py diff --git a/plugins/codex/graqle/assets/README.md b/plugins/codex/graqle/assets/README.md new file mode 100644 index 0000000..72398b8 --- /dev/null +++ b/plugins/codex/graqle/assets/README.md @@ -0,0 +1,41 @@ +# Codex listing assets + +## Present + +- `graqle-logo-256.png` — the listing icon, referenced by `interface.logo`. + +## Still needed — three screenshots + +Drop these in **this directory** with **exactly these filenames**, then add them to +`interface.screenshots` in `../.codex-plugin/plugin.json`: + +| Filename | Must show | +|---|---| +| `screenshot-1-graph.png` | The knowledge graph rendered — nodes and connections. Hundreds of nodes, not a 5-node toy. Most visually distinctive asset we have. | +| `screenshot-2-reasoning.png` | A real `graq reason` answer **with the confidence score visible in frame**. Do not crop the score out — it is the differentiator. A 70–90% score is more credible than a perfect one. | +| `screenshot-3-studio.png` | A GraQle Studio view showing **graph or reasoning output** — not billing or account settings, which the plugin cannot do and would confuse a reviewer about what they are approving. | + +### Specifications + +- **PNG**, under 2 MB each +- **1280×800** preferred; 1440×900 or 1920×1200 fine — keep all three the **same aspect ratio** +- Text must stay legible when scaled to ~600px wide + +### ⚠️ Check before committing — these ship publicly and permanently + +- [ ] No API keys, tokens, licence keys or `.env` contents +- [ ] No client or employer code, module names, repo names or internal URLs +- [ ] No personal data — emails, real names in commit authorship +- [ ] Nothing from the **CrawlQ / TraceGov** side (product-separation rule) +- [ ] No trade-secret internals — weights, thresholds, `AGREEMENT_THRESHOLD`, calibration values +- [ ] No local filesystem paths revealing machine or directory structure + +**Use a public open-source repo as the demo subject.** Safe by construction, and it gives +a reviewer something recognisable. + +### Why `screenshots` is currently `[]` + +`tests/test_packaging/test_codex_plugin_assets.py` asserts that every referenced asset +resolves to a real file. The paths were wired ahead of the images and the guard correctly +failed, so they were removed again — an empty list is allowed, a dangling reference is +not. **Add the files first, then the entries**, and the guard will confirm both. diff --git a/tests/test_packaging/test_codex_plugin_assets.py b/tests/test_packaging/test_codex_plugin_assets.py new file mode 100644 index 0000000..7c9e17e --- /dev/null +++ b/tests/test_packaging/test_codex_plugin_assets.py @@ -0,0 +1,90 @@ +"""CR-DIST-06: every asset a directory listing references must actually exist. + +`plugin.json` declares a logo and screenshots by relative path. Nothing verified those +paths resolved, so a typo — or a screenshot referenced before the file was added — would +ship a manifest pointing at nothing. A directory reviewer fetching a 404 image is a +rejection, and it is invisible until someone else looks. + +`screenshots` is allowed to be EMPTY (assets not supplied yet) but every entry that IS +listed must resolve to a real file. That distinction is deliberate: it lets the paths be +wired ahead of the images without the guard producing a false failure, while still +catching the case the guard exists for. +""" + +from __future__ import annotations + +import json +import pathlib + +import pytest + +ROOT = pathlib.Path(__file__).resolve().parents[2] + +PLUGINS = [ + "plugins/codex/graqle/.codex-plugin/plugin.json", + "plugins/claude-code/graqle/.claude-plugin/plugin.json", +] + + +def _manifests(): + for rel in PLUGINS: + p = ROOT / rel + if p.exists(): + yield rel, p, json.loads(p.read_text(encoding="utf-8")) + + +@pytest.mark.parametrize("rel", PLUGINS) +def test_manifest_parses(rel): + path = ROOT / rel + if not path.exists(): + pytest.skip(f"{rel} not present") + json.loads(path.read_text(encoding="utf-8")) + + +def test_referenced_assets_exist(): + """Logo and every listed screenshot must resolve, relative to the plugin dir.""" + checked = 0 + for rel, path, data in _manifests(): + base = path.parent.parent # .../graqle/ — assets/ lives beside .codex-plugin/ + iface = data.get("interface", {}) + + logo = iface.get("logo") + if logo: + assert (base / logo).exists(), ( + f"{rel}: interface.logo -> {logo!r} does not exist at {base / logo}. " + "A directory reviewer would fetch a 404." + ) + checked += 1 + + for shot in iface.get("screenshots", []): + # Accept either a bare path or an object carrying one, so the guard keeps + # working if the schema shape changes. + candidate = shot if isinstance(shot, str) else shot.get("path", "") + assert candidate, f"{rel}: a screenshot entry has no path: {shot!r}" + assert (base / candidate).exists(), ( + f"{rel}: screenshot -> {candidate!r} is referenced but missing at " + f"{base / candidate}. Add the file or remove the entry — never ship a " + "manifest pointing at an image that does not exist." + ) + checked += 1 + + assert checked > 0, "no assets were checked — the manifests may have moved" + + +def test_screenshot_files_are_real_pngs(): + """A .png that is not a PNG fails silently in a directory listing.""" + png_magic = b"\x89PNG\r\n\x1a\n" + for rel, path, data in _manifests(): + base = path.parent.parent + iface = data.get("interface", {}) + refs = [iface["logo"]] if iface.get("logo") else [] + refs += [s if isinstance(s, str) else s.get("path", "") + for s in iface.get("screenshots", [])] + + for ref in refs: + f = base / ref + if not f.exists() or not ref.lower().endswith(".png"): + continue + assert f.read_bytes()[:8] == png_magic, ( + f"{rel}: {ref} has a .png extension but is not a PNG file." + ) From b9515ef8fafe2dfc4787f096fa7a899631f6ae8c Mon Sep 17 00:00:00 2001 From: quantamixsol Date: Sun, 2 Aug 2026 22:59:28 +0200 Subject: [PATCH 2/2] Sentinel fixes: skip on absent manifests, add an armed pre-submission gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sentinel BLOCKED pass 1 on one defect and raised two advisories. All addressed. BLOCKER — `assert checked > 0` failed in a legitimate environment. A sparse checkout without plugins/ is not a defect, but the assert turned it into a cryptic red CI with no diagnostic. Now pytest.skip with a message saying exactly what was not found. A test that fails for environmental reasons rather than code defects is a bad test. ADVISORY 1 (base-path depth) — VERIFIED, no change needed. Both manifests sit at plugins//graqle/.-plugin/plugin.json with assets/ beside the plugin dir, so path.parent.parent is correct for both. Added a comment recording the expected structure so the next reader does not have to re-derive it. ADVISORY 2 (empty screenshots could reach submission unnoticed) — this was the real gap allowing screenshots:[] creates. Added test_screenshots_populated_before_submission, SKIPPED by default and armed with GRAQLE_SUBMISSION_CHECK=1 immediately before packaging a submission. Not always-on deliberately: failing today would make the suite red for an asset still being produced. ADVISORY 3 (guessed schema) — accepted, no code change. plugin.json keeps screenshots:[] rather than shipping a guessed entry shape; the guard already accepts both plain-string and object forms, so it survives either. Verified in all three states: default 4 passed/1 skipped; armed, the gate correctly FAILS on the empty list; with no plugins/ present, it skips instead of failing. Co-Authored-By: Claude Opus 5 (1M context) --- .../test_codex_plugin_assets.py | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/tests/test_packaging/test_codex_plugin_assets.py b/tests/test_packaging/test_codex_plugin_assets.py index 7c9e17e..26877d8 100644 --- a/tests/test_packaging/test_codex_plugin_assets.py +++ b/tests/test_packaging/test_codex_plugin_assets.py @@ -14,6 +14,7 @@ from __future__ import annotations import json +import os import pathlib import pytest @@ -45,7 +46,11 @@ def test_referenced_assets_exist(): """Logo and every listed screenshot must resolve, relative to the plugin dir.""" checked = 0 for rel, path, data in _manifests(): - base = path.parent.parent # .../graqle/ — assets/ lives beside .codex-plugin/ + # Structure (verified identical for both plugins): + # plugins//graqle/.-plugin/plugin.json + # plugins//graqle/assets/... + # so the asset root is two levels up from the manifest. + base = path.parent.parent iface = data.get("interface", {}) logo = iface.get("logo") @@ -68,7 +73,35 @@ def test_referenced_assets_exist(): ) checked += 1 - assert checked > 0, "no assets were checked — the manifests may have moved" + if checked == 0: + # SKIP, not fail. A sparse checkout without plugins/ is a legitimate + # environment, not a defect, and failing there produces a cryptic red CI for a + # reason unrelated to the code. Skipping keeps the signal honest: this test + # reports on assets it can see, and says plainly when it can see none. + pytest.skip("no plugin manifests found — plugins/ not checked out") + + +def test_screenshots_populated_before_submission(): + """Guards the gap that allowing ``screenshots: []`` creates. + + An empty list is correct during development, but a submission with zero screenshots + would sail past every other check here and only be caught by a human reviewer — + i.e. a rejection. This test is SKIPPED by default and armed by setting + ``GRAQLE_SUBMISSION_CHECK=1`` immediately before packaging a directory submission. + + It is deliberately not always-on: failing it today would make the whole suite red + for an asset that is legitimately still being produced. + """ + if not os.environ.get("GRAQLE_SUBMISSION_CHECK"): + pytest.skip("set GRAQLE_SUBMISSION_CHECK=1 to arm the pre-submission gate") + + for rel, _path, data in _manifests(): + shots = data.get("interface", {}).get("screenshots", []) + assert shots, ( + f"{rel}: interface.screenshots is empty. A directory submission needs " + "screenshots — see plugins/codex/graqle/assets/README.md for the three " + "required images and their specs." + ) def test_screenshot_files_are_real_pngs():