enigma-api-client 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. enigma_api_client/__init__.py +16 -0
  2. enigma_api_client/client.py +134 -0
  3. enigma_api_client/client_inject_id_and_typename_helper.py +248 -0
  4. enigma_api_client/client_inject_urls_helper.py +26 -0
  5. enigma_api_client/plugins/.DS_Store +0 -0
  6. enigma_api_client/plugins/enigma-api/.claude-plugin/plugin.json +8 -0
  7. enigma_api_client/plugins/enigma-api/README.md +82 -0
  8. enigma_api_client/plugins/enigma-api/harness/conftest.py +42 -0
  9. enigma_api_client/plugins/enigma-api/harness/test_linter.py +863 -0
  10. enigma_api_client/plugins/enigma-api/harness/test_queries.py +143 -0
  11. enigma_api_client/plugins/enigma-api/harness/test_skill_rules.py +351 -0
  12. enigma_api_client/plugins/enigma-api/pyproject.toml +9 -0
  13. enigma_api_client/plugins/enigma-api/scripts/__pycache__/lint_query.cpython-313.pyc +0 -0
  14. enigma_api_client/plugins/enigma-api/scripts/__pycache__/query_validator.cpython-313.pyc +0 -0
  15. enigma_api_client/plugins/enigma-api/scripts/__pycache__/query_validator.cpython-314.pyc +0 -0
  16. enigma_api_client/plugins/enigma-api/scripts/generate_report.py +384 -0
  17. enigma_api_client/plugins/enigma-api/scripts/lint_query.py +712 -0
  18. enigma_api_client/plugins/enigma-api/scripts/query_validator.py +352 -0
  19. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/SKILL.md +620 -0
  20. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/brand/03_brand_profile.md +163 -0
  21. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/brand/13_brand_dump.md +95 -0
  22. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/derived/07_coalesce_ein_formation.md +146 -0
  23. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/discovery/01_coffee_shops_zip.md +287 -0
  24. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/discovery/11_ol_industry_discovery.md +116 -0
  25. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/export/08_segment_export.md +96 -0
  26. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/kyb/02_officers_lookup.md +366 -0
  27. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/kyb/10_kyb_verify.md +176 -0
  28. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/legal_entity/09_legal_entity_profile.md +97 -0
  29. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/locations/05_brand_locations_by_state.md +120 -0
  30. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/person_search/04_find_person_companies.md +160 -0
  31. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/revenue/06_brand_revenue.md +176 -0
  32. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/risk/12_risk_signals.md +118 -0
  33. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/references/card_transactions.md +91 -0
  34. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/references/field_corrections.json +106 -0
  35. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/references/filter_syntax.md +203 -0
  36. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/references/naics_2022_mapping.json +184 -0
  37. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/references/pagination_template.py +115 -0
  38. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/schema.graphql +44971 -0
  39. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/scripts/test_examples.py +1122 -0
  40. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/scripts/validate_query.py +177 -0
  41. enigma_api_client/plugins/enigma-api/test-cases/graphql-test-cases.md +196 -0
  42. enigma_api_client/plugins/enigma-api/tests/accuracy_cases.csv +116 -0
  43. enigma_api_client/plugins/enigma-api/tests/applicability_review.csv +64 -0
  44. enigma_api_client/plugins/enigma-api/tests/parallel-ground-truth.csv +101 -0
  45. enigma_api_client/plugins/enigma-api/tests/run_tests.sh +320 -0
  46. enigma_api_client/plugins/enigma-api/tests/test_cases.yaml +213 -0
  47. enigma_api_client/plugins/enigma-api/tests/validation_cases.csv +101 -0
  48. enigma_api_client/py.typed +0 -0
  49. enigma_api_client/query_router/__init__.py +3 -0
  50. enigma_api_client/query_router/geo.py +121 -0
  51. enigma_api_client/query_router/templates.py +475 -0
  52. enigma_api_client/strategies/__init__.py +16 -0
  53. enigma_api_client/strategies/agent.py +63 -0
  54. enigma_api_client/strategies/base.py +15 -0
  55. enigma_api_client/strategies/generative.py +143 -0
  56. enigma_api_client/strategies/llm_provider.py +137 -0
  57. enigma_api_client/strategies/service.py +103 -0
  58. enigma_api_client/strategies/template.py +189 -0
  59. enigma_api_client-0.3.1.dist-info/METADATA +257 -0
  60. enigma_api_client-0.3.1.dist-info/RECORD +63 -0
  61. enigma_api_client-0.3.1.dist-info/WHEEL +5 -0
  62. enigma_api_client-0.3.1.dist-info/licenses/LICENSE +201 -0
  63. enigma_api_client-0.3.1.dist-info/top_level.txt +5 -0
