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.
- enigma_api_client/__init__.py +16 -0
- enigma_api_client/client.py +134 -0
- enigma_api_client/client_inject_id_and_typename_helper.py +248 -0
- enigma_api_client/client_inject_urls_helper.py +26 -0
- enigma_api_client/plugins/.DS_Store +0 -0
- enigma_api_client/plugins/enigma-api/.claude-plugin/plugin.json +8 -0
- enigma_api_client/plugins/enigma-api/README.md +82 -0
- enigma_api_client/plugins/enigma-api/harness/conftest.py +42 -0
- enigma_api_client/plugins/enigma-api/harness/test_linter.py +863 -0
- enigma_api_client/plugins/enigma-api/harness/test_queries.py +143 -0
- enigma_api_client/plugins/enigma-api/harness/test_skill_rules.py +351 -0
- enigma_api_client/plugins/enigma-api/pyproject.toml +9 -0
- enigma_api_client/plugins/enigma-api/scripts/__pycache__/lint_query.cpython-313.pyc +0 -0
- enigma_api_client/plugins/enigma-api/scripts/__pycache__/query_validator.cpython-313.pyc +0 -0
- enigma_api_client/plugins/enigma-api/scripts/__pycache__/query_validator.cpython-314.pyc +0 -0
- enigma_api_client/plugins/enigma-api/scripts/generate_report.py +384 -0
- enigma_api_client/plugins/enigma-api/scripts/lint_query.py +712 -0
- enigma_api_client/plugins/enigma-api/scripts/query_validator.py +352 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/SKILL.md +620 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/brand/03_brand_profile.md +163 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/brand/13_brand_dump.md +95 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/derived/07_coalesce_ein_formation.md +146 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/discovery/01_coffee_shops_zip.md +287 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/discovery/11_ol_industry_discovery.md +116 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/export/08_segment_export.md +96 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/kyb/02_officers_lookup.md +366 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/kyb/10_kyb_verify.md +176 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/legal_entity/09_legal_entity_profile.md +97 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/locations/05_brand_locations_by_state.md +120 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/person_search/04_find_person_companies.md +160 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/revenue/06_brand_revenue.md +176 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/risk/12_risk_signals.md +118 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/references/card_transactions.md +91 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/references/field_corrections.json +106 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/references/filter_syntax.md +203 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/references/naics_2022_mapping.json +184 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/references/pagination_template.py +115 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/schema.graphql +44971 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/scripts/test_examples.py +1122 -0
- enigma_api_client/plugins/enigma-api/skills/enigma-graphql/scripts/validate_query.py +177 -0
- enigma_api_client/plugins/enigma-api/test-cases/graphql-test-cases.md +196 -0
- enigma_api_client/plugins/enigma-api/tests/accuracy_cases.csv +116 -0
- enigma_api_client/plugins/enigma-api/tests/applicability_review.csv +64 -0
- enigma_api_client/plugins/enigma-api/tests/parallel-ground-truth.csv +101 -0
- enigma_api_client/plugins/enigma-api/tests/run_tests.sh +320 -0
- enigma_api_client/plugins/enigma-api/tests/test_cases.yaml +213 -0
- enigma_api_client/plugins/enigma-api/tests/validation_cases.csv +101 -0
- enigma_api_client/py.typed +0 -0
- enigma_api_client/query_router/__init__.py +3 -0
- enigma_api_client/query_router/geo.py +121 -0
- enigma_api_client/query_router/templates.py +475 -0
- enigma_api_client/strategies/__init__.py +16 -0
- enigma_api_client/strategies/agent.py +63 -0
- enigma_api_client/strategies/base.py +15 -0
- enigma_api_client/strategies/generative.py +143 -0
- enigma_api_client/strategies/llm_provider.py +137 -0
- enigma_api_client/strategies/service.py +103 -0
- enigma_api_client/strategies/template.py +189 -0
- enigma_api_client-0.3.1.dist-info/METADATA +257 -0
- enigma_api_client-0.3.1.dist-info/RECORD +63 -0
- enigma_api_client-0.3.1.dist-info/WHEEL +5 -0
- enigma_api_client-0.3.1.dist-info/licenses/LICENSE +201 -0
- 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
|
+
)
|
|
Binary file
|
|
Binary file
|