aevum-cli 0.2.0__tar.gz → 0.4.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.
- aevum_cli-0.4.0/.gitignore +49 -0
- {aevum_cli-0.2.0 → aevum_cli-0.4.0}/PKG-INFO +3 -1
- {aevum_cli-0.2.0 → aevum_cli-0.4.0}/pyproject.toml +7 -3
- {aevum_cli-0.2.0 → aevum_cli-0.4.0}/src/aevum/cli/__init__.py +1 -1
- aevum_cli-0.4.0/src/aevum/cli/app.py +252 -0
- {aevum_cli-0.2.0 → aevum_cli-0.4.0}/src/aevum/cli/commands/server.py +2 -2
- {aevum_cli-0.2.0 → aevum_cli-0.4.0}/src/aevum/cli/commands/store.py +2 -2
- {aevum_cli-0.2.0 → aevum_cli-0.4.0}/src/aevum/cli/commands/version.py +0 -3
- aevum_cli-0.4.0/tests/test_phase8_cli.py +352 -0
- aevum_cli-0.2.0/.gitignore +0 -31
- aevum_cli-0.2.0/src/aevum/cli/app.py +0 -22
- {aevum_cli-0.2.0 → aevum_cli-0.4.0}/README.md +0 -0
- {aevum_cli-0.2.0 → aevum_cli-0.4.0}/src/aevum/cli/__main__.py +0 -0
- {aevum_cli-0.2.0 → aevum_cli-0.4.0}/src/aevum/cli/commands/__init__.py +0 -0
- {aevum_cli-0.2.0 → aevum_cli-0.4.0}/src/aevum/cli/commands/complication.py +0 -0
- {aevum_cli-0.2.0 → aevum_cli-0.4.0}/src/aevum/cli/commands/conformance.py +0 -0
- {aevum_cli-0.2.0 → aevum_cli-0.4.0}/src/aevum/cli/py.typed +0 -0
- {aevum_cli-0.2.0 → aevum_cli-0.4.0}/tests/test_cli.py +0 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.pyc
|
|
4
|
+
*.pyo
|
|
5
|
+
*.pyd
|
|
6
|
+
.venv/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
|
|
9
|
+
# Build
|
|
10
|
+
dist/
|
|
11
|
+
build/
|
|
12
|
+
site/
|
|
13
|
+
|
|
14
|
+
# Tools
|
|
15
|
+
.mypy_cache/
|
|
16
|
+
.ruff_cache/
|
|
17
|
+
.pytest_cache/
|
|
18
|
+
.hypothesis/
|
|
19
|
+
.cache/
|
|
20
|
+
|
|
21
|
+
# IDE
|
|
22
|
+
.vscode/
|
|
23
|
+
.idea/
|
|
24
|
+
*.swp
|
|
25
|
+
*.swo
|
|
26
|
+
|
|
27
|
+
# OS
|
|
28
|
+
.DS_Store
|
|
29
|
+
Thumbs.db
|
|
30
|
+
|
|
31
|
+
# Verify scripts (run locally, never commit)
|
|
32
|
+
verify_*.py
|
|
33
|
+
scripts/verify_*.py
|
|
34
|
+
|
|
35
|
+
# Aevum development — never commit (Phase 0+)
|
|
36
|
+
aevum_principles.key
|
|
37
|
+
signed_principles_draft.yaml
|
|
38
|
+
tools/sign_principles.py
|
|
39
|
+
|
|
40
|
+
# Private keys — never commit
|
|
41
|
+
*.key
|
|
42
|
+
*.pem
|
|
43
|
+
|
|
44
|
+
# OpenSSF Scorecard output (Phase 0+)
|
|
45
|
+
results.sarif
|
|
46
|
+
verify_phase3.py
|
|
47
|
+
verify_phase7.py
|
|
48
|
+
verify_phase8.py
|
|
49
|
+
verify_phase*.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aevum-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Aevum -- command-line interface for operating Aevum nodes.
|
|
5
5
|
Project-URL: Homepage, https://aevum.build
|
|
6
6
|
Project-URL: Repository, https://github.com/aevum-labs/aevum
|
|
@@ -15,6 +15,8 @@ Requires-Dist: aevum-core
|
|
|
15
15
|
Requires-Dist: aevum-server
|
|
16
16
|
Requires-Dist: typer[all]>=0.12
|
|
17
17
|
Requires-Dist: uvicorn[standard]>=0.30
|
|
18
|
+
Provides-Extra: conform
|
|
19
|
+
Requires-Dist: aevum-conformance; extra == 'conform'
|
|
18
20
|
Provides-Extra: dev
|
|
19
21
|
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
20
22
|
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "aevum-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "Aevum -- command-line interface for operating Aevum nodes."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -34,8 +34,9 @@ build-backend = "hatchling.build"
|
|
|
34
34
|
packages = ["src/aevum"]
|
|
35
35
|
|
|
36
36
|
[tool.uv.sources]
|
|
37
|
-
aevum-core
|
|
38
|
-
aevum-server
|
|
37
|
+
aevum-core = { workspace = true }
|
|
38
|
+
aevum-server = { workspace = true }
|
|
39
|
+
aevum-conformance = { workspace = true }
|
|
39
40
|
|
|
40
41
|
[tool.pytest.ini_options]
|
|
41
42
|
testpaths = ["tests"]
|
|
@@ -58,6 +59,9 @@ select = ["E", "F", "UP", "B", "SIM", "I", "ANN"]
|
|
|
58
59
|
ignore = ["ANN401"]
|
|
59
60
|
|
|
60
61
|
[project.optional-dependencies]
|
|
62
|
+
conform = [
|
|
63
|
+
"aevum-conformance",
|
|
64
|
+
]
|
|
61
65
|
dev = [
|
|
62
66
|
"pytest>=8.0",
|
|
63
67
|
"pytest-asyncio>=0.23",
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2024-2026 Aevum Labs contributors
|
|
3
|
+
"""
|
|
4
|
+
Top-level typer app — CLI v2. Sub-commands and direct commands registered here.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Annotated
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
|
|
14
|
+
from aevum.cli.commands import complication, conformance, server, store, version
|
|
15
|
+
|
|
16
|
+
# Module-level import for mock.patch patchability (Rule 57).
|
|
17
|
+
# Soft import: aevum-conformance is a workspace package not on PyPI, so
|
|
18
|
+
# callers without it installed still get a usable CLI (conform command
|
|
19
|
+
# shows a helpful error instead of crashing at startup).
|
|
20
|
+
try:
|
|
21
|
+
from aevum.conformance.suite import ConformanceSuite
|
|
22
|
+
except ImportError: # pragma: no cover
|
|
23
|
+
ConformanceSuite = None # type: ignore[assignment,misc]
|
|
24
|
+
|
|
25
|
+
app = typer.Typer(
|
|
26
|
+
name="aevum",
|
|
27
|
+
help="Aevum governed context kernel — CLI v2",
|
|
28
|
+
no_args_is_help=True,
|
|
29
|
+
pretty_exceptions_enable=False,
|
|
30
|
+
rich_markup_mode="markdown",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
app.add_typer(server.app, name="server")
|
|
34
|
+
app.add_typer(store.app, name="store")
|
|
35
|
+
app.add_typer(complication.app, name="complication")
|
|
36
|
+
app.add_typer(conformance.app, name="conformance")
|
|
37
|
+
app.command(name="version")(version.version_command)
|
|
38
|
+
|
|
39
|
+
_DEFAULT_STATE = Path.home() / ".aevum"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.command()
|
|
43
|
+
def init(
|
|
44
|
+
state_dir: Annotated[
|
|
45
|
+
Path,
|
|
46
|
+
typer.Option("--state-dir", "-s", help="State directory path"),
|
|
47
|
+
] = _DEFAULT_STATE,
|
|
48
|
+
principles: Annotated[
|
|
49
|
+
Path,
|
|
50
|
+
typer.Option("--principles", "-p", help="Path to signed_principles.yaml"),
|
|
51
|
+
] = Path("signed_principles.yaml"),
|
|
52
|
+
) -> None:
|
|
53
|
+
"""
|
|
54
|
+
Initialize Aevum state directory and verify principles.
|
|
55
|
+
|
|
56
|
+
Creates the state directory, generates dual signing keys (Ed25519 +
|
|
57
|
+
ML-DSA-65), and verifies the signed_principles.yaml file.
|
|
58
|
+
"""
|
|
59
|
+
typer.echo(f"Initializing Aevum state at {state_dir}...")
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
from aevum.core.principles import PrinciplesVerifier
|
|
63
|
+
verifier = PrinciplesVerifier(principles)
|
|
64
|
+
p = verifier.verify()
|
|
65
|
+
typer.echo(f" Principles: OK (sequence={p.sequence}, signed_by={p.signed_by[:30]}...)")
|
|
66
|
+
except Exception as exc: # noqa: BLE001
|
|
67
|
+
typer.echo(f" Principles: FAILED — {exc}", err=True)
|
|
68
|
+
raise typer.Exit(code=1) from None
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
from aevum.core.kernel import Kernel
|
|
72
|
+
kernel = Kernel.local(
|
|
73
|
+
state_dir=state_dir,
|
|
74
|
+
principles_path=principles,
|
|
75
|
+
tsa_enabled=False,
|
|
76
|
+
)
|
|
77
|
+
ed25519_pub = kernel.signer.ed25519_public_key.hex()[:16]
|
|
78
|
+
typer.echo(f" Keys: OK (ed25519={ed25519_pub}...)")
|
|
79
|
+
typer.echo(f" Canaries: PASS ({len(kernel.principles.immutable_ids())} immutable principles)")
|
|
80
|
+
except Exception as exc: # noqa: BLE001
|
|
81
|
+
typer.echo(f" Kernel init: FAILED — {exc}", err=True)
|
|
82
|
+
raise typer.Exit(code=1) from None
|
|
83
|
+
|
|
84
|
+
typer.echo(typer.style("Aevum initialized successfully.", fg=typer.colors.GREEN))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@app.command()
|
|
88
|
+
def verify(
|
|
89
|
+
session_id: Annotated[str, typer.Argument(help="Session ID to verify")],
|
|
90
|
+
state_dir: Annotated[
|
|
91
|
+
Path,
|
|
92
|
+
typer.Option("--state-dir", "-s"),
|
|
93
|
+
] = _DEFAULT_STATE,
|
|
94
|
+
) -> None:
|
|
95
|
+
"""
|
|
96
|
+
Verify a session's Merkle root and signatures.
|
|
97
|
+
|
|
98
|
+
Re-reads the stored session events from SQLite, recomputes the
|
|
99
|
+
Merkle root, and compares it to the signed root in the sigchain.
|
|
100
|
+
"""
|
|
101
|
+
db_path = state_dir / "aevum.db"
|
|
102
|
+
if not db_path.exists():
|
|
103
|
+
typer.echo(f"Database not found: {db_path}", err=True)
|
|
104
|
+
raise typer.Exit(code=1)
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
from aevum.core.replay import ReplayEngine
|
|
108
|
+
engine = ReplayEngine(db_path)
|
|
109
|
+
result = engine.replay(session_id)
|
|
110
|
+
|
|
111
|
+
if result.all_matched:
|
|
112
|
+
typer.echo(typer.style(
|
|
113
|
+
f"Session {session_id[:8]}... VERIFIED",
|
|
114
|
+
fg=typer.colors.GREEN,
|
|
115
|
+
))
|
|
116
|
+
typer.echo(f" Merkle root: {result.original_merkle_root[:16]}...")
|
|
117
|
+
typer.echo(f" Events: {len(result.event_results)}")
|
|
118
|
+
else:
|
|
119
|
+
typer.echo(typer.style(
|
|
120
|
+
f"Session {session_id[:8]}... TAMPERED",
|
|
121
|
+
fg=typer.colors.RED,
|
|
122
|
+
), err=True)
|
|
123
|
+
typer.echo(
|
|
124
|
+
f" First divergence: event #{result.first_divergence}", err=True
|
|
125
|
+
)
|
|
126
|
+
raise typer.Exit(code=1)
|
|
127
|
+
|
|
128
|
+
except ValueError as exc:
|
|
129
|
+
typer.echo(f"Session not found: {exc}", err=True)
|
|
130
|
+
raise typer.Exit(code=1) from None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@app.command(name="audit-pack")
|
|
134
|
+
def audit_pack(
|
|
135
|
+
session_id: Annotated[str, typer.Argument(help="Session ID")],
|
|
136
|
+
output: Annotated[
|
|
137
|
+
Path | None,
|
|
138
|
+
typer.Option("--output", "-o", help="Output file path (default: stdout)"),
|
|
139
|
+
] = None,
|
|
140
|
+
state_dir: Annotated[
|
|
141
|
+
Path,
|
|
142
|
+
typer.Option("--state-dir", "-s"),
|
|
143
|
+
] = _DEFAULT_STATE,
|
|
144
|
+
) -> None:
|
|
145
|
+
"""
|
|
146
|
+
Export EU AI Act Article 12 audit pack for a session.
|
|
147
|
+
|
|
148
|
+
Produces a JSON-LD document using the PROV-O vocabulary.
|
|
149
|
+
"""
|
|
150
|
+
db_path = state_dir / "aevum.db"
|
|
151
|
+
if not db_path.exists():
|
|
152
|
+
typer.echo(f"Database not found: {db_path}", err=True)
|
|
153
|
+
raise typer.Exit(code=1)
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
from aevum.core.audit.audit_pack import AuditPackExporter
|
|
157
|
+
exporter = AuditPackExporter(db_path)
|
|
158
|
+
json_text = exporter.export_json(session_id)
|
|
159
|
+
|
|
160
|
+
if output:
|
|
161
|
+
output.write_text(json_text, encoding="utf-8")
|
|
162
|
+
typer.echo(f"Audit pack written to {output}")
|
|
163
|
+
else:
|
|
164
|
+
typer.echo(json_text)
|
|
165
|
+
|
|
166
|
+
except Exception as exc: # noqa: BLE001
|
|
167
|
+
typer.echo(f"Audit pack error: {exc}", err=True)
|
|
168
|
+
raise typer.Exit(code=1) from None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@app.command()
|
|
172
|
+
def conform(
|
|
173
|
+
output: Annotated[
|
|
174
|
+
str,
|
|
175
|
+
typer.Option("--output", "-o", help="Output format: text or json"),
|
|
176
|
+
] = "text",
|
|
177
|
+
) -> None:
|
|
178
|
+
"""
|
|
179
|
+
Run the 9-invariant conformance suite.
|
|
180
|
+
|
|
181
|
+
Tests all required Aevum behavioral invariants and prints a report.
|
|
182
|
+
Exit code 0 = all pass, 1 = one or more fail.
|
|
183
|
+
"""
|
|
184
|
+
if ConformanceSuite is None:
|
|
185
|
+
typer.echo(
|
|
186
|
+
"aevum-conformance is not installed. Install it with: pip install aevum-conformance",
|
|
187
|
+
err=True,
|
|
188
|
+
)
|
|
189
|
+
raise typer.Exit(code=1)
|
|
190
|
+
suite = ConformanceSuite()
|
|
191
|
+
result = suite.run_all()
|
|
192
|
+
|
|
193
|
+
if output == "json":
|
|
194
|
+
typer.echo(json.dumps(result.to_dict(), indent=2))
|
|
195
|
+
else:
|
|
196
|
+
typer.echo(result.render())
|
|
197
|
+
|
|
198
|
+
if not result.all_passed:
|
|
199
|
+
raise typer.Exit(code=1)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@app.command()
|
|
203
|
+
def replay(
|
|
204
|
+
session_id: Annotated[str, typer.Argument(help="Session ID to replay")],
|
|
205
|
+
verbose: Annotated[
|
|
206
|
+
bool,
|
|
207
|
+
typer.Option("--verbose", "-v", help="Show per-event results"),
|
|
208
|
+
] = False,
|
|
209
|
+
state_dir: Annotated[
|
|
210
|
+
Path,
|
|
211
|
+
typer.Option("--state-dir", "-s"),
|
|
212
|
+
] = _DEFAULT_STATE,
|
|
213
|
+
) -> None:
|
|
214
|
+
"""
|
|
215
|
+
Replay a session and verify Merkle chain integrity.
|
|
216
|
+
|
|
217
|
+
Re-reads all events and recomputes the Merkle root. Reports any
|
|
218
|
+
divergence from the stored root (indicating tampering).
|
|
219
|
+
"""
|
|
220
|
+
db_path = state_dir / "aevum.db"
|
|
221
|
+
if not db_path.exists():
|
|
222
|
+
typer.echo(f"Database not found: {db_path}", err=True)
|
|
223
|
+
raise typer.Exit(code=1)
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
from aevum.core.replay import ReplayEngine
|
|
227
|
+
engine = ReplayEngine(db_path)
|
|
228
|
+
result = engine.replay(session_id)
|
|
229
|
+
|
|
230
|
+
status = (
|
|
231
|
+
typer.style("PASS", fg=typer.colors.GREEN)
|
|
232
|
+
if result.all_matched
|
|
233
|
+
else typer.style("FAIL", fg=typer.colors.RED)
|
|
234
|
+
)
|
|
235
|
+
typer.echo(f"Replay {session_id[:8]}...: {status}")
|
|
236
|
+
typer.echo(f" Events: {len(result.event_results)}")
|
|
237
|
+
typer.echo(f" Merkle root: {result.original_merkle_root[:16]}...")
|
|
238
|
+
|
|
239
|
+
if verbose:
|
|
240
|
+
for ev in result.event_results:
|
|
241
|
+
ev_status = "OK" if ev.matched else "DIVERGED"
|
|
242
|
+
typer.echo(f" [{ev.sequence:3d}] {ev.event_type:<12} {ev_status}")
|
|
243
|
+
|
|
244
|
+
if not result.all_matched:
|
|
245
|
+
typer.echo(
|
|
246
|
+
f" First divergence: event #{result.first_divergence}", err=True
|
|
247
|
+
)
|
|
248
|
+
raise typer.Exit(code=1)
|
|
249
|
+
|
|
250
|
+
except ValueError as exc:
|
|
251
|
+
typer.echo(f"Session not found: {exc}", err=True)
|
|
252
|
+
raise typer.Exit(code=1) from None
|
|
@@ -17,7 +17,7 @@ app = typer.Typer(help="Manage the Aevum HTTP API server.")
|
|
|
17
17
|
|
|
18
18
|
@app.command("start")
|
|
19
19
|
def start(
|
|
20
|
-
host: Annotated[str, typer.Option(help="Bind host")] = "0.0.0.0",
|
|
20
|
+
host: Annotated[str, typer.Option(help="Bind host")] = "0.0.0.0", # nosec B104
|
|
21
21
|
port: Annotated[int, typer.Option(help="Bind port")] = 8000,
|
|
22
22
|
workers: Annotated[int, typer.Option(help="Number of uvicorn workers")] = 1,
|
|
23
23
|
graph: Annotated[
|
|
@@ -78,7 +78,7 @@ def _build_engine(graph: str) -> Engine:
|
|
|
78
78
|
try:
|
|
79
79
|
import psycopg
|
|
80
80
|
from aevum.store.postgres import PostgresStore
|
|
81
|
-
from aevum.store.postgres.
|
|
81
|
+
from aevum.store.postgres.schema import initialize_schema
|
|
82
82
|
conn = psycopg.connect(dsn)
|
|
83
83
|
initialize_schema(conn)
|
|
84
84
|
typer.echo("Graph backend: PostgreSQL")
|
|
@@ -32,7 +32,7 @@ def migrate(
|
|
|
32
32
|
raise typer.Exit(code=1)
|
|
33
33
|
|
|
34
34
|
try:
|
|
35
|
-
from aevum.store.postgres.
|
|
35
|
+
from aevum.store.postgres.migrate import migrate_from_oxigraph
|
|
36
36
|
except ImportError:
|
|
37
37
|
typer.echo("Error: aevum-store-postgres is not installed.", err=True)
|
|
38
38
|
raise typer.Exit(code=1) from None
|
|
@@ -44,7 +44,7 @@ def migrate(
|
|
|
44
44
|
try:
|
|
45
45
|
import psycopg
|
|
46
46
|
conn = psycopg.connect(postgres_dsn)
|
|
47
|
-
migrated = migrate_from_oxigraph(oxigraph_path, conn)
|
|
47
|
+
migrated = migrate_from_oxigraph(oxigraph_path, conn) # type: ignore[arg-type] # Phase 2: wire store construction
|
|
48
48
|
typer.echo(f"Migration complete: {migrated} entities transferred.")
|
|
49
49
|
except Exception as e:
|
|
50
50
|
typer.echo(f"Migration failed: {e}", err=True)
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2024-2026 Aevum Labs contributors
|
|
3
|
+
"""
|
|
4
|
+
CLI tests — Phase 8. Rule 05: always strip ANSI codes before asserting on text output.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
import sqlite3
|
|
12
|
+
from datetime import UTC, datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from unittest.mock import MagicMock, patch
|
|
15
|
+
|
|
16
|
+
from typer.testing import CliRunner
|
|
17
|
+
|
|
18
|
+
from aevum.cli.app import app
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def strip_ansi(text: str) -> str:
|
|
22
|
+
"""Rule 05: strip ANSI escape codes before any assertion."""
|
|
23
|
+
return re.sub(r"\x1b\[[0-9;]*[mGKH]", "", text)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
runner = CliRunner()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TestConformCommand:
|
|
30
|
+
def test_conform_exits_0_when_all_pass(self) -> None:
|
|
31
|
+
with patch("aevum.cli.app.ConformanceSuite") as mock_cls:
|
|
32
|
+
mock_suite = MagicMock()
|
|
33
|
+
mock_result = MagicMock()
|
|
34
|
+
mock_result.all_passed = True
|
|
35
|
+
mock_result.render.return_value = "STATUS: PASS (9/9)"
|
|
36
|
+
mock_suite.run_all.return_value = mock_result
|
|
37
|
+
mock_cls.return_value = mock_suite
|
|
38
|
+
|
|
39
|
+
result = runner.invoke(app, ["conform"])
|
|
40
|
+
assert result.exit_code == 0
|
|
41
|
+
assert "PASS" in strip_ansi(result.output)
|
|
42
|
+
|
|
43
|
+
def test_conform_exits_1_when_any_fail(self) -> None:
|
|
44
|
+
with patch("aevum.cli.app.ConformanceSuite") as mock_cls:
|
|
45
|
+
mock_suite = MagicMock()
|
|
46
|
+
mock_result = MagicMock()
|
|
47
|
+
mock_result.all_passed = False
|
|
48
|
+
mock_result.render.return_value = "STATUS: FAIL (8/9)"
|
|
49
|
+
mock_suite.run_all.return_value = mock_result
|
|
50
|
+
mock_cls.return_value = mock_suite
|
|
51
|
+
|
|
52
|
+
result = runner.invoke(app, ["conform"])
|
|
53
|
+
assert result.exit_code == 1
|
|
54
|
+
|
|
55
|
+
def test_conform_json_output(self) -> None:
|
|
56
|
+
with patch("aevum.cli.app.ConformanceSuite") as mock_cls:
|
|
57
|
+
mock_suite = MagicMock()
|
|
58
|
+
mock_result = MagicMock()
|
|
59
|
+
mock_result.all_passed = True
|
|
60
|
+
mock_result.to_dict.return_value = {"passed": True, "total_count": 9}
|
|
61
|
+
mock_suite.run_all.return_value = mock_result
|
|
62
|
+
mock_cls.return_value = mock_suite
|
|
63
|
+
|
|
64
|
+
result = runner.invoke(app, ["conform", "--output", "json"])
|
|
65
|
+
assert result.exit_code == 0
|
|
66
|
+
parsed = json.loads(strip_ansi(result.output))
|
|
67
|
+
assert parsed["passed"] is True
|
|
68
|
+
|
|
69
|
+
def test_conform_output_no_ansi_in_text(self) -> None:
|
|
70
|
+
"""ANSI stripping must clean the output (Rule 05)."""
|
|
71
|
+
with patch("aevum.cli.app.ConformanceSuite") as mock_cls:
|
|
72
|
+
mock_suite = MagicMock()
|
|
73
|
+
mock_result = MagicMock()
|
|
74
|
+
mock_result.all_passed = True
|
|
75
|
+
mock_result.render.return_value = "PASS (9/9)"
|
|
76
|
+
mock_suite.run_all.return_value = mock_result
|
|
77
|
+
mock_cls.return_value = mock_suite
|
|
78
|
+
result = runner.invoke(app, ["conform"])
|
|
79
|
+
clean = strip_ansi(result.output)
|
|
80
|
+
assert "\x1b" not in clean
|
|
81
|
+
|
|
82
|
+
def test_conform_json_fails_raises_exit_1(self) -> None:
|
|
83
|
+
with patch("aevum.cli.app.ConformanceSuite") as mock_cls:
|
|
84
|
+
mock_suite = MagicMock()
|
|
85
|
+
mock_result = MagicMock()
|
|
86
|
+
mock_result.all_passed = False
|
|
87
|
+
mock_result.to_dict.return_value = {"passed": False, "total_count": 9}
|
|
88
|
+
mock_suite.run_all.return_value = mock_result
|
|
89
|
+
mock_cls.return_value = mock_suite
|
|
90
|
+
|
|
91
|
+
result = runner.invoke(app, ["conform", "--output", "json"])
|
|
92
|
+
assert result.exit_code == 1
|
|
93
|
+
|
|
94
|
+
def test_conform_default_output_is_text(self) -> None:
|
|
95
|
+
with patch("aevum.cli.app.ConformanceSuite") as mock_cls:
|
|
96
|
+
mock_suite = MagicMock()
|
|
97
|
+
mock_result = MagicMock()
|
|
98
|
+
mock_result.all_passed = True
|
|
99
|
+
mock_result.render.return_value = "PASS (9/9)"
|
|
100
|
+
mock_suite.run_all.return_value = mock_result
|
|
101
|
+
mock_cls.return_value = mock_suite
|
|
102
|
+
|
|
103
|
+
runner.invoke(app, ["conform"])
|
|
104
|
+
# text mode calls render(), not to_dict()
|
|
105
|
+
assert mock_result.render.called
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class TestVerifyCommand:
|
|
109
|
+
def _make_db(self, tmp_path: Path) -> None:
|
|
110
|
+
"""Create a minimal sessions DB for testing."""
|
|
111
|
+
db = tmp_path / "aevum.db"
|
|
112
|
+
conn = sqlite3.connect(str(db))
|
|
113
|
+
conn.executescript("""
|
|
114
|
+
CREATE TABLE sessions (
|
|
115
|
+
session_id TEXT PRIMARY KEY, commit_type TEXT,
|
|
116
|
+
principal TEXT, purpose TEXT, started_at TEXT,
|
|
117
|
+
closed_at TEXT, event_count INTEGER, fact_count INTEGER,
|
|
118
|
+
checkpoint_count INTEGER, merkle_root TEXT,
|
|
119
|
+
ed25519_sig TEXT, mldsa65_sig TEXT, ed25519_pub TEXT,
|
|
120
|
+
mldsa65_pub TEXT, tsa_token TEXT, sigchain_entry_id INTEGER
|
|
121
|
+
);
|
|
122
|
+
CREATE TABLE session_events (
|
|
123
|
+
event_id TEXT PRIMARY KEY, session_id TEXT,
|
|
124
|
+
sequence INTEGER, event_type TEXT, occurred_at TEXT,
|
|
125
|
+
input_hash TEXT, output_hash TEXT, latency_ms INTEGER
|
|
126
|
+
);
|
|
127
|
+
""")
|
|
128
|
+
h = hashlib.sha256(b"").hexdigest()
|
|
129
|
+
now = datetime.now(UTC).isoformat()
|
|
130
|
+
conn.execute(
|
|
131
|
+
"INSERT INTO sessions VALUES (?,?,?,?,?,?,?,?,?,?,NULL,NULL,NULL,NULL,NULL,NULL)",
|
|
132
|
+
("sess-abc", "complete", "alice", "test", now, now, 0, 0, 0, h),
|
|
133
|
+
)
|
|
134
|
+
conn.commit()
|
|
135
|
+
conn.close()
|
|
136
|
+
|
|
137
|
+
def test_verify_missing_db_exits_1(self, tmp_path: Path) -> None:
|
|
138
|
+
result = runner.invoke(
|
|
139
|
+
app, ["verify", "sess-abc", "--state-dir", str(tmp_path)]
|
|
140
|
+
)
|
|
141
|
+
assert result.exit_code == 1
|
|
142
|
+
assert "not found" in strip_ansi(result.output).lower()
|
|
143
|
+
|
|
144
|
+
def test_verify_valid_session_exits_0(self, tmp_path: Path) -> None:
|
|
145
|
+
self._make_db(tmp_path)
|
|
146
|
+
result = runner.invoke(
|
|
147
|
+
app, ["verify", "sess-abc", "--state-dir", str(tmp_path)]
|
|
148
|
+
)
|
|
149
|
+
assert result.exit_code == 0
|
|
150
|
+
assert "VERIFIED" in strip_ansi(result.output)
|
|
151
|
+
|
|
152
|
+
def test_verify_missing_session_exits_1(self, tmp_path: Path) -> None:
|
|
153
|
+
self._make_db(tmp_path)
|
|
154
|
+
result = runner.invoke(
|
|
155
|
+
app, ["verify", "nonexistent", "--state-dir", str(tmp_path)]
|
|
156
|
+
)
|
|
157
|
+
assert result.exit_code == 1
|
|
158
|
+
|
|
159
|
+
def test_verify_shows_merkle_root(self, tmp_path: Path) -> None:
|
|
160
|
+
self._make_db(tmp_path)
|
|
161
|
+
result = runner.invoke(
|
|
162
|
+
app, ["verify", "sess-abc", "--state-dir", str(tmp_path)]
|
|
163
|
+
)
|
|
164
|
+
assert result.exit_code == 0
|
|
165
|
+
assert "Merkle root:" in strip_ansi(result.output)
|
|
166
|
+
|
|
167
|
+
def test_verify_shows_event_count(self, tmp_path: Path) -> None:
|
|
168
|
+
self._make_db(tmp_path)
|
|
169
|
+
result = runner.invoke(
|
|
170
|
+
app, ["verify", "sess-abc", "--state-dir", str(tmp_path)]
|
|
171
|
+
)
|
|
172
|
+
assert result.exit_code == 0
|
|
173
|
+
assert "Events: 0" in strip_ansi(result.output)
|
|
174
|
+
|
|
175
|
+
def test_verify_help(self) -> None:
|
|
176
|
+
result = runner.invoke(app, ["verify", "--help"])
|
|
177
|
+
assert result.exit_code == 0
|
|
178
|
+
clean = strip_ansi(result.output)
|
|
179
|
+
assert "session" in clean.lower() or "verify" in clean.lower()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class TestReplayCommand:
|
|
183
|
+
def _make_db(self, tmp_path: Path) -> None:
|
|
184
|
+
db = tmp_path / "aevum.db"
|
|
185
|
+
conn = sqlite3.connect(str(db))
|
|
186
|
+
conn.executescript("""
|
|
187
|
+
CREATE TABLE sessions (
|
|
188
|
+
session_id TEXT PRIMARY KEY, commit_type TEXT,
|
|
189
|
+
principal TEXT, purpose TEXT, started_at TEXT,
|
|
190
|
+
closed_at TEXT, event_count INTEGER, fact_count INTEGER,
|
|
191
|
+
checkpoint_count INTEGER, merkle_root TEXT,
|
|
192
|
+
ed25519_sig TEXT, mldsa65_sig TEXT, ed25519_pub TEXT,
|
|
193
|
+
mldsa65_pub TEXT, tsa_token TEXT, sigchain_entry_id INTEGER
|
|
194
|
+
);
|
|
195
|
+
CREATE TABLE session_events (
|
|
196
|
+
event_id TEXT PRIMARY KEY, session_id TEXT,
|
|
197
|
+
sequence INTEGER, event_type TEXT, occurred_at TEXT,
|
|
198
|
+
input_hash TEXT, output_hash TEXT, latency_ms INTEGER
|
|
199
|
+
);
|
|
200
|
+
""")
|
|
201
|
+
h = hashlib.sha256(b"").hexdigest()
|
|
202
|
+
now = datetime.now(UTC).isoformat()
|
|
203
|
+
conn.execute(
|
|
204
|
+
"INSERT INTO sessions VALUES (?,?,?,?,?,?,?,?,?,?,NULL,NULL,NULL,NULL,NULL,NULL)",
|
|
205
|
+
("sess-xyz", "complete", "bob", "replay-test", now, now, 0, 0, 0, h),
|
|
206
|
+
)
|
|
207
|
+
conn.commit()
|
|
208
|
+
conn.close()
|
|
209
|
+
|
|
210
|
+
def test_replay_help_text_parseable(self) -> None:
|
|
211
|
+
result = runner.invoke(app, ["replay", "--help"])
|
|
212
|
+
assert result.exit_code == 0
|
|
213
|
+
clean = strip_ansi(result.output)
|
|
214
|
+
assert "replay" in clean.lower() or "session" in clean.lower()
|
|
215
|
+
|
|
216
|
+
def test_replay_missing_db_exits_1(self, tmp_path: Path) -> None:
|
|
217
|
+
result = runner.invoke(
|
|
218
|
+
app, ["replay", "sess-xyz", "--state-dir", str(tmp_path)]
|
|
219
|
+
)
|
|
220
|
+
assert result.exit_code == 1
|
|
221
|
+
assert "not found" in strip_ansi(result.output).lower()
|
|
222
|
+
|
|
223
|
+
def test_replay_valid_session_exits_0(self, tmp_path: Path) -> None:
|
|
224
|
+
self._make_db(tmp_path)
|
|
225
|
+
result = runner.invoke(
|
|
226
|
+
app, ["replay", "sess-xyz", "--state-dir", str(tmp_path)]
|
|
227
|
+
)
|
|
228
|
+
assert result.exit_code == 0
|
|
229
|
+
assert "PASS" in strip_ansi(result.output)
|
|
230
|
+
|
|
231
|
+
def test_replay_missing_session_exits_1(self, tmp_path: Path) -> None:
|
|
232
|
+
self._make_db(tmp_path)
|
|
233
|
+
result = runner.invoke(
|
|
234
|
+
app, ["replay", "no-such-session", "--state-dir", str(tmp_path)]
|
|
235
|
+
)
|
|
236
|
+
assert result.exit_code == 1
|
|
237
|
+
|
|
238
|
+
def test_replay_verbose_flag_accepted(self, tmp_path: Path) -> None:
|
|
239
|
+
self._make_db(tmp_path)
|
|
240
|
+
result = runner.invoke(
|
|
241
|
+
app, ["replay", "sess-xyz", "--verbose", "--state-dir", str(tmp_path)]
|
|
242
|
+
)
|
|
243
|
+
assert result.exit_code == 0
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class TestAuditPackCommand:
|
|
247
|
+
def _make_db(self, tmp_path: Path, session_id: str = "s1") -> None:
|
|
248
|
+
db = tmp_path / "aevum.db"
|
|
249
|
+
conn = sqlite3.connect(str(db))
|
|
250
|
+
conn.executescript("""
|
|
251
|
+
CREATE TABLE sessions (
|
|
252
|
+
session_id TEXT PRIMARY KEY, commit_type TEXT,
|
|
253
|
+
principal TEXT, purpose TEXT, started_at TEXT,
|
|
254
|
+
closed_at TEXT, event_count INTEGER, fact_count INTEGER,
|
|
255
|
+
checkpoint_count INTEGER, merkle_root TEXT,
|
|
256
|
+
ed25519_sig TEXT, mldsa65_sig TEXT, ed25519_pub TEXT,
|
|
257
|
+
mldsa65_pub TEXT, tsa_token TEXT, sigchain_entry_id INTEGER
|
|
258
|
+
);
|
|
259
|
+
CREATE TABLE session_events (
|
|
260
|
+
event_id TEXT PRIMARY KEY, session_id TEXT,
|
|
261
|
+
sequence INTEGER, event_type TEXT, occurred_at TEXT,
|
|
262
|
+
input_hash TEXT, output_hash TEXT, latency_ms INTEGER
|
|
263
|
+
);
|
|
264
|
+
""")
|
|
265
|
+
h = hashlib.sha256(b"").hexdigest()
|
|
266
|
+
now = datetime.now(UTC).isoformat()
|
|
267
|
+
conn.execute(
|
|
268
|
+
"INSERT INTO sessions VALUES (?,?,?,?,?,?,?,?,?,?,NULL,NULL,NULL,NULL,NULL,NULL)",
|
|
269
|
+
(session_id, "complete", "alice", "test", now, now, 0, 0, 0, h),
|
|
270
|
+
)
|
|
271
|
+
conn.commit()
|
|
272
|
+
conn.close()
|
|
273
|
+
|
|
274
|
+
def test_audit_pack_missing_db_exits_1(self, tmp_path: Path) -> None:
|
|
275
|
+
result = runner.invoke(
|
|
276
|
+
app, ["audit-pack", "sess-1", "--state-dir", str(tmp_path)]
|
|
277
|
+
)
|
|
278
|
+
assert result.exit_code == 1
|
|
279
|
+
|
|
280
|
+
def test_audit_pack_to_stdout(self, tmp_path: Path) -> None:
|
|
281
|
+
self._make_db(tmp_path)
|
|
282
|
+
result = runner.invoke(
|
|
283
|
+
app, ["audit-pack", "s1", "--state-dir", str(tmp_path)]
|
|
284
|
+
)
|
|
285
|
+
assert result.exit_code == 0
|
|
286
|
+
pack = json.loads(strip_ansi(result.output))
|
|
287
|
+
assert "@context" in pack
|
|
288
|
+
assert "@graph" in pack
|
|
289
|
+
|
|
290
|
+
def test_audit_pack_to_file(self, tmp_path: Path) -> None:
|
|
291
|
+
"""Audit pack writes JSON-LD to a file."""
|
|
292
|
+
self._make_db(tmp_path)
|
|
293
|
+
out_file = tmp_path / "pack.json"
|
|
294
|
+
result = runner.invoke(app, [
|
|
295
|
+
"audit-pack", "s1",
|
|
296
|
+
"--state-dir", str(tmp_path),
|
|
297
|
+
"--output", str(out_file),
|
|
298
|
+
])
|
|
299
|
+
assert result.exit_code == 0
|
|
300
|
+
assert out_file.exists()
|
|
301
|
+
pack = json.loads(out_file.read_text())
|
|
302
|
+
assert "@context" in pack
|
|
303
|
+
assert "@graph" in pack
|
|
304
|
+
|
|
305
|
+
def test_audit_pack_missing_session_exits_1(self, tmp_path: Path) -> None:
|
|
306
|
+
self._make_db(tmp_path)
|
|
307
|
+
result = runner.invoke(
|
|
308
|
+
app, ["audit-pack", "no-such-session", "--state-dir", str(tmp_path)]
|
|
309
|
+
)
|
|
310
|
+
assert result.exit_code == 1
|
|
311
|
+
|
|
312
|
+
def test_audit_pack_help(self) -> None:
|
|
313
|
+
result = runner.invoke(app, ["audit-pack", "--help"])
|
|
314
|
+
assert result.exit_code == 0
|
|
315
|
+
clean = strip_ansi(result.output)
|
|
316
|
+
assert "session" in clean.lower() or "audit" in clean.lower()
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class TestCLIHelp:
|
|
320
|
+
def test_help_shows_all_new_commands(self) -> None:
|
|
321
|
+
result = runner.invoke(app, ["--help"])
|
|
322
|
+
assert result.exit_code == 0
|
|
323
|
+
clean = strip_ansi(result.output)
|
|
324
|
+
for cmd in ("init", "verify", "audit-pack", "conform", "replay"):
|
|
325
|
+
assert cmd in clean
|
|
326
|
+
|
|
327
|
+
def test_help_still_shows_existing_commands(self) -> None:
|
|
328
|
+
result = runner.invoke(app, ["--help"])
|
|
329
|
+
assert result.exit_code == 0
|
|
330
|
+
clean = strip_ansi(result.output)
|
|
331
|
+
for cmd in ("version", "server", "store", "complication", "conformance"):
|
|
332
|
+
assert cmd in clean
|
|
333
|
+
|
|
334
|
+
def test_each_new_command_has_help(self) -> None:
|
|
335
|
+
for cmd in ("init", "verify", "audit-pack", "conform", "replay"):
|
|
336
|
+
result = runner.invoke(app, [cmd, "--help"])
|
|
337
|
+
assert result.exit_code == 0, f"--help failed for command: {cmd}"
|
|
338
|
+
|
|
339
|
+
def test_init_help_mentions_state_dir(self) -> None:
|
|
340
|
+
result = runner.invoke(app, ["init", "--help"])
|
|
341
|
+
assert result.exit_code == 0
|
|
342
|
+
assert "state-dir" in strip_ansi(result.output)
|
|
343
|
+
|
|
344
|
+
def test_conform_help_mentions_output(self) -> None:
|
|
345
|
+
result = runner.invoke(app, ["conform", "--help"])
|
|
346
|
+
assert result.exit_code == 0
|
|
347
|
+
assert "output" in strip_ansi(result.output).lower()
|
|
348
|
+
|
|
349
|
+
def test_replay_help_mentions_verbose(self) -> None:
|
|
350
|
+
result = runner.invoke(app, ["replay", "--help"])
|
|
351
|
+
assert result.exit_code == 0
|
|
352
|
+
assert "verbose" in strip_ansi(result.output).lower()
|
aevum_cli-0.2.0/.gitignore
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# Python
|
|
2
|
-
__pycache__/
|
|
3
|
-
*.pyc
|
|
4
|
-
*.pyo
|
|
5
|
-
*.pyd
|
|
6
|
-
.venv/
|
|
7
|
-
*.egg-info/
|
|
8
|
-
|
|
9
|
-
# Build
|
|
10
|
-
dist/
|
|
11
|
-
build/
|
|
12
|
-
|
|
13
|
-
# Tools
|
|
14
|
-
.mypy_cache/
|
|
15
|
-
.ruff_cache/
|
|
16
|
-
.pytest_cache/
|
|
17
|
-
.hypothesis/
|
|
18
|
-
|
|
19
|
-
# IDE
|
|
20
|
-
.vscode/
|
|
21
|
-
.idea/
|
|
22
|
-
*.swp
|
|
23
|
-
*.swo
|
|
24
|
-
|
|
25
|
-
# OS
|
|
26
|
-
.DS_Store
|
|
27
|
-
Thumbs.db
|
|
28
|
-
|
|
29
|
-
# Verify scripts (run locally, never commit)
|
|
30
|
-
verify_phase*.py
|
|
31
|
-
scripts/verify_phase*.py
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Top-level typer app. Sub-commands registered here.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from __future__ import annotations
|
|
6
|
-
|
|
7
|
-
import typer
|
|
8
|
-
|
|
9
|
-
from aevum.cli.commands import complication, conformance, server, store, version
|
|
10
|
-
|
|
11
|
-
app = typer.Typer(
|
|
12
|
-
name="aevum",
|
|
13
|
-
help="Aevum context kernel -- command-line interface.",
|
|
14
|
-
no_args_is_help=True,
|
|
15
|
-
pretty_exceptions_enable=False,
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
app.add_typer(server.app, name="server")
|
|
19
|
-
app.add_typer(store.app, name="store")
|
|
20
|
-
app.add_typer(complication.app, name="complication")
|
|
21
|
-
app.add_typer(conformance.app, name="conformance")
|
|
22
|
-
app.command(name="version")(version.version_command)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|