Skip to content

Add AI-augmented codegen: openapi, tests, register, config-docs subco…#98

Merged
wrhalpin merged 1 commit intomainfrom
claude/add-claude-documentation-k8vvJ
Apr 9, 2026
Merged

Add AI-augmented codegen: openapi, tests, register, config-docs subco…#98
wrhalpin merged 1 commit intomainfrom
claude/add-claude-documentation-k8vvJ

Conversation

@wrhalpin
Copy link
Copy Markdown
Owner

@wrhalpin wrhalpin commented Apr 9, 2026

…mmands

  • gnat/codegen/openapi_generator.py: add --ai/--config flags; _try_load_llm(), _ai_enhance(), _ai_test_fixtures() helpers; ai_impls injection in _render_client() and _render_tests() so Claude generates complete implementations instead of scaffolds
  • gnat/codegen/test_generator.py (new): generate_connector_tests() introspects any CLIENT_REGISTRY connector and produces a full pytest scaffold; --ai generates realistic fixtures via Claude
  • gnat/codegen/registry_sync.py (new): scan_unregistered() finds connector client.py files missing from CLIENT_REGISTRY; sync_registry() patches gnat/clients/init.py in-place with sorted import + registry entry
  • gnat/codegen/config_docs_generator.py (new): generate_config_docs() reads config.ini.example and splices a per-section Markdown table into docs/reference/configuration.md between sentinel comments; --ai enriches descriptions
  • gnat/codegen/init.py: export all four new public functions
  • gnat/cli/main.py: wire --ai/--config onto openapi subparser; add tests, register, and config-docs subparsers with full argument definitions and _cmd_codegen() handlers

https://claude.ai/code/session_01BDoue9HxB83ijLzFARAugq

…mmands

- gnat/codegen/openapi_generator.py: add --ai/--config flags; _try_load_llm(),
  _ai_enhance(), _ai_test_fixtures() helpers; ai_impls injection in _render_client()
  and _render_tests() so Claude generates complete implementations instead of scaffolds
- gnat/codegen/test_generator.py (new): generate_connector_tests() introspects any
  CLIENT_REGISTRY connector and produces a full pytest scaffold; --ai generates
  realistic fixtures via Claude
- gnat/codegen/registry_sync.py (new): scan_unregistered() finds connector client.py
  files missing from CLIENT_REGISTRY; sync_registry() patches gnat/clients/__init__.py
  in-place with sorted import + registry entry
- gnat/codegen/config_docs_generator.py (new): generate_config_docs() reads
  config.ini.example and splices a per-section Markdown table into
  docs/reference/configuration.md between sentinel comments; --ai enriches descriptions
- gnat/codegen/__init__.py: export all four new public functions
- gnat/cli/main.py: wire --ai/--config onto openapi subparser; add tests, register,
  and config-docs subparsers with full argument definitions and _cmd_codegen() handlers

https://claude.ai/code/session_01BDoue9HxB83ijLzFARAugq
Copilot AI review requested due to automatic review settings April 9, 2026 13:34
@wrhalpin wrhalpin merged commit 14bc66f into main Apr 9, 2026
1 of 7 checks passed
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds new AI-augmented codegen utilities to GNAT (OpenAPI connector generation, unit test scaffolding, CLIENT_REGISTRY syncing, and config-docs regeneration) and wires them into the gnat codegen CLI.

Changes:

  • Extend OpenAPI connector generator with --ai/--config to optionally produce AI-complete implementations + richer fixtures.
  • Add new codegen modules: connector test generator, CLIENT_REGISTRY sync tool, and config-docs generator (INI → docs injection).
  • Expose new codegen APIs from gnat.codegen and add new gnat codegen tests/register/config-docs subcommands.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
