testa-agent 0.1.0__tar.gz

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.
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: testa-agent
3
+ Version: 0.1.0
4
+ Summary: Testa - AI-powered post-deploy testing
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: httpx>=0.26.0
7
+ Requires-Dist: playwright>=1.41.2
8
+ Requires-Dist: pyyaml>=6.0.1
9
+ Requires-Dist: click>=8.1.0
10
+ Provides-Extra: vision
11
+ Requires-Dist: anthropic>=0.40.0; extra == "vision"
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "testa-agent"
7
+ version = "0.1.0"
8
+ description = "Testa - AI-powered post-deploy testing"
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "httpx>=0.26.0",
12
+ "playwright>=1.41.2",
13
+ "pyyaml>=6.0.1",
14
+ "click>=8.1.0",
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ vision = ["anthropic>=0.40.0"]
19
+
20
+ [project.scripts]
21
+ testa = "testa_agent.cli:main"
22
+
23
+ [tool.setuptools.packages.find]
24
+ include = ["testa_agent*"]
25
+
26
+ [tool.mypy]
27
+ python_version = "3.11"
28
+ warn_return_any = false
29
+ warn_unused_ignores = false
30
+ disallow_untyped_defs = false
31
+ ignore_missing_imports = true
32
+ check_untyped_defs = true
33
+ disable_error_code = ["import-untyped"]
34
+
35
+ [[tool.mypy.overrides]]
36
+ module = ["testa_agent.executor", "testa_agent.vision"]
37
+ disable_error_code = ["union-attr", "return-value"]
38
+
39
+ [tool.ruff]
40
+ target-version = "py311"
41
+ line-length = 100
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """Testa - AI-powered post-deploy testing agent."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,108 @@
1
+ """CLI entry point for the Testa agent."""
2
+
3
+ import asyncio
4
+ import logging
5
+ import os
6
+ import sys
7
+
8
+ import click
9
+
10
+
11
+ @click.group()
12
+ @click.version_option(package_name="testa")
13
+ def main():
14
+ """Testa - AI-powered post-deploy testing."""
15
+ pass
16
+
17
+
18
+ @main.command()
19
+ @click.option(
20
+ "--config",
21
+ "config_path",
22
+ default="./testa.yaml",
23
+ help="Path to testa.yaml config file.",
24
+ type=click.Path(exists=True),
25
+ )
26
+ @click.option(
27
+ "--api-url",
28
+ default=None,
29
+ help="Testa API URL (or set TESTA_API_URL).",
30
+ )
31
+ @click.option(
32
+ "--license-key",
33
+ default=None,
34
+ help="License key (or set TESTA_LICENSE_KEY).",
35
+ )
36
+ @click.option(
37
+ "--headless/--no-headless",
38
+ default=True,
39
+ help="Run browser in headless mode.",
40
+ )
41
+ @click.option(
42
+ "--timeout",
43
+ default=30000,
44
+ help="Default step timeout in ms.",
45
+ type=int,
46
+ )
47
+ def run(config_path, api_url, license_key, headless, timeout):
48
+ """Run tests once and exit (CI mode).
49
+
50
+ Reads tests from a local testa.yaml, executes them, reports results
51
+ to the Testa API, and exits with code 0 (all pass) or 1 (any fail).
52
+
53
+ Example:
54
+ testa run --config ./testa.yaml
55
+ """
56
+ # Configure logging for CI output
57
+ logging.basicConfig(
58
+ level=logging.INFO,
59
+ format="%(asctime)s [%(levelname)s] %(message)s",
60
+ handlers=[logging.StreamHandler(sys.stdout)],
61
+ )
62
+
63
+ # Resolve config from args or env vars
64
+ resolved_key = license_key or os.getenv("TESTA_LICENSE_KEY") or os.getenv("LICENSE_KEY")
65
+ if not resolved_key:
66
+ click.echo(
67
+ "Error: License key required. Use --license-key or set TESTA_LICENSE_KEY.", err=True
68
+ )
69
+ sys.exit(1)
70
+
71
+ resolved_url: str = (
72
+ api_url or os.getenv("TESTA_API_URL") or os.getenv("API_URL") or "http://localhost:8000"
73
+ )
74
+
75
+ from testa_agent.runner import ci_run
76
+
77
+ exit_code = asyncio.run(
78
+ ci_run(
79
+ config_path=config_path,
80
+ api_url=resolved_url,
81
+ license_key=resolved_key,
82
+ headless=headless,
83
+ default_timeout_ms=timeout,
84
+ )
85
+ )
86
+ sys.exit(exit_code)
87
+
88
+
89
+ @main.command()
90
+ def serve():
91
+ """Start the agent in polling mode (self-hosted).
92
+
93
+ Registers with the Testa API and continuously polls for work.
94
+ This is the existing agent behavior for self-hosted deployments.
95
+
96
+ Environment variables:
97
+ LICENSE_KEY: Required license key
98
+ API_URL: Testa API URL (default: http://localhost:8000)
99
+ CONFIG_PATH: Path to testa.yaml (default: /config/testa.yaml)
100
+ ANTHROPIC_API_KEY: For direct vision calls
101
+ """
102
+ from testa_agent.main import run
103
+
104
+ run()
105
+
106
+
107
+ if __name__ == "__main__":
108
+ main()
@@ -0,0 +1,64 @@
1
+ import os
2
+ from dataclasses import dataclass
3
+ from typing import Optional
4
+
5
+
6
+ @dataclass
7
+ class SlackConfig:
8
+ """Slack notification configuration."""
9
+
10
+ webhook_url: Optional[str] = None
11
+ bot_token: Optional[str] = None # For screenshot uploads
12
+ channel_id: Optional[str] = None # Required if bot_token is set
13
+ channel_name: Optional[str] = None
14
+
15
+ @property
16
+ def is_configured(self) -> bool:
17
+ """Check if Slack is configured."""
18
+ return bool(self.webhook_url)
19
+
20
+ @property
21
+ def supports_screenshots(self) -> bool:
22
+ """Check if screenshot uploads are supported."""
23
+ return bool(self.bot_token and self.channel_id)
24
+
25
+
26
+ @dataclass
27
+ class AgentConfig:
28
+ """Agent configuration loaded from environment variables."""
29
+
30
+ license_key: str
31
+ api_url: str
32
+ config_path: str
33
+ poll_interval_seconds: int = 30
34
+ slack: Optional[SlackConfig] = None
35
+
36
+ @classmethod
37
+ def from_env(cls) -> "AgentConfig":
38
+ """Load configuration from environment variables."""
39
+ license_key = os.getenv("LICENSE_KEY")
40
+ if not license_key:
41
+ raise ValueError("LICENSE_KEY environment variable is required")
42
+
43
+ api_url = os.getenv("API_URL", "http://localhost:8000")
44
+ config_path = os.getenv("CONFIG_PATH", "/config/testa.yaml")
45
+ poll_interval = int(os.getenv("POLL_INTERVAL_SECONDS", "30"))
46
+
47
+ # Load Slack config from environment
48
+ slack_config = None
49
+ slack_webhook = os.getenv("SLACK_WEBHOOK_URL")
50
+ if slack_webhook:
51
+ slack_config = SlackConfig(
52
+ webhook_url=slack_webhook,
53
+ bot_token=os.getenv("SLACK_BOT_TOKEN"),
54
+ channel_id=os.getenv("SLACK_CHANNEL_ID"),
55
+ channel_name=os.getenv("SLACK_CHANNEL_NAME"),
56
+ )
57
+
58
+ return cls(
59
+ license_key=license_key,
60
+ api_url=api_url,
61
+ config_path=config_path,
62
+ poll_interval_seconds=poll_interval,
63
+ slack=slack_config,
64
+ )