lingtest-cli 0.2.0__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.
lingtest/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ """LingTest public API."""
2
+
3
+ from lingtest.ai.models import AmbiguityAnalysis, FailureDiagnosis, GeneratedTestCase, TestCasePlan
4
+ from lingtest.models import RunReport, TestCase, TestResult
5
+ from lingtest.openapi import generate_cases
6
+ from lingtest.runner import run_cases
7
+
8
+ __all__ = [
9
+ "AmbiguityAnalysis",
10
+ "FailureDiagnosis",
11
+ "GeneratedTestCase",
12
+ "RunReport",
13
+ "TestCase",
14
+ "TestCasePlan",
15
+ "TestResult",
16
+ "generate_cases",
17
+ "run_cases",
18
+ ]
19
+ __version__ = "0.2.0"
@@ -0,0 +1,5 @@
1
+ """AI-assisted testing capabilities."""
2
+
3
+ from lingtest.ai.service import analyze_prd, diagnose_failures, generate_test_plan
4
+
5
+ __all__ = ["analyze_prd", "diagnose_failures", "generate_test_plan"]
lingtest/ai/models.py ADDED
@@ -0,0 +1,73 @@
1
+ from typing import Any, Literal
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ Severity = Literal["low", "medium", "high", "critical"]
6
+ CaseType = Literal["functional", "boundary", "exception", "api"]
7
+
8
+
9
+ class Ambiguity(BaseModel):
10
+ title: str = Field(min_length=1)
11
+ description: str = Field(min_length=1)
12
+ severity: Severity
13
+ evidence: str = ""
14
+ question: str = Field(min_length=1)
15
+
16
+
17
+ class Contradiction(BaseModel):
18
+ title: str = Field(min_length=1)
19
+ statements: list[str] = Field(min_length=2)
20
+ severity: Severity
21
+ recommendation: str = Field(min_length=1)
22
+
23
+
24
+ class AmbiguityAnalysis(BaseModel):
25
+ summary: str = Field(min_length=1)
26
+ ambiguities: list[Ambiguity] = Field(default_factory=list)
27
+ contradictions: list[Contradiction] = Field(default_factory=list)
28
+ missing_requirements: list[str] = Field(default_factory=list)
29
+ testability_risks: list[str] = Field(default_factory=list)
30
+
31
+
32
+ class GeneratedTestCase(BaseModel):
33
+ id: str = Field(min_length=1)
34
+ title: str = Field(min_length=1)
35
+ case_type: CaseType
36
+ priority: Literal["P0", "P1", "P2", "P3"] = "P1"
37
+ preconditions: list[str] = Field(default_factory=list)
38
+ steps: list[str] = Field(min_length=1)
39
+ expected_result: str = Field(min_length=1)
40
+ source_requirement: str = ""
41
+ method: str | None = None
42
+ path: str | None = None
43
+ expected_status: int | None = Field(default=None, ge=100, le=599)
44
+ headers: dict[str, str] = Field(default_factory=dict)
45
+ query: dict[str, Any] = Field(default_factory=dict)
46
+ body: Any | None = None
47
+
48
+
49
+ class TestCasePlan(BaseModel):
50
+ summary: str = Field(min_length=1)
51
+ risks: list[str] = Field(default_factory=list)
52
+ test_cases: list[GeneratedTestCase] = Field(min_length=1)
53
+
54
+
55
+ class FailureDiagnosis(BaseModel):
56
+ case_id: str = Field(min_length=1)
57
+ failure_type: Literal[
58
+ "product_defect",
59
+ "test_defect",
60
+ "environment",
61
+ "data",
62
+ "dependency",
63
+ "unknown",
64
+ ]
65
+ confidence: float = Field(ge=0, le=1)
66
+ diagnosis: str = Field(min_length=1)
67
+ evidence: list[str] = Field(default_factory=list)
68
+ suggestions: list[str] = Field(default_factory=list)
69
+
70
+
71
+ class DiagnosisReport(BaseModel):
72
+ summary: str = Field(min_length=1)
73
+ diagnoses: list[FailureDiagnosis] = Field(default_factory=list)
lingtest/ai/prompts.py ADDED
@@ -0,0 +1,43 @@
1
+ ANALYZE_SYSTEM = """You are a senior software quality architect.
2
+ Analyze product requirements for ambiguity, contradictions, missing requirements, and
3
+ testability risks. Do not invent requirements. Return one JSON object matching this shape:
4
+ {
5
+ "summary": "string",
6
+ "ambiguities": [{"title":"string","description":"string","severity":"low|medium|high|critical",
7
+ "evidence":"string","question":"string"}],
8
+ "contradictions": [{"title":"string","statements":["string","string"],
9
+ "severity":"low|medium|high|critical","recommendation":"string"}],
10
+ "missing_requirements": ["string"],
11
+ "testability_risks": ["string"]
12
+ }"""
13
+
14
+ GENERATE_SYSTEM = """You are a senior test designer. Generate reviewable test cases grounded
15
+ only in the supplied document. Cover normal, boundary, and exception behavior. For OpenAPI
16
+ documents, include API cases with method, path, expected_status, headers, query, and body.
17
+ Never place credentials in a case. Return one JSON object matching this shape:
18
+ {
19
+ "summary": "string",
20
+ "risks": ["string"],
21
+ "test_cases": [{
22
+ "id":"string","title":"string","case_type":"functional|boundary|exception|api",
23
+ "priority":"P0|P1|P2|P3","preconditions":["string"],"steps":["string"],
24
+ "expected_result":"string","source_requirement":"string",
25
+ "method":null,"path":null,"expected_status":null,"headers":{},"query":{},"body":null
26
+ }]
27
+ }"""
28
+
29
+ DIAGNOSE_SYSTEM = """You are a senior test failure analyst. Diagnose only failed or errored
30
+ test results. Separate product defects from test defects, environment, data, and dependency
31
+ issues. Ground each diagnosis in supplied evidence and avoid false certainty. Return one JSON
32
+ object matching this shape:
33
+ {
34
+ "summary":"string",
35
+ "diagnoses":[{
36
+ "case_id":"string",
37
+ "failure_type":"product_defect|test_defect|environment|data|dependency|unknown",
38
+ "confidence":0.0,
39
+ "diagnosis":"string",
40
+ "evidence":["string"],
41
+ "suggestions":["string"]
42
+ }]
43
+ }"""
@@ -0,0 +1,87 @@
1
+ import os
2
+ from dataclasses import dataclass
3
+ from typing import Any, Protocol
4
+
5
+ import httpx
6
+
7
+
8
+ class ProviderError(RuntimeError):
9
+ """Raised when an LLM provider request fails."""
10
+
11
+
12
+ class LLMProvider(Protocol):
13
+ def complete(self, messages: list[dict[str, str]], *, json_mode: bool = True) -> str: ...
14
+
15
+
16
+ @dataclass(slots=True)
17
+ class OpenAICompatibleProvider:
18
+ api_key: str
19
+ model: str
20
+ base_url: str = "https://api.openai.com/v1"
21
+ timeout: float = 60.0
22
+ temperature: float = 0.2
23
+
24
+ def complete(self, messages: list[dict[str, str]], *, json_mode: bool = True) -> str:
25
+ payload: dict[str, Any] = {
26
+ "model": self.model,
27
+ "messages": messages,
28
+ "temperature": self.temperature,
29
+ }
30
+ if json_mode:
31
+ payload["response_format"] = {"type": "json_object"}
32
+ try:
33
+ response = httpx.post(
34
+ f"{self.base_url.rstrip('/')}/chat/completions",
35
+ headers={
36
+ "Authorization": f"Bearer {self.api_key}",
37
+ "Content-Type": "application/json",
38
+ },
39
+ json=payload,
40
+ timeout=self.timeout,
41
+ )
42
+ response.raise_for_status()
43
+ data = response.json()
44
+ return data["choices"][0]["message"]["content"]
45
+ except (httpx.HTTPError, KeyError, TypeError, ValueError) as exc:
46
+ raise ProviderError(f"LLM request failed: {exc}") from exc
47
+
48
+
49
+ PROVIDERS = {
50
+ "openai": ("https://api.openai.com/v1", "gpt-4.1-mini", "OPENAI_API_KEY"),
51
+ "deepseek": ("https://api.deepseek.com/v1", "deepseek-chat", "DEEPSEEK_API_KEY"),
52
+ "qwen": (
53
+ "https://dashscope.aliyuncs.com/compatible-mode/v1",
54
+ "qwen-plus",
55
+ "DASHSCOPE_API_KEY",
56
+ ),
57
+ }
58
+
59
+
60
+ def create_provider(
61
+ provider: str,
62
+ *,
63
+ model: str | None = None,
64
+ base_url: str | None = None,
65
+ api_key: str | None = None,
66
+ timeout: float = 60.0,
67
+ ) -> OpenAICompatibleProvider:
68
+ name = provider.lower()
69
+ if name == "custom":
70
+ if not base_url or not model:
71
+ raise ValueError("Custom provider requires --base-url and --model")
72
+ env_name = "LINGTEST_API_KEY"
73
+ default_url, default_model = base_url, model
74
+ elif name in PROVIDERS:
75
+ default_url, default_model, env_name = PROVIDERS[name]
76
+ else:
77
+ raise ValueError(f"Unknown provider {provider!r}; use openai, deepseek, qwen, or custom")
78
+
79
+ resolved_key = api_key or os.getenv(env_name) or os.getenv("LINGTEST_API_KEY")
80
+ if not resolved_key:
81
+ raise ValueError(f"Missing API key; set {env_name} or LINGTEST_API_KEY")
82
+ return OpenAICompatibleProvider(
83
+ api_key=resolved_key,
84
+ model=model or default_model,
85
+ base_url=base_url or default_url,
86
+ timeout=timeout,
87
+ )
lingtest/ai/service.py ADDED
@@ -0,0 +1,84 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ from lingtest.ai.models import AmbiguityAnalysis, DiagnosisReport, TestCasePlan
5
+ from lingtest.ai.prompts import ANALYZE_SYSTEM, DIAGNOSE_SYSTEM, GENERATE_SYSTEM
6
+ from lingtest.ai.provider import LLMProvider
7
+ from lingtest.ai.structured import request_structured
8
+ from lingtest.models import RunReport
9
+
10
+ MAX_DOCUMENT_CHARS = 60_000
11
+
12
+
13
+ def read_document(path: str | Path) -> str:
14
+ document = Path(path)
15
+ if document.suffix.lower() not in {".txt", ".md", ".json", ".yaml", ".yml"}:
16
+ raise ValueError("Supported document types: .txt, .md, .json, .yaml, .yml")
17
+ content = document.read_text(encoding="utf-8")
18
+ if not content.strip():
19
+ raise ValueError("Document is empty")
20
+ return content[:MAX_DOCUMENT_CHARS]
21
+
22
+
23
+ def analyze_prd(
24
+ path: str | Path, provider: LLMProvider, *, language: str = "zh-CN"
25
+ ) -> AmbiguityAnalysis:
26
+ content = read_document(path)
27
+ return request_structured(
28
+ provider,
29
+ [
30
+ {"role": "system", "content": ANALYZE_SYSTEM},
31
+ {
32
+ "role": "user",
33
+ "content": f"Respond in {language}. Analyze this document:\n\n{content}",
34
+ },
35
+ ],
36
+ AmbiguityAnalysis,
37
+ )
38
+
39
+
40
+ def generate_test_plan(
41
+ path: str | Path,
42
+ provider: LLMProvider,
43
+ *,
44
+ language: str = "zh-CN",
45
+ ) -> TestCasePlan:
46
+ content = read_document(path)
47
+ return request_structured(
48
+ provider,
49
+ [
50
+ {"role": "system", "content": GENERATE_SYSTEM},
51
+ {
52
+ "role": "user",
53
+ "content": f"Respond in {language}. Generate tests for this document:\n\n{content}",
54
+ },
55
+ ],
56
+ TestCasePlan,
57
+ )
58
+
59
+
60
+ def diagnose_failures(
61
+ report: RunReport,
62
+ provider: LLMProvider,
63
+ *,
64
+ language: str = "zh-CN",
65
+ ) -> DiagnosisReport:
66
+ failures = [result for result in report.results if result.status != "passed"]
67
+ if not failures:
68
+ return DiagnosisReport(summary="No failed or errored test cases to diagnose.", diagnoses=[])
69
+ evidence = json.dumps(
70
+ [result.model_dump(mode="json") for result in failures],
71
+ ensure_ascii=False,
72
+ indent=2,
73
+ )
74
+ return request_structured(
75
+ provider,
76
+ [
77
+ {"role": "system", "content": DIAGNOSE_SYSTEM},
78
+ {
79
+ "role": "user",
80
+ "content": f"Respond in {language}. Diagnose these test results:\n\n{evidence}",
81
+ },
82
+ ],
83
+ DiagnosisReport,
84
+ )
@@ -0,0 +1,86 @@
1
+ import json
2
+ from typing import TypeVar
3
+
4
+ from pydantic import BaseModel, ValidationError
5
+
6
+ from lingtest.ai.provider import LLMProvider
7
+
8
+ T = TypeVar("T", bound=BaseModel)
9
+
10
+
11
+ class StructuredOutputError(ValueError):
12
+ """Raised when an LLM response cannot satisfy the requested schema."""
13
+
14
+
15
+ def parse_json_object(raw: str) -> dict:
16
+ text = raw.strip()
17
+ if text.startswith("```"):
18
+ lines = text.splitlines()
19
+ text = "\n".join(lines[1:-1] if lines[-1].strip() == "```" else lines[1:])
20
+ try:
21
+ value = json.loads(text)
22
+ except json.JSONDecodeError:
23
+ value = json.loads(_extract_object(text))
24
+ if not isinstance(value, dict):
25
+ raise StructuredOutputError("LLM response must be a JSON object")
26
+ return value
27
+
28
+
29
+ def request_structured(
30
+ provider: LLMProvider,
31
+ messages: list[dict[str, str]],
32
+ schema: type[T],
33
+ *,
34
+ retries: int = 1,
35
+ ) -> T:
36
+ current = list(messages)
37
+ last_error: Exception | None = None
38
+ for attempt in range(retries + 1):
39
+ raw = provider.complete(current, json_mode=True)
40
+ try:
41
+ return schema.model_validate(parse_json_object(raw))
42
+ except (json.JSONDecodeError, StructuredOutputError, ValidationError) as exc:
43
+ last_error = exc
44
+ if attempt >= retries:
45
+ break
46
+ current.extend(
47
+ [
48
+ {"role": "assistant", "content": raw},
49
+ {
50
+ "role": "user",
51
+ "content": (
52
+ "The response failed schema validation. Return only a corrected JSON "
53
+ f"object. Validation error: {exc}"
54
+ ),
55
+ },
56
+ ]
57
+ )
58
+ raise StructuredOutputError(f"LLM output failed validation: {last_error}") from last_error
59
+
60
+
61
+ def _extract_object(text: str) -> str:
62
+ start = text.find("{")
63
+ if start < 0:
64
+ raise StructuredOutputError("LLM response does not contain a JSON object")
65
+ depth = 0
66
+ in_string = False
67
+ escaped = False
68
+ for index in range(start, len(text)):
69
+ char = text[index]
70
+ if in_string:
71
+ if escaped:
72
+ escaped = False
73
+ elif char == "\\":
74
+ escaped = True
75
+ elif char == '"':
76
+ in_string = False
77
+ continue
78
+ if char == '"':
79
+ in_string = True
80
+ elif char == "{":
81
+ depth += 1
82
+ elif char == "}":
83
+ depth -= 1
84
+ if depth == 0:
85
+ return text[start : index + 1]
86
+ raise StructuredOutputError("LLM response contains an incomplete JSON object")
lingtest/cli.py ADDED
@@ -0,0 +1,171 @@
1
+ import json
2
+ from pathlib import Path
3
+ from typing import Annotated
4
+
5
+ import typer
6
+
7
+ from lingtest import __version__
8
+ from lingtest.ai.provider import ProviderError, create_provider
9
+ from lingtest.ai.service import analyze_prd, diagnose_failures, generate_test_plan
10
+ from lingtest.ai.structured import StructuredOutputError
11
+ from lingtest.io import read_cases, read_report, write_cases, write_junit, write_report
12
+ from lingtest.openapi import generate_cases
13
+ from lingtest.runner import run_cases
14
+
15
+ app = typer.Typer(help="AI-assisted testing for PRDs and OpenAPI.", no_args_is_help=True)
16
+
17
+
18
+ def version_callback(value: bool) -> None:
19
+ if value:
20
+ typer.echo(__version__)
21
+ raise typer.Exit()
22
+
23
+
24
+ @app.callback()
25
+ def main(
26
+ version: Annotated[
27
+ bool | None,
28
+ typer.Option("--version", callback=version_callback, is_eager=True, help="Show version."),
29
+ ] = None,
30
+ ) -> None:
31
+ pass
32
+
33
+
34
+ @app.command()
35
+ def generate(
36
+ spec: Annotated[Path, typer.Argument(help="OpenAPI JSON or YAML file.")],
37
+ output: Annotated[Path, typer.Option("-o", "--output")] = Path("lingtest-cases.json"),
38
+ ) -> None:
39
+ """Generate editable API test cases."""
40
+ try:
41
+ cases = generate_cases(spec)
42
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
43
+ typer.echo(f"Error: {exc}", err=True)
44
+ raise typer.Exit(2) from exc
45
+ write_cases(cases, output)
46
+ typer.echo(f"Generated {len(cases)} cases -> {output}")
47
+
48
+
49
+ @app.command()
50
+ def analyze(
51
+ document: Annotated[Path, typer.Argument(help="PRD or requirements document.")],
52
+ output: Annotated[Path, typer.Option("-o", "--output")] = Path("lingtest-analysis.json"),
53
+ provider: Annotated[str, typer.Option(help="openai, deepseek, qwen, or custom.")] = "openai",
54
+ model: Annotated[str | None, typer.Option()] = None,
55
+ base_url: Annotated[str | None, typer.Option()] = None,
56
+ language: Annotated[str, typer.Option()] = "zh-CN",
57
+ ) -> None:
58
+ """Find ambiguities, contradictions, missing requirements, and testability risks."""
59
+ try:
60
+ result = analyze_prd(
61
+ document,
62
+ _create_provider(provider, model, base_url),
63
+ language=language,
64
+ )
65
+ _write_model(result, output)
66
+ except (OSError, ValueError, ProviderError, StructuredOutputError) as exc:
67
+ _fail(exc)
68
+ typer.echo(
69
+ f"Found {len(result.ambiguities)} ambiguities and "
70
+ f"{len(result.contradictions)} contradictions -> {output}"
71
+ )
72
+
73
+
74
+ @app.command("ai-generate")
75
+ def ai_generate(
76
+ document: Annotated[Path, typer.Argument(help="PRD, requirements, or OpenAPI document.")],
77
+ output: Annotated[Path, typer.Option("-o", "--output")] = Path("lingtest-ai-cases.json"),
78
+ provider: Annotated[str, typer.Option(help="openai, deepseek, qwen, or custom.")] = "openai",
79
+ model: Annotated[str | None, typer.Option()] = None,
80
+ base_url: Annotated[str | None, typer.Option()] = None,
81
+ language: Annotated[str, typer.Option()] = "zh-CN",
82
+ ) -> None:
83
+ """Use an LLM to generate functional, boundary, exception, and API cases."""
84
+ try:
85
+ result = generate_test_plan(
86
+ document,
87
+ _create_provider(provider, model, base_url),
88
+ language=language,
89
+ )
90
+ _write_model(result, output)
91
+ except (OSError, ValueError, ProviderError, StructuredOutputError) as exc:
92
+ _fail(exc)
93
+ typer.echo(f"Generated {len(result.test_cases)} AI test cases -> {output}")
94
+
95
+
96
+ @app.command()
97
+ def diagnose(
98
+ report_file: Annotated[Path, typer.Argument(help="LingTest JSON run report.")],
99
+ output: Annotated[Path, typer.Option("-o", "--output")] = Path("lingtest-diagnosis.json"),
100
+ provider: Annotated[str, typer.Option(help="openai, deepseek, qwen, or custom.")] = "openai",
101
+ model: Annotated[str | None, typer.Option()] = None,
102
+ base_url: Annotated[str | None, typer.Option()] = None,
103
+ language: Annotated[str, typer.Option()] = "zh-CN",
104
+ ) -> None:
105
+ """Use an LLM to diagnose failed and errored test results."""
106
+ try:
107
+ result = diagnose_failures(
108
+ read_report(report_file),
109
+ _create_provider(provider, model, base_url),
110
+ language=language,
111
+ )
112
+ _write_model(result, output)
113
+ except (OSError, ValueError, ProviderError, StructuredOutputError) as exc:
114
+ _fail(exc)
115
+ typer.echo(f"Diagnosed {len(result.diagnoses)} failures -> {output}")
116
+
117
+
118
+ @app.command("run")
119
+ def run_command(
120
+ cases_file: Annotated[Path, typer.Argument(help="Generated cases JSON file.")],
121
+ base_url: Annotated[str, typer.Option("--base-url", envvar="LINGTEST_BASE_URL")],
122
+ output: Annotated[Path, typer.Option("-o", "--output")] = Path("lingtest-report.json"),
123
+ junit: Annotated[Path | None, typer.Option("--junit")] = None,
124
+ timeout: Annotated[float, typer.Option(min=0.1)] = 10.0,
125
+ header: Annotated[
126
+ list[str] | None, typer.Option("--header", "-H", help="HTTP header as NAME:VALUE.")
127
+ ] = None,
128
+ ) -> None:
129
+ """Run generated test cases against an API."""
130
+ try:
131
+ headers = _parse_headers(header or [])
132
+ report = run_cases(
133
+ read_cases(cases_file), base_url, timeout=timeout, extra_headers=headers
134
+ )
135
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
136
+ typer.echo(f"Error: {exc}", err=True)
137
+ raise typer.Exit(2) from exc
138
+ write_report(report, output)
139
+ if junit:
140
+ write_junit(report, junit)
141
+ typer.echo(f"{report.passed} passed, {report.failed} failed -> {output}")
142
+ if report.failed:
143
+ raise typer.Exit(1)
144
+
145
+
146
+ def _parse_headers(values: list[str]) -> dict[str, str]:
147
+ headers: dict[str, str] = {}
148
+ for value in values:
149
+ if ":" not in value:
150
+ raise ValueError(f"Invalid header {value!r}; expected NAME:VALUE")
151
+ name, content = value.split(":", 1)
152
+ headers[name.strip()] = content.strip()
153
+ return headers
154
+
155
+
156
+ def _create_provider(provider: str, model: str | None, base_url: str | None):
157
+ return create_provider(provider, model=model, base_url=base_url)
158
+
159
+
160
+ def _write_model(model, path: Path) -> None:
161
+ path.parent.mkdir(parents=True, exist_ok=True)
162
+ path.write_text(model.model_dump_json(indent=2), encoding="utf-8")
163
+
164
+
165
+ def _fail(exc: Exception) -> None:
166
+ typer.echo(f"Error: {exc}", err=True)
167
+ raise typer.Exit(2) from exc
168
+
169
+
170
+ if __name__ == "__main__":
171
+ app()
lingtest/io.py ADDED
@@ -0,0 +1,48 @@
1
+ import json
2
+ from pathlib import Path
3
+ from xml.etree.ElementTree import Element, ElementTree, SubElement
4
+
5
+ from lingtest.models import RunReport, TestCase
6
+
7
+
8
+ def write_cases(cases: list[TestCase], path: Path) -> None:
9
+ path.parent.mkdir(parents=True, exist_ok=True)
10
+ path.write_text(
11
+ json.dumps([case.model_dump(mode="json") for case in cases], indent=2),
12
+ encoding="utf-8",
13
+ )
14
+
15
+
16
+ def read_cases(path: Path) -> list[TestCase]:
17
+ return [TestCase.model_validate(item) for item in json.loads(path.read_text(encoding="utf-8"))]
18
+
19
+
20
+ def write_report(report: RunReport, path: Path) -> None:
21
+ path.parent.mkdir(parents=True, exist_ok=True)
22
+ path.write_text(report.model_dump_json(indent=2), encoding="utf-8")
23
+
24
+
25
+ def read_report(path: Path) -> RunReport:
26
+ return RunReport.model_validate_json(path.read_text(encoding="utf-8"))
27
+
28
+
29
+ def write_junit(report: RunReport, path: Path) -> None:
30
+ suite = Element(
31
+ "testsuite",
32
+ name="lingtest",
33
+ tests=str(len(report.results)),
34
+ failures=str(report.failed),
35
+ )
36
+ for result in report.results:
37
+ case = SubElement(
38
+ suite,
39
+ "testcase",
40
+ name=result.title,
41
+ classname=result.case_id,
42
+ time=f"{result.duration_ms / 1000:.6f}",
43
+ )
44
+ if result.status != "passed":
45
+ failure = SubElement(case, "failure", message=result.error or result.status)
46
+ failure.text = result.error or ""
47
+ path.parent.mkdir(parents=True, exist_ok=True)
48
+ ElementTree(suite).write(path, encoding="utf-8", xml_declaration=True)
lingtest/models.py ADDED
@@ -0,0 +1,40 @@
1
+ from datetime import UTC, datetime
2
+ from typing import Any, Literal
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+
7
+ class TestCase(BaseModel):
8
+ id: str
9
+ title: str
10
+ method: str
11
+ path: str
12
+ expected_status: int
13
+ headers: dict[str, str] = Field(default_factory=dict)
14
+ query: dict[str, Any] = Field(default_factory=dict)
15
+ body: Any | None = None
16
+
17
+
18
+ class TestResult(BaseModel):
19
+ case_id: str
20
+ title: str
21
+ status: Literal["passed", "failed", "error"]
22
+ duration_ms: float
23
+ expected_status: int
24
+ actual_status: int | None = None
25
+ error: str | None = None
26
+
27
+
28
+ class RunReport(BaseModel):
29
+ started_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
30
+ base_url: str
31
+ results: list[TestResult]
32
+
33
+ @property
34
+ def passed(self) -> int:
35
+ return sum(result.status == "passed" for result in self.results)
36
+
37
+ @property
38
+ def failed(self) -> int:
39
+ return len(self.results) - self.passed
40
+
lingtest/openapi.py ADDED
@@ -0,0 +1,108 @@
1
+ import json
2
+ import re
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ import yaml
7
+
8
+ from lingtest.models import TestCase
9
+
10
+ HTTP_METHODS = {"get", "post", "put", "patch", "delete", "head", "options"}
11
+
12
+
13
+ def load_spec(path: Path) -> dict[str, Any]:
14
+ raw = path.read_text(encoding="utf-8")
15
+ data = json.loads(raw) if path.suffix.lower() == ".json" else yaml.safe_load(raw)
16
+ if not isinstance(data, dict) or "paths" not in data:
17
+ raise ValueError("Input is not an OpenAPI document: missing 'paths'")
18
+ return data
19
+
20
+
21
+ def generate_cases(path: str | Path) -> list[TestCase]:
22
+ spec = load_spec(Path(path))
23
+ cases: list[TestCase] = []
24
+ for route, path_item in spec["paths"].items():
25
+ if not isinstance(path_item, dict):
26
+ continue
27
+ common_parameters = path_item.get("parameters", [])
28
+ for method, operation in path_item.items():
29
+ if method.lower() not in HTTP_METHODS or not isinstance(operation, dict):
30
+ continue
31
+ parameters = [*common_parameters, *operation.get("parameters", [])]
32
+ rendered_path, query, headers = _parameters(route, parameters)
33
+ expected = _expected_status(operation.get("responses", {}), method)
34
+ operation_id = operation.get("operationId") or _slug(f"{method}_{route}")
35
+ cases.append(
36
+ TestCase(
37
+ id=operation_id,
38
+ title=operation.get("summary") or operation_id.replace("_", " ").title(),
39
+ method=method.upper(),
40
+ path=rendered_path,
41
+ expected_status=expected,
42
+ query=query,
43
+ headers=headers,
44
+ body=_request_body(operation.get("requestBody", {})),
45
+ )
46
+ )
47
+ return cases
48
+
49
+
50
+ def _parameters(
51
+ route: str, parameters: list[dict[str, Any]]
52
+ ) -> tuple[str, dict[str, Any], dict[str, str]]:
53
+ query: dict[str, Any] = {}
54
+ headers: dict[str, str] = {}
55
+ for parameter in parameters:
56
+ name = parameter.get("name")
57
+ if not name:
58
+ continue
59
+ value = _example(parameter.get("schema", {}), parameter.get("example"))
60
+ location = parameter.get("in")
61
+ if location == "path":
62
+ route = route.replace(f"{{{name}}}", str(value))
63
+ elif location == "query":
64
+ query[name] = value
65
+ elif location == "header":
66
+ headers[name] = str(value)
67
+ return route, query, headers
68
+
69
+
70
+ def _request_body(request_body: dict[str, Any]) -> Any | None:
71
+ content = request_body.get("content", {})
72
+ media = content.get("application/json")
73
+ if not isinstance(media, dict):
74
+ return None
75
+ if "example" in media:
76
+ return media["example"]
77
+ return _example(media.get("schema", {}))
78
+
79
+
80
+ def _example(schema: dict[str, Any], explicit: Any = None) -> Any:
81
+ if explicit is not None:
82
+ return explicit
83
+ if "example" in schema:
84
+ return schema["example"]
85
+ if "default" in schema:
86
+ return schema["default"]
87
+ if "enum" in schema and schema["enum"]:
88
+ return schema["enum"][0]
89
+ kind = schema.get("type")
90
+ if kind == "object":
91
+ return {name: _example(value) for name, value in schema.get("properties", {}).items()}
92
+ if kind == "array":
93
+ return [_example(schema.get("items", {}))]
94
+ return {"integer": 1, "number": 1.0, "boolean": True, "string": "test"}.get(kind, "test")
95
+
96
+
97
+ def _expected_status(responses: dict[str, Any], method: str) -> int:
98
+ success = sorted(
99
+ int(code) for code in responses if str(code).isdigit() and 200 <= int(code) < 300
100
+ )
101
+ if success:
102
+ return success[0]
103
+ return 201 if method.lower() == "post" else 200
104
+
105
+
106
+ def _slug(value: str) -> str:
107
+ return re.sub(r"[^a-z0-9]+", "_", value.lower()).strip("_")
108
+
lingtest/runner.py ADDED
@@ -0,0 +1,48 @@
1
+ from time import perf_counter
2
+
3
+ import httpx
4
+
5
+ from lingtest.models import RunReport, TestCase, TestResult
6
+
7
+
8
+ def run_cases(
9
+ cases: list[TestCase],
10
+ base_url: str,
11
+ *,
12
+ timeout: float = 10.0,
13
+ extra_headers: dict[str, str] | None = None,
14
+ ) -> RunReport:
15
+ results: list[TestResult] = []
16
+ with httpx.Client(base_url=base_url.rstrip("/"), timeout=timeout) as client:
17
+ for case in cases:
18
+ started = perf_counter()
19
+ actual_status = None
20
+ error = None
21
+ status = "error"
22
+ try:
23
+ response = client.request(
24
+ case.method,
25
+ case.path,
26
+ params=case.query,
27
+ headers={**case.headers, **(extra_headers or {})},
28
+ json=case.body,
29
+ )
30
+ actual_status = response.status_code
31
+ status = "passed" if actual_status == case.expected_status else "failed"
32
+ if status == "failed":
33
+ error = f"expected HTTP {case.expected_status}, got HTTP {actual_status}"
34
+ except httpx.HTTPError as exc:
35
+ error = str(exc)
36
+ results.append(
37
+ TestResult(
38
+ case_id=case.id,
39
+ title=case.title,
40
+ status=status,
41
+ duration_ms=(perf_counter() - started) * 1000,
42
+ expected_status=case.expected_status,
43
+ actual_status=actual_status,
44
+ error=error,
45
+ )
46
+ )
47
+ return RunReport(base_url=base_url, results=results)
48
+
@@ -0,0 +1,410 @@
1
+ Metadata-Version: 2.4
2
+ Name: lingtest-cli
3
+ Version: 0.2.0
4
+ Summary: AI testing agent for PRDs, OpenAPI specifications, and Vibe Coding workflows
5
+ Project-URL: Homepage, https://github.com/sunweitest/lingtest-cli
6
+ Project-URL: Repository, https://github.com/sunweitest/lingtest-cli
7
+ Project-URL: Issues, https://github.com/sunweitest/lingtest-cli/issues
8
+ Author: LingTest Contributors
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai,api,cli,openapi,pytest,testing,vibe-coding
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Testing
19
+ Requires-Python: >=3.11
20
+ Requires-Dist: httpx<1,>=0.27
21
+ Requires-Dist: pydantic<3,>=2.8
22
+ Requires-Dist: pyyaml<7,>=6.0
23
+ Requires-Dist: typer<1,>=0.12
24
+ Provides-Extra: ai
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest-cov<7,>=5; extra == 'dev'
27
+ Requires-Dist: pytest<9,>=8.2; extra == 'dev'
28
+ Requires-Dist: ruff>=0.6; extra == 'dev'
29
+ Requires-Dist: twine<7,>=5; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # LingTest CLI
33
+
34
+ [简体中文](README.zh-CN.md) | English
35
+
36
+ LingTest CLI is a local-first AI testing agent for Vibe Coding workflows. It
37
+ analyzes PRDs, generates validated test plans from requirements and OpenAPI,
38
+ runs deterministic HTTP tests, and diagnoses failures with structured evidence.
39
+
40
+ The deterministic core does not require an LLM or API key. Generated cases are
41
+ plain JSON, so they can be reviewed and changed before any request is sent.
42
+
43
+ > Status: `0.2.0` alpha. The file format and Python API may evolve before 1.0.
44
+
45
+ ## What's New in 0.2.0
46
+
47
+ - Independent OpenAI-compatible provider layer with no FastAPI, Redis, database,
48
+ or worker dependency.
49
+ - Built-in provider presets for OpenAI, DeepSeek, and Qwen, plus custom
50
+ `base_url` and model support.
51
+ - `lingtest analyze` for PRD ambiguity, contradiction, missing requirement, and
52
+ testability-risk analysis.
53
+ - `lingtest ai-generate` for functional, boundary, exception, and API test plans.
54
+ - `lingtest diagnose` for evidence-grounded failure classification and advice.
55
+ - Pydantic validation for every AI result, with one controlled correction retry.
56
+ - Provider credentials read from environment variables and excluded from cases
57
+ and reports.
58
+ - Expanded Chinese and English documentation, product design, and Vibe Coding
59
+ testing article.
60
+
61
+ ## Features
62
+
63
+ - Read OpenAPI specifications in YAML or JSON.
64
+ - Discover GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS operations.
65
+ - Generate values from `example`, `default`, `enum`, and schema types.
66
+ - Resolve path parameters and generate query, header, and JSON body values.
67
+ - Select the first documented 2xx response as the expected status.
68
+ - Run cases with HTTPX using a configurable base URL and timeout.
69
+ - Add runtime headers without storing credentials in generated cases.
70
+ - Write detailed JSON reports and optional JUnit XML.
71
+ - Return CI-friendly exit codes.
72
+ - Use the same core through a small Python API.
73
+ - Analyze PRDs for ambiguity, contradictions, missing requirements, and testability risks.
74
+ - Generate validated functional, boundary, exception, and API test plans with an LLM.
75
+ - Diagnose failed HTTP tests with evidence-grounded structured output.
76
+ - Connect to OpenAI, DeepSeek, Qwen, or a custom OpenAI-compatible provider.
77
+
78
+ ## Requirements
79
+
80
+ - Python 3.11 or newer
81
+ - An OpenAPI 3.x YAML or JSON document
82
+ - Network access to the API under test when running cases
83
+
84
+ ## Installation
85
+
86
+ ### Standard `pip`
87
+
88
+ Install into the active Python environment or virtual environment:
89
+
90
+ ```bash
91
+ python -m pip install lingtest-cli
92
+ lingtest --version
93
+ ```
94
+
95
+ Use this when LingTest should share an existing project environment or when
96
+ `pip` is your standard package manager.
97
+
98
+ ### Isolated CLI with `pipx` (recommended)
99
+
100
+ ```bash
101
+ pipx install lingtest-cli
102
+ ```
103
+
104
+ `pipx` creates a dedicated environment while making the `lingtest` command
105
+ available globally. This avoids dependency conflicts with application projects.
106
+
107
+ ### Isolated CLI with `uv`
108
+
109
+ ```bash
110
+ uv tool install lingtest-cli
111
+ ```
112
+
113
+ ### Add to a UV project
114
+
115
+ ```bash
116
+ uv add --dev lingtest-cli
117
+ uv run lingtest --help
118
+ ```
119
+
120
+ All installation methods provide the same `lingtest` command and Python API.
121
+
122
+ ## Quick Start
123
+
124
+ Generate cases:
125
+
126
+ ```bash
127
+ lingtest generate openapi.yaml -o cases.json
128
+ ```
129
+
130
+ Review `cases.json`, then run it:
131
+
132
+ ```bash
133
+ lingtest run cases.json \
134
+ --base-url http://localhost:8000 \
135
+ --junit junit.xml \
136
+ -o report.json
137
+ ```
138
+
139
+ PowerShell authentication example:
140
+
141
+ ```powershell
142
+ lingtest run cases.json `
143
+ --base-url https://api.example.com `
144
+ -H "Authorization:Bearer $env:API_TOKEN" `
145
+ --junit junit.xml
146
+ ```
147
+
148
+ Use `-H` multiple times to add more headers:
149
+
150
+ ```bash
151
+ lingtest run cases.json --base-url https://api.example.com \
152
+ -H "Authorization:Bearer ${API_TOKEN}" \
153
+ -H "X-Tenant-ID:demo"
154
+ ```
155
+
156
+ Analyze requirements and generate an AI test plan:
157
+
158
+ ```bash
159
+ export OPENAI_API_KEY="..."
160
+ lingtest analyze requirements.md -o analysis.json
161
+ lingtest ai-generate requirements.md -o ai-cases.json
162
+ ```
163
+
164
+ Diagnose failed cases from a LingTest JSON report:
165
+
166
+ ```bash
167
+ lingtest diagnose report.json -o diagnosis.json
168
+ ```
169
+
170
+ Use `--provider deepseek`, `--provider qwen`, or a custom OpenAI-compatible
171
+ endpoint with `--provider custom --base-url URL --model MODEL`.
172
+
173
+ ## Test Data Flow
174
+
175
+ ```mermaid
176
+ flowchart LR
177
+ PRD["PRD / Requirements"] --> Analyze["lingtest analyze"]
178
+ Analyze --> Analysis["analysis.json<br/>ambiguities and risks"]
179
+
180
+ PRD --> AIGenerate["lingtest ai-generate"]
181
+ OpenAPI["OpenAPI YAML / JSON"] --> AIGenerate
182
+ AIGenerate --> AIPlan["ai-cases.json<br/>validated test plan"]
183
+ AIPlan --> Review["Human review"]
184
+
185
+ OpenAPI --> Generate["lingtest generate"]
186
+ Generate --> Cases["cases.json<br/>executable HTTP cases"]
187
+ Review -.->|planned 0.3 conversion| Cases
188
+
189
+ Cases --> Run["lingtest run"]
190
+ Runtime["Base URL + runtime headers"] --> Run
191
+ Run --> JSONReport["report.json<br/>original evidence"]
192
+ Run --> JUnit["junit.xml<br/>CI result"]
193
+
194
+ JSONReport --> Diagnose["lingtest diagnose"]
195
+ Diagnose --> Diagnosis["diagnosis.json<br/>classification, evidence, advice"]
196
+ ```
197
+
198
+ The solid path is implemented in `0.2.0`. AI plans remain review artifacts;
199
+ automatic conversion from an approved AI plan to executable HTTP cases is
200
+ planned for `0.3`. Diagnosis produces a separate file and never overwrites the
201
+ original run report.
202
+
203
+ ## Commands
204
+
205
+ ### `lingtest analyze`
206
+
207
+ Finds ambiguities, contradictions, missing requirements, and testability risks
208
+ in `.txt`, `.md`, `.json`, `.yaml`, and `.yml` documents.
209
+
210
+ ### `lingtest ai-generate`
211
+
212
+ Generates a validated plan containing functional, boundary, exception, and API
213
+ test cases. AI cases are review artifacts and are never executed implicitly.
214
+
215
+ ### `lingtest diagnose`
216
+
217
+ Reads a LingTest JSON run report and classifies failed cases using the supplied
218
+ execution evidence.
219
+
220
+ ### `lingtest generate`
221
+
222
+ ```text
223
+ lingtest generate SPEC [-o OUTPUT]
224
+ ```
225
+
226
+ `SPEC` is an OpenAPI YAML or JSON file. The default output is
227
+ `lingtest-cases.json`.
228
+
229
+ ### `lingtest run`
230
+
231
+ ```text
232
+ lingtest run CASES_FILE --base-url URL [OPTIONS]
233
+ ```
234
+
235
+ Important options:
236
+
237
+ | Option | Description | Default |
238
+ | --- | --- | --- |
239
+ | `--base-url` | Base URL of the API under test. It can also be provided through `LINGTEST_BASE_URL`. | Required |
240
+ | `-o`, `--output` | JSON report path. | `lingtest-report.json` |
241
+ | `--junit` | Optional JUnit XML report path. | Disabled |
242
+ | `--timeout` | Per-request timeout in seconds. | `10.0` |
243
+ | `-H`, `--header` | Runtime HTTP header in `NAME:VALUE` form. Repeatable. | None |
244
+
245
+ Exit codes:
246
+
247
+ | Code | Meaning |
248
+ | --- | --- |
249
+ | `0` | All test cases passed. |
250
+ | `1` | One or more test cases failed or produced a request error. |
251
+ | `2` | The command input or case file is invalid. |
252
+
253
+ ## Case File
254
+
255
+ Generated files are JSON arrays. A case looks like this:
256
+
257
+ ```json
258
+ {
259
+ "id": "get_user",
260
+ "title": "Get user",
261
+ "method": "GET",
262
+ "path": "/users/42",
263
+ "expected_status": 200,
264
+ "headers": {},
265
+ "query": {
266
+ "verbose": true
267
+ },
268
+ "body": null
269
+ }
270
+ ```
271
+
272
+ Edit `expected_status`, request values, or titles before execution. Do not put
273
+ long-lived credentials in this file; inject them with `--header` and environment
274
+ variables at runtime.
275
+
276
+ ## Reports
277
+
278
+ The JSON report contains the start time, base URL, and a result for each case:
279
+
280
+ - case ID and title
281
+ - `passed`, `failed`, or `error` status
282
+ - request duration in milliseconds
283
+ - expected and actual HTTP status
284
+ - a concise error message
285
+
286
+ JUnit output can be uploaded to GitHub Actions, Jenkins, GitLab CI, Azure
287
+ Pipelines, and other systems that support JUnit XML.
288
+
289
+ ## Python API
290
+
291
+ ```python
292
+ from lingtest import generate_cases, run_cases
293
+
294
+ cases = generate_cases("openapi.yaml")
295
+ report = run_cases(cases, "http://localhost:8000")
296
+
297
+ print(f"{report.passed} passed, {report.failed} failed")
298
+ for result in report.results:
299
+ print(result.case_id, result.status, result.duration_ms)
300
+ ```
301
+
302
+ The public data models are `TestCase`, `TestResult`, and `RunReport`.
303
+
304
+ ## CI Example
305
+
306
+ ```yaml
307
+ name: API tests
308
+
309
+ on: [push, pull_request]
310
+
311
+ jobs:
312
+ lingtest:
313
+ runs-on: ubuntu-latest
314
+ steps:
315
+ - uses: actions/checkout@v4
316
+ - uses: astral-sh/setup-uv@v6
317
+ - run: uv tool install lingtest-cli
318
+ - run: lingtest generate openapi.yaml -o cases.json
319
+ - run: lingtest run cases.json --base-url "${{ secrets.TEST_API_URL }}" --junit junit.xml
320
+ ```
321
+
322
+ The target API must be reachable from the runner. Start the service in an
323
+ earlier step or point the command at a deployed test environment.
324
+
325
+ ## AI Providers
326
+
327
+ | Provider | Option | API key environment variable | Default model |
328
+ | --- | --- | --- | --- |
329
+ | OpenAI | `--provider openai` | `OPENAI_API_KEY` | `gpt-4.1-mini` |
330
+ | DeepSeek | `--provider deepseek` | `DEEPSEEK_API_KEY` | `deepseek-chat` |
331
+ | Qwen | `--provider qwen` | `DASHSCOPE_API_KEY` | `qwen-plus` |
332
+ | Custom | `--provider custom` | `LINGTEST_API_KEY` | Set with `--model` |
333
+
334
+ Provider responses are parsed as JSON and validated with Pydantic. LingTest
335
+ requests one correction when a response fails validation. Documents are sent
336
+ to the configured provider; review its privacy policy before processing
337
+ confidential requirements.
338
+
339
+ ## Current Limitations
340
+
341
+ - `$ref` resolution and advanced schema composition are not implemented yet.
342
+ - Assertions currently compare the HTTP status only.
343
+ - Cases run sequentially.
344
+ - OpenAPI security schemes are not applied automatically.
345
+ - PDF and DOCX document extraction are not implemented.
346
+ - AI-generated cases are review artifacts and are not automatically converted
347
+ into executable HTTP cases.
348
+
349
+ See [product-design.html](product-design.html) for implemented scope, design
350
+ decisions, and the roadmap.
351
+
352
+ The Chinese essay [AI Agent 时代如何打造高质量软件](docs/blog/ai-testing-for-vibe-coding.zh-CN.md)
353
+ explains the Vibe Coding quality problem behind this project.
354
+
355
+ ## Development
356
+
357
+ Clone the repository and install all development dependencies:
358
+
359
+ ```bash
360
+ uv sync --extra dev
361
+ uv run pytest
362
+ uv run ruff check .
363
+ ```
364
+
365
+ Build and validate distributions:
366
+
367
+ ```bash
368
+ uv build
369
+ uv run twine check dist/*
370
+ ```
371
+
372
+ Run the development CLI:
373
+
374
+ ```bash
375
+ uv run lingtest --version
376
+ uv run lingtest generate examples/openapi.yaml -o cases.json
377
+ ```
378
+
379
+ ## Release
380
+
381
+ 1. Update the version in `pyproject.toml` and `src/lingtest/__init__.py`.
382
+ 2. Run tests, Ruff, `uv build`, and `twine check`.
383
+ 3. Commit the release and create a matching Git tag.
384
+ 4. Publish with `UV_PUBLISH_TOKEN`:
385
+
386
+ ```bash
387
+ uv publish
388
+ ```
389
+
390
+ ## Security
391
+
392
+ Only run generated cases against systems you are authorized to test. Runtime
393
+ headers are not written to the report by version 0.2, but generated case files
394
+ and reports should still be treated as project data.
395
+
396
+ ## Roadmap
397
+
398
+ - Better OpenAPI schema and `$ref` support
399
+ - Configuration files and named environments
400
+ - Filtering by tag, operation, path, and method
401
+ - JSON-path, schema, header, and latency assertions
402
+ - Authentication strategies
403
+ - Controlled concurrency and retries
404
+ - Static HTML reports
405
+ - Conversion of reviewed AI API cases into deterministic executable cases
406
+ - pytest export and a stable plugin API
407
+
408
+ ## License
409
+
410
+ [MIT](LICENSE)
@@ -0,0 +1,17 @@
1
+ lingtest/__init__.py,sha256=Kj7egmnbwx5-G6wnVN9KGLGLHdDwXMRlgg45Qz0SVyI,491
2
+ lingtest/cli.py,sha256=CMn3RIYlN3HLARGqBTaLJHiPKROHZFGG-wzMjhndyQk,6352
3
+ lingtest/io.py,sha256=ogAFoQ1kVM7f62poEJAMWZAdE3Y6LoDNdeEzxpdqdIE,1591
4
+ lingtest/models.py,sha256=WTU44cyK2FubVmvCHknoe8chOzWUoY78wERRz3UQfbg,948
5
+ lingtest/openapi.py,sha256=rhut-vnzsDXtfj0fs26jeaUx6bhBLXPpSd2Dd10Bf6Q,3825
6
+ lingtest/runner.py,sha256=NTvyxx8HyQMEMJ_fm7VWbEwMjMss1rs2aqlPKHv0PuM,1636
7
+ lingtest/ai/__init__.py,sha256=oDLWAjkgT_VYLgkeqm1HVuzfafrHPVLpHTxm8ZtRo8o,194
8
+ lingtest/ai/models.py,sha256=MXKkvr_-WPPVsc5JEBf4MaTzDjLKM4Isk85-aPM0MDg,2314
9
+ lingtest/ai/prompts.py,sha256=Aitq_RcKGi9l54aL_Dx7Uffuq_CH573BfqLQcA15QaE,1947
10
+ lingtest/ai/provider.py,sha256=f6yvz4V5FhjvvN0LNYWbBAC-EVuyfaOpLXRrYBPce-0,2819
11
+ lingtest/ai/service.py,sha256=t9gWoT4rTBBc991FY6uXaicIImxx-emAhTpGViKXfw4,2513
12
+ lingtest/ai/structured.py,sha256=OL8CA2TNWMRUpimN3Q7G8UfGMy5BGtxEDvD_bmqnUHY,2685
13
+ lingtest_cli-0.2.0.dist-info/METADATA,sha256=AuonYvq_3xcnGksSxlfoknWYkH-DabQWxGk5TanhuSE,12271
14
+ lingtest_cli-0.2.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
15
+ lingtest_cli-0.2.0.dist-info/entry_points.txt,sha256=mtYoLIDZU9DPvmZFLRqT4ZGXumUeHJF67XvFuiWBiII,46
16
+ lingtest_cli-0.2.0.dist-info/licenses/LICENSE,sha256=EBImwImAFQQnHaM-j_RcQNC50PWI251smhca_ly_Sy8,1079
17
+ lingtest_cli-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ lingtest = lingtest.cli:app
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LingTest Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+