Skip to content

feat(int-autentique): add Autentique e-signature integration skill#97

Open
mt-alarcon wants to merge 1 commit into
evolution-foundation:mainfrom
mt-alarcon:feat/int-autentique-skill
Open

feat(int-autentique): add Autentique e-signature integration skill#97
mt-alarcon wants to merge 1 commit into
evolution-foundation:mainfrom
mt-alarcon:feat/int-autentique-skill

Conversation

@mt-alarcon

@mt-alarcon mt-alarcon commented Jun 3, 2026

Copy link
Copy Markdown

What

Adds a new integration skill int-autentique — wrapper for Autentique (Brazilian e-signature platform, GraphQL v2 API).

Why

Autentique is a widely-used BR e-signature provider (legally backed by MP 2.200-2/2001 + Lei 14.063/2020). This gives EvoNexus a first-class way to send contracts, NDAs, proposals, and termination agreements for formal signature instead of relying on scanned PDFs.

Commands

  • whoami — authenticated account
  • documents list / get / create / download / delete
  • create supports multipart/form-data upload (GraphQL multipart spec), sandbox mode (no credit consumption), reminder cadence, and ICP-Brasil qualified signatures
  • smoke — standardized health-check (always exit 0 + JSON {overall, steps[], duration_ms})

Implementation notes

  • stdlib-only (urllib), no third-party deps
  • Auth via AUTENTIQUE_API_TOKEN env var (no hardcoded secrets)
  • JSON to stdout on every command; structured {error, details} + exit codes (0 ok / 1 usage / 2 API) on failure

Tests

31 unit tests, network-blocked (mock-based) — pytest tests/ → 31 passed.

🤖 Generated with Claude Code

Summary by Sourcery

Add a new Autentique e-signature integration skill with a Python CLI client, documentation, and comprehensive tests.

New Features:

  • Introduce a Python-based CLI client for Autentique GraphQL v2 API supporting user info, document management, folder listing, and a smoke health check command.

Enhancements:

  • Implement resilient HTTP and GraphQL handling with retries, structured JSON output, and environment-driven configuration for the Autentique integration skill.

Documentation:

  • Document the Autentique integration skill usage, setup, and typical workflows in SKILL.md.

Tests:

  • Add a pytest suite with network-blocking fixtures and helpers to validate Autentique client behavior, CLI commands, error handling, and argument parsing.

Autentique (BR e-signature, GraphQL v2) integration with:
- whoami, documents list/get/create/download/delete
- multipart/form-data upload (GraphQL multipart spec)
- sandbox mode (no credit consumption), reminder cadence, ICP-Brasil qualified signatures
- standardized smoke command (exit 0 + JSON {overall, steps[]})
- 31 unit tests (network-blocked, mock-based)

Token via AUTENTIQUE_API_TOKEN env. stdlib-only (urllib).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds a new stdlib-only Python CLI skill int-autentique that wraps Autentique’s GraphQL v2 API for Brazilian e-signature workflows, including robust transport/error handling, document/folder operations, and a health-check smoke command, with comprehensive mocked unit tests and documentation.

Sequence diagram for Autentique document creation command

sequenceDiagram
  actor User
  participant CLI as AutentiqueCLI
  participant Client as cmd_documents_create
  participant Transport as gql_upload
  participant Autentique as AutentiqueAPI

  User->>CLI: run documents create
  CLI->>Client: main parses args
  Client->>Client: json.loads signers
  Client->>Client: build document_input and variables
  Client->>Client: read AUTENTIQUE_ORGANIZATION_ID
  Client->>Transport: gql_upload(query, variables, file)
  Transport->>Transport: _token reads AUTENTIQUE_API_TOKEN
  Transport->>Autentique: POST multipart/form-data
  Autentique-->>Transport: GraphQL response
  Transport-->>Client: data
  Client-->>CLI: output(data)
  CLI-->>User: JSON result
Loading

File-Level Changes

