aevum-cli 0.3.0__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,52 @@
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
50
+
51
+ # Maintenance generated files — local only, never commit
52
+ maintenance/generated/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aevum-cli
3
- Version: 0.3.0
3
+ Version: 0.5.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.0"
3
+ version = "0.5.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 = { workspace = true }
38
- aevum-server = { workspace = true }
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",
@@ -11,4 +11,4 @@ Commands:
11
11
  conformance run Run the conformance suite
12
12
  """
13
13
 
14
- __version__ = "0.1.0"
14
+ __version__ = "0.4.0"
@@ -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.store import initialize_schema
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.store import migrate_from_oxigraph
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)
@@ -11,12 +11,9 @@ import typer
11
11
  _PACKAGES = [
12
12
  "aevum-core",
13
13
  "aevum-server",
14
- "aevum-sdk",
15
14
  "aevum-store-oxigraph",
16
15
  "aevum-store-postgres",
17
16
  "aevum-mcp",
18
- "aevum-oidc",
19
- "aevum-llm",
20
17
  "aevum-cli",
21
18
  ]
22
19
 
@@ -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()
@@ -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