@@ -0,0 +1,143 @@
1
+ # ABOUTME: Auto-discovers and validates all GraphQL blocks from SKILL.md
2
+ # ABOUTME: Runs syntax validation and linter on each extracted query
3
+
4
+ """
5
+ Auto-discover & validate all GraphQL code blocks in SKILL.md.
6
+
7
+ For each ```graphql block found:
8
+ 1. Validate syntax (balanced braces, structure)
9
+ 2. Run the linter and check for ERROR-severity findings
10
+ 3. Extract and report metadata
11
+
12
+ Incomplete snippets (field-only patterns, filter examples) are
13
+ detected and marked with pytest.xfail.
14
+
15
+ Run:
16
+ uv run pytest harness/test_queries.py -v
17
+ """
18
+
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ import pytest
23
+
24
+ # Add scripts/ to sys.path
25
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
26
+
27
+ from lint_query import lint_query, Severity # noqa: E402
28
+ from query_validator import ( # noqa: E402
29
+ validate_query,
30
+ is_complete_query,
31
+ )
32
+
33
+
34
+ def _make_test_id(block: dict) -> str:
35
+ """Create a readable test ID from a block dict."""
36
+ section = block.get("section", "unknown") or "unknown"
37
+ line = block.get("line_number", 0)
38
+ # Truncate section to 60 chars
39
+ section = section[:60].replace(" ", "_").replace("/", "_")
40
+ return f"L{line}_{section}"
41
+
42
+
43
+ class TestSkillQueries:
44
+ """Validate every GraphQL block extracted from SKILL.md."""
45
+
46
+ def test_at_least_one_block_found(self, graphql_blocks):
47
+ """Sanity check: SKILL.md should have GraphQL blocks."""
48
+ assert len(graphql_blocks) > 0, "No GraphQL blocks found in SKILL.md"
49
+
50
+ def test_minimum_block_count(self, graphql_blocks):
51
+ """SKILL.md should have at least 10 GraphQL blocks."""
52
+ assert len(graphql_blocks) >= 10, (
53
+ f"Expected ≥10 GraphQL blocks, found {len(graphql_blocks)}"
54
+ )
55
+
56
+
57
+ def pytest_generate_tests(metafunc):
58
+ """Parametrize tests with graphql blocks from SKILL.md."""
59
+ if "query_block" in metafunc.fixturenames:
60
+ # We need the blocks fixture — get it via the skill_content fixture chain
61
+ plugin_root = Path(__file__).resolve().parent.parent
62
+ skill_path = plugin_root / "skills" / "enigma-graphql" / "SKILL.md"
63
+ if skill_path.exists():
64
+ sys.path.insert(0, str(plugin_root / "scripts"))
65
+ from query_validator import extract_graphql_blocks_with_context
66
+ content = skill_path.read_text(encoding="utf-8")
67
+ blocks = extract_graphql_blocks_with_context(content)
68
+ ids = [_make_test_id(b) for b in blocks]
69
+ metafunc.parametrize("query_block", blocks, ids=ids)
70
+ else:
71
+ metafunc.parametrize("query_block", [])
72
+
73
+
74
+ class TestQuerySyntax:
75
+ """Validate syntax of each GraphQL block."""
76
+
77
+ def test_syntax_valid(self, query_block):
78
+ query = query_block["query"]
79
+ section = query_block.get("section", "")
80
+ line = query_block.get("line_number", 0)
81
+
82
+ # Skip incomplete snippets
83
+ if not is_complete_query(query):
84
+ pytest.xfail(
85
+ f"Incomplete snippet at line {line} "
86
+ f"(section: {section}) — not a full query"
87
+ )
88
+
89
+ result = validate_query(query)
90
+ assert result.valid, (
91
+ f"Syntax error in query at line {line} "
92
+ f"(section: {section}): {result.errors}"
93
+ )
94
+
95
+
96
+ class TestQueryLint:
97
+ """Lint each GraphQL block for ERROR-severity findings."""
98
+
99
+ def test_no_errors(self, query_block):
100
+ query = query_block["query"]
101
+ section = query_block.get("section", "")
102
+ line = query_block.get("line_number", 0)
103
+
104
+ # Skip incomplete snippets
105
+ if not is_complete_query(query):
106
+ pytest.xfail(
107
+ f"Incomplete snippet at line {line} "
108
+ f"(section: {section}) — not a full query"
109
+ )
110
+
111
+ result = lint_query(query)
112
+
113
+ # Filter to only ERROR severity
114
+ errors = [
115
+ f for f in result.findings
116
+ if f.severity == Severity.ERROR
117
+ ]
118
+
119
+ # Build readable message
120
+ if errors:
121
+ msg_parts = [
122
+ f"Query at line {line} (section: {section}) "
123
+ f"has {len(errors)} error(s):"
124
+ ]
125
+ for f in errors:
126
+ msg_parts.append(f" [{f.rule_id}] {f.message}")
127
+ assert False, "\n".join(msg_parts)
128
+
129
+
130
+ class TestQueryMetadata:
131
+ """Verify metadata extraction for complete queries."""
132
+
133
+ def test_has_operation_type(self, query_block):
134
+ query = query_block["query"]
135
+ line = query_block.get("line_number", 0)
136
+
137
+ if not is_complete_query(query):
138
+ pytest.xfail(f"Incomplete snippet at line {line}")
139
+
140
+ result = validate_query(query)
141
+ assert result.metadata.get("operation_type") is not None, (
142
+ f"Query at line {line} missing operation_type"
143
+ )
@@ -0,0 +1,351 @@
1
+ # ABOUTME: Static structural checks on the SKILL.md documentation
2
+ # ABOUTME: Validates limits, warnings, and field naming conventions
3
+
4
+ """
5
+ Static structural checks on SKILL.md.
6
+
7
+ These don't test individual queries — they test that the documentation
8
+ itself contains the right guidance, limits, warnings, and conventions.
9
+
10
+ Run:
11
+ uv run pytest harness/test_skill_rules.py -v
12
+ """
13
+
14
+ import json
15
+ import re
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ import pytest
20
+
21
+ # Add scripts/ to sys.path
22
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
23
+
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Helpers
27
+ # ---------------------------------------------------------------------------
28
+
29
+ def _extract_json_blocks(content: str) -> list[tuple[str, int]]:
30
+ """Extract all ```json fenced blocks with line numbers."""
31
+ results = []
32
+ for m in re.finditer(r'^```json\s*\n(.*?)\n```', content, re.MULTILINE | re.DOTALL):
33
+ line_no = content[:m.start()].count('\n') + 1
34
+ results.append((m.group(1), line_no))
35
+ return results
36
+
37
+
38
+ def _extract_graphql_blocks(content: str) -> list[tuple[str, int]]:
39
+ """Extract all ```graphql fenced blocks with line numbers."""
40
+ results = []
41
+ for m in re.finditer(r'^```graphql\s*\n(.*?)\n```', content, re.MULTILINE | re.DOTALL):
42
+ line_no = content[:m.start()].count('\n') + 1
43
+ results.append((m.group(1), line_no))
44
+ return results
45
+
46
+
47
+ def _get_section(content: str, heading_pattern: str) -> str | None:
48
+ """Extract the content of a section matching the heading pattern."""
49
+ lines = content.split('\n')
50
+ capturing = False
51
+ captured = []
52
+ heading_level = None
53
+
54
+ for line in lines:
55
+ heading_match = re.match(r'^(#{1,4})\s+(.*)', line)
56
+ if heading_match:
57
+ level = len(heading_match.group(1))
58
+ text = heading_match.group(2).strip()
59
+ if re.search(heading_pattern, text, re.IGNORECASE):
60
+ capturing = True
61
+ heading_level = level
62
+ captured.append(line)
63
+ continue
64
+ elif capturing and level <= heading_level:
65
+ break
66
+ if capturing:
67
+ captured.append(line)
68
+
69
+ return '\n'.join(captured) if captured else None
70
+
71
+
72
+ def _get_discovery_section(content: str) -> str | None:
73
+ """Get the Discovery recipe section."""
74
+ return _get_section(content, r'Discovery.*Find All|Discovery.*Businesses')
75
+
76
+
77
+ def _get_ol_section(content: str) -> str | None:
78
+ """Get the Operating Location Revenue/Card section."""
79
+ return _get_section(content, r'Operating Location.*Revenue|Operating Location Card')
80
+
81
+
82
+ def _get_brand_revenue_section(content: str) -> str | None:
83
+ """Get the Brand Revenue section."""
84
+ return _get_section(content, r'Brand Revenue Query')
85
+
86
+
87
+ def _get_aggregate_section(content: str) -> str | None:
88
+ """Get the Aggregate Query section, excluding Count on Search subsection."""
89
+ full_section = _get_section(content, r'Aggregate Query')
90
+ if full_section is None:
91
+ return None
92
+ # Stop at "Count on Search" subsection to avoid mixing up plural/singular rules
93
+ lines = full_section.split('\n')
94
+ result = []
95
+ for line in lines:
96
+ if re.match(r'^#{1,4}\s+Count on Search', line, re.IGNORECASE):
97
+ break
98
+ result.append(line)
99
+ return '\n'.join(result) if result else full_section
100
+
101
+
102
+ def _get_count_search_section(content: str) -> str | None:
103
+ """Get the Count on Search section."""
104
+ return _get_section(content, r'Count on Search')
105
+
106
+
107
+ # ---------------------------------------------------------------------------
108
+ # Tests
109
+ # ---------------------------------------------------------------------------
110
+
111
+ class TestNoLimit50InCodeBlocks:
112
+ """No `"limit": 50` should appear in any JSON code block."""
113
+
114
+ def test_no_limit_50(self, skill_content):
115
+ json_blocks = _extract_json_blocks(skill_content)
116
+ violations = []
117
+ for block_text, line_no in json_blocks:
118
+ # Look for "limit": 50 or "limit" : 50 patterns
119
+ if re.search(r'"limit"\s*:\s*50\b', block_text):
120
+ violations.append(f"Line {line_no}: found '\"limit\": 50' in JSON block")
121
+ assert not violations, (
122
+ f"Found limit: 50 in JSON code blocks:\n" +
123
+ "\n".join(violations)
124
+ )
125
+
126
+
127
+ class TestDiscoveryLimitsLTE10:
128
+ """Discovery section JSON blocks should have limit ≤ 10."""
129
+
130
+ def test_discovery_limits(self, skill_content):
131
+ section = _get_discovery_section(skill_content)
132
+ if section is None:
133
+ pytest.skip("Discovery section not found")
134
+
135
+ json_blocks = _extract_json_blocks(section)
136
+ violations = []
137
+ for block_text, line_no in json_blocks:
138
+ for m in re.finditer(r'"limit"\s*:\s*(\d+)', block_text):
139
+ limit_val = int(m.group(1))
140
+ if limit_val > 10:
141
+ violations.append(
142
+ f"Line {line_no}: limit={limit_val} > 10 in Discovery section"
143
+ )
144
+ assert not violations, (
145
+ f"Discovery section has limit > 10:\n" +
146
+ "\n".join(violations)
147
+ )
148
+
149
+
150
+ class TestOLFirstLTE5InDiscovery:
151
+ """Discovery GraphQL should have operatingLocations(first: ≤5)."""
152
+
153
+ def test_ol_first_lte_5(self, skill_content):
154
+ section = _get_discovery_section(skill_content)
155
+ if section is None:
156
+ pytest.skip("Discovery section not found")
157
+
158
+ graphql_blocks = _extract_graphql_blocks(section)
159
+ violations = []
160
+ for block_text, line_no in graphql_blocks:
161
+ for m in re.finditer(r'operatingLocations\s*\(\s*first\s*:\s*(\d+)', block_text):
162
+ first_val = int(m.group(1))
163
+ if first_val > 5:
164
+ violations.append(
165
+ f"Line {line_no}: operatingLocations(first: {first_val}) > 5 "
166
+ f"in Discovery section"
167
+ )
168
+ assert not violations, (
169
+ f"Discovery section has operatingLocations first > 5:\n" +
170
+ "\n".join(violations)
171
+ )
172
+
173
+
174
+ class TestNoPlatformBrandIdInOLRecipes:
175
+ """OL section code blocks should NOT include platformBrandId."""
176
+
177
+ def test_no_platform_brand_id(self, skill_content):
178
+ section = _get_ol_section(skill_content)
179
+ if section is None:
180
+ pytest.skip("OL Revenue section not found")
181
+
182
+ graphql_blocks = _extract_graphql_blocks(section)
183
+ json_blocks = _extract_json_blocks(section)
184
+ violations = []
185
+
186
+ for block_text, line_no in graphql_blocks + json_blocks:
187
+ if "platformBrandId" in block_text:
188
+ violations.append(
189
+ f"Line {line_no}: 'platformBrandId' found in OL section code block"
190
+ )
191
+
192
+ assert not violations, (
193
+ f"OL section should not reference platformBrandId:\n" +
194
+ "\n".join(violations)
195
+ )
196
+
197
+
198
+ class TestPlatformBrandIdInBrandRecipes:
199
+ """Brand revenue code blocks should include platformBrandId."""
200
+
201
+ def test_has_platform_brand_id(self, skill_content):
202
+ section = _get_brand_revenue_section(skill_content)
203
+ if section is None:
204
+ pytest.skip("Brand Revenue section not found")
205
+
206
+ json_blocks = _extract_json_blocks(section)
207
+ if not json_blocks:
208
+ pytest.skip("No JSON blocks in Brand Revenue section")
209
+
210
+ found = False
211
+ for block_text, line_no in json_blocks:
212
+ if "platformBrandId" in block_text:
213
+ found = True
214
+ break
215
+
216
+ assert found, (
217
+ "Brand Revenue section JSON blocks should include platformBrandId filter"
218
+ )
219
+
220
+
221
+ class TestCountSingularInAggregate:
222
+ """Aggregate count uses singular field names."""
223
+
224
+ def test_singular_fields(self, skill_content):
225
+ section = _get_aggregate_section(skill_content)
226
+ if section is None:
227
+ pytest.skip("Aggregate Query section not found")
228
+
229
+ graphql_blocks = _extract_graphql_blocks(section)
230
+ violations = []
231
+ for block_text, line_no in graphql_blocks:
232
+ # Look for count(field: "...") — field value should be singular
233
+ for m in re.finditer(r'count\s*\(\s*field\s*:\s*"(\w+)"', block_text):
234
+ field_name = m.group(1)
235
+ # Singular forms: brand, operatingLocation, legalEntity
236
+ if field_name in ("operatingLocations", "legalEntities", "brands"):
237
+ violations.append(
238
+ f"Line {line_no}: count(field: \"{field_name}\") should use "
239
+ f"singular form in aggregate query"
240
+ )
241
+
242
+ assert not violations, (
243
+ f"Aggregate section uses plural field names in count():\n" +
244
+ "\n".join(violations)
245
+ )
246
+
247
+
248
+ class TestCountPluralInSearch:
249
+ """Search-level count uses plural field names."""
250
+
251
+ def test_plural_fields(self, skill_content):
252
+ section = _get_count_search_section(skill_content)
253
+ if section is None:
254
+ pytest.skip("Count on Search section not found")
255
+
256
+ graphql_blocks = _extract_graphql_blocks(section)
257
+ violations = []
258
+ for block_text, line_no in graphql_blocks:
259
+ for m in re.finditer(r'count\s*\(\s*field\s*:\s*"(\w+)"', block_text):
260
+ field_name = m.group(1)
261
+ # Plural forms: operatingLocations, legalEntities
262
+ if field_name in ("operatingLocation", "legalEntity", "brand"):
263
+ violations.append(
264
+ f"Line {line_no}: count(field: \"{field_name}\") should use "
265
+ f"plural form in search-level count"
266
+ )
267
+
268
+ assert not violations, (
269
+ f"Search count section uses singular field names in count():\n" +
270
+ "\n".join(violations)
271
+ )
272
+
273
+
274
+ class TestCoalescePairedWithSkip:
275
+ """Every @coalesce block should have a corresponding @skip(if: true)."""
276
+
277
+ def test_coalesce_has_skip(self, skill_content):
278
+ graphql_blocks = _extract_graphql_blocks(skill_content)
279
+ violations = []
280
+ for block_text, line_no in graphql_blocks:
281
+ if "@coalesce" in block_text:
282
+ if "@skip(if: true)" not in block_text and '@skip(if:true)' not in block_text:
283
+ violations.append(
284
+ f"Line {line_no}: @coalesce found without @skip(if: true)"
285
+ )
286
+
287
+ assert not violations, (
288
+ f"@coalesce without @skip(if: true):\n" +
289
+ "\n".join(violations)
290
+ )
291
+
292
+
293
+ class TestStreet1AgentWarningPresent:
294
+ """AGENT BEHAVIOR directive about street1 should exist."""
295
+
296
+ def test_agent_behavior_present(self, skill_content):
297
+ assert "AGENT BEHAVIOR" in skill_content, (
298
+ "SKILL.md should contain an 'AGENT BEHAVIOR' directive for street1"
299
+ )
300
+ # Check it mentions street1
301
+ # Find the AGENT BEHAVIOR block
302
+ agent_idx = skill_content.index("AGENT BEHAVIOR")
303
+ context = skill_content[agent_idx:agent_idx + 500]
304
+ assert "street1" in context.lower() or "street" in context.lower(), (
305
+ "AGENT BEHAVIOR directive should mention street1/street address"
306
+ )
307
+
308
+
309
+ class TestFranchiseWarningPresent:
310
+ """Franchise/chain resolution warning section should exist."""
311
+
312
+ def test_franchise_section_exists(self, skill_content):
313
+ assert "Franchise" in skill_content or "franchise" in skill_content, (
314
+ "SKILL.md should contain franchise/chain resolution guidance"
315
+ )
316
+
317
+ def test_franchise_chain_warning(self, skill_content):
318
+ # Should have the warning section header
319
+ assert re.search(
320
+ r'Franchise.*Chain.*Resolution|Chain.*Resolution',
321
+ skill_content,
322
+ re.IGNORECASE,
323
+ ), "SKILL.md should have a Franchise & Chain Resolution section"
324
+
325
+ def test_brand_revenue_chain_warning(self, skill_content):
326
+ """Brand Revenue section should warn about chain resolution."""
327
+ section = _get_brand_revenue_section(skill_content)
328
+ if section is None:
329
+ pytest.skip("Brand Revenue section not found")
330
+ assert "chain" in section.lower() or "franchise" in section.lower(), (
331
+ "Brand Revenue section should warn about franchise/chain resolution"
332
+ )
333
+
334
+
335
+ class TestNoTotalCountInCodeBlocks:
336
+ """totalCount should never appear in code blocks."""
337
+
338
+ def test_no_total_count(self, skill_content):
339
+ graphql_blocks = _extract_graphql_blocks(skill_content)
340
+ violations = []
341
+ for block_text, line_no in graphql_blocks:
342
+ if "totalCount" in block_text:
343
+ violations.append(
344
+ f"Line {line_no}: 'totalCount' found in GraphQL block "
345
+ f"(not supported by Enigma API)"
346
+ )
347
+
348
+ assert not violations, (
349
+ f"totalCount in code blocks (causes HTTP 400):\n" +
350
+ "\n".join(violations)
351
+ )
@@ -0,0 +1,9 @@
1
+ [project]
2
+ name = "enigma-api-harness"
3
+ version = "0.1.0"
4
+ description = "Query linter and test harness for the enigma-graphql skill"
5
+ requires-python = ">=3.11"
6
+ dependencies = ["pytest>=8.0"]
7
+
8
+ [tool.pytest.ini_options]
9
+ testpaths = ["harness"]