create-workframe 0.1.11 → 0.1.13
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.
- package/README.md +1 -1
- package/bin/create-workframe.js +2 -2
- package/bin/workframe.js +130 -2
- package/docs/security.md +85 -0
- package/package.json +1 -1
- package/scripts/bundle-workframe-ui.mjs +9 -0
- package/scripts/sync-canonical-to-package.mjs +9 -0
- package/scripts/verify-public-deploy.sh +121 -3
- package/workframe-api/action_proxy.py +23 -17
- package/workframe-api/activity_feed.py +798 -0
- package/workframe-api/api_meta.py +157 -0
- package/workframe-api/auth_gate.py +566 -0
- package/workframe-api/avatar_registry.py +343 -0
- package/workframe-api/broker_audit.py +164 -0
- package/workframe-api/cell_authority.py +231 -0
- package/workframe-api/chat_bind.py +319 -0
- package/workframe-api/chat_sessions.py +509 -0
- package/workframe-api/chat_stream.py +694 -0
- package/workframe-api/cleanup_dogfood_smoke.py +252 -0
- package/workframe-api/credential_broker.py +162 -0
- package/workframe-api/credential_resolve.py +202 -0
- package/workframe-api/credential_store.py +245 -0
- package/workframe-api/crew_registry.py +250 -0
- package/workframe-api/db_schema.py +755 -0
- package/workframe-api/docker_gateway.py +166 -0
- package/workframe-api/doctor_runtime.py +109 -0
- package/workframe-api/domain/__init__.py +47 -0
- package/workframe-api/domain/entities.py +252 -0
- package/workframe-api/domain/schema/workframe-domain.schema.json +278 -0
- package/workframe-api/egress_policy.py +42 -0
- package/workframe-api/handler_modules/__init__.py +17 -0
- package/workframe-api/handler_modules/handler_admin.py +542 -0
- package/workframe-api/handler_modules/handler_auth.py +512 -0
- package/workframe-api/handler_modules/handler_chat.py +641 -0
- package/workframe-api/handler_modules/handler_install.py +178 -0
- package/workframe-api/handler_modules/handler_provider.py +325 -0
- package/workframe-api/handler_modules/handler_workspace.py +1420 -0
- package/workframe-api/health_monitor.py +80 -0
- package/workframe-api/hermes_admin.py +756 -0
- package/workframe-api/hermes_profiles.py +1328 -0
- package/workframe-api/install_api.py +123 -0
- package/workframe-api/kanban_cron.py +473 -0
- package/workframe-api/lane_bindings.py +423 -0
- package/workframe-api/llm_proxy.py +17 -43
- package/workframe-api/mention_helpers.py +180 -0
- package/workframe-api/mention_invoke.py +431 -0
- package/workframe-api/model_surface.py +1139 -0
- package/workframe-api/oauth_pending.py +85 -0
- package/workframe-api/oauth_redirect.py +381 -0
- package/workframe-api/openrouter_catalog.py +4 -4
- package/workframe-api/package.json +2 -2
- package/workframe-api/profile_api_lifecycle.py +66 -0
- package/workframe-api/profile_config_yaml.py +126 -0
- package/workframe-api/profile_gateway.py +436 -0
- package/workframe-api/provider_bindings.py +673 -0
- package/workframe-api/provider_bootstrap.py +347 -0
- package/workframe-api/provider_catalog.py +180 -0
- package/workframe-api/rooms.py +1613 -0
- package/workframe-api/route_registry.py +470 -0
- package/workframe-api/run-typecheck.mjs +26 -0
- package/workframe-api/run_authority.py +287 -0
- package/workframe-api/run_ledger.py +470 -0
- package/workframe-api/run_surface_wiring.py +344 -0
- package/workframe-api/runtime_cohort.py +752 -0
- package/workframe-api/runtime_tokens.py +127 -0
- package/workframe-api/server.py +1376 -19305
- package/workframe-api/snapshot_feed.py +49 -0
- package/workframe-api/stack_config.py +33 -1
- package/workframe-api/supervisor_client.py +154 -0
- package/workframe-api/test_api_meta_build_stamp.py +34 -0
- package/workframe-api/test_cell_authority.py +79 -0
- package/workframe-api/test_chat_bind.py +48 -0
- package/workframe-api/test_credential_lease_matrix.py +171 -0
- package/workframe-api/test_credential_resolve.py +51 -0
- package/workframe-api/test_credential_store.py +27 -0
- package/workframe-api/test_dogfood_flows.py +346 -0
- package/workframe-api/test_domain_entities.py +152 -0
- package/workframe-api/test_exception_hygiene.py +12 -0
- package/workframe-api/test_hermes_admin.py +72 -0
- package/workframe-api/test_install_wizard_resume.py +78 -0
- package/workframe-api/test_local_bootstrap.py +67 -0
- package/workframe-api/test_mention_helpers.py +35 -0
- package/workframe-api/test_mention_invoke.py +26 -0
- package/workframe-api/test_model_surface_consistency.py +117 -0
- package/workframe-api/test_oauth_pending.py +41 -0
- package/workframe-api/test_profile_model_yaml.py +99 -0
- package/workframe-api/test_provider_bindings.py +52 -0
- package/workframe-api/test_provider_catalog.py +28 -0
- package/workframe-api/test_public_routes.py +41 -0
- package/workframe-api/test_route_registry.py +245 -0
- package/workframe-api/test_run_authority.py +112 -0
- package/workframe-api/test_run_ledger.py +157 -0
- package/workframe-api/test_run_surface_wiring.py +130 -0
- package/workframe-api/test_runtime_cohort.py +85 -0
- package/workframe-api/test_secure_mode_docker_boundary.py +39 -0
- package/workframe-api/test_server_reexports.py +99 -0
- package/workframe-api/test_stack_config_install_smtp.py +7 -0
- package/workframe-api/time-bind-steps.py +75 -0
- package/workframe-api/time-perf.py +81 -0
- package/workframe-api/time-warm.py +107 -0
- package/workframe-api/turn_credentials.py +28 -13
- package/workframe-api/turn_overlay.py +715 -0
- package/workframe-api/updates.py +16 -1
- package/workframe-api/user_prefs.py +387 -0
- package/workframe-api/wf032_extract_remaining_handlers.py +417 -0
- package/workframe-api/workspace_bootstrap.py +536 -0
- package/workframe-api/workspace_files.py +344 -0
- package/workframe-api/workspace_messaging.py +206 -0
- package/workframe-api/zk_auth.py +3 -2
- package/workframe-supervisor/server.py +10 -1
- package/workframe-supervisor/test_supervisor_negative.py +75 -0
- package/workframe-ui/public/assets/{arc-DjpS9a2b.js → arc-aUUffclm.js} +1 -1
- package/workframe-ui/public/assets/architecture-7EHR7CIX-BSUP4S8J.js +1 -0
- package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-puCKmKHV.js → architectureDiagram-3BPJPVTR-DU6oaqIb.js} +1 -1
- package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-C2P0g-eH.js → blockDiagram-GPEHLZMM-D2p8BU6-.js} +1 -1
- package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-DdvYEITH.js → c4Diagram-AAUBKEIU-ouwyPmTJ.js} +1 -1
- package/workframe-ui/public/assets/channel-DOunZZ0X.js +1 -0
- package/workframe-ui/public/assets/{chunk-2J33WTMH-Cxe8QsEF.js → chunk-2J33WTMH-DsE7U5dj.js} +1 -1
- package/workframe-ui/public/assets/{chunk-3OPIFGDE-DPNyTwR3.js → chunk-3OPIFGDE-DM0kkZ9h.js} +1 -1
- package/workframe-ui/public/assets/{chunk-4BX2VUAB-XKG9gfRN.js → chunk-4BX2VUAB-CQ8mAyXp.js} +1 -1
- package/workframe-ui/public/assets/{chunk-55IACEB6-oPMTulgb.js → chunk-55IACEB6-DN1BDtbH.js} +1 -1
- package/workframe-ui/public/assets/{chunk-5ZQYHXKU-BWwXvpa_.js → chunk-5ZQYHXKU-BzK2KqOt.js} +1 -1
- package/workframe-ui/public/assets/{chunk-727SXJPM-Cml2twO_.js → chunk-727SXJPM-C3AxtOi9.js} +1 -1
- package/workframe-ui/public/assets/{chunk-AQP2D5EJ-DzrRBhQu.js → chunk-AQP2D5EJ-4mVwEFuD.js} +1 -1
- package/workframe-ui/public/assets/{chunk-BSJP7CBP-EZ7STU6h.js → chunk-BSJP7CBP-23kN2q0A.js} +1 -1
- package/workframe-ui/public/assets/{chunk-CSCIHK7Q-BGTQzWYw.js → chunk-CSCIHK7Q-CPBKVW-L.js} +2 -2
- package/workframe-ui/public/assets/{chunk-FMBD7UC4-Cm4stfRa.js → chunk-FMBD7UC4-CKqcYq7K.js} +1 -1
- package/workframe-ui/public/assets/{chunk-KSCS5N6A-exbMijjy.js → chunk-KSCS5N6A-JRQnhxQE.js} +1 -1
- package/workframe-ui/public/assets/{chunk-L5ZTLDWV-C75kbQwN.js → chunk-L5ZTLDWV-BLZrdIwa.js} +1 -1
- package/workframe-ui/public/assets/chunk-LZXEDZCA-DisG0XJT.js +2 -0
- package/workframe-ui/public/assets/{chunk-ND2GUHAM-D84zyIu2.js → chunk-ND2GUHAM-ClAOxArS.js} +1 -1
- package/workframe-ui/public/assets/{chunk-NZK2D7GU-CUNNo1bj.js → chunk-NZK2D7GU-XUE1aNkm.js} +1 -1
- package/workframe-ui/public/assets/{chunk-O5CBEL6O-DN6WEIh-.js → chunk-O5CBEL6O-DDN-Ifon.js} +1 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-EVQ0VMd5.js +1 -0
- package/workframe-ui/public/assets/chunk-WU5MYG2G-k_-ZsLOS.js +1 -0
- package/workframe-ui/public/assets/{chunk-XPW4576I-Ct5VQv2D.js → chunk-XPW4576I-B4_iYZ6r.js} +1 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-W9WVQsby.js +1 -0
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-W9WVQsby.js +1 -0
- package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-Bj4W5oMk.js → cose-bilkent-S5V4N54A-Cy1HhS7j.js} +1 -1
- package/workframe-ui/public/assets/{dagre-BM42HDAG-DAjHBkAy.js → dagre-BM42HDAG-Bjal81Ie.js} +1 -1
- package/workframe-ui/public/assets/{diagram-2AECGRRQ-Ch2gGFPG.js → diagram-2AECGRRQ-v5Gdvzp_.js} +1 -1
- package/workframe-ui/public/assets/{diagram-5GNKFQAL-BzIIvgYH.js → diagram-5GNKFQAL-DuyRdfmY.js} +1 -1
- package/workframe-ui/public/assets/{diagram-KO2AKTUF-Df0Q8nv6.js → diagram-KO2AKTUF-WwWCGbIW.js} +1 -1
- package/workframe-ui/public/assets/{diagram-LMA3HP47-D-G3ekmF.js → diagram-LMA3HP47-DTYwZALT.js} +1 -1
- package/workframe-ui/public/assets/{diagram-OG6HWLK6-TBHhT9f5.js → diagram-OG6HWLK6-B46kBIqE.js} +1 -1
- package/workframe-ui/public/assets/{dist-CPbiDkEZ.js → dist-mQWmB3z6.js} +1 -1
- package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-YxkUmLLV.js → erDiagram-TEJ5UH35-__40xO-y.js} +1 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-Cm1uRTxU.js +1 -0
- package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-kfZ7-RSd.js → flowDiagram-I6XJVG4X-Cwj6dVUz.js} +1 -1
- package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-DmTx3hzR.js → ganttDiagram-6RSMTGT7-D_IOSwAS.js} +1 -1
- package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-BXa0ewp3.js → gitGraph-WXDBUCRP-C1vGecee.js} +1 -1
- package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-CQr2qMxU.js → gitGraphDiagram-PVQCEYII-DMTxFJxn.js} +1 -1
- package/workframe-ui/public/assets/index-BdiM4-lI.js +130 -0
- package/workframe-ui/public/assets/index-Fnad47vV.css +1 -0
- package/workframe-ui/public/assets/{info-J43DQDTF-t-Qlx8zv.js → info-J43DQDTF-CwjmyPmD.js} +1 -1
- package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-CzkYofPx.js → infoDiagram-5YYISTIA-BGzh7ZL2.js} +1 -1
- package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-djK3Oxgb.js → ishikawaDiagram-YF4QCWOH-BLXdnR_5.js} +1 -1
- package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-CWxYXxdF.js → journeyDiagram-JHISSGLW-BK3behMe.js} +1 -1
- package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-D7leCAD9.js → kanban-definition-UN3LZRKU-Bx172cCl.js} +1 -1
- package/workframe-ui/public/assets/{line-BSFj2OC5.js → line-BTCEHb7l.js} +1 -1
- package/workframe-ui/public/assets/{linear-pfPwnu-o.js → linear-CkbydpnK.js} +1 -1
- package/workframe-ui/public/assets/{mermaid-parser.core-Dm2L9Ib_.js → mermaid-parser.core-DvrtArpk.js} +2 -2
- package/workframe-ui/public/assets/{mermaid.core-Bevx9bif.js → mermaid.core-yj_1x_qj.js} +3 -3
- package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-UkjDssmP.js → mindmap-definition-RKZ34NQL-BXbMltQ0.js} +1 -1
- package/workframe-ui/public/assets/{packet-YPE3B663-JO2ezH3_.js → packet-YPE3B663-9pVCBQ7S.js} +1 -1
- package/workframe-ui/public/assets/{pie-LRSECV5Y-DdMFsfR8.js → pie-LRSECV5Y-CdqoWesP.js} +1 -1
- package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-CEPHFBU1.js → pieDiagram-4H26LBE5-kGs-VfXd.js} +1 -1
- package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-_RF9_86k.js → quadrantDiagram-W4KKPZXB-D9D8gQw5.js} +1 -1
- package/workframe-ui/public/assets/{radar-GUYGQ44K-Cb-YfdaA.js → radar-GUYGQ44K-d4zMZpQS.js} +1 -1
- package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-DBVOxmBn.js → requirementDiagram-4Y6WPE33-BI7EQ9zc.js} +1 -1
- package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-BTpxsGUA.js → sankeyDiagram-5OEKKPKP-Bcz19jQB.js} +1 -1
- package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-DHP_Bq3l.js → sequenceDiagram-3UESZ5HK-iQiCEfYp.js} +1 -1
- package/workframe-ui/public/assets/{src-CY_By-r7.js → src-D6mvlP96.js} +1 -1
- package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BVtp_LPR.js → stateDiagram-AJRCARHV-CxaTXs02.js} +1 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CXikj9xi.js +1 -0
- package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-D38HJhop.js → timeline-definition-PNZ67QCA-Bej-EAnf.js} +1 -1
- package/workframe-ui/public/assets/{treeView-BLDUP644-CFCG3Sc_.js → treeView-BLDUP644-C7PnnzwX.js} +1 -1
- package/workframe-ui/public/assets/{treemap-LRROVOQU-BeiMs_dF.js → treemap-LRROVOQU-CqISLmkO.js} +1 -1
- package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-CKMeT6PW.js → vennDiagram-CIIHVFJN-eJMTTEW-.js} +1 -1
- package/workframe-ui/public/assets/{wardley-L42UT6IY-BljDy8ph.js → wardley-L42UT6IY-D4sMroBF.js} +1 -1
- package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DEE8D94J.js → wardleyDiagram-YWT4CUSO-DC3DCUs7.js} +1 -1
- package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-BRaAWV_g.js → xychartDiagram-2RQKCTM6-BeMaM6Aq.js} +1 -1
- package/workframe-ui/public/index.html +7 -6
- package/workframe-ui/public/workframe-build.json +5 -0
- package/workframe-ui/public/assets/architecture-7EHR7CIX-CvhbXEvt.js +0 -1
- package/workframe-ui/public/assets/channel-D63s_yN9.js +0 -1
- package/workframe-ui/public/assets/chunk-LZXEDZCA-CsRdBqUB.js +0 -2
- package/workframe-ui/public/assets/chunk-QZHKN3VN-Dj_Yxhgo.js +0 -1
- package/workframe-ui/public/assets/chunk-WU5MYG2G-AFchshb6.js +0 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-Opj8t01I.js +0 -1
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-Opj8t01I.js +0 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-LQOYL50n.js +0 -1
- package/workframe-ui/public/assets/index-B08aShJy.css +0 -1
- package/workframe-ui/public/assets/index-DCF3W3G_.js +0 -129
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CvJhS_BJ.js +0 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""WF-032 credential_store self-check."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
9
|
+
|
|
10
|
+
import credential_store # noqa: E402
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_quote_env_value() -> None:
|
|
14
|
+
assert credential_store._quote_env_value("sk-abc123") == "sk-abc123"
|
|
15
|
+
assert credential_store._quote_env_value('has "quotes"') == '"has \\"quotes\\""'
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_remove_env_secret_missing_file(tmp_path: Path) -> None:
|
|
19
|
+
path = tmp_path / ".env"
|
|
20
|
+
credential_store._remove_env_secret(path, "OPENROUTER_API_KEY")
|
|
21
|
+
assert not path.exists()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if __name__ == "__main__":
|
|
25
|
+
test_quote_env_value()
|
|
26
|
+
test_remove_env_secret_missing_file(Path("/tmp/wf-cred-store-check"))
|
|
27
|
+
print("test_credential_store: ok")
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Dogfood API smoke — rooms, mention, session, invite, OTP, agent, project.
|
|
3
|
+
|
|
4
|
+
Run against live stack:
|
|
5
|
+
python test_dogfood_flows.py --base http://127.0.0.1:18644
|
|
6
|
+
|
|
7
|
+
Mention tests default to invoke_agents=False (routing only, no LLM spend).
|
|
8
|
+
Pass --live-mention to invoke agents; dogfood should bill via Codex gpt-5.4-mini.
|
|
9
|
+
|
|
10
|
+
Requires WORKFRAME_E2E=1 + install window OR DEV_LOCAL_UNSAFE for OTP in auth/start response.
|
|
11
|
+
Falls back to zk session mint when --data-dir points at install data (same host as API).
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import sqlite3
|
|
19
|
+
import sys
|
|
20
|
+
import time
|
|
21
|
+
import urllib.error
|
|
22
|
+
import urllib.request
|
|
23
|
+
import uuid
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
ROOT = Path(__file__).resolve().parent
|
|
28
|
+
sys.path.insert(0, str(ROOT))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _resolve_install_id(data_dir: Path) -> str:
|
|
32
|
+
env_id = os.environ.get("WORKFRAME_INSTALL_ID", "").strip()
|
|
33
|
+
if env_id:
|
|
34
|
+
return env_id
|
|
35
|
+
env_file = data_dir.parent.parent / ".env"
|
|
36
|
+
if env_file.is_file():
|
|
37
|
+
for line in env_file.read_text(encoding="utf-8").splitlines():
|
|
38
|
+
if line.startswith("WORKFRAME_INSTALL_ID="):
|
|
39
|
+
return line.split("=", 1)[1].strip().strip("'\"")
|
|
40
|
+
raise RuntimeError("WORKFRAME_INSTALL_ID not set — export it or pass --data-dir with install .env")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _req(
|
|
44
|
+
base: str,
|
|
45
|
+
method: str,
|
|
46
|
+
path: str,
|
|
47
|
+
*,
|
|
48
|
+
body: dict | None = None,
|
|
49
|
+
cookie: str = "",
|
|
50
|
+
session_id: str = "",
|
|
51
|
+
timeout: float = 60,
|
|
52
|
+
) -> tuple[int, dict[str, Any]]:
|
|
53
|
+
url = f"{base.rstrip('/')}{path}"
|
|
54
|
+
data = json.dumps(body).encode("utf-8") if body is not None else None
|
|
55
|
+
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
|
56
|
+
if session_id and not cookie:
|
|
57
|
+
import zk_auth
|
|
58
|
+
|
|
59
|
+
if not os.environ.get("WORKFRAME_INSTALL_ID", "").strip():
|
|
60
|
+
raise RuntimeError("WORKFRAME_INSTALL_ID required when passing session_id without cookie")
|
|
61
|
+
cookie = f"{zk_auth.session_cookie_name()}={session_id}"
|
|
62
|
+
if cookie:
|
|
63
|
+
headers["Cookie"] = cookie
|
|
64
|
+
if session_id:
|
|
65
|
+
headers["X-Workframe-Session"] = session_id
|
|
66
|
+
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
67
|
+
try:
|
|
68
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
69
|
+
raw = resp.read().decode("utf-8")
|
|
70
|
+
return resp.status, json.loads(raw) if raw else {}
|
|
71
|
+
except urllib.error.HTTPError as exc:
|
|
72
|
+
raw = exc.read().decode("utf-8")
|
|
73
|
+
try:
|
|
74
|
+
payload = json.loads(raw) if raw else {"error": exc.reason}
|
|
75
|
+
except json.JSONDecodeError:
|
|
76
|
+
payload = {"error": raw or str(exc)}
|
|
77
|
+
return exc.code, payload
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _mint_session_cookie(data_dir: Path, user_id: str) -> tuple[str, str]:
|
|
81
|
+
"""Return (cookie_header, session_id) for API auth."""
|
|
82
|
+
import zk_auth
|
|
83
|
+
|
|
84
|
+
os.environ["WORKFRAME_API_DATA_DIR"] = str(data_dir)
|
|
85
|
+
os.environ["WORKFRAME_INSTALL_ID"] = _resolve_install_id(data_dir)
|
|
86
|
+
|
|
87
|
+
# Read-only sqlite — avoid WAL sidecars on the shared data mount.
|
|
88
|
+
db_path = data_dir / "zk_auth.db"
|
|
89
|
+
conn = sqlite3.connect(f"file:{db_path.as_posix()}?mode=ro", uri=True, timeout=5.0)
|
|
90
|
+
conn.row_factory = sqlite3.Row
|
|
91
|
+
try:
|
|
92
|
+
row = conn.execute(
|
|
93
|
+
"""
|
|
94
|
+
SELECT s.id
|
|
95
|
+
FROM sessions s
|
|
96
|
+
JOIN users u ON u.id = s.user_id
|
|
97
|
+
WHERE s.user_id = ?
|
|
98
|
+
AND s.revoked_at IS NULL
|
|
99
|
+
AND s.expires_at > datetime('now')
|
|
100
|
+
AND u.status = 'active'
|
|
101
|
+
ORDER BY s.created_at DESC
|
|
102
|
+
LIMIT 1
|
|
103
|
+
""",
|
|
104
|
+
(user_id,),
|
|
105
|
+
).fetchone()
|
|
106
|
+
finally:
|
|
107
|
+
conn.close()
|
|
108
|
+
|
|
109
|
+
session_id = str(row["id"]) if row else ""
|
|
110
|
+
if not session_id:
|
|
111
|
+
raise RuntimeError(f"no valid session for user {user_id}")
|
|
112
|
+
cookie_name = zk_auth.session_cookie_name()
|
|
113
|
+
return f"{cookie_name}={session_id}", session_id
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _pick_user(data_dir: Path) -> tuple[str, str]:
|
|
117
|
+
db = data_dir / "workframe.db"
|
|
118
|
+
conn = sqlite3.connect(db)
|
|
119
|
+
conn.row_factory = sqlite3.Row
|
|
120
|
+
row = conn.execute(
|
|
121
|
+
"""
|
|
122
|
+
SELECT u.id, u.email
|
|
123
|
+
FROM users u
|
|
124
|
+
JOIN workspace_memberships m ON m.user_id = u.id AND m.deleted_at IS NULL
|
|
125
|
+
WHERE u.deleted_at IS NULL AND m.role IN ('owner', 'admin')
|
|
126
|
+
ORDER BY CASE m.role WHEN 'owner' THEN 0 ELSE 1 END, u.created_at ASC
|
|
127
|
+
LIMIT 1
|
|
128
|
+
"""
|
|
129
|
+
).fetchone()
|
|
130
|
+
if not row:
|
|
131
|
+
row = conn.execute(
|
|
132
|
+
"SELECT id, email FROM users WHERE deleted_at IS NULL ORDER BY created_at ASC LIMIT 1"
|
|
133
|
+
).fetchone()
|
|
134
|
+
conn.close()
|
|
135
|
+
if not row:
|
|
136
|
+
raise SystemExit("no users in workframe.db")
|
|
137
|
+
return str(row["id"]), str(row["email"])
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def main() -> None:
|
|
141
|
+
ap = argparse.ArgumentParser()
|
|
142
|
+
ap.add_argument("--base", default="http://127.0.0.1:18644")
|
|
143
|
+
ap.add_argument("--data-dir", default=os.environ.get("WORKFRAME_API_DATA_DIR", ""))
|
|
144
|
+
ap.add_argument("--email", default="")
|
|
145
|
+
ap.add_argument("--live-mention", action="store_true", help="invoke agents on @mention (Codex gpt-5.4-mini; costs tokens)")
|
|
146
|
+
args = ap.parse_args()
|
|
147
|
+
base = args.base
|
|
148
|
+
failures: list[str] = []
|
|
149
|
+
|
|
150
|
+
def check(name: str, ok: bool, detail: str = "") -> None:
|
|
151
|
+
mark = "ok" if ok else "FAIL"
|
|
152
|
+
print(f"[{mark}] {name}" + (f" — {detail}" if detail else ""))
|
|
153
|
+
if not ok:
|
|
154
|
+
failures.append(name)
|
|
155
|
+
|
|
156
|
+
code, meta = _req(base, "GET", "/api/meta")
|
|
157
|
+
check("meta", code == 200 and meta.get("ok"), str(meta.get("error") or meta.get("project_name")))
|
|
158
|
+
|
|
159
|
+
cookie = ""
|
|
160
|
+
session_id = ""
|
|
161
|
+
user_id = ""
|
|
162
|
+
email = args.email.strip().lower()
|
|
163
|
+
|
|
164
|
+
def api(method: str, path: str, body: dict | None = None, *, timeout: float = 60) -> tuple[int, dict[str, Any]]:
|
|
165
|
+
return _req(base, method, path, body=body, cookie=cookie, session_id=session_id, timeout=timeout)
|
|
166
|
+
|
|
167
|
+
if args.data_dir:
|
|
168
|
+
data_dir = Path(args.data_dir)
|
|
169
|
+
user_id, db_email = _pick_user(data_dir)
|
|
170
|
+
if not email:
|
|
171
|
+
email = db_email
|
|
172
|
+
try:
|
|
173
|
+
cookie, session_id = _mint_session_cookie(data_dir, user_id)
|
|
174
|
+
check("session mint", bool(session_id), user_id)
|
|
175
|
+
except Exception as exc: # noqa: BLE001
|
|
176
|
+
check("session mint", False, str(exc))
|
|
177
|
+
|
|
178
|
+
if not session_id and not cookie:
|
|
179
|
+
if not email:
|
|
180
|
+
email = f"smoke-{uuid.uuid4().hex[:8]}@workframe.test"
|
|
181
|
+
code, start = api("POST", "/api/auth/start", body={"email": email})
|
|
182
|
+
otp = str(start.get("otp_code") or "")
|
|
183
|
+
check("auth/start", code == 200 and start.get("ok"), start.get("error") or ("otp" if otp else "no otp"))
|
|
184
|
+
if otp:
|
|
185
|
+
code, verify = api("POST", "/api/auth/verify", body={"email": email, "code": otp})
|
|
186
|
+
check("auth/verify", code == 200 and verify.get("ok"), verify.get("error") or "")
|
|
187
|
+
user_id = str(verify.get("user_id") or "")
|
|
188
|
+
session_id = str(verify.get("session_id") or "")
|
|
189
|
+
if session_id:
|
|
190
|
+
import zk_auth
|
|
191
|
+
|
|
192
|
+
cookie = f"{zk_auth.session_cookie_name()}={session_id}"
|
|
193
|
+
elif args.data_dir and user_id:
|
|
194
|
+
cookie, session_id = _mint_session_cookie(Path(args.data_dir), user_id)
|
|
195
|
+
|
|
196
|
+
if not session_id:
|
|
197
|
+
raise SystemExit("no session — pass --data-dir or enable OTP exposure")
|
|
198
|
+
|
|
199
|
+
code, me = api("GET", "/api/me")
|
|
200
|
+
me_user = me.get("user") if isinstance(me.get("user"), dict) else me
|
|
201
|
+
check("me", code == 200 and (me.get("ok", True) or me_user.get("id")), me.get("error") or str(me_user.get("email") or me_user.get("id")))
|
|
202
|
+
user_id = str(me_user.get("id") or me_user.get("user_id") or user_id)
|
|
203
|
+
|
|
204
|
+
workspaces = me.get("workspaces") if isinstance(me.get("workspaces"), list) else []
|
|
205
|
+
current_ws = me.get("current_workspace") if isinstance(me.get("current_workspace"), dict) else None
|
|
206
|
+
ws_id = str((current_ws or {}).get("id") or (workspaces[0].get("id") if workspaces else "") or "")
|
|
207
|
+
if not ws_id and args.data_dir:
|
|
208
|
+
conn = sqlite3.connect(str(Path(args.data_dir) / "workframe.db"))
|
|
209
|
+
conn.row_factory = sqlite3.Row
|
|
210
|
+
row = conn.execute(
|
|
211
|
+
"SELECT workspace_id FROM workspace_memberships WHERE user_id = ? AND deleted_at IS NULL LIMIT 1",
|
|
212
|
+
(user_id,),
|
|
213
|
+
).fetchone()
|
|
214
|
+
conn.close()
|
|
215
|
+
if row:
|
|
216
|
+
ws_id = str(row["workspace_id"])
|
|
217
|
+
if not ws_id:
|
|
218
|
+
code, snap = api("GET", "/api/snapshot")
|
|
219
|
+
ws = snap.get("workspace") if isinstance(snap.get("workspace"), dict) else {}
|
|
220
|
+
ws_id = str(ws.get("id") or "")
|
|
221
|
+
check("workspace", bool(ws_id), ws_id or "missing")
|
|
222
|
+
|
|
223
|
+
code, rooms_payload = api("GET", f"/api/workspace/{ws_id}/rooms")
|
|
224
|
+
if rooms_payload.get("error") == "workspace_not_found":
|
|
225
|
+
slug_candidates = ["default"]
|
|
226
|
+
if workspaces:
|
|
227
|
+
slug_candidates.extend(str(ws.get("slug") or "") for ws in workspaces)
|
|
228
|
+
for alt in slug_candidates:
|
|
229
|
+
if not alt or alt == ws_id:
|
|
230
|
+
continue
|
|
231
|
+
code, rooms_payload = api("GET", f"/api/workspace/{alt}/rooms")
|
|
232
|
+
if code == 200:
|
|
233
|
+
ws_id = str(rooms_payload.get("workspace_id") or ws_id)
|
|
234
|
+
break
|
|
235
|
+
rooms = rooms_payload.get("rooms") or []
|
|
236
|
+
check("list rooms", code == 200 and isinstance(rooms, list), f"count={len(rooms)}")
|
|
237
|
+
|
|
238
|
+
channel = next((r for r in rooms if r.get("room_type") == "channel"), rooms[0] if rooms else None)
|
|
239
|
+
room_id = str(channel.get("id") if channel else "")
|
|
240
|
+
check("pick channel room", bool(room_id), room_id)
|
|
241
|
+
|
|
242
|
+
# Create project (room)
|
|
243
|
+
proj_slug = f"smoke-{int(time.time())}"
|
|
244
|
+
code, proj = api(
|
|
245
|
+
"POST",
|
|
246
|
+
f"/api/workspace/{ws_id}/rooms/create",
|
|
247
|
+
body={"name": f"Smoke {proj_slug}", "slug": proj_slug, "topic": "smoke test", "room_type": "channel"},
|
|
248
|
+
)
|
|
249
|
+
check("create project", code in (200, 201) and proj.get("ok"), proj.get("error") or str(proj.get("room_id")))
|
|
250
|
+
|
|
251
|
+
# Create agent
|
|
252
|
+
agent_slug = f"smoke-agent-{int(time.time()) % 100000}"
|
|
253
|
+
code, agent = api(
|
|
254
|
+
"POST",
|
|
255
|
+
"/api/hermes/profiles/create",
|
|
256
|
+
body={
|
|
257
|
+
"name": agent_slug,
|
|
258
|
+
"display_name": "Smoke Agent",
|
|
259
|
+
"workspace_id": ws_id,
|
|
260
|
+
"role": "helper",
|
|
261
|
+
"model": "gpt-5.4-mini",
|
|
262
|
+
},
|
|
263
|
+
timeout=180,
|
|
264
|
+
)
|
|
265
|
+
check("create agent", code == 200 and agent.get("ok"), agent.get("error") or str(agent.get("profile")))
|
|
266
|
+
|
|
267
|
+
# Invite email
|
|
268
|
+
invite_email = f"invite-{uuid.uuid4().hex[:6]}@workframe.test"
|
|
269
|
+
code, inv = api(
|
|
270
|
+
"POST",
|
|
271
|
+
f"/api/workspace/{ws_id}/invites",
|
|
272
|
+
body={"email": invite_email, "role": "member"},
|
|
273
|
+
)
|
|
274
|
+
check("invite email", code in (200, 201) and inv.get("ok"), inv.get("error") or invite_email)
|
|
275
|
+
|
|
276
|
+
# Add contact = bootstrap DM with native agent template
|
|
277
|
+
native = str(meta.get("native_profile") or "workframe-agent")
|
|
278
|
+
code, dm = api(
|
|
279
|
+
"POST",
|
|
280
|
+
f"/api/hermes/profiles/{native}/bootstrap-dm",
|
|
281
|
+
body={"workspace_id": ws_id},
|
|
282
|
+
timeout=120,
|
|
283
|
+
)
|
|
284
|
+
check("add contact (bootstrap-dm)", code == 200 and dm.get("ok"), dm.get("error") or str(dm.get("room_id")))
|
|
285
|
+
|
|
286
|
+
# New session — UI uses room bind with new_session, not activate with a fresh uuid.
|
|
287
|
+
if room_id:
|
|
288
|
+
code, sessions = api("GET", f"/api/rooms/{room_id}/sessions")
|
|
289
|
+
check("list sessions", code == 200, str(len(sessions.get("sessions") or [])))
|
|
290
|
+
code, bound = api(
|
|
291
|
+
"POST",
|
|
292
|
+
f"/api/rooms/{room_id}/bind",
|
|
293
|
+
body={
|
|
294
|
+
"source_id": "room",
|
|
295
|
+
"client_id": room_id,
|
|
296
|
+
"new_session": True,
|
|
297
|
+
},
|
|
298
|
+
timeout=120,
|
|
299
|
+
)
|
|
300
|
+
new_sid = str(bound.get("session_id") or "")
|
|
301
|
+
check("new session", code == 200 and bound.get("ok") and bool(new_sid), bound.get("error") or new_sid)
|
|
302
|
+
if new_sid:
|
|
303
|
+
code, activated = api(
|
|
304
|
+
"POST",
|
|
305
|
+
f"/api/rooms/{room_id}/sessions/activate",
|
|
306
|
+
body={"session_id": new_sid, "source_id": "room", "client_id": room_id},
|
|
307
|
+
)
|
|
308
|
+
check("activate session", code == 200 and activated.get("ok"), activated.get("error") or "")
|
|
309
|
+
|
|
310
|
+
# @mention message
|
|
311
|
+
mention_slug = agent_slug if agent.get("ok") else native
|
|
312
|
+
content = f"Hello @{mention_slug} — smoke mention {int(time.time())}"
|
|
313
|
+
code, sent = api(
|
|
314
|
+
"POST",
|
|
315
|
+
f"/api/rooms/{room_id}/messages/send",
|
|
316
|
+
body={"content": content, "invoke_agents": bool(args.live_mention)},
|
|
317
|
+
)
|
|
318
|
+
mentions = sent.get("agent_mentions") or []
|
|
319
|
+
check(
|
|
320
|
+
"mention send",
|
|
321
|
+
code == 200 and sent.get("ok"),
|
|
322
|
+
f"mentions={mentions} err={sent.get('error')}",
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# Reply (thread)
|
|
326
|
+
if sent.get("message_id"):
|
|
327
|
+
code, reply = api(
|
|
328
|
+
"POST",
|
|
329
|
+
f"/api/rooms/{room_id}/messages/send",
|
|
330
|
+
body={
|
|
331
|
+
"content": "Reply in thread — smoke",
|
|
332
|
+
"parent_message_id": sent["message_id"],
|
|
333
|
+
"invoke_agents": False,
|
|
334
|
+
},
|
|
335
|
+
)
|
|
336
|
+
check("reply send", code == 200 and reply.get("ok"), reply.get("error") or "")
|
|
337
|
+
|
|
338
|
+
print("---")
|
|
339
|
+
if failures:
|
|
340
|
+
print(f"FAILED ({len(failures)}): {', '.join(failures)}")
|
|
341
|
+
raise SystemExit(1)
|
|
342
|
+
print("dogfood flows smoke: all ok")
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
if __name__ == "__main__":
|
|
346
|
+
main()
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""WF-039 domain entity self-check — fails if round-trip or imports break."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
11
|
+
|
|
12
|
+
from domain.entities import ( # noqa: E402
|
|
13
|
+
ActorType,
|
|
14
|
+
AgentIdentity,
|
|
15
|
+
Cell,
|
|
16
|
+
CredentialPolicy,
|
|
17
|
+
CredentialRef,
|
|
18
|
+
DeploymentMode,
|
|
19
|
+
FundingSource,
|
|
20
|
+
Grant,
|
|
21
|
+
GrantCapability,
|
|
22
|
+
Lease,
|
|
23
|
+
LeaseStatus,
|
|
24
|
+
Run,
|
|
25
|
+
RunEvent,
|
|
26
|
+
RunStatus,
|
|
27
|
+
RunSurface,
|
|
28
|
+
RuntimeBinding,
|
|
29
|
+
RuntimeBindingStatus,
|
|
30
|
+
RuntimeKind,
|
|
31
|
+
User,
|
|
32
|
+
Workspace,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _ts() -> datetime:
|
|
37
|
+
return datetime(2026, 7, 6, 12, 0, 0, tzinfo=timezone.utc)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_round_trip() -> None:
|
|
41
|
+
cell = Cell(
|
|
42
|
+
cell_id="cell-1",
|
|
43
|
+
install_root="/tmp/wf/MyBusiness",
|
|
44
|
+
manifest_path="/tmp/wf/MyBusiness/workframe-manifest.json",
|
|
45
|
+
package_name="create-workframe",
|
|
46
|
+
package_version="0.1.12",
|
|
47
|
+
deployment_mode=DeploymentMode.SINGLE_USER_LOCAL,
|
|
48
|
+
created_at=_ts(),
|
|
49
|
+
)
|
|
50
|
+
payload = cell.to_dict()
|
|
51
|
+
assert payload["deployment_mode"] == "single_user_local"
|
|
52
|
+
assert payload["cell_id"] == "cell-1"
|
|
53
|
+
json.dumps(payload)
|
|
54
|
+
|
|
55
|
+
run = Run(
|
|
56
|
+
run_id="run-1",
|
|
57
|
+
workspace_id="ws-default",
|
|
58
|
+
surface=RunSurface.CHAT,
|
|
59
|
+
actor_type=ActorType.USER,
|
|
60
|
+
actor_id="user-1",
|
|
61
|
+
triggering_user_id="user-1",
|
|
62
|
+
agent_id="agent-native",
|
|
63
|
+
runtime_binding_id="bind-1",
|
|
64
|
+
status=RunStatus.PENDING,
|
|
65
|
+
payer_user_id="user-1",
|
|
66
|
+
funding_source=FundingSource.BYOK,
|
|
67
|
+
created_at=_ts(),
|
|
68
|
+
)
|
|
69
|
+
run_payload = run.to_dict()
|
|
70
|
+
assert run_payload["surface"] == "chat"
|
|
71
|
+
assert run_payload["status"] == "pending"
|
|
72
|
+
|
|
73
|
+
lease = Lease(
|
|
74
|
+
lease_id="wf_rt_test",
|
|
75
|
+
run_id="run-1",
|
|
76
|
+
workspace_id="ws-default",
|
|
77
|
+
payer_user_id="user-1",
|
|
78
|
+
provider="openrouter",
|
|
79
|
+
profile_slug="u-user-1-native",
|
|
80
|
+
credential_ref_id="cred-1",
|
|
81
|
+
status=LeaseStatus.ACTIVE,
|
|
82
|
+
expires_at=_ts(),
|
|
83
|
+
)
|
|
84
|
+
assert Lease.from_dict(lease.to_dict()).provider == "openrouter"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_schema_file_present() -> None:
|
|
88
|
+
schema = Path(__file__).resolve().parent / "domain" / "schema" / "workframe-domain.schema.json"
|
|
89
|
+
data = json.loads(schema.read_text(encoding="utf-8"))
|
|
90
|
+
defs = data["$defs"]
|
|
91
|
+
for name in (
|
|
92
|
+
"Cell",
|
|
93
|
+
"User",
|
|
94
|
+
"Workspace",
|
|
95
|
+
"AgentIdentity",
|
|
96
|
+
"RuntimeBinding",
|
|
97
|
+
"Run",
|
|
98
|
+
"RunEvent",
|
|
99
|
+
"Lease",
|
|
100
|
+
"Grant",
|
|
101
|
+
"CredentialRef",
|
|
102
|
+
):
|
|
103
|
+
assert name in defs, f"missing schema def: {name}"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_agent_runtime_seam_types() -> None:
|
|
107
|
+
agent = AgentIdentity(
|
|
108
|
+
agent_id="agent-1",
|
|
109
|
+
workspace_id="ws-1",
|
|
110
|
+
template_slug="dev",
|
|
111
|
+
display_name="Dev",
|
|
112
|
+
)
|
|
113
|
+
binding = RuntimeBinding(
|
|
114
|
+
binding_id="bind-1",
|
|
115
|
+
agent_id=agent.agent_id,
|
|
116
|
+
runtime_kind=RuntimeKind.HERMES_MANAGED,
|
|
117
|
+
profile_slug="u-alice-dev",
|
|
118
|
+
status=RuntimeBindingStatus.ACTIVE,
|
|
119
|
+
)
|
|
120
|
+
assert binding.agent_id == agent.agent_id
|
|
121
|
+
|
|
122
|
+
grant = Grant(
|
|
123
|
+
grant_id="grant-1",
|
|
124
|
+
run_id="run-1",
|
|
125
|
+
capability=GrantCapability.LLM_TURN,
|
|
126
|
+
)
|
|
127
|
+
assert grant.capability == GrantCapability.LLM_TURN
|
|
128
|
+
|
|
129
|
+
user = User(
|
|
130
|
+
user_id="u-1",
|
|
131
|
+
workspace_id="ws-1",
|
|
132
|
+
email="a@example.com",
|
|
133
|
+
display_name="Alice",
|
|
134
|
+
)
|
|
135
|
+
ws = Workspace(workspace_id="ws-1", cell_id="cell-1", slug="default", name="Workframe")
|
|
136
|
+
cred = CredentialRef(
|
|
137
|
+
ref_id="cred-1",
|
|
138
|
+
workspace_id=ws.workspace_id,
|
|
139
|
+
provider="openrouter",
|
|
140
|
+
policy=CredentialPolicy.BYOK,
|
|
141
|
+
)
|
|
142
|
+
assert cred.provider == "openrouter"
|
|
143
|
+
|
|
144
|
+
event = RunEvent(event_id="ev-1", run_id="run-1", event_type="run.authorized")
|
|
145
|
+
assert event.event_type == "run.authorized"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
test_round_trip()
|
|
150
|
+
test_schema_file_present()
|
|
151
|
+
test_agent_runtime_seam_types()
|
|
152
|
+
print("test_domain_entities: ok")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""WF-035 smoke: auth/vault handler paths log structured errors."""
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
ROOT = Path(__file__).resolve().parent
|
|
6
|
+
src = (ROOT / "server.py").read_text(encoding="utf-8")
|
|
7
|
+
messaging_src = (ROOT / "workspace_messaging.py").read_text(encoding="utf-8")
|
|
8
|
+
assert "_log_handler_error" in src
|
|
9
|
+
assert 'POST /api/auth/start' in src
|
|
10
|
+
assert 'POST /api/admin/vault/init' in src
|
|
11
|
+
assert "_sync_workspace_messaging_gateway restart" in messaging_src
|
|
12
|
+
print("exception hygiene self-check ok")
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""WF-032 hermes_admin self-check."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
API_DIR = Path(__file__).resolve().parent
|
|
10
|
+
if str(API_DIR) not in sys.path:
|
|
11
|
+
sys.path.insert(0, str(API_DIR))
|
|
12
|
+
|
|
13
|
+
os.environ.setdefault("WORKFRAME_API_DATA_DIR", str(API_DIR / ".tmp-test-data"))
|
|
14
|
+
os.environ.setdefault("HERMES_DATA", str(API_DIR / ".tmp-test-hermes"))
|
|
15
|
+
os.environ.setdefault("DEV_LOCAL_UNSAFE", "true")
|
|
16
|
+
|
|
17
|
+
import hermes_admin # noqa: E402
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_resolve_command_known() -> None:
|
|
21
|
+
entry = hermes_admin._resolve_command("/help")
|
|
22
|
+
assert entry is not None
|
|
23
|
+
assert entry["name"] == "/help"
|
|
24
|
+
assert entry["dispatch"] == "client:openHelp"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_resolve_command_alias() -> None:
|
|
28
|
+
entry = hermes_admin._resolve_command("reset")
|
|
29
|
+
assert entry is not None
|
|
30
|
+
assert entry["name"] == "/new"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_resolve_command_unknown() -> None:
|
|
34
|
+
assert hermes_admin._resolve_command("/not-a-real-command") is None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_commands_catalog_shape() -> None:
|
|
38
|
+
catalog = hermes_admin.hermes_commands_catalog()
|
|
39
|
+
assert catalog["ok"] is True
|
|
40
|
+
assert isinstance(catalog["categories"], list)
|
|
41
|
+
assert isinstance(catalog["entries"], list)
|
|
42
|
+
assert all(entry.get("wired") is True for entry in catalog["entries"])
|
|
43
|
+
assert not any(entry.get("wip") for entry in catalog["entries"])
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_commands_exec_client_dispatch() -> None:
|
|
47
|
+
result = hermes_admin.hermes_commands_exec("/help")
|
|
48
|
+
assert result["ok"] is True
|
|
49
|
+
assert result["dispatched"] == "client"
|
|
50
|
+
assert result["handler"] == "openHelp"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_commands_exec_not_slash() -> None:
|
|
54
|
+
result = hermes_admin.hermes_commands_exec("hello")
|
|
55
|
+
assert result["ok"] is False
|
|
56
|
+
assert result["error"] == "not a slash command"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_gquota_stub() -> None:
|
|
60
|
+
result = hermes_admin.hermes_gquota()
|
|
61
|
+
assert result["ok"] is False or "configured" in result
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
test_resolve_command_known()
|
|
66
|
+
test_resolve_command_alias()
|
|
67
|
+
test_resolve_command_unknown()
|
|
68
|
+
test_commands_catalog_shape()
|
|
69
|
+
test_commands_exec_client_dispatch()
|
|
70
|
+
test_commands_exec_not_slash()
|
|
71
|
+
test_gquota_stub()
|
|
72
|
+
print("test_hermes_admin: ok")
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Install wizard resume + admin verify persistence self-check."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
11
|
+
|
|
12
|
+
import install_api # noqa: E402
|
|
13
|
+
import stack_config # noqa: E402
|
|
14
|
+
|
|
15
|
+
td = Path(tempfile.mkdtemp())
|
|
16
|
+
stack_config.DATA_DIR = td
|
|
17
|
+
stack_config.CONFIG_PATH = td / "stack_config.json"
|
|
18
|
+
db_path = str(td / "workframe.db")
|
|
19
|
+
|
|
20
|
+
# SMTP saved + tested
|
|
21
|
+
stack_config.patch_stack_config(
|
|
22
|
+
{
|
|
23
|
+
"deployment_mode": "public_multi_user",
|
|
24
|
+
"app_base_url": "https://dev.example.com",
|
|
25
|
+
"smtp": {
|
|
26
|
+
"host": "smtp.example.com",
|
|
27
|
+
"port": 587,
|
|
28
|
+
"user": "relay@example.com",
|
|
29
|
+
"password": "secret",
|
|
30
|
+
"from": "noreply@example.com",
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
stack_config.mark_smtp_tested()
|
|
35
|
+
stack_config.patch_stack_config({"smtp": {"admin_email": "owner@example.com"}})
|
|
36
|
+
|
|
37
|
+
assert stack_config.smtp_setup_complete()
|
|
38
|
+
assert not stack_config.install_admin_verified()
|
|
39
|
+
assert install_api.resolve_install_wizard_step(db_path) == "smtp"
|
|
40
|
+
|
|
41
|
+
stack_config.mark_install_admin_verified("owner@example.com")
|
|
42
|
+
assert stack_config.install_admin_verified()
|
|
43
|
+
raw = stack_config.read_stack_raw()
|
|
44
|
+
assert raw["smtp"]["admin_email"] == "owner@example.com"
|
|
45
|
+
assert raw.get("wizard_step") == "workframe"
|
|
46
|
+
|
|
47
|
+
payload = stack_config.public_stack_payload()
|
|
48
|
+
assert payload["smtp"]["admin_email"] == "owner@example.com"
|
|
49
|
+
assert payload["smtp"]["admin_verified"] is True
|
|
50
|
+
|
|
51
|
+
wizard = install_api.install_wizard_public_payload(db_path)
|
|
52
|
+
assert wizard["admin_verified"] is True
|
|
53
|
+
assert wizard["resume_step"] == "workframe"
|
|
54
|
+
|
|
55
|
+
# Workspace progress → billing
|
|
56
|
+
import sqlite3
|
|
57
|
+
|
|
58
|
+
conn = sqlite3.connect(db_path)
|
|
59
|
+
conn.executescript(
|
|
60
|
+
"""
|
|
61
|
+
CREATE TABLE workspaces (
|
|
62
|
+
id TEXT PRIMARY KEY, slug TEXT, display_name TEXT, owner_id TEXT,
|
|
63
|
+
settings_json TEXT, deleted_at INTEGER, status TEXT, created_at TEXT, updated_at TEXT
|
|
64
|
+
);
|
|
65
|
+
"""
|
|
66
|
+
)
|
|
67
|
+
settings = {"admin_onboarding_done": True}
|
|
68
|
+
conn.execute(
|
|
69
|
+
"INSERT INTO workspaces (id, slug, display_name, owner_id, settings_json, deleted_at, status, created_at, updated_at) "
|
|
70
|
+
"VALUES ('ws1', 'default', 'Workframe', 'u1', ?, NULL, 'active', '1', '1')",
|
|
71
|
+
(json.dumps(settings),),
|
|
72
|
+
)
|
|
73
|
+
conn.commit()
|
|
74
|
+
conn.close()
|
|
75
|
+
|
|
76
|
+
assert install_api.resolve_install_wizard_step(db_path) == "billing"
|
|
77
|
+
|
|
78
|
+
print("test_install_wizard_resume: ok")
|