Change Details Files
Introduce Autentique GraphQL v2 Python client CLI implementing e-signature operations (whoami, documents*, folders, smoke) with structured JSON output and retry-aware transport layer.
  • Implement env bootstrap and token loading with optional workspace .env support and required AUTENTIQUE_API_TOKEN validation.
  • Add generic GraphQL JSON request helper with HTTP/connection error handling, GraphQL error surfacing, and bounded retries for 5xx/429 responses.
  • Implement GraphQL multipart upload helper for createDocument using the GraphQL multipart request spec and stdlib-only HTTP/mime handling.
  • Add command handlers for whoami, listing/getting/creating/downloading/deleting documents, listing folders, and a smoke test that always exits 0 with summarized step results.
  • Wire commands through an argparse-based CLI with subcommands, validation of required arguments, and a main() dispatcher with consistent exit codes and JSON error formatting.
.claude/skills/int-autentique/scripts/autentique_client.py
Document the new int-autentique skill capabilities, configuration, and usage for integration into the workspace tooling.
  • Describe the skill metadata for the clawdbot environment, including env/binary requirements and entry script mapping.
  • Provide setup instructions for obtaining and configuring AUTENTIQUE_API_TOKEN and optional AUTENTIQUE_ORGANIZATION_ID.
  • Document CLI commands, signer JSON schema, typical usage scenarios (contracts, NDAs, distratos), cost model notes, and output/error conventions.
.claude/skills/int-autentique/SKILL.md
Add a comprehensive mocked test suite for the Autentique client, including network blocking, helpers, and coverage for transport, commands, argparse wiring, and anti-fabrication guarantees.
  • Bootstrap the autentique_client module via conftest and enforce a global urlopen stub to prevent real network access in tests.
  • Provide helper factories to simulate successful GraphQL responses, GraphQL errors, and HTTP error responses.
  • Cover gql transport behaviors (success, GraphQL errors, HTTP 4xx/5xx with retry, connection errors) and token-missing handling.
  • Test each command handler’s happy paths and failure cases, including validation of --signers, file-not-found and URL-missing scenarios, delete confirmation, folders list, and download behavior.
  • Verify smoke command semantics (always exit 0 with overall/steps/duration fields) and argparse wiring for subcommands and defaults, as well as anti-fabrication guarantees on error outputs.
.claude/skills/int-autentique/tests/test_autentique_client.py
.claude/skills/int-autentique/tests/conftest.py
.claude/skills/int-autentique/tests/helpers.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • The _load_dotenv helper assumes a fixed directory depth (parents[4]) to find the project .env, which is brittle if the repo layout changes; consider either resolving from a known anchor (e.g. .claude root) or allowing an explicit ENV_PATH override instead of hardcoding the parent count.
  • cmd_smoke only wraps gql calls in a generic except Exception, so failures that trigger sys.exit inside gql (e.g. HTTP 5xx / GraphQL errors) will terminate with a non-zero status and bypass the intended 'always exit 0 with JSON' contract; you may want to catch SystemExit around gql calls and convert them into FAIL steps instead.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `_load_dotenv` helper assumes a fixed directory depth (`parents[4]`) to find the project `.env`, which is brittle if the repo layout changes; consider either resolving from a known anchor (e.g. `.claude` root) or allowing an explicit `ENV_PATH` override instead of hardcoding the parent count.
- `cmd_smoke` only wraps `gql` calls in a generic `except Exception`, so failures that trigger `sys.exit` inside `gql` (e.g. HTTP 5xx / GraphQL errors) will terminate with a non-zero status and bypass the intended 'always exit 0 with JSON' contract; you may want to catch `SystemExit` around `gql` calls and convert them into `FAIL` steps instead.

## Individual Comments

### Comment 1
<location path=".claude/skills/int-autentique/scripts/autentique_client.py" line_range="346-355" />
<code_context>
+def cmd_smoke(args):
</code_context>
<issue_to_address>
**issue (bug_risk):** The `smoke` command is structured as if `gql` could raise, but `gql` currently exits the process instead of propagating errors.

