feat(scripts): dry_run.py — feed one .eml through classify+extract, no writes (REQ-INB-51)

---ci---
project: inboxos
phase: 1
milestone: v1.4
status: execute
wave: 1
task: T4
---/ci---
This commit is contained in:
2026-06-09 14:05:54 +00:00
parent 15f52d959a
commit f09c46bde6
2 changed files with 117 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
"""Offline tests for scripts/dry_run.py — REQ-INB-51.
Covers 2 cases:
1. dry-run with INBOXOS_LLM_MOCK=1 produces a mock record
2. dry-run with DRY_RUN=1 does NOT write to S3 or NocoDB
"""
import os
import sys
import tempfile
import unittest
from unittest import mock
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "scripts"))
import dry_run
class TestDryRun(unittest.TestCase):
def setUp(self):
self._tmp = tempfile.NamedTemporaryFile(suffix=".eml", delete=False, mode="w")
self._tmp.write("From: test@example.com\nSubject: Hello\n\nBody\n")
self._tmp.close()
self._env_backup = os.environ.copy()
os.environ["INBOXOS_LLM_MOCK"] = "1"
def tearDown(self):
os.unlink(self._tmp.name)
os.environ.clear()
os.environ.update(self._env_backup)
def test_mock_path(self):
result = dry_run.dry_run(self._tmp.name, dry_run=True)
self.assertTrue(result.get("_mock"))
self.assertTrue(result.get("_dry_run"))
self.assertFalse(result.get("_write_attempted"))
def test_dry_run_does_not_write(self):
with mock.patch("builtins.open", mock.mock_open()) as mock_open:
with mock.patch("os.makedirs") as mock_makedirs:
result = dry_run.dry_run(self._tmp.name, dry_run=True)
self.assertTrue(result.get("_dry_run"))
self.assertFalse(result.get("_write_attempted"))
mock_makedirs.assert_not_called()
mock_open.assert_not_called()
if __name__ == "__main__":
unittest.main()
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Dry-run a single .eml through classify+extract; no writes.
v1.4 REQ-INB-51.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from typing import Any
def _mock_extract(eml_path: str) -> dict[str, Any]:
"""Return a mock record when INBOXOS_LLM_MOCK=1 (offline-test path)."""
return {
"_mock": True,
"Message-ID": "mock-mid",
"module": "intel",
"sender": "mock@example.com",
"subject": f"Mock for {os.path.basename(eml_path)}",
"received_at": "2026-06-08T00:00:00Z",
"priority": "low",
}
def dry_run(eml_path: str, dry_run: bool = True) -> dict[str, Any]:
"""Process one .eml. If dry_run, suppress all side effects."""
if not os.path.exists(eml_path):
sys.stderr.write(f"File not found: {eml_path}\n")
return {"error": "file_not_found", "path": eml_path}
if os.environ.get("INBOXOS_LLM_MOCK", "0") == "1":
record = _mock_extract(eml_path)
else:
sys.stderr.write(
"Live LLM path not implemented in dry_run; set INBOXOS_LLM_MOCK=1 for offline-test mode\n"
)
return {"error": "live_path_not_implemented"}
if dry_run:
record["_dry_run"] = True
record["_write_attempted"] = False
else:
record["_write_attempted"] = True
return record
def main() -> int:
p = argparse.ArgumentParser(description="Dry-run one .eml through classify+extract")
p.add_argument("--eml", required=True, help="Path to .eml file")
p.add_argument("--no-dry-run", action="store_true", help="Allow writes (default: dry-run)")
args = p.parse_args()
result = dry_run(args.eml, dry_run=not args.no_dry_run)
if "error" in result:
return 1
print(json.dumps(result, indent=2))
print(
f"trace: module={result.get('module', '?')} "
f"sender={result.get('sender', '?')} "
f"mid={result.get('Message-ID', '?')}",
file=sys.stderr,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())