ordel-cli 0.4.0__tar.gz → 0.4.2__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.
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/PKG-INFO +3 -3
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/SKILL.md +20 -7
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/adopt.py +23 -8
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/cli.py +84 -7
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/ingest.py +12 -2
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/mcp_server.py +171 -38
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/qa_plan.py +1 -1
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/record.py +106 -15
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/references/scenarios-and-oracles.md +18 -3
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/references/tools.md +14 -6
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/runner.py +9 -0
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/scenarios.py +36 -3
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/store.py +35 -2
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/pyproject.toml +3 -3
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/.gitignore +0 -0
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/CATALOG.md +0 -0
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/LICENSE +0 -0
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/PRIVACY.md +0 -0
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/README.md +0 -0
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/__init__.py +0 -0
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/branding.py +0 -0
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/capture.cjs +0 -0
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/capture_extract.cjs +0 -0
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/catalog.py +0 -0
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/context.py +0 -0
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/coverage.py +0 -0
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/doctor.py +0 -0
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/explore.py +0 -0
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/heal_service.py +0 -0
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/human_record.cjs +0 -0
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/mcp_supervisor.cjs +0 -0
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/ordel_config.cjs +0 -0
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/ordel_evidence.cjs +0 -0
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/pom.py +0 -0
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/py.typed +0 -0
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/record.cjs +0 -0
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/references/communication-and-harnesses.md +0 -0
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/references/execution-and-triage.md +0 -0
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/references/intake-and-readiness.md +0 -0
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/references/maintenance.md +0 -0
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/session.py +0 -0
- {ordel_cli-0.4.0 → ordel_cli-0.4.2}/ordel_cli/session_driver.cjs +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ordel-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Ordel free CLI — local QA-automation for individual devs. Drives the deterministic ordel-engine and exposes it to a BYO coding agent over MCP. No account, no DB, no Ordel LLM.
|
|
5
5
|
Project-URL: Homepage, https://ordel.io
|
|
6
6
|
Author: Ordel
|
|
@@ -14,8 +14,8 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
14
14
|
Classifier: Topic :: Software Development :: Testing
|
|
15
15
|
Requires-Python: >=3.11
|
|
16
16
|
Requires-Dist: mcp<2.0,>=1.0
|
|
17
|
-
Requires-Dist: ordel-engine==0.4.
|
|
18
|
-
Requires-Dist: ordel-tools==0.4.
|
|
17
|
+
Requires-Dist: ordel-engine==0.4.2
|
|
18
|
+
Requires-Dist: ordel-tools==0.4.2
|
|
19
19
|
Requires-Dist: typer>=0.12
|
|
20
20
|
Provides-Extra: dev
|
|
21
21
|
Requires-Dist: hatchling; extra == 'dev'
|
|
@@ -68,7 +68,7 @@ failure means. Three rules hold throughout:
|
|
|
68
68
|
| `pass` | ran, and its assertions (listed in `evidence.checks`) held | passed those checks on that target |
|
|
69
69
|
| `fail` | ran and failed; `message` says why | a failure to triage: app, expectation, test, data or environment |
|
|
70
70
|
| `flaky` | failed, then passed on retry | unstable; never a stable pass |
|
|
71
|
-
| `unjudged` | green in Playwright but NOT proven: no assertion ran, no evidence was captured,
|
|
71
|
+
| `unjudged` | green in Playwright but NOT proven: no assertion ran, no evidence was captured, it depends on a heal no human has reviewed, or what it found after a sign-in a visitor who never signed in sees too (`reasons` says which) | **not proven**: never "passed" |
|
|
72
72
|
| `skipped` | did not run | skipped, and why |
|
|
73
73
|
|
|
74
74
|
A run whose `status` is `no_tests` checked nothing, and `error` means the runner or its setup
|
|
@@ -80,15 +80,21 @@ failed: neither is "all green". Before you call a new test done, run it and conf
|
|
|
80
80
|
These are where agents most often go wrong. The references expand on them.
|
|
81
81
|
|
|
82
82
|
- **Identity is an expectation, not an observation.** For a flow that signs in,
|
|
83
|
-
`generate_scenarios` adds an identity check
|
|
83
|
+
`generate_scenarios` adds an identity check: the sign-in took effect (its form is gone) and
|
|
84
|
+
the signed-in page names the account used. A name that a visitor who never signed in also
|
|
85
|
+
sees at that address proves no session, so the check reports `unjudged`: assert an element
|
|
86
|
+
only a signed-in page shows. When sign-in lands somewhere other than the site root, it adds a
|
|
84
87
|
signed-out check (a visitor who never signed in is not served that page). If identity fails
|
|
85
88
|
because the app shows a display name, set THIS account's name from the user or the
|
|
86
89
|
requirements; never copy whatever name the page shows ("Welcome back, Dana Smith" for user
|
|
87
90
|
`agent` is a finding, not the expected value).
|
|
88
|
-
- **A rejection needs the app's own rejection.** A negative test passes on an
|
|
89
|
-
field marked invalid, a 4xx, or staying on the form; a 5xx or an error page
|
|
90
|
-
is a finding, never a correct rejection.
|
|
91
|
-
|
|
91
|
+
- **A rejection needs the app's own rejection, and its effect.** A negative test passes on an
|
|
92
|
+
error message, a field marked invalid, a 4xx, or staying on the form; a 5xx or an error page
|
|
93
|
+
is a crash, which is a finding, never a correct rejection. Staying on the form is not enough
|
|
94
|
+
for a sign-in: rejected means no session, so the signed-in page must stay closed, or, when
|
|
95
|
+
any visitor may open it, must not name the account (the generated sign-in negatives check
|
|
96
|
+
this). A denial that crashes (5xx) is not a denial. The
|
|
97
|
+
browser blocking a form is not the server validating it.
|
|
92
98
|
- **Only confirmed rules are defects.** A failure against a rule you inferred (a format you
|
|
93
99
|
expected, a limit you guessed) is a question for the user, listed separately from defects.
|
|
94
100
|
- **Never silence a failure.** Do not `test.skip`, delete an assertion or widen a pattern to get
|
|
@@ -98,7 +104,14 @@ These are where agents most often go wrong. The references expand on them.
|
|
|
98
104
|
add it to `.ordel/secrets.env` (gitignored; `run_test` loads it). If the user is not
|
|
99
105
|
available and the app itself shows the credential publicly (a demo account printed on its
|
|
100
106
|
sign-in page), you may add it there yourself and say so. Never put a secret in a spec,
|
|
101
|
-
another file or the chat, and never print it in a report.
|
|
107
|
+
another file or the chat, and never print it in a report. `run_test` lists what it finds:
|
|
108
|
+
`secret_leaks` (a secrets-file value in a file) and `hardcoded_secrets` (a spec line that
|
|
109
|
+
types a literal into a password field); move those to `.ordel/secrets.env`.
|
|
110
|
+
For interactive fills, pass `fill(env="ORDEL_SECRET_AGENT_PASSWORD", testid="password-input")`
|
|
111
|
+
instead of a literal `value`. The server resolves the variable from its environment or
|
|
112
|
+
`.ordel/secrets.env`; never read the value into an agent-visible command or tool argument.
|
|
113
|
+
For recording, an env-only fill step (`{"op": "fill", "testid": "password-input",
|
|
114
|
+
"env": "ORDEL_SECRET_AGENT_PASSWORD"}`) resolves on the server too. Leave out `value`.
|
|
102
115
|
- **Coverage is not correctness.** `coverage_report` counts which mapped elements some spec
|
|
103
116
|
touches; it says nothing about whether those checks pass.
|
|
104
117
|
|
|
@@ -180,7 +180,17 @@ def adopt(store: OrdelStore, path: str = "", runs: int = 3,
|
|
|
180
180
|
report["ingest"] = {k: v for k, v in ingest(store, path).items() if k != "ascii"}
|
|
181
181
|
|
|
182
182
|
playwright = find_playwright(store.root) is not None
|
|
183
|
-
|
|
183
|
+
# The selection bounds both what runs and which runs count as its history: a run of the
|
|
184
|
+
# whole project (or of another path) says nothing about this suite's flakiness, and running
|
|
185
|
+
# everything executed tests the user deliberately left out (the 0.4.0 audit's G2).
|
|
186
|
+
selection = Path(path).as_posix() if path else ""
|
|
187
|
+
report["selection"] = selection or "(whole project)"
|
|
188
|
+
|
|
189
|
+
def _selected(runs: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
190
|
+
return [r for r in runs if r.get("schema") == RUN_SCHEMA
|
|
191
|
+
and str(r.get("spec") or "") == selection]
|
|
192
|
+
|
|
193
|
+
history = _selected(_judged_runs(store))
|
|
184
194
|
executed: list[str] = []
|
|
185
195
|
if not playwright:
|
|
186
196
|
report["notes"].append("no @playwright/test in this project: runs, flake and heal "
|
|
@@ -189,20 +199,22 @@ def adopt(store: OrdelStore, path: str = "", runs: int = 3,
|
|
|
189
199
|
else:
|
|
190
200
|
if runs and len(history) >= runs:
|
|
191
201
|
report["notes"].append(
|
|
192
|
-
f".ordel/ already holds {len(history)} judged run(s)
|
|
193
|
-
f"
|
|
202
|
+
f".ordel/ already holds {len(history)} judged run(s) of {report['selection']}, "
|
|
203
|
+
f"so nothing was run: --runs builds that selection's history up to that many. "
|
|
204
|
+
f"Pass --runs {len(history) + 1} to run once more")
|
|
194
205
|
for _ in range(max(0, runs - len(history))):
|
|
195
|
-
doc = run_and_record(store, base_url=base_url)
|
|
206
|
+
doc = run_and_record(store, spec=selection or None, base_url=base_url)
|
|
196
207
|
if doc["status"] == "error":
|
|
197
208
|
report["notes"].append(f"run failed: {doc.get('error')}")
|
|
198
209
|
break
|
|
199
210
|
executed.append(str(doc["run_id"]))
|
|
200
211
|
report["runs_executed"] = executed
|
|
201
212
|
|
|
202
|
-
all_runs = _judged_runs(store)
|
|
213
|
+
all_runs = _selected(_judged_runs(store))
|
|
214
|
+
report["history"] = [str(r.get("run_id")) for r in all_runs]
|
|
203
215
|
report["flake"] = build_flake_report(all_runs)
|
|
204
216
|
|
|
205
|
-
latest = next(
|
|
217
|
+
latest = next(reversed(all_runs), None)
|
|
206
218
|
heal_items: list[dict[str, Any]] = []
|
|
207
219
|
failures = [v for v in (latest or {}).get("verdicts", []) or [] if v.get("status") == "fail"]
|
|
208
220
|
if latest is not None:
|
|
@@ -224,7 +236,8 @@ def adopt(store: OrdelStore, path: str = "", runs: int = 3,
|
|
|
224
236
|
|
|
225
237
|
def render_adopt(report: dict[str, Any]) -> str:
|
|
226
238
|
ing = report["ingest"]
|
|
227
|
-
lines = [f"Adopted
|
|
239
|
+
lines = [f"Adopted {report.get('selection', '(whole project)')}: "
|
|
240
|
+
f"{ing.get('specs_ingested', 0)} functional spec(s) -> "
|
|
228
241
|
f"{ing.get('flows_created', 0)} flow(s)"
|
|
229
242
|
+ (f" ({ing['cypress_specs']} Cypress)" if ing.get("cypress_specs") else "")]
|
|
230
243
|
if ing.get("unparsed_actions"):
|
|
@@ -235,7 +248,9 @@ def render_adopt(report: dict[str, Any]) -> str:
|
|
|
235
248
|
c = fl["counts"]
|
|
236
249
|
just_ran = (f", {len(report['runs_executed'])} just executed" if report["runs_executed"]
|
|
237
250
|
else "")
|
|
238
|
-
|
|
251
|
+
runs_used = ", ".join(report.get("history") or []) or "none"
|
|
252
|
+
lines += ["", (f"Flake report ({fl['runs_considered']} judged run(s) of this selection"
|
|
253
|
+
f"{just_ran}: {runs_used}):"),
|
|
239
254
|
(f" flaky {c['flaky']} regressed {c['regressed']} failing {c['failing']} "
|
|
240
255
|
f"recovered {c['recovered']} stable {c['stable']} "
|
|
241
256
|
f"too few runs {c['insufficient']}")]
|
|
@@ -15,6 +15,7 @@ import json
|
|
|
15
15
|
import os
|
|
16
16
|
import shutil
|
|
17
17
|
import sys
|
|
18
|
+
from datetime import UTC, datetime
|
|
18
19
|
from importlib.metadata import version as _dist_version
|
|
19
20
|
from pathlib import Path
|
|
20
21
|
from typing import Annotated, Any
|
|
@@ -321,11 +322,14 @@ def coverage(fmt: str = typer.Option("ascii", help="ascii | markdown | mermaid")
|
|
|
321
322
|
|
|
322
323
|
|
|
323
324
|
@app.command()
|
|
324
|
-
def scenarios(flow_name: str, show: bool = typer.Option(False, help="print the spec source")
|
|
325
|
+
def scenarios(flow_name: str, show: bool = typer.Option(False, help="print the spec source"),
|
|
326
|
+
repeat_submit: str | None = typer.Option(
|
|
327
|
+
None, help="what a second submit of the same record must do: refused | "
|
|
328
|
+
"same-record | new-record (kept on the flow)")) -> None:
|
|
325
329
|
"""From a recorded flow, emit happy+NEGATIVE+boundary+replay tests that hunt bugs."""
|
|
326
330
|
from ordel_cli.scenarios import generate_scenarios
|
|
327
331
|
store = _store()
|
|
328
|
-
res = generate_scenarios(store, flow_name)
|
|
332
|
+
res = generate_scenarios(store, flow_name, repeat_submit=repeat_submit)
|
|
329
333
|
if res["status"] != "generated":
|
|
330
334
|
typer.echo(res.get("note", res["status"]))
|
|
331
335
|
raise typer.Exit(1)
|
|
@@ -404,7 +408,7 @@ def plan() -> None:
|
|
|
404
408
|
def regression(
|
|
405
409
|
pages: Annotated[list[str] | None, typer.Argument(help="changed page_ids (omit=all)")] = None,
|
|
406
410
|
) -> None:
|
|
407
|
-
"""
|
|
411
|
+
"""Candidate specs to re-run after a change: those touching the changed pages."""
|
|
408
412
|
from ordel_cli.qa_plan import regression_set
|
|
409
413
|
res = regression_set(_store(), list(pages) if pages else None)
|
|
410
414
|
typer.echo(res.get("ascii", res.get("note", res["status"])))
|
|
@@ -416,8 +420,10 @@ def report(
|
|
|
416
420
|
) -> None:
|
|
417
421
|
"""Emit a copy-pasteable Markdown report (Slack/Teams/Jira) of what `.ordel/` can prove.
|
|
418
422
|
|
|
419
|
-
Every line is
|
|
420
|
-
|
|
423
|
+
Every line is read from the local store: the map, the last recorded run's verdicts with
|
|
424
|
+
links to its evidence, the heals still waiting for review, the mapped pages no spec
|
|
425
|
+
touches, and a pointer to the QA context your agent keeps (.ordel/qa-context.md), which
|
|
426
|
+
Ordel shows but does not verify. It makes no claim the store can't back."""
|
|
421
427
|
store = _store()
|
|
422
428
|
if not store.exists:
|
|
423
429
|
typer.echo("no .ordel/ here — run `ordel init`")
|
|
@@ -441,9 +447,49 @@ def report(
|
|
|
441
447
|
if last is not None:
|
|
442
448
|
doc["last_run"] = {k: last.get(k) for k in (
|
|
443
449
|
"run_id", "at", "status", "passed", "failed", "flaky", "unjudged", "skipped")}
|
|
450
|
+
doc["last_run"]["record"] = f".ordel/runs/{runs[-1]}.json"
|
|
451
|
+
verdicts = last.get("verdicts", []) or []
|
|
452
|
+
|
|
453
|
+
def _where(v: dict[str, Any]) -> str:
|
|
454
|
+
# Playwright names files relative to its testDir; the report links project paths
|
|
455
|
+
f = str(v.get("file") or "")
|
|
456
|
+
if f and not (store.root / f).exists() and (store.root / "tests" / f).exists():
|
|
457
|
+
f = f"tests/{f}"
|
|
458
|
+
return f"{f}:{v.get('line')}"
|
|
459
|
+
|
|
444
460
|
doc["last_run"]["unjudged_tests"] = [
|
|
445
461
|
{"file": v.get("file"), "title": v.get("title"), "reasons": v.get("reasons", [])}
|
|
446
|
-
for v in
|
|
462
|
+
for v in verdicts if v.get("status") == "unjudged"]
|
|
463
|
+
doc["last_run"]["failed_tests"] = [
|
|
464
|
+
{"where": _where(v), "title": v.get("title"),
|
|
465
|
+
"message": str(v.get("message") or "").splitlines()[0][:160] if v.get("message") else ""}
|
|
466
|
+
for v in verdicts if v.get("status") == "fail"]
|
|
467
|
+
doc["last_run"]["skipped_tests"] = [
|
|
468
|
+
{"where": _where(v), "title": v.get("title"),
|
|
469
|
+
"reasons": v.get("reasons", [])}
|
|
470
|
+
for v in verdicts if v.get("status") == "skipped"]
|
|
471
|
+
doc["pending_heals"] = [
|
|
472
|
+
{"id": h.get("id"), "spec": h.get("spec"),
|
|
473
|
+
"change": f"{h.get('element_id')} -> {h.get('healed_to')}", "exact": bool(h.get("exact"))}
|
|
474
|
+
for h in pending]
|
|
475
|
+
from ordel_cli.coverage import coverage_report as _cov
|
|
476
|
+
cov = _cov(store, force=True)
|
|
477
|
+
doc["untested_pages"] = list((cov.get("summary") or {}).get("untested_pages") or [])
|
|
478
|
+
ctx = store.dir / "qa-context.md"
|
|
479
|
+
if ctx.is_file():
|
|
480
|
+
text = ctx.read_text(encoding="utf-8", errors="replace")
|
|
481
|
+
doc["qa_context"] = {
|
|
482
|
+
"path": ".ordel/qa-context.md",
|
|
483
|
+
"updated": datetime.fromtimestamp(ctx.stat().st_mtime, UTC).isoformat(
|
|
484
|
+
timespec="seconds"),
|
|
485
|
+
"sections": [ln.lstrip("# ").strip() for ln in text.splitlines()
|
|
486
|
+
if ln.startswith("## ")]}
|
|
487
|
+
else:
|
|
488
|
+
doc["qa_context"] = None
|
|
489
|
+
doc["limits"] = ("a pass means its listed checks held on that run's target and build; "
|
|
490
|
+
"mapped pages are what was captured, not the whole app; expectations, "
|
|
491
|
+
"readiness and selections live in the QA context, which Ordel does not "
|
|
492
|
+
"verify")
|
|
447
493
|
if as_json:
|
|
448
494
|
typer.echo(json.dumps(doc, indent=2))
|
|
449
495
|
return
|
|
@@ -470,10 +516,35 @@ def report(
|
|
|
470
516
|
lines.append(f"| Tests flaky | {lr['flaky']} |")
|
|
471
517
|
lines.append(f"| Tests unjudged | {lr['unjudged']} |")
|
|
472
518
|
lines.append(f"| Heals pending human review | {len(pending)} of {len(heals)} |")
|
|
519
|
+
if lr and lr.get("at"):
|
|
520
|
+
lines.append(f"| Last run at | {lr['at']} (evidence: `{lr['record']}`) |")
|
|
521
|
+
qc = doc["qa_context"]
|
|
522
|
+
lines.append(f"| QA context | `{qc['path']}`, updated {qc['updated']} |" if qc else
|
|
523
|
+
"| QA context | none saved: expectations, readiness and selections are not "
|
|
524
|
+
"recorded |")
|
|
525
|
+
failed = (lr or {}).get("failed_tests") or []
|
|
526
|
+
if failed:
|
|
527
|
+
lines += ["", "Failed (triage before calling any of these a defect):"]
|
|
528
|
+
lines += [f"- `{f['where']}` {f['title']}: {f['message']}" for f in failed]
|
|
473
529
|
unjudged = (lr or {}).get("unjudged_tests") or []
|
|
474
530
|
if unjudged:
|
|
475
531
|
lines += ["", "Unjudged (green in Playwright, not proven by Ordel):"]
|
|
476
532
|
lines += [f"- `{u['file']}` {u['title']}: {'; '.join(u['reasons'])}" for u in unjudged]
|
|
533
|
+
skipped = (lr or {}).get("skipped_tests") or []
|
|
534
|
+
if skipped:
|
|
535
|
+
lines += ["", "Skipped (not run):"]
|
|
536
|
+
lines += [f"- `{k['where']}` {k['title']}"
|
|
537
|
+
+ (f": {'; '.join(k['reasons'])}" if k["reasons"] else "") for k in skipped]
|
|
538
|
+
if doc["pending_heals"]:
|
|
539
|
+
lines += ["", "Heals waiting for review (`ordel adopt --apply|--reject <id>`):"]
|
|
540
|
+
lines += [f"- {h['id']} `{h['spec']}`: {h['change']}"
|
|
541
|
+
+ (" (exact match)" if h["exact"] else "") for h in doc["pending_heals"]]
|
|
542
|
+
if doc["untested_pages"]:
|
|
543
|
+
lines += ["", f"Mapped pages no spec touches: {', '.join(doc['untested_pages'])}"]
|
|
544
|
+
if qc and qc["sections"]:
|
|
545
|
+
lines += ["", (f"QA context sections (written by your agent, not verified by Ordel): "
|
|
546
|
+
f"{', '.join(qc['sections'])}")]
|
|
547
|
+
lines += ["", f"_Limits: {doc['limits']}._"]
|
|
477
548
|
typer.echo("\n".join(lines))
|
|
478
549
|
|
|
479
550
|
|
|
@@ -693,7 +764,8 @@ def record(
|
|
|
693
764
|
typer.echo(" no assertion recorded: `ordel run` will report this test unjudged; "
|
|
694
765
|
"Alt+click the result you expect next time")
|
|
695
766
|
for var in res["needs_env"]:
|
|
696
|
-
typer.echo(f"
|
|
767
|
+
typer.echo(f" add {var}=<value> to .ordel/secrets.env to run it: a password was "
|
|
768
|
+
"typed and is never stored")
|
|
697
769
|
raise typer.Exit(0 if res["status"] == "recorded" else 1)
|
|
698
770
|
|
|
699
771
|
|
|
@@ -811,6 +883,8 @@ def _render_run(doc: dict[str, Any]) -> str:
|
|
|
811
883
|
f"skipped={doc.get('skipped', 0)}")]
|
|
812
884
|
if status == "no_tests":
|
|
813
885
|
lines.append(" no test executed - nothing was checked, so nothing passed")
|
|
886
|
+
for hit in doc.get("hardcoded_secrets") or []:
|
|
887
|
+
lines.append(f" SECRET IN A SPEC: {hit}: read it from .ordel/secrets.env instead")
|
|
814
888
|
if doc.get("secret_leaks"):
|
|
815
889
|
lines.append(f" SECRET IN A FILE: a value from .ordel/secrets.env is written in "
|
|
816
890
|
f"{', '.join(doc['secret_leaks'])}: read it from the environment instead")
|
|
@@ -824,6 +898,9 @@ def _render_run(doc: dict[str, Any]) -> str:
|
|
|
824
898
|
where = f"{v.get('file')}:{v.get('line')} › {v.get('title')}"
|
|
825
899
|
detail = v.get("message") if v.get("status") == "fail" else "; ".join(v.get("reasons", []))
|
|
826
900
|
lines.append(f" {mark} {where}" + (f": {detail}" if detail else ""))
|
|
901
|
+
if v.get("status") == "fail" and v.get("unresolved_locator"):
|
|
902
|
+
lines.append(f" locator never found: {v['unresolved_locator']} (heal_selector can "
|
|
903
|
+
"look for its replacement; a person reviews the heal)")
|
|
827
904
|
checked = sum((v.get("evidence") or {}).get("assertions") or 0
|
|
828
905
|
for v in verdicts if v.get("status") == "pass")
|
|
829
906
|
if doc.get("passed"):
|
|
@@ -47,6 +47,7 @@ def ingest(store: OrdelStore, path: str = "") -> dict[str, Any]:
|
|
|
47
47
|
if fn.lower().endswith(_SPEC_SUFFIXES)]
|
|
48
48
|
|
|
49
49
|
ingested = flows_created = unparsed = skipped_unit = skipped_ordel = cypress_specs = 0
|
|
50
|
+
kept_asserts = dropped_asserts = 0
|
|
50
51
|
created: list[str] = []
|
|
51
52
|
capped = False
|
|
52
53
|
for f in sorted(targets):
|
|
@@ -79,10 +80,15 @@ def ingest(store: OrdelStore, path: str = "") -> dict[str, Any]:
|
|
|
79
80
|
n += 1
|
|
80
81
|
uniq = f"{name[:60]}-{n}"
|
|
81
82
|
start = next((s["url"] for s in t["steps"] if s.get("op") == "goto"), "")
|
|
83
|
+
kept = int(t.get("assertions_kept", 0))
|
|
84
|
+
dropped = int(t.get("assertions_dropped", 0))
|
|
85
|
+
kept_asserts += kept
|
|
86
|
+
dropped_asserts += dropped
|
|
82
87
|
store.save_flow(uniq, {"name": uniq, "source": "ingested",
|
|
83
88
|
"framework": "cypress" if cypress else "playwright",
|
|
84
89
|
"spec": f.name, "spec_path": f.relative_to(root).as_posix(),
|
|
85
|
-
"start": start, "final_url": "", "steps": t["steps"]
|
|
90
|
+
"start": start, "final_url": "", "steps": t["steps"],
|
|
91
|
+
"assertions_kept": kept, "assertions_dropped": dropped})
|
|
86
92
|
created.append(uniq)
|
|
87
93
|
flows_created += 1
|
|
88
94
|
if capped:
|
|
@@ -98,6 +104,9 @@ def ingest(store: OrdelStore, path: str = "") -> dict[str, Any]:
|
|
|
98
104
|
lines.append(f" skipped {skipped_ordel} Ordel-generated spec(s) (already ours)")
|
|
99
105
|
if unparsed:
|
|
100
106
|
lines.append(f" {unparsed} action line(s) used idioms we couldn't parse — re-record those flows")
|
|
107
|
+
lines.append(f" assertions carried into the flows: {kept_asserts}"
|
|
108
|
+
+ (f"; NOT carried: {dropped_asserts} (the original specs still check them, "
|
|
109
|
+
"the generated scenarios do not)" if dropped_asserts else ""))
|
|
101
110
|
if capped:
|
|
102
111
|
lines.append(f" stopped at the {_MAX_FLOWS}-flow cap — ingest a subdir at a time for the rest")
|
|
103
112
|
if flows_created:
|
|
@@ -106,4 +115,5 @@ def ingest(store: OrdelStore, path: str = "") -> dict[str, Any]:
|
|
|
106
115
|
return {"status": "ok", "specs_ingested": ingested, "flows_created": flows_created,
|
|
107
116
|
"flows": created, "cypress_specs": cypress_specs,
|
|
108
117
|
"skipped_unit": skipped_unit, "skipped_ordel": skipped_ordel,
|
|
109
|
-
"unparsed_actions": unparsed, "
|
|
118
|
+
"unparsed_actions": unparsed, "assertions_kept": kept_asserts,
|
|
119
|
+
"assertions_dropped": dropped_asserts, "ascii": "\n".join(lines)}
|