Because `gql` prints JSON and calls `sys.exit(2)` on errors, the process ends before your `try/except` and `steps` reporting logic can run, so smoke JSON is never produced on failures. To make this work, `gql` needs to raise or return structured errors instead of exiting, so `cmd_smoke` can capture them, emit the smoke report, and still control the final exit code (e.g. remain 0).
</issue_to_address>

### Comment 2
<location path=".claude/skills/int-autentique/tests/test_autentique_client.py" line_range="322-331" />
<code_context>
+class TestCmdDocumentsDownload:
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests for default output filename and download error handling in `cmd_documents_download`

Current tests cover only the missing-URL case and a successful download with an explicit `--output` path. Please also add (1) a test that omits `output` and asserts the created filename matches `<name>.<version>.pdf` (or `<id>.<version>.pdf`), and (2) a test where `urllib.request.urlopen` raises, asserting the JSON error payload (`{"error": "Falha ao baixar", ...}`) and exit code 2. This will exercise the remaining download paths.

Suggested implementation:

```python

class TestCmdDocumentsDownload:
=======
# cmd_documents_download
# ===========================================================================

class TestCmdDocumentsDownload:
    def test_nome_arquivo_padrao_usa_nome_e_versao(self, monkeypatch, tmp_path, aut):
        """Quando --output é omitido → usa `<name>.<version>.pdf` (ou `<id>.<version>.pdf`) como nome do arquivo."""
        import io
        import json
        from pathlib import Path
        from types import SimpleNamespace
        import urllib.error

        # payload semelhante ao usado em outros testes de download, mas com URL presente
        payload = {
            "document": {
                "id": "doc1",
                "name": "Distrato",
                "files": {
                    "original": "https://exemplo.com/documento.pdf",
                    "signed": "https://exemplo.com/documento-assinado.pdf",
                },
            }
        }

        # preparar stdin com o payload
        monkeypatch.setattr(
            "sys.stdin",
            io.StringIO(json.dumps(payload)),
        )
        monkeypatch.setenv("AUTENTIQUE_API_TOKEN", "tok")

        # trabalhar em diretório temporário
        monkeypatch.chdir(tmp_path)

        # simular resposta do urlopen
        class _FakeResponse:
            def __init__(self, data: bytes):
                self._data = data

            def __enter__(self):
                return self

            def __exit__(self, exc_type, exc, tb):
                return False

            def read(self) -> bytes:
                return self._data

        def fake_urlopen(url, *args, **kwargs):
            # deve ser chamada com a URL do arquivo
            assert url == "https://exemplo.com/documento-assinado.pdf"
            return _FakeResponse(b"%PDF-1.4\n...")

        monkeypatch.setattr("urllib.request.urlopen", fake_urlopen)

        # sem --output → deve usar `<name>.<version>.pdf` (ou `<id>.<version>.pdf`)
        args = SimpleNamespace(
            id="doc1",
            version="1",
            output=None,
            sandbox=True,
        )

        aut.cmd_documents_download(args)

        # nome preferencialmente com `name`, senão com `id`
        expected_by_name = tmp_path / "Distrato.1.pdf"
        expected_by_id = tmp_path / "doc1.1.pdf"
        assert expected_by_name.is_file() or expected_by_id.is_file()

    def test_erro_ao_baixar_emite_json_e_exit_code_2(self, monkeypatch, capsys, tmp_path, aut):
        """Quando urlopen levanta exceção → imprime JSON de erro e sai com código 2."""
        import io
        import json
        from types import SimpleNamespace
        import urllib.error

        payload = {
            "document": {
                "id": "doc1",
                "name": "Distrato",
                "files": {
                    "original": "https://exemplo.com/documento.pdf",
                    "signed": "https://exemplo.com/documento-assinado.pdf",
                },
            }
        }

        monkeypatch.setattr(
            "sys.stdin",
            io.StringIO(json.dumps(payload)),
        )
        monkeypatch.setenv("AUTENTIQUE_API_TOKEN", "tok")
        monkeypatch.chdir(tmp_path)

        def failing_urlopen(url, *args, **kwargs):
            raise urllib.error.URLError("boom")

        monkeypatch.setattr("urllib.request.urlopen", failing_urlopen)

        args = SimpleNamespace(
            id="doc1",
            version="1",
            output=None,
            sandbox=True,
        )

        with pytest.raises(SystemExit) as excinfo:
            aut.cmd_documents_download(args)

        assert excinfo.value.code == 2

        captured = capsys.readouterr()
        # saída deve ser um JSON com a chave "error" e mensagem "Falha ao baixar"
        data = json.loads(captured.out)
        assert data.get("error") == "Falha ao baixar"

```