gnat/codegen/test_generator.py New generator that introspects CLIENT_REGISTRY connectors and emits pytest scaffolds (optionally AI-enhanced fixtures).
gnat/codegen/registry_sync.py New helper to scan gnat/connectors/*/client.py and patch missing imports/registry entries into gnat/clients/__init__.py.
gnat/codegen/openapi_generator.py Adds AI enhancement + fixture generation and new CLI flags, and injects AI-generated method bodies into rendered output.
gnat/codegen/config_docs_generator.py New generator that parses config/config.ini.example and splices per-connector markdown tables into docs (optionally AI-enriched descriptions).
gnat/codegen/init.py Exports the new codegen public functions.
gnat/cli/main.py Adds CLI plumbing for --ai/--config and new tests, register, and config-docs codegen subcommands.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +390 to +393
monkeypatch.setattr(client, "put", mock_put)
monkeypatch.setattr(client, "post", MagicMock())
client.upsert_object("{primary_stix_type}", {{"id": "obj-1", "name": "test"}})
assert mock_put.called or True # some connectors may POST for updates
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the generated scaffold, assert mock_put.called or True will always pass, so this test provides no signal. Consider asserting the expected update verb (PUT) for connectors that support it, or parameterize the generator to emit an assertion that actually fails when the wrong method is used.

Suggested change
monkeypatch.setattr(client, "put", mock_put)
monkeypatch.setattr(client, "post", MagicMock())
client.upsert_object("{primary_stix_type}", {{"id": "obj-1", "name": "test"}})
assert mock_put.called or True # some connectors may POST for updates
mock_post = MagicMock(return_value={native_fixture})
monkeypatch.setattr(client, "put", mock_put)
monkeypatch.setattr(client, "post", mock_post)
client.upsert_object("{primary_stix_type}", {{"id": "obj-1", "name": "test"}})
assert mock_put.called or mock_post.called, (
"Expected update to use PUT or POST"
)

Copilot uses AI. Check for mistakes.
Comment on lines +335 to +348
class TestAuthentication:
def test_auth_headers_populated(self, monkeypatch):
"""authenticate() must set at least one header."""
c = {class_name}({auth_kwargs_str})
# Patch the underlying _request so no network call is made
monkeypatch.setattr(c, "_request", MagicMock(
return_value=type("R", (), {{"status": 200, "data": b\'{{"access_token":"tok"}}\'}})()
))
try:
c.authenticate()
except Exception:
pass # Some auth flows need live creds; just ensure no crash on import
# The class must have _auth_headers as a dict
assert isinstance(c._auth_headers, dict)
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generated auth test mocks BaseClient._request to return an object with .status/.data, but BaseClient._request() actually returns parsed JSON (dict/list/str/None). With the current mock and broad except Exception: pass, the test can pass even if authenticate() never populates headers (it only checks _auth_headers is a dict, which is already true from BaseClient.init). Consider mocking _request to return a dict like {..."access_token": "tok"...} and asserting _auth_headers becomes non-empty (or that a specific header key is set) without swallowing all exceptions.

Copilot uses AI. Check for mistakes.
Comment on lines +88 to +93
# --- Optional AI fixtures ---
ai_fixtures: dict[str, str] = {}
if use_ai:
from gnat.codegen.openapi_generator import _try_load_llm
llm = _try_load_llm(config_path)
if llm is None:
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generate_connector_tests() imports _try_load_llm from openapi_generator, but _try_load_llm is a private helper (leading underscore) and is also being reused by other modules. This creates tight coupling between unrelated generators and forces type: ignore elsewhere. Consider moving LLM-loading into a small shared helper module (e.g. gnat.codegen.ai_utils) or making it a supported public function.

Copilot uses AI. Check for mistakes.
Comment on lines +130 to +135
# Optional AI enhancement for descriptions
llm = None
if use_ai:
from gnat.codegen.openapi_generator import _try_load_llm # type: ignore[attr-defined]
llm = _try_load_llm(config_path)
if llm is None:
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generate_config_docs() imports _try_load_llm from openapi_generator (and suppresses typing with type: ignore). Since _try_load_llm is a private helper, this cross-module dependency is brittle. Consider moving the LLM-loading helper into a shared, non-private location (or exposing it as a public API) so codegen modules don't depend on each other’s internals.

Copilot uses AI. Check for mistakes.
Comment on lines +382 to +405
def _splice(source: str, key: str, content: str) -> str:
"""
Replace the text between sentinel comments in *source* with *content*.

If no sentinels are found, appends a new block at the end (before the
final ``---`` separator if present).
"""
begin_tag = f"<!-- codegen:begin:{key} -->"
end_tag = f"<!-- codegen:end:{key} -->"

if begin_tag in source and end_tag in source:
pattern = re.compile(
re.escape(begin_tag) + r".*?" + re.escape(end_tag),
re.DOTALL,
)
replacement = f"{begin_tag}\n{content}\n{end_tag}"
return pattern.sub(replacement, source)

# No sentinel found — inject before the last "---\n" or at end
block = f"\n{begin_tag}\n{content}\n{end_tag}\n"
last_sep = source.rfind("\n---\n")
if last_sep != -1:
return source[:last_sep] + block + source[last_sep:]
return source + block
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default target docs file (docs/reference/configuration.md) currently does not contain the <!-- codegen:begin:platform-connectors --> / <!-- codegen:end:platform-connectors --> sentinels. In that case _splice() will append the generated connector block near the end of the file, which likely isn’t the intended location (the docs already have a “### Platform sections” area). Consider adding the sentinel comments to the docs file in this PR, or enhancing _splice() to insert at a deterministic spot (e.g., immediately after the “### Platform sections” heading) when sentinels are absent.

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +46
# Regex to detect the class definition in client.py
_CLASS_RE = re.compile(
r"^class\s+(\w+Client)\s*\(", re.MULTILINE
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_CLASS_RE only matches class definitions of the form class <SomethingClient>(...), but the docstring for _detect_class_name() says it finds a BaseClient subclass. As written, it will miss valid connectors where the main class doesn’t end with Client or uses class X: without parentheses, and it may match the wrong class name if multiple classes end with Client. Consider detecting the BaseClient subclass more robustly (e.g., parse the AST and find classes inheriting BaseClient, or at least relax the regex and validate the base list contains BaseClient).

Suggested change
# Regex to detect the class definition in client.py
_CLASS_RE = re.compile(
r"^class\s+(\w+Client)\s*\(", re.MULTILINE
# Regex to detect the BaseClient subclass defined in client.py
_CLASS_RE = re.compile(
r"^class\s+([A-Za-z_]\w*)\s*\(\s*[^)]*\bBaseClient\b[^)]*\)\s*:",
re.MULTILINE,

Copilot uses AI. Check for mistakes.
Comment on lines +234 to +261
def _insert_import(source: str, import_line: str) -> str:
"""Insert an import line into the sorted block of connector imports."""
lines = source.splitlines(keepends=True)

# Find all existing 'from gnat.connectors.' import lines
connector_import_re = re.compile(r"^from gnat\.connectors\.")
indices = [i for i, ln in enumerate(lines) if connector_import_re.match(ln)]

if not indices:
# Fallback: insert after the last 'from gnat.' import
from_gnat_re = re.compile(r"^from gnat\.")
indices = [i for i, ln in enumerate(lines) if from_gnat_re.match(ln)]
if not indices:
return source

# Insert in alphabetical order within the connector import block
insert_line = import_line + "\n"

# Find the right position (keep sorted)
for idx in indices:
if lines[idx].strip() >= import_line:
lines.insert(idx, insert_line)
return "".join(lines)

# Append after the last connector import
last_idx = indices[-1]
lines.insert(last_idx + 1, insert_line)
return "".join(lines)
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_insert_import() unconditionally inserts import_line and doesn’t check whether the exact import already exists. If gnat/clients/__init__.py already has the import (but the registry key is missing), sync_registry() can introduce duplicate imports. Consider adding a guard like if import_line in source: return source before attempting insertion.

Copilot uses AI. Check for mistakes.
Comment on lines +319 to +334
def _try_load_llm(config_path: str | None) -> Any:
"""
Attempt to load LLMClient from GNAT config.

Returns ``None`` if Claude is not configured or dependencies are missing.
"""
try:
from gnat.config import GNATConfig
from gnat.agents.llm import LLMClient

cfg = GNATConfig(config_path=config_path)
claude_cfg = cfg.get("claude")
return LLMClient(backend="claude", **claude_cfg)
except Exception:
return None

Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_try_load_llm() is defined as a private helper but is imported and reused by other codegen modules (tests/config-docs). Consider moving this into a shared utility module or making it a supported public function, so these modules don’t depend on openapi_generator internals.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants