4e91f4e754
* feat(resolve): category-peer subreddit map for Step 0.55 Introduces scripts/lib/categories.py with a curated category->peer-subs map and wires scripts/lib/resolve.py auto_resolve() to merge peers into the WebSearch-extracted subreddit list. Named 2026-04-22 failure mode: a "Prompting GPT Image 2" run resolved only r/OpenAI + r/ChatGPT and missed r/StableDiffusion, r/midjourney, r/dalle2, r/aiArt where prompting techniques actually live. Map is static, curated, ~11 categories (ai_image_generation, ai_video_generation, ai_music_generation, ai_coding_agent, ai_agent_framework, ai_chat_model, saas_screen_recording, saas_productivity, prediction_markets, crypto_defi, dev_tool_cli). First-match-wins ordering from most-specific to least-specific. Compound-term patterns only (no bare common nouns like "image", "ai"). auto_resolve now: - calls detect_category(topic) after _extract_subreddits - merges peer_subs case-insensitively, caps at MAX_SUBS (10) - preserves every WebSearch-returned sub (freshest signal) - emits [Resolve] Matched category=<id>, adding peers: <list> on stderr only when peers were actually added - returns new "category" key in the result dict for observability - wraps classifier in try/except so failures degrade to unwidened list Includes drive-by: test_full_resolve / test_partial_failure searches_run expectations bumped from 3->4 / 2->3 to match the current queries dict (subreddit + news + x_handle + github). * feat(skill): Step 0.55 category-peer expansion and self-check Adds Section 2a (category-peer expansion, MANDATORY for product topics) and the Step 0.55 self-check checkpoint that fires immediately before the Resolved block displays. Structural mirror of the engine-side categories.py map: same categories, same peer subs, same priority order. The model-side path now: - Applies category-peer expansion to the WebSearch-resolved subs on every product-in-a-known-category run. - Emits the (+ <category_id> peers) annotation on the Reddit line of the Resolved block as the observable contract. Absence on a product-in-a-known-category topic is a Step 0.55 regression. - Runs a self-check before emitting Resolved: "does the resolved list include at least 2 peer subs for the matched category? if not, widen NOW and do not run the engine yet." Mirror of the Python map lives inside Step 0.55 as a table for the model to pattern-match against; extrapolation to unlisted categories is explicitly allowed. Worked example (the exact failing query) appears below the table so reviewers can see before/after at a glance. Both changes land inside the existing Step 0.55 block. No new top-level section, no new LAW. LAWs 1-6 wording unchanged. * test: end-to-end regression for GPT Image 2 failure mode Stubs grounding.web_search to return the OpenAI-only subs that caused the 2026-04-22 failure, then asserts that auto_resolve widens to include the image-gen peers and emits the [Resolve] Matched category=ai_image_generation stderr line. Covers the cap boundary and the uncategorized-topic no-op path. Fixture tests/fixtures/prompting-gpt-image-2-resolved-block.md is documentation-grade (not parsed by tests) and shows the pre-fix vs post-fix Resolved block shape so reviewers can evaluate future categories.py edits against the original bug. --------- Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
146 lines
4.9 KiB
Python
146 lines
4.9 KiB
Python
"""End-to-end regression test for the 2026-04-22 `Prompting GPT Image 2` bug.
|
|
|
|
Guards the failing run's Resolved-block shape end-to-end: stubs
|
|
`grounding.web_search` to return the OpenAI-only subs that caused the
|
|
original failure, then asserts that `auto_resolve` now returns the widened
|
|
list and emits the expected stderr trace.
|
|
|
|
If this test starts failing after a `scripts/lib/categories.py` edit, either
|
|
the fix regressed or the map intentionally dropped the `ai_image_generation`
|
|
category — update the test deliberately.
|
|
|
|
Fixture reference: `tests/fixtures/prompting-gpt-image-2-resolved-block.md`.
|
|
"""
|
|
|
|
import io
|
|
import sys
|
|
import unittest
|
|
from contextlib import redirect_stderr
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
|
|
|
|
from lib import resolve
|
|
|
|
|
|
OPENAI_BRAND_SUBREDDIT_RESULTS = [
|
|
{
|
|
"title": "r/OpenAI community hub",
|
|
"snippet": "Discussion at r/ChatGPT and r/singularity about GPT Image 2.",
|
|
"url": "https://reddit.com/r/OpenAI/",
|
|
},
|
|
{
|
|
"title": "r/ChatGPTpromptengineering prompt collection",
|
|
"snippet": "Also see r/artificial for broader AI chatter.",
|
|
"url": "",
|
|
},
|
|
]
|
|
|
|
EMPTY_RESULTS: list[dict] = []
|
|
|
|
|
|
def _fake_websearch(label_to_items: dict[str, list[dict]]):
|
|
def _search(query, date_range, config):
|
|
if "subreddit" in query:
|
|
return label_to_items.get("subreddit", EMPTY_RESULTS), {}
|
|
if "news" in query:
|
|
return label_to_items.get("news", EMPTY_RESULTS), {}
|
|
if "handle" in query:
|
|
return label_to_items.get("x_handle", EMPTY_RESULTS), {}
|
|
if "github" in query:
|
|
return label_to_items.get("github", EMPTY_RESULTS), {}
|
|
return EMPTY_RESULTS, {}
|
|
|
|
return _search
|
|
|
|
|
|
class PromptingGptImage2RegressionGuard(unittest.TestCase):
|
|
"""The named 2026-04-22 failure mode. Resolved block must include peers."""
|
|
|
|
@patch("lib.resolve.grounding.web_search")
|
|
def test_auto_resolve_widens_to_image_gen_peers(self, mock_search):
|
|
mock_search.side_effect = _fake_websearch({
|
|
"subreddit": OPENAI_BRAND_SUBREDDIT_RESULTS,
|
|
})
|
|
|
|
result = resolve.auto_resolve(
|
|
"Prompting GPT Image 2",
|
|
{"BRAVE_API_KEY": "fake"},
|
|
)
|
|
|
|
subs_lower = [s.lower() for s in result["subreddits"]]
|
|
|
|
# Original WebSearch-returned brand subs preserved
|
|
self.assertIn("openai", subs_lower)
|
|
self.assertIn("chatgpt", subs_lower)
|
|
self.assertIn("singularity", subs_lower)
|
|
|
|
# At least three of the image-gen peers were added
|
|
expected_peers = {"stablediffusion", "midjourney", "dalle2", "aiart", "promptengineering"}
|
|
found_peers = expected_peers.intersection(subs_lower)
|
|
self.assertGreaterEqual(
|
|
len(found_peers),
|
|
3,
|
|
f"Expected at least 3 image-gen peer subs, found: {found_peers}. "
|
|
f"Actual subs: {result['subreddits']}",
|
|
)
|
|
|
|
self.assertEqual(result["category"], "ai_image_generation")
|
|
|
|
@patch("lib.resolve.grounding.web_search")
|
|
def test_stderr_contains_category_match_log_line(self, mock_search):
|
|
mock_search.side_effect = _fake_websearch({
|
|
"subreddit": OPENAI_BRAND_SUBREDDIT_RESULTS,
|
|
})
|
|
|
|
buf = io.StringIO()
|
|
with redirect_stderr(buf):
|
|
resolve.auto_resolve(
|
|
"Prompting GPT Image 2",
|
|
{"BRAVE_API_KEY": "fake"},
|
|
)
|
|
|
|
self.assertIn("Matched category=ai_image_generation", buf.getvalue())
|
|
|
|
@patch("lib.resolve.grounding.web_search")
|
|
def test_cap_enforced_end_to_end(self, mock_search):
|
|
# Synthesize a subreddit response with 9 brand subs
|
|
many_subs_items = [
|
|
{"title": f"r/Brand{i}", "snippet": "", "url": ""}
|
|
for i in range(9)
|
|
]
|
|
mock_search.side_effect = _fake_websearch({
|
|
"subreddit": many_subs_items,
|
|
})
|
|
|
|
result = resolve.auto_resolve(
|
|
"Prompting GPT Image 2",
|
|
{"BRAVE_API_KEY": "fake"},
|
|
)
|
|
|
|
self.assertLessEqual(len(result["subreddits"]), resolve.MAX_SUBS)
|
|
# The first WebSearch sub is still present (brand subs never evicted)
|
|
self.assertIn("Brand0", result["subreddits"])
|
|
|
|
@patch("lib.resolve.grounding.web_search")
|
|
def test_uncategorized_topic_does_not_inject_peers(self, mock_search):
|
|
mock_search.side_effect = _fake_websearch({
|
|
"subreddit": [{"title": "r/Kanye is wild", "snippet": "", "url": ""}],
|
|
})
|
|
|
|
buf = io.StringIO()
|
|
with redirect_stderr(buf):
|
|
result = resolve.auto_resolve(
|
|
"Kanye West latest album",
|
|
{"BRAVE_API_KEY": "fake"},
|
|
)
|
|
|
|
self.assertEqual(result["subreddits"], ["Kanye"])
|
|
self.assertIsNone(result["category"])
|
|
self.assertNotIn("Matched category=", buf.getvalue())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|