1. Estes testes assumem que:
   - Existe um fixture `aut` com o método `cmd_documents_download(args)`, similar ao usado em `cmd_documents_create`.
   - `cmd_documents_download` lê um JSON de `sys.stdin` no formato usado em `test_url_ausente_exits2`.
   - Quando `urllib.request.urlopen` lança uma exceção, `cmd_documents_download` captura o erro, escreve um JSON com `{"error": "Falha ao baixar", ...}` em `stdout` e encerra com `SystemExit(2)`.

2. Caso a função `cmd_documents_download` use outros nomes de argumentos (por exemplo, `document_id` em vez de `id`, ou `version_number` em vez de `version`), ajuste o `SimpleNamespace` nos testes para refletir a assinatura real.

3. Se o comando não estiver a ser chamado via o fixture `aut`, mas sim por uma função global (por ex. `cmd_documents_download(args)` importada diretamente), adapte as chamadas nos testes para usar o ponto de entrada correto.

4. Os testes usam `pytest` explicitamente (`pytest.raises`); se ainda não estiver importado neste arquivo, adicione `import pytest` ao topo do módulo.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +346 to +355
def cmd_smoke(args):
import time as _time
overall = "PASS"
steps = []
t_total = _time.monotonic()
try:
# step 1: auth — verifica token e retorna dados do usuário (reutiliza whoami)
t0 = _time.monotonic()
try:
token_val = os.environ.get("AUTENTIQUE_API_TOKEN", "")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): The smoke command is structured as if gql could raise, but gql currently exits the process instead of propagating errors.

Because gql prints JSON and calls sys.exit(2) on errors, the process ends before your try/except and steps reporting logic can run, so smoke JSON is never produced on failures. To make this work, gql needs to raise or return structured errors instead of exiting, so cmd_smoke can capture them, emit the smoke report, and still control the final exit code (e.g. remain 0).

