methodproof 0.7.7__tar.gz → 0.7.8__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.
- {methodproof-0.7.7 → methodproof-0.7.8}/.gitignore +2 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/PKG-INFO +1 -1
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/__init__.py +1 -1
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/cli.py +112 -2
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/config.py +58 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/store.py +15 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/pyproject.toml +12 -2
- methodproof-0.7.8/tests/conftest.py +131 -0
- methodproof-0.7.8/tests/test_cli_auth.py +258 -0
- methodproof-0.7.8/tests/test_cli_config.py +123 -0
- methodproof-0.7.8/tests/test_cli_helpers.py +156 -0
- methodproof-0.7.8/tests/test_cli_session.py +153 -0
- methodproof-0.7.8/tests/test_cli_share.py +140 -0
- methodproof-0.7.8/tests/test_cli_start.py +456 -0
- methodproof-0.7.8/tests/test_cli_update.py +121 -0
- methodproof-0.7.8/tests/test_e2e_integration.py +159 -0
- methodproof-0.7.8/tests/test_profiles.py +262 -0
- methodproof-0.7.8/tests/test_sync.py +268 -0
- methodproof-0.7.8/tests/test_viewer.py +151 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/.github/workflows/ci.yml +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/CHANGELOG.md +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/LICENSE +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/README.md +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/__main__.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/_daemon.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/agents/__init__.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/agents/base.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/agents/music.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/agents/terminal.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/agents/watcher.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/analysis.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/binding.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/bip39.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/bridge.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/crypto.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/e2e.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/graph.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/hook.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/hooks/__init__.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/hooks/claude_code.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/hooks/claude_code.sh +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/hooks/cline_hook.sh +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/hooks/codex_hook.sh +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/hooks/gemini_hook.sh +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/hooks/install.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/hooks/kiro_hook.sh +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/hooks/mcp_register.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/hooks/openclaw/HOOK.md +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/hooks/openclaw/handler.ts +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/hooks/openclaw_install.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/hooks/opencode_plugin.js +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/hooks/wrappers.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/integrity.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/kdf.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/keychain.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/live.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/lock.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/mcp.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/migrate_db.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/proxy.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/proxy_daemon.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/repos.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/skills/methodproof/SKILL.md +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/sync.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/viewer.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/methodproof/wordlist.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/test_windows_compat.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/tests/__init__.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/tests/test_analysis.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/tests/test_graph.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/tests/test_hooks.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/tests/test_live.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/tests/test_openclaw_hooks.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/tests/test_security.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/tests/test_store.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/tests/test_wrappers.py +0 -0
- {methodproof-0.7.7 → methodproof-0.7.8}/uv.lock +0 -0
|
@@ -502,6 +502,8 @@ def _print_commands() -> None:
|
|
|
502
502
|
print()
|
|
503
503
|
print(f" {_W}ACCOUNT{R}")
|
|
504
504
|
print(f" {_M}mp login{R} Connect to platform (opens browser)")
|
|
505
|
+
print(f" {_M}mp accounts{R} List all accounts on this device")
|
|
506
|
+
print(f" {_M}mp switch{R} {_D}[query]{R} Quick-swap to another account")
|
|
505
507
|
print(f" {_M}mp consent{R} Change capture, research, and redaction settings")
|
|
506
508
|
print(f" {_M}mp lock{R} Destroy local encryption key {_D}(reversible){R}")
|
|
507
509
|
print(f" {_M}mp lock --purge{R} Delete all local data {_D}(irreversible){R}")
|
|
@@ -561,6 +563,8 @@ def _print_commands_plain() -> None:
|
|
|
561
563
|
print()
|
|
562
564
|
print(" ACCOUNT")
|
|
563
565
|
print(" mp login Connect to platform (opens browser)")
|
|
566
|
+
print(" mp accounts List all accounts on this device")
|
|
567
|
+
print(" mp switch [query] Quick-swap to another account")
|
|
564
568
|
print(" mp consent Change capture, research, and redaction settings")
|
|
565
569
|
print(" mp lock Destroy local encryption key (reversible)")
|
|
566
570
|
print(" mp lock --purge Delete all local data (irreversible)")
|
|
@@ -982,6 +986,13 @@ def cmd_start(args: argparse.Namespace) -> None:
|
|
|
982
986
|
_log_step("Creating session")
|
|
983
987
|
sid = uuid.uuid4().hex
|
|
984
988
|
watch_dir = os.path.abspath(args.dir or ".")
|
|
989
|
+
|
|
990
|
+
# Prevent concurrent sessions watching overlapping directories
|
|
991
|
+
conflict = store.find_active_for_dir(watch_dir)
|
|
992
|
+
if conflict:
|
|
993
|
+
print(f"Active session {conflict['id'][:8]} already watches {conflict['watch_dir']}")
|
|
994
|
+
print("Run `methodproof stop` first, or choose a different directory.")
|
|
995
|
+
sys.exit(1)
|
|
985
996
|
repo_url = args.repo or repos.detect_repo(watch_dir)
|
|
986
997
|
tags = args.tags.split(",") if args.tags else []
|
|
987
998
|
visibility = "public" if args.public else "private"
|
|
@@ -1327,6 +1338,9 @@ def cmd_status(args: argparse.Namespace) -> None:
|
|
|
1327
1338
|
print(f" auth: signed in")
|
|
1328
1339
|
print(f" account: {account_id[:8]}...{account_id[-4:]}")
|
|
1329
1340
|
print(f" role: {role} | type: {acct_type} | token: {expiry}")
|
|
1341
|
+
n_profiles = len(cfg.get("profiles", {}))
|
|
1342
|
+
if n_profiles > 1:
|
|
1343
|
+
print(f" accounts: {n_profiles} on this device (`mp switch` to swap)")
|
|
1330
1344
|
if claims.get("is_superadmin"):
|
|
1331
1345
|
print(" superadmin: yes")
|
|
1332
1346
|
|
|
@@ -1391,6 +1405,92 @@ def cmd_logout(args: argparse.Namespace) -> None:
|
|
|
1391
1405
|
print(f"Logged out ({label}). Run `mp login` to sign in again.")
|
|
1392
1406
|
|
|
1393
1407
|
|
|
1408
|
+
def cmd_accounts(args: argparse.Namespace) -> None:
|
|
1409
|
+
"""List all accounts stored on this device."""
|
|
1410
|
+
cfg = config.load()
|
|
1411
|
+
profiles = config.list_profiles(cfg)
|
|
1412
|
+
if not profiles:
|
|
1413
|
+
print("No accounts. Run `mp login` to sign in.")
|
|
1414
|
+
return
|
|
1415
|
+
print()
|
|
1416
|
+
for p in profiles:
|
|
1417
|
+
marker = "*" if p.get("active") else " "
|
|
1418
|
+
email = p.get("email", "")
|
|
1419
|
+
aid = p.get("account_id", "")[:8]
|
|
1420
|
+
label = email or aid or "unknown"
|
|
1421
|
+
# Token status
|
|
1422
|
+
token = p.get("token", "")
|
|
1423
|
+
if token:
|
|
1424
|
+
claims = _decode_jwt_claims(token)
|
|
1425
|
+
exp = claims.get("exp", 0)
|
|
1426
|
+
if exp and time.time() > exp:
|
|
1427
|
+
status = "expired"
|
|
1428
|
+
elif exp:
|
|
1429
|
+
remaining = int(exp - time.time())
|
|
1430
|
+
h, m = remaining // 3600, (remaining % 3600) // 60
|
|
1431
|
+
status = f"{h}h {m}m"
|
|
1432
|
+
else:
|
|
1433
|
+
status = "valid"
|
|
1434
|
+
else:
|
|
1435
|
+
status = "no token"
|
|
1436
|
+
print(f" {marker} {label} ({aid}) token: {status}")
|
|
1437
|
+
print(f"\n Switch: `mp switch <email or id prefix>`\n")
|
|
1438
|
+
|
|
1439
|
+
|
|
1440
|
+
def cmd_switch(args: argparse.Namespace) -> None:
|
|
1441
|
+
"""Quick-swap to a stored account profile."""
|
|
1442
|
+
cfg = config.load()
|
|
1443
|
+
profiles = cfg.get("profiles", {})
|
|
1444
|
+
|
|
1445
|
+
if not profiles:
|
|
1446
|
+
print("No stored accounts. Run `mp login` to add one.")
|
|
1447
|
+
return
|
|
1448
|
+
|
|
1449
|
+
query = getattr(args, "account", None)
|
|
1450
|
+
|
|
1451
|
+
if query:
|
|
1452
|
+
# Direct match
|
|
1453
|
+
target = config.find_profile(cfg, query)
|
|
1454
|
+
if not target:
|
|
1455
|
+
print(f"No account matching '{query}'. Run `mp accounts` to see stored accounts.")
|
|
1456
|
+
return
|
|
1457
|
+
else:
|
|
1458
|
+
# Interactive picker
|
|
1459
|
+
items = [(aid, p) for aid, p in profiles.items() if aid != cfg.get("account_id")]
|
|
1460
|
+
if not items:
|
|
1461
|
+
print("Only one account stored. Run `mp login` to add another.")
|
|
1462
|
+
return
|
|
1463
|
+
print()
|
|
1464
|
+
for i, (aid, p) in enumerate(items, 1):
|
|
1465
|
+
label = p.get("email") or aid[:8]
|
|
1466
|
+
print(f" {i}. {label} ({aid[:8]})")
|
|
1467
|
+
print()
|
|
1468
|
+
try:
|
|
1469
|
+
choice = input(f" Switch to [1-{len(items)}]: ").strip()
|
|
1470
|
+
idx = int(choice) - 1
|
|
1471
|
+
if not 0 <= idx < len(items):
|
|
1472
|
+
print("Cancelled.")
|
|
1473
|
+
return
|
|
1474
|
+
except (ValueError, EOFError, KeyboardInterrupt):
|
|
1475
|
+
print("\nCancelled.")
|
|
1476
|
+
return
|
|
1477
|
+
target = items[idx][0]
|
|
1478
|
+
|
|
1479
|
+
if target == cfg.get("account_id"):
|
|
1480
|
+
label = cfg.get("email") or target[:8]
|
|
1481
|
+
print(f"Already active: {label}")
|
|
1482
|
+
return
|
|
1483
|
+
|
|
1484
|
+
if config.restore_profile(cfg, target):
|
|
1485
|
+
cfg = config.load() # reload after swap
|
|
1486
|
+
label = cfg.get("email") or cfg.get("account_id", "")[:8]
|
|
1487
|
+
print(f"Switched to {label} ({cfg.get('account_id', '')[:8]}).")
|
|
1488
|
+
# Re-setup master key for this account
|
|
1489
|
+
_setup_master_key(cfg)
|
|
1490
|
+
else:
|
|
1491
|
+
print(f"Profile not found for {target[:8]}. Run `mp login`.")
|
|
1492
|
+
|
|
1493
|
+
|
|
1394
1494
|
def cmd_login(args: argparse.Namespace) -> None:
|
|
1395
1495
|
import webbrowser
|
|
1396
1496
|
from methodproof.sync import _request
|
|
@@ -1404,6 +1504,8 @@ def cmd_login(args: argparse.Namespace) -> None:
|
|
|
1404
1504
|
answer = input(" Switch accounts? [y/N]: ").strip().lower()
|
|
1405
1505
|
if answer not in ("y", "yes"):
|
|
1406
1506
|
return
|
|
1507
|
+
# Stash current profile before switching
|
|
1508
|
+
config.save_active_profile(cfg)
|
|
1407
1509
|
|
|
1408
1510
|
# Start device auth flow
|
|
1409
1511
|
result = _request("POST", "/auth/cli/start", api, "")
|
|
@@ -1437,12 +1539,17 @@ def cmd_login(args: argparse.Namespace) -> None:
|
|
|
1437
1539
|
cfg["last_auth_at"] = time.time()
|
|
1438
1540
|
cfg["master_key_fingerprint"] = "" # clear stale fingerprint from previous account
|
|
1439
1541
|
config.save(cfg)
|
|
1542
|
+
config.save_active_profile(cfg)
|
|
1440
1543
|
print(" done.\n")
|
|
1441
1544
|
if not getattr(args, "no_key", False):
|
|
1442
1545
|
_setup_master_key(cfg)
|
|
1443
1546
|
from methodproof.sync import sync_research_consent
|
|
1444
1547
|
sync_research_consent(cfg["token"], cfg["api_url"])
|
|
1445
|
-
|
|
1548
|
+
label = cfg.get("email") or cfg.get("account_id", "")[:8]
|
|
1549
|
+
profiles = cfg.get("profiles", {})
|
|
1550
|
+
n = len(profiles)
|
|
1551
|
+
print(f"Logged in as {label}. {n} account{'s' if n != 1 else ''} on this device.")
|
|
1552
|
+
print(" Quick-swap: `mp switch`")
|
|
1446
1553
|
return
|
|
1447
1554
|
except Exception:
|
|
1448
1555
|
pass
|
|
@@ -1868,6 +1975,9 @@ def main() -> None:
|
|
|
1868
1975
|
l.add_argument("--force", "-f", action="store_true", help="Skip switch-account prompt")
|
|
1869
1976
|
l.add_argument("--no-key", action="store_true", help="Skip master key generation (test accounts)")
|
|
1870
1977
|
sub.add_parser("logout", help="Clear login credentials (keeps consent and sessions)")
|
|
1978
|
+
sub.add_parser("accounts", help="List all accounts on this device")
|
|
1979
|
+
sw = sub.add_parser("switch", help="Quick-swap to another account")
|
|
1980
|
+
sw.add_argument("account", nargs="?", help="Email or account ID prefix")
|
|
1871
1981
|
pu = sub.add_parser("push", help="Upload privately to your account")
|
|
1872
1982
|
pu.add_argument("session_id", nargs="?")
|
|
1873
1983
|
pu.add_argument("--local", action="store_true", help="Push to local dev API (localhost:8000)")
|
|
@@ -1928,7 +2038,7 @@ def main() -> None:
|
|
|
1928
2038
|
cmds = {
|
|
1929
2039
|
"init": cmd_init, "start": cmd_start, "stop": cmd_stop,
|
|
1930
2040
|
"view": cmd_view, "log": cmd_log, "status": cmd_status,
|
|
1931
|
-
"login": cmd_login, "logout": cmd_logout,
|
|
2041
|
+
"login": cmd_login, "logout": cmd_logout, "accounts": cmd_accounts, "switch": cmd_switch,
|
|
1932
2042
|
"push": cmd_push, "tag": cmd_tag, "publish": cmd_publish,
|
|
1933
2043
|
"delete": cmd_delete, "review": cmd_review, "consent": cmd_consent,
|
|
1934
2044
|
"update": cmd_update, "lock": cmd_lock, "reset": cmd_reset, "uninstall": cmd_uninstall,
|
|
@@ -50,6 +50,7 @@ _DEFAULTS: dict[str, Any] = {
|
|
|
50
50
|
"ai_responses": True,
|
|
51
51
|
"code_capture": True,
|
|
52
52
|
},
|
|
53
|
+
"profiles": {},
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
FREE_JOURNAL_MAX_HOURS = 4
|
|
@@ -154,3 +155,60 @@ def save(cfg: dict[str, Any]) -> None:
|
|
|
154
155
|
ensure_dirs()
|
|
155
156
|
CONFIG.write_text(json.dumps(cfg, indent=2) + "\n")
|
|
156
157
|
secure_file(CONFIG)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# --- Multi-account profiles ---
|
|
161
|
+
|
|
162
|
+
_PROFILE_KEYS = [
|
|
163
|
+
"token", "refresh_token", "email", "account_id",
|
|
164
|
+
"last_auth_at", "master_key_fingerprint",
|
|
165
|
+
"e2e_mode", "e2e_fingerprint",
|
|
166
|
+
"journal_mode", "journal_credits",
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def save_active_profile(cfg: dict[str, Any]) -> None:
|
|
171
|
+
"""Stash current auth state into profiles dict, keyed by account_id."""
|
|
172
|
+
aid = cfg.get("account_id")
|
|
173
|
+
if not aid:
|
|
174
|
+
return
|
|
175
|
+
profiles = cfg.setdefault("profiles", {})
|
|
176
|
+
profiles[aid] = {k: cfg.get(k, _DEFAULTS.get(k, "")) for k in _PROFILE_KEYS}
|
|
177
|
+
save(cfg)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def restore_profile(cfg: dict[str, Any], account_id: str) -> bool:
|
|
181
|
+
"""Swap active auth state to a stored profile. Returns False if not found."""
|
|
182
|
+
profiles = cfg.get("profiles", {})
|
|
183
|
+
profile = profiles.get(account_id)
|
|
184
|
+
if not profile:
|
|
185
|
+
return False
|
|
186
|
+
save_active_profile(cfg)
|
|
187
|
+
for k in _PROFILE_KEYS:
|
|
188
|
+
cfg[k] = profile.get(k, _DEFAULTS.get(k, ""))
|
|
189
|
+
save(cfg)
|
|
190
|
+
return True
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def list_profiles(cfg: dict[str, Any]) -> list[dict[str, Any]]:
|
|
194
|
+
"""Return stored profiles with 'active' flag on the current account."""
|
|
195
|
+
active_id = cfg.get("account_id", "")
|
|
196
|
+
profiles = cfg.get("profiles", {})
|
|
197
|
+
result = []
|
|
198
|
+
for aid, p in profiles.items():
|
|
199
|
+
result.append({**p, "active": aid == active_id})
|
|
200
|
+
if active_id and active_id not in profiles:
|
|
201
|
+
result.append({
|
|
202
|
+
k: cfg.get(k, _DEFAULTS.get(k, "")) for k in _PROFILE_KEYS
|
|
203
|
+
} | {"active": True})
|
|
204
|
+
return result
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def find_profile(cfg: dict[str, Any], query: str) -> str | None:
|
|
208
|
+
"""Match a profile by email or account_id prefix. Returns account_id or None."""
|
|
209
|
+
profiles = cfg.get("profiles", {})
|
|
210
|
+
q = query.lower().strip()
|
|
211
|
+
for aid, p in profiles.items():
|
|
212
|
+
if p.get("email", "").lower() == q or aid.startswith(q):
|
|
213
|
+
return aid
|
|
214
|
+
return None
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""SQLite store — sessions, events, graph relationships."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import os
|
|
4
5
|
import sqlite3
|
|
5
6
|
import time
|
|
6
7
|
import uuid
|
|
@@ -165,6 +166,20 @@ def create_session(
|
|
|
165
166
|
_db().commit()
|
|
166
167
|
|
|
167
168
|
|
|
169
|
+
def find_active_for_dir(watch_dir: str) -> dict[str, str] | None:
|
|
170
|
+
"""Find an active session watching the same or overlapping directory."""
|
|
171
|
+
rows = _db().execute(
|
|
172
|
+
"SELECT id, watch_dir FROM sessions WHERE completed_at IS NULL",
|
|
173
|
+
).fetchall()
|
|
174
|
+
for r in rows:
|
|
175
|
+
existing = r[1]
|
|
176
|
+
if (watch_dir == existing
|
|
177
|
+
or watch_dir.startswith(existing + os.sep)
|
|
178
|
+
or existing.startswith(watch_dir + os.sep)):
|
|
179
|
+
return {"id": r[0], "watch_dir": r[1]}
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
|
|
168
183
|
def complete_session(session_id: str) -> None:
|
|
169
184
|
db = _db()
|
|
170
185
|
count = db.execute(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "methodproof"
|
|
3
|
-
version = "0.7.
|
|
3
|
+
version = "0.7.8"
|
|
4
4
|
description = "See how you code. Capture and visualize your engineering process."
|
|
5
5
|
requires-python = ">=3.11"
|
|
6
6
|
dependencies = ["watchdog>=4.0", "websocket-client>=1.7", "cryptography>=43.0", "keyring>=25.0"]
|
|
@@ -11,7 +11,7 @@ readme = "README.md"
|
|
|
11
11
|
proxy = ["mitmproxy>=10.0"]
|
|
12
12
|
|
|
13
13
|
[dependency-groups]
|
|
14
|
-
dev = ["pytest>=8.0"]
|
|
14
|
+
dev = ["pytest>=8.0", "pytest-cov>=5.0"]
|
|
15
15
|
|
|
16
16
|
[project.scripts]
|
|
17
17
|
methodproof = "methodproof.cli:main"
|
|
@@ -25,3 +25,13 @@ packages = ["methodproof"]
|
|
|
25
25
|
|
|
26
26
|
[tool.pytest.ini_options]
|
|
27
27
|
testpaths = ["tests"]
|
|
28
|
+
markers = [
|
|
29
|
+
"e2e: requires Docker infrastructure (just infra-up && just seed && just platform)",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[tool.coverage.run]
|
|
33
|
+
source = ["methodproof"]
|
|
34
|
+
omit = ["methodproof/wordlist.py"]
|
|
35
|
+
|
|
36
|
+
[tool.coverage.report]
|
|
37
|
+
show_missing = true
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Shared fixtures for CLI tests."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import base64
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
import uuid
|
|
8
|
+
import zlib
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from methodproof import config, store
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ── Filesystem isolation ──
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.fixture(autouse=True)
|
|
21
|
+
def isolate_fs(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
|
22
|
+
"""Isolate all config/DB/log paths to tmp_path. Writes defaults to disk
|
|
23
|
+
so config.load() always parses JSON (avoids _DEFAULTS shallow-copy mutation)."""
|
|
24
|
+
mp_dir = tmp_path / ".methodproof"
|
|
25
|
+
mp_dir.mkdir()
|
|
26
|
+
monkeypatch.setattr(config, "DIR", mp_dir)
|
|
27
|
+
monkeypatch.setattr(config, "CONFIG", mp_dir / "config.json")
|
|
28
|
+
monkeypatch.setattr(config, "DB_PATH", mp_dir / "methodproof.db")
|
|
29
|
+
monkeypatch.setattr(config, "CMD_LOG", mp_dir / "commands.jsonl")
|
|
30
|
+
(mp_dir / "config.json").write_text(json.dumps(dict(config._DEFAULTS), indent=2))
|
|
31
|
+
monkeypatch.setattr(store, "_conn", None)
|
|
32
|
+
store.init_db()
|
|
33
|
+
return tmp_path
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ── JWT helper ──
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def fake_jwt():
|
|
41
|
+
"""Factory: builds a real base64-encoded JWT so _decode_jwt_claims works."""
|
|
42
|
+
def _make(
|
|
43
|
+
user_id: str = "test-account-123",
|
|
44
|
+
role: str = "admin",
|
|
45
|
+
account_type: str = "pro",
|
|
46
|
+
exp: float | None = None,
|
|
47
|
+
**extra: Any,
|
|
48
|
+
) -> str:
|
|
49
|
+
header = base64.urlsafe_b64encode(b'{"alg":"HS256","typ":"JWT"}').rstrip(b"=").decode()
|
|
50
|
+
claims = {
|
|
51
|
+
"user_id": user_id,
|
|
52
|
+
"role": role,
|
|
53
|
+
"account_type": account_type,
|
|
54
|
+
"exp": exp if exp is not None else time.time() + 3600,
|
|
55
|
+
**extra,
|
|
56
|
+
}
|
|
57
|
+
payload = base64.urlsafe_b64encode(json.dumps(claims).encode()).rstrip(b"=").decode()
|
|
58
|
+
sig = base64.urlsafe_b64encode(b"fakesig").rstrip(b"=").decode()
|
|
59
|
+
return f"{header}.{payload}.{sig}"
|
|
60
|
+
return _make
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ── Config helpers ──
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@pytest.fixture
|
|
67
|
+
def logged_in_cfg(fake_jwt):
|
|
68
|
+
"""Factory: writes a config with valid auth state, returns cfg dict."""
|
|
69
|
+
def _make(
|
|
70
|
+
account_id: str = "test-account-123",
|
|
71
|
+
email: str = "test@methodproof.com",
|
|
72
|
+
token: str | None = None,
|
|
73
|
+
**extra: Any,
|
|
74
|
+
) -> dict[str, Any]:
|
|
75
|
+
cfg = config.load()
|
|
76
|
+
cfg["token"] = token or fake_jwt(user_id=account_id)
|
|
77
|
+
cfg["account_id"] = account_id
|
|
78
|
+
cfg["email"] = email
|
|
79
|
+
cfg["last_auth_at"] = time.time()
|
|
80
|
+
cfg.update(extra)
|
|
81
|
+
config.save(cfg)
|
|
82
|
+
return cfg
|
|
83
|
+
return _make
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ── Session factory ──
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@pytest.fixture
|
|
90
|
+
def make_session():
|
|
91
|
+
"""Factory: creates a completed session with N events in SQLite."""
|
|
92
|
+
def _make(n_events: int = 5, account_id: str = "test-account-123",
|
|
93
|
+
watch_dir: str = "/tmp/test", **session_kwargs: Any) -> tuple[str, list[dict]]:
|
|
94
|
+
sid = uuid.uuid4().hex
|
|
95
|
+
store.create_session(sid, watch_dir, account_id=account_id, **session_kwargs)
|
|
96
|
+
events = []
|
|
97
|
+
base_ts = time.time() - 300
|
|
98
|
+
for i in range(n_events):
|
|
99
|
+
e = {
|
|
100
|
+
"id": uuid.uuid4().hex,
|
|
101
|
+
"type": ["file_edit", "terminal_cmd", "llm_prompt", "git_commit", "test_run"][i % 5],
|
|
102
|
+
"timestamp": base_ts + i * 10,
|
|
103
|
+
"duration_ms": 100 + i * 50,
|
|
104
|
+
"metadata": {"path": f"file_{i}.py", "language": "python"},
|
|
105
|
+
}
|
|
106
|
+
events.append(e)
|
|
107
|
+
store.insert_events(sid, events)
|
|
108
|
+
store.complete_session(sid)
|
|
109
|
+
return sid, events
|
|
110
|
+
return _make
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ── Argparse helper ──
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@pytest.fixture
|
|
117
|
+
def cli_args():
|
|
118
|
+
"""Factory: builds argparse.Namespace with sensible defaults."""
|
|
119
|
+
def _make(**kwargs: Any) -> argparse.Namespace:
|
|
120
|
+
defaults = {
|
|
121
|
+
"session_id": None, "dir": None, "repo": None, "tags": None,
|
|
122
|
+
"public": False, "live": False, "live_public": False,
|
|
123
|
+
"journal": False, "e2e": False, "no_e2e": False,
|
|
124
|
+
"verbose": False, "streaming": False, "force": False,
|
|
125
|
+
"local": False, "api_url": None, "no_key": False, "auto": None,
|
|
126
|
+
"account": None, "purge": False, "keep_sessions": False,
|
|
127
|
+
"anonymous": False,
|
|
128
|
+
}
|
|
129
|
+
defaults.update(kwargs)
|
|
130
|
+
return argparse.Namespace(**defaults)
|
|
131
|
+
return _make
|