Comment on lines +322 to +331
class TestCmdDocumentsDownload:
def test_url_ausente_exits2(self, monkeypatch, capsys, tmp_path):
"""Se URL da versão solicitada não estiver disponível → error, exit 2."""
payload = {
"document": {
"id": "doc1",
"name": "Distrato",
"files": {"original": "http://x.com/orig.pdf", "signed": None},
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Add tests for default output filename and download error handling in cmd_documents_download

Current tests cover only the missing-URL case and a successful download with an explicit --output path. Please also add (1) a test that omits output and asserts the created filename matches <name>.<version>.pdf (or <id>.<version>.pdf), and (2) a test where urllib.request.urlopen raises, asserting the JSON error payload ({"error": "Falha ao baixar", ...}) and exit code 2. This will exercise the remaining download paths.

Suggested implementation:

class TestCmdDocumentsDownload:
=======
# cmd_documents_download
# ===========================================================================

class TestCmdDocumentsDownload:
    def test_nome_arquivo_padrao_usa_nome_e_versao(self, monkeypatch, tmp_path, aut):
        """Quando --output é omitido → usa `<name>.<version>.pdf` (ou `<id>.<version>.pdf`) como nome do arquivo."""
        import io
        import json
        from pathlib import Path
        from types import SimpleNamespace
        import urllib.error

        # payload semelhante ao usado em outros testes de download, mas com URL presente
        payload = {
            "document": {
                "id": "doc1",
                "name": "Distrato",
                "files": {
                    "original": "https://exemplo.com/documento.pdf",
                    "signed": "https://exemplo.com/documento-assinado.pdf",
                },
            }
        }

        # preparar stdin com o payload
        monkeypatch.setattr(
            "sys.stdin",
            io.StringIO(json.dumps(payload)),
        )
        monkeypatch.setenv("AUTENTIQUE_API_TOKEN", "tok")

        # trabalhar em diretório temporário
        monkeypatch.chdir(tmp_path)

        # simular resposta do urlopen
        class _FakeResponse:
            def __init__(self, data: bytes):
                self._data = data

            def __enter__(self):
                return self

            def __exit__(self, exc_type, exc, tb):
                return False

            def read(self) -> bytes:
                return self._data

        def fake_urlopen(url, *args, **kwargs):
            # deve ser chamada com a URL do arquivo
            assert url == "https://exemplo.com/documento-assinado.pdf"
            return _FakeResponse(b"%PDF-1.4\n...")

        monkeypatch.setattr("urllib.request.urlopen", fake_urlopen)

        # sem --output → deve usar `<name>.<version>.pdf` (ou `<id>.<version>.pdf`)
        args = SimpleNamespace(
            id="doc1",
            version="1",
            output=None,
            sandbox=True,
        )

        aut.cmd_documents_download(args)

        # nome preferencialmente com `name`, senão com `id`
        expected_by_name = tmp_path / "Distrato.1.pdf"
        expected_by_id = tmp_path / "doc1.1.pdf"
        assert expected_by_name.is_file() or expected_by_id.is_file()

    def test_erro_ao_baixar_emite_json_e_exit_code_2(self, monkeypatch, capsys, tmp_path, aut):
        """Quando urlopen levanta exceção → imprime JSON de erro e sai com código 2."""
        import io
        import json
        from types import SimpleNamespace
        import urllib.error

        payload = {
            "document": {
                "id": "doc1",
                "name": "Distrato",
                "files": {
                    "original": "https://exemplo.com/documento.pdf",
                    "signed": "https://exemplo.com/documento-assinado.pdf",
                },
            }
        }

        monkeypatch.setattr(
            "sys.stdin",
            io.StringIO(json.dumps(payload)),
        )
        monkeypatch.setenv("AUTENTIQUE_API_TOKEN", "tok")
        monkeypatch.chdir(tmp_path)

        def failing_urlopen(url, *args, **kwargs):
            raise urllib.error.URLError("boom")

        monkeypatch.setattr("urllib.request.urlopen", failing_urlopen)

        args = SimpleNamespace(
            id="doc1",
            version="1",
            output=None,
            sandbox=True,
        )

        with pytest.raises(SystemExit) as excinfo:
            aut.cmd_documents_download(args)

        assert excinfo.value.code == 2

        captured = capsys.readouterr()
        # saída deve ser um JSON com a chave "error" e mensagem "Falha ao baixar"
        data = json.loads(captured.out)
        assert data.get("error") == "Falha ao baixar"
  1. Estes testes assumem que:

    • Existe um fixture aut com o método cmd_documents_download(args), similar ao usado em cmd_documents_create.
    • cmd_documents_download lê um JSON de sys.stdin no formato usado em test_url_ausente_exits2.
    • Quando urllib.request.urlopen lança uma exceção, cmd_documents_download captura o erro, escreve um JSON com {"error": "Falha ao baixar", ...} em stdout e encerra com SystemExit(2).
  2. Caso a função cmd_documents_download use outros nomes de argumentos (por exemplo, document_id em vez de id, ou version_number em vez de version), ajuste o SimpleNamespace nos testes para refletir a assinatura real.

  3. Se o comando não estiver a ser chamado via o fixture aut, mas sim por uma função global (por ex. cmd_documents_download(args) importada diretamente), adapte as chamadas nos testes para usar o ponto de entrada correto.

  4. Os testes usam pytest explicitamente (pytest.raises); se ainda não estiver importado neste arquivo, adicione import pytest ao topo do módulo.

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.

1 participant