create-workframe 0.1.0 → 0.1.1

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.
Files changed (206) hide show
  1. package/LICENSE +201 -201
  2. package/NOTICE +12 -12
  3. package/README.md +8 -92
  4. package/SECURITY.md +40 -40
  5. package/bin/workframe.js +329 -329
  6. package/docs/workspace-instructions/WORKFRAME_ONBOARDING.md +1 -1
  7. package/docs/workspace-instructions/WORKFRAME_ROUTING.md +8 -8
  8. package/package.json +3 -6
  9. package/profiles/architect/AGENTS.md +29 -29
  10. package/profiles/architect/SOUL.md +2 -2
  11. package/profiles/architect/skills/devops/kanban-worker/SKILL.md +27 -27
  12. package/profiles/designer/AGENTS.md +26 -26
  13. package/profiles/designer/skills/devops/kanban-worker/SKILL.md +27 -27
  14. package/profiles/dev/AGENTS.md +28 -28
  15. package/profiles/dev/skills/devops/kanban-worker/SKILL.md +27 -27
  16. package/profiles/docs/AGENTS.md +27 -27
  17. package/profiles/docs/skills/devops/kanban-worker/SKILL.md +27 -27
  18. package/profiles/research/AGENTS.md +26 -26
  19. package/profiles/research/skills/devops/kanban-worker/SKILL.md +27 -27
  20. package/profiles/visionary/AGENTS.md +25 -25
  21. package/profiles/visionary/skills/devops/kanban-worker/SKILL.md +27 -27
  22. package/profiles/workframe-agent/AGENTS.md +37 -37
  23. package/profiles/workframe-agent/skills/devops/botfather/SKILL.md +85 -85
  24. package/profiles/workframe-agent/skills/devops/kanban-handoff-pattern/SKILL.md +58 -58
  25. package/profiles/workframe-agent/skills/devops/workframe-cohort/SKILL.md +54 -54
  26. package/rules/workspace-README.md +5 -5
  27. package/scripts/apply-update-hermes.sh +17 -17
  28. package/scripts/apply-update-workframe.sh +77 -77
  29. package/scripts/bootstrap-workspace-link.sh +8 -8
  30. package/scripts/bundle-workframe-ui.mjs +3 -3
  31. package/scripts/compose-docker-host.sh +37 -37
  32. package/scripts/ensure-compose-host-paths.mjs +51 -51
  33. package/scripts/fix-zk-encryption-key.sh +35 -35
  34. package/scripts/lib/install-identity.mjs +212 -212
  35. package/scripts/restart-gateway-hermes.sh +12 -12
  36. package/scripts/set-compose-public-url.mjs +92 -92
  37. package/scripts/setup-stack-secrets.sh +50 -50
  38. package/scripts/sync-canonical-to-package.mjs +8 -7
  39. package/scripts/verify-public-deploy.sh +105 -105
  40. package/shared/WORKFRAME_AGENT_LIBRARY.md +17 -17
  41. package/shared/WORKFRAME_AGENT_OPERATIONS.md +15 -15
  42. package/shared/WORKFRAME_AGENT_PACKS.json +18 -18
  43. package/shared/WORKFRAME_AGENT_PACKS.yaml +8 -8
  44. package/shared/WORKFRAME_SKILL_CURATION.md +4 -4
  45. package/workframe-api/README.md +28 -28
  46. package/workframe-api/action_proxy.py +131 -131
  47. package/workframe-api/auth_rate_limit.py +49 -49
  48. package/workframe-api/credential_vault.py +445 -445
  49. package/workframe-api/data/avatar-catalog.json +41 -41
  50. package/workframe-api/email_sender.py +220 -220
  51. package/workframe-api/google_auth.py +90 -90
  52. package/workframe-api/install_api.py +359 -359
  53. package/workframe-api/internal_proxy_auth.py +150 -150
  54. package/workframe-api/llm_proxy.py +277 -277
  55. package/workframe-api/oidc_jwt.py +108 -108
  56. package/workframe-api/package.json +12 -13
  57. package/workframe-api/public/assets/index-DPXu_lGn.css +1 -1
  58. package/workframe-api/public/assets/index-DYnLrCZZ.js +8 -8
  59. package/workframe-api/requirements.txt +2 -2
  60. package/workframe-api/site_meta.py +271 -271
  61. package/workframe-api/stack_config.py +427 -427
  62. package/workframe-api/time-bind-chat.py +99 -99
  63. package/workframe-api/turn_credentials.py +226 -226
  64. package/workframe-api/updates.py +417 -417
  65. package/workframe-api/vault_kek.py +159 -159
  66. package/workframe-api/zk_auth.py +633 -633
  67. package/workframe-supervisor/Dockerfile +11 -11
  68. package/workframe-supervisor/server.py +787 -787
  69. package/workframe-ui/docker/nginx.conf +85 -85
  70. package/workframe-ui/public/assets/{arc-CBDYvkAF.js → arc-COAT3laO.js} +1 -1
  71. package/workframe-ui/public/assets/architecture-7EHR7CIX-DUyH3hWG.js +1 -0
  72. package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-XnBRKeW0.js → architectureDiagram-3BPJPVTR-BFjWV24l.js} +1 -1
  73. package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-VYHUfVhd.js → blockDiagram-GPEHLZMM-DSQLPfrj.js} +1 -1
  74. package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-BTjUcJpm.js → c4Diagram-AAUBKEIU-DKEHv1t2.js} +1 -1
  75. package/workframe-ui/public/assets/channel-g7r_RGaY.js +1 -0
  76. package/workframe-ui/public/assets/{chunk-2J33WTMH-w7uu7R-b.js → chunk-2J33WTMH-DHZg-DUi.js} +1 -1
  77. package/workframe-ui/public/assets/{chunk-3OPIFGDE-Cb9LtnDX.js → chunk-3OPIFGDE-BB-OYTfp.js} +1 -1
  78. package/workframe-ui/public/assets/{chunk-4BX2VUAB-DiQ-qCwH.js → chunk-4BX2VUAB-C93q0YIm.js} +1 -1
  79. package/workframe-ui/public/assets/{chunk-55IACEB6-C-mLFr7z.js → chunk-55IACEB6-MAYniqik.js} +1 -1
  80. package/workframe-ui/public/assets/{chunk-5ZQYHXKU-DOesfiCI.js → chunk-5ZQYHXKU-ChgN6YJs.js} +1 -1
  81. package/workframe-ui/public/assets/{chunk-727SXJPM-BJ3oBZuz.js → chunk-727SXJPM-B_FYwdAv.js} +1 -1
  82. package/workframe-ui/public/assets/{chunk-AQP2D5EJ-CCA6xpGs.js → chunk-AQP2D5EJ-1_Hw_h1A.js} +1 -1
  83. package/workframe-ui/public/assets/{chunk-BSJP7CBP-a0cMNFb2.js → chunk-BSJP7CBP-CFiDQ1Rv.js} +1 -1
  84. package/workframe-ui/public/assets/{chunk-CSCIHK7Q-kuqN8EIY.js → chunk-CSCIHK7Q-DZ9UMTlB.js} +1 -1
  85. package/workframe-ui/public/assets/{chunk-FMBD7UC4-DyPgYHCg.js → chunk-FMBD7UC4-DlMlyFgw.js} +1 -1
  86. package/workframe-ui/public/assets/{chunk-KSCS5N6A-CdUuvR0V.js → chunk-KSCS5N6A-DHXtQ_Hf.js} +1 -1
  87. package/workframe-ui/public/assets/{chunk-L5ZTLDWV-Dq9NoWmK.js → chunk-L5ZTLDWV-CuQzg-QG.js} +1 -1
  88. package/workframe-ui/public/assets/{chunk-LZXEDZCA-p74rddlO.js → chunk-LZXEDZCA-BHzjzCGg.js} +2 -2
  89. package/workframe-ui/public/assets/{chunk-ND2GUHAM-DBD2u1Gz.js → chunk-ND2GUHAM-DHXx05n2.js} +1 -1
  90. package/workframe-ui/public/assets/{chunk-NZK2D7GU-BeIeYFnd.js → chunk-NZK2D7GU-CV5pmDM_.js} +1 -1
  91. package/workframe-ui/public/assets/{chunk-O5CBEL6O-ClHc56ib.js → chunk-O5CBEL6O-6tkCHxsV.js} +1 -1
  92. package/workframe-ui/public/assets/chunk-QZHKN3VN-C5UQehWY.js +1 -0
  93. package/workframe-ui/public/assets/chunk-WU5MYG2G-DhWllrI8.js +1 -0
  94. package/workframe-ui/public/assets/{chunk-XPW4576I-EFr8R_1p.js → chunk-XPW4576I-BClwIiCp.js} +1 -1
  95. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BBM_8T8E.js +1 -0
  96. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BBM_8T8E.js +1 -0
  97. package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-C7aPBODd.js → cose-bilkent-S5V4N54A-DOrGV6DQ.js} +1 -1
  98. package/workframe-ui/public/assets/{dagre-BM42HDAG-BdU1Rv-H.js → dagre-BM42HDAG-DXTPvJkX.js} +1 -1
  99. package/workframe-ui/public/assets/{diagram-2AECGRRQ-DWowSo85.js → diagram-2AECGRRQ-xX_v-pbf.js} +1 -1
  100. package/workframe-ui/public/assets/{diagram-5GNKFQAL-MnxBbceO.js → diagram-5GNKFQAL-Cd2pXbBe.js} +1 -1
  101. package/workframe-ui/public/assets/{diagram-KO2AKTUF-DQaLRXFf.js → diagram-KO2AKTUF-Df3XvUtk.js} +1 -1
  102. package/workframe-ui/public/assets/{diagram-LMA3HP47-CQaBud9k.js → diagram-LMA3HP47-CsijIPaD.js} +1 -1
  103. package/workframe-ui/public/assets/{diagram-OG6HWLK6-D8bAXbY9.js → diagram-OG6HWLK6-aq5fmfHd.js} +1 -1
  104. package/workframe-ui/public/assets/{dist-DGpTLHr_.js → dist-D1c0mkbB.js} +1 -1
  105. package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-1E-xSvBK.js → erDiagram-TEJ5UH35-DnFysVRY.js} +1 -1
  106. package/workframe-ui/public/assets/eventmodeling-FCH6USID-Ci8mdb44.js +1 -0
  107. package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-CgOVD5hu.js → flowDiagram-I6XJVG4X-C6Ebi3su.js} +1 -1
  108. package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-JFYAIauo.js → ganttDiagram-6RSMTGT7-BQXQtUpa.js} +1 -1
  109. package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-B9REenIl.js → gitGraph-WXDBUCRP-Dt0zIs_M.js} +1 -1
  110. package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-BQ7NcMSn.js → gitGraphDiagram-PVQCEYII-BF8gHzRn.js} +1 -1
  111. package/workframe-ui/public/assets/index-DpoUZAxh.css +1 -0
  112. package/workframe-ui/public/assets/{index-Dnw6vjqb.js → index-lRpzpNPT.js} +2 -2
  113. package/workframe-ui/public/assets/{info-J43DQDTF-CL6-eTjH.js → info-J43DQDTF-CSmszQJT.js} +1 -1
  114. package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-LJTODW4W.js → infoDiagram-5YYISTIA-CVTKGW6p.js} +1 -1
  115. package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-bchrQVuo.js → ishikawaDiagram-YF4QCWOH-Z8pT09Lv.js} +1 -1
  116. package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-DkrvYuxP.js → journeyDiagram-JHISSGLW-r3wD68_T.js} +1 -1
  117. package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-DFRbj0IG.js → kanban-definition-UN3LZRKU-Il8VglqN.js} +1 -1
  118. package/workframe-ui/public/assets/{line-Vd48P7-O.js → line-oyjpfz2A.js} +1 -1
  119. package/workframe-ui/public/assets/{linear-Ckizh2G7.js → linear-Cf7p5tVp.js} +1 -1
  120. package/workframe-ui/public/assets/{mermaid-parser.core-Bkimsnqj.js → mermaid-parser.core-YmbZ-AfY.js} +2 -2
  121. package/workframe-ui/public/assets/{mermaid.core-x0TvVuPo.js → mermaid.core-BFdCAqCo.js} +3 -3
  122. package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-6ykAFPEz.js → mindmap-definition-RKZ34NQL-Cy2iCtEl.js} +1 -1
  123. package/workframe-ui/public/assets/{packet-YPE3B663-Dw3xgMDt.js → packet-YPE3B663-DwOBZL6K.js} +1 -1
  124. package/workframe-ui/public/assets/{pie-LRSECV5Y-DATysawG.js → pie-LRSECV5Y-04PPhnKK.js} +1 -1
  125. package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-SJKD1S0S.js → pieDiagram-4H26LBE5-LxIpgHqi.js} +1 -1
  126. package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-BrYDZX8q.js → quadrantDiagram-W4KKPZXB-0nBYfYm4.js} +1 -1
  127. package/workframe-ui/public/assets/{radar-GUYGQ44K-BmWYPCds.js → radar-GUYGQ44K-D2-vBqps.js} +1 -1
  128. package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-DwL9Mc8e.js → requirementDiagram-4Y6WPE33-DbuU0nlu.js} +1 -1
  129. package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-DYIFsL8h.js → sankeyDiagram-5OEKKPKP-B2hQ6B2x.js} +1 -1
  130. package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-0-FPkFk8.js → sequenceDiagram-3UESZ5HK-BBrU30e1.js} +1 -1
  131. package/workframe-ui/public/assets/{src-B_od6b6h.js → src-BJEDmV70.js} +1 -1
  132. package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BQCiBk6u.js → stateDiagram-AJRCARHV-7FGO4kkH.js} +1 -1
  133. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-DLTSizMg.js +1 -0
  134. package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-DS3tFcXj.js → timeline-definition-PNZ67QCA-ptDm4rCN.js} +1 -1
  135. package/workframe-ui/public/assets/{treeView-BLDUP644-DSyUCKLY.js → treeView-BLDUP644-CS6Z-0q8.js} +1 -1
  136. package/workframe-ui/public/assets/{treemap-LRROVOQU-CEZaNh5Y.js → treemap-LRROVOQU-DqV4Y2VA.js} +1 -1
  137. package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-CD-Vc9NF.js → vennDiagram-CIIHVFJN-C0UrZJYt.js} +1 -1
  138. package/workframe-ui/public/assets/{wardley-L42UT6IY-Drq5w1Mc.js → wardley-L42UT6IY-bNDN3_Sa.js} +1 -1
  139. package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DouXDJoF.js → wardleyDiagram-YWT4CUSO-jWiJsefM.js} +1 -1
  140. package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-DDf_Lol5.js → xychartDiagram-2RQKCTM6-Dsh_fLCy.js} +1 -1
  141. package/workframe-ui/public/favicon.svg +7 -7
  142. package/workframe-ui/public/index.html +50 -50
  143. package/workframe-ui/public/workframe-config.json +3 -3
  144. package/scripts/security_audit.py +0 -156
  145. package/scripts/test-scaffold.mjs +0 -390
  146. package/workframe-api/tests/__init__.py +0 -0
  147. package/workframe-api/tests/db_setup.py +0 -13
  148. package/workframe-api/tests/test_admin_updates_gated.py +0 -30
  149. package/workframe-api/tests/test_agent_dm_bootstrap.py +0 -196
  150. package/workframe-api/tests/test_agent_profile_sync.py +0 -76
  151. package/workframe-api/tests/test_auth_email.py +0 -222
  152. package/workframe-api/tests/test_auth_hole_fix_selfcheck.py +0 -99
  153. package/workframe-api/tests/test_auth_rate_limit.py +0 -19
  154. package/workframe-api/tests/test_avatar_resolve.py +0 -77
  155. package/workframe-api/tests/test_child_soul_template.py +0 -71
  156. package/workframe-api/tests/test_credential_canary.py +0 -135
  157. package/workframe-api/tests/test_credential_isolation.py +0 -448
  158. package/workframe-api/tests/test_credential_resolution.py +0 -206
  159. package/workframe-api/tests/test_device_oauth.py +0 -108
  160. package/workframe-api/tests/test_doctor_repair.py +0 -103
  161. package/workframe-api/tests/test_ensure_profile_api.py +0 -77
  162. package/workframe-api/tests/test_gateway_compose_security.py +0 -136
  163. package/workframe-api/tests/test_install_secure_host.py +0 -39
  164. package/workframe-api/tests/test_internal_proxy_auth.py +0 -125
  165. package/workframe-api/tests/test_invite_runtime_bootstrap.py +0 -72
  166. package/workframe-api/tests/test_kanban_delegation.py +0 -185
  167. package/workframe-api/tests/test_llm_proxy.py +0 -155
  168. package/workframe-api/tests/test_login_access_policy.py +0 -183
  169. package/workframe-api/tests/test_mvp_model_bootstrap.py +0 -75
  170. package/workframe-api/tests/test_onboarding_bootstrap.py +0 -248
  171. package/workframe-api/tests/test_platform_auth.py +0 -47
  172. package/workframe-api/tests/test_profile_config_path.py +0 -56
  173. package/workframe-api/tests/test_profile_config_yaml_repair.py +0 -63
  174. package/workframe-api/tests/test_profile_create.py +0 -72
  175. package/workframe-api/tests/test_profile_identity_overlay.py +0 -61
  176. package/workframe-api/tests/test_profile_install_health.py +0 -45
  177. package/workframe-api/tests/test_profile_secret_policy.py +0 -57
  178. package/workframe-api/tests/test_profile_workspace_cwd.py +0 -34
  179. package/workframe-api/tests/test_provider_bootstrap.py +0 -75
  180. package/workframe-api/tests/test_provider_connect.py +0 -54
  181. package/workframe-api/tests/test_room_crud.py +0 -192
  182. package/workframe-api/tests/test_room_tenancy.py +0 -701
  183. package/workframe-api/tests/test_runtime_identity_backfill.py +0 -34
  184. package/workframe-api/tests/test_site_meta.py +0 -81
  185. package/workframe-api/tests/test_soul_stub.py +0 -42
  186. package/workframe-api/tests/test_space_member_sync.py +0 -99
  187. package/workframe-api/tests/test_stripe_stack_config.py +0 -37
  188. package/workframe-api/tests/test_supervisor_lifecycle.py +0 -52
  189. package/workframe-api/tests/test_turn_credential_vault.py +0 -125
  190. package/workframe-api/tests/test_updates.py +0 -176
  191. package/workframe-api/tests/test_user_cohort.py +0 -113
  192. package/workframe-api/tests/test_vault_envelope.py +0 -110
  193. package/workframe-api/tests/test_workspace_members.py +0 -183
  194. package/workframe-api/tests/test_workspace_messaging_sync.py +0 -125
  195. package/workframe-api/tests/test_workspace_provider_list.py +0 -57
  196. package/workframe-supervisor/tests/test_exec_guard.py +0 -42
  197. package/workframe-supervisor/tests/test_server_import.py +0 -21
  198. package/workframe-ui/public/assets/architecture-7EHR7CIX-CtbQKTuT.js +0 -1
  199. package/workframe-ui/public/assets/channel-Dy4Z4-jn.js +0 -1
  200. package/workframe-ui/public/assets/chunk-QZHKN3VN-CtBEchFK.js +0 -1
  201. package/workframe-ui/public/assets/chunk-WU5MYG2G-B9pBtriN.js +0 -1
  202. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BMAEA8jI.js +0 -1
  203. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BMAEA8jI.js +0 -1
  204. package/workframe-ui/public/assets/eventmodeling-FCH6USID-D75cstNT.js +0 -1
  205. package/workframe-ui/public/assets/index-DpAGxump.css +0 -1
  206. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-B89jAMFF.js +0 -1
@@ -1,34 +0,0 @@
1
- """Runtime identity backfill from template profiles."""
2
- from __future__ import annotations
3
-
4
- import tempfile
5
- import unittest
6
- from pathlib import Path
7
- from unittest import mock
8
-
9
- import server
10
-
11
-
12
- class RuntimeIdentityBackfillTests(unittest.TestCase):
13
- def test_backfill_copies_missing_soul_and_skills_only(self) -> None:
14
- with tempfile.TemporaryDirectory() as tmp:
15
- root = Path(tmp)
16
- template = root / "profiles" / "workframe-agent"
17
- runtime = root / "profiles" / "u-user-workframe-agent"
18
- template.mkdir(parents=True)
19
- runtime.mkdir(parents=True)
20
- (template / "SOUL.md").write_text("template soul\n", encoding="utf-8")
21
- (runtime / "SOUL.md").write_text("custom soul\n", encoding="utf-8")
22
- skill = template / "skills" / "devops" / "botfather" / "SKILL.md"
23
- skill.parent.mkdir(parents=True)
24
- skill.write_text("botfather\n", encoding="utf-8")
25
-
26
- with mock.patch.object(server, "_profile_dir", side_effect=lambda p: root / "profiles" / p):
27
- server._backfill_runtime_identity("u-user-workframe-agent", "workframe-agent")
28
-
29
- self.assertEqual((runtime / "SOUL.md").read_text(encoding="utf-8"), "custom soul\n")
30
- self.assertTrue((runtime / "skills" / "devops" / "botfather" / "SKILL.md").is_file())
31
-
32
-
33
- if __name__ == "__main__":
34
- unittest.main()
@@ -1,81 +0,0 @@
1
- import tempfile
2
- import unittest
3
- from pathlib import Path
4
-
5
- import site_meta
6
- import stack_config
7
-
8
-
9
- class SiteMetaTests(unittest.TestCase):
10
- def setUp(self) -> None:
11
- self._tmpdir = tempfile.TemporaryDirectory()
12
- stack_config.DATA_DIR = Path(self._tmpdir.name)
13
- stack_config.CONFIG_PATH = stack_config.DATA_DIR / "stack_config.json"
14
-
15
- def tearDown(self) -> None:
16
- self._tmpdir.cleanup()
17
-
18
- def test_defaults_before_install(self) -> None:
19
- meta = site_meta.resolve_site_meta(
20
- app_base_url="https://dev.alanborger.com",
21
- install_complete=False,
22
- )
23
- self.assertEqual(meta["title"], "Workframe")
24
- self.assertIn("Hermes", meta["description"])
25
- self.assertTrue(meta["og_image"].endswith("/assets/branding/og-default.png"))
26
- self.assertEqual(meta["source"]["title"], "default")
27
-
28
- def test_workspace_identity_when_configured(self) -> None:
29
- meta = site_meta.resolve_site_meta(
30
- app_base_url="https://dev.alanborger.com",
31
- install_complete=True,
32
- workspace={
33
- "display_name": "Acme Labs",
34
- "description": "Agent ops for the product team",
35
- "tagline": "Ship with agents",
36
- "avatar_url": "/assets/project-logos/7.png",
37
- },
38
- normalize_logo=lambda url: url,
39
- )
40
- self.assertEqual(meta["title"], "Acme Labs")
41
- self.assertEqual(meta["description"], "Agent ops for the product team")
42
- self.assertEqual(meta["tagline"], "Ship with agents")
43
- self.assertTrue(meta["og_image"].endswith("/assets/project-logos/7.png"))
44
- self.assertEqual(meta["source"]["title"], "workspace")
45
-
46
- def test_stack_overrides_win(self) -> None:
47
- stack_config.patch_stack_config(
48
- {
49
- "site_branding": {
50
- "title": "Custom Public Name",
51
- "description": "Custom public pitch",
52
- },
53
- },
54
- )
55
- meta = site_meta.resolve_site_meta(
56
- app_base_url="https://dev.alanborger.com",
57
- install_complete=True,
58
- workspace={"display_name": "Acme Labs", "description": "ignored"},
59
- )
60
- self.assertEqual(meta["title"], "Custom Public Name")
61
- self.assertEqual(meta["description"], "Custom public pitch")
62
- self.assertEqual(meta["source"]["title"], "stack")
63
-
64
- def test_uploaded_og_image_url(self) -> None:
65
- site_meta.save_branding_asset("og", b"\x89PNG\r\n\x1a\n", "image/png")
66
- meta = site_meta.resolve_site_meta(app_base_url="https://dev.alanborger.com", install_complete=False)
67
- self.assertIn("/api/public/branding/og", meta["og_image"])
68
- self.assertEqual(meta["source"]["og_image"], "upload")
69
-
70
- def test_loopback_base_uses_relative_browser_assets(self) -> None:
71
- meta = site_meta.resolve_site_meta(
72
- app_base_url="http://127.0.0.1:28644",
73
- install_complete=False,
74
- )
75
- self.assertEqual(meta["favicon"], "/favicon.svg")
76
- self.assertEqual(meta["manifest_url"], "/manifest.webmanifest")
77
- self.assertTrue(meta["og_image"].startswith("/assets/"))
78
-
79
-
80
- if __name__ == "__main__":
81
- unittest.main()
@@ -1,42 +0,0 @@
1
- """SOUL stub detection and Workframe identity rendering."""
2
- from __future__ import annotations
3
-
4
- import tempfile
5
- import unittest
6
- from pathlib import Path
7
- from unittest import mock
8
-
9
- import server
10
-
11
-
12
- class SoulStubTests(unittest.TestCase):
13
- def test_stub_detects_test_placeholder(self) -> None:
14
- self.assertTrue(server._soul_is_stub("test"))
15
- self.assertTrue(server._soul_is_stub("{nativeAgentName} concierge"))
16
-
17
- def test_profile_soul_text_falls_back_to_template(self) -> None:
18
- with tempfile.TemporaryDirectory() as tmp:
19
- root = Path(tmp)
20
- template = root / "profiles" / "workframe-agent"
21
- runtime = root / "profiles" / "u-user-workframe-agent"
22
- template.mkdir(parents=True)
23
- runtime.mkdir(parents=True)
24
- (template / "SOUL.md").write_text(
25
- "# {nativeAgentName}\n\nWorkframe concierge for {projectName}.\n" + ("x" * 80),
26
- encoding="utf-8",
27
- )
28
- (runtime / "SOUL.md").write_text("test", encoding="utf-8")
29
-
30
- with mock.patch.object(server, "_profile_dir", side_effect=lambda p: root / "profiles" / p), mock.patch.object(
31
- server, "PROJECT_NAME", "Workframe"
32
- ), mock.patch.object(server, "NATIVE_PROFILE", "workframe-agent"), mock.patch.object(
33
- server, "_runtime_template_slug", return_value="workframe-agent"
34
- ), mock.patch.object(server, "_is_runtime_profile_slug", return_value=True):
35
- text = server._profile_soul_text("u-user-workframe-agent")
36
- self.assertIn("Workframe Agent", text)
37
- self.assertIn("Workframe concierge", text)
38
- self.assertNotIn("OWL", text.split("\n")[0])
39
-
40
-
41
- if __name__ == "__main__":
42
- unittest.main()
@@ -1,99 +0,0 @@
1
- import tempfile
2
- import unittest
3
- from pathlib import Path
4
-
5
- import server
6
- from db_setup import ensure_workframe_schemas
7
-
8
-
9
- class SpaceMemberSyncTests(unittest.TestCase):
10
- def setUp(self) -> None:
11
- self._tmp = tempfile.TemporaryDirectory()
12
- self.addCleanup(self._tmp.cleanup)
13
- self._old_data_dir = server.DATA_DIR
14
- self._old_auth_db_path = server.AUTH_DB_PATH
15
- server.DATA_DIR = Path(self._tmp.name)
16
- server.AUTH_DB_PATH = Path(self._tmp.name) / "auth.db"
17
- ensure_workframe_schemas()
18
- self.workspace_id = "ws-space"
19
- self.owner_id = "user-owner"
20
- self.member_id = "user-member"
21
- conn = server._workframe_db()
22
- try:
23
- now = "1"
24
- for uid, name in ((self.owner_id, "Owner"), (self.member_id, "Member")):
25
- conn.execute(
26
- "INSERT INTO users (id, display_name, role, status, created_at, updated_at) VALUES (?,?,?,?,?,?)",
27
- (uid, name, "member", "active", now, now),
28
- )
29
- conn.execute(
30
- """
31
- INSERT INTO workspaces (id, slug, display_name, owner_id, status, created_at, updated_at)
32
- VALUES (?, ?, ?, ?, ?, ?, ?)
33
- """,
34
- (self.workspace_id, "biz", "My Business", self.owner_id, "active", now, now),
35
- )
36
- for uid, role in ((self.owner_id, "owner"), (self.member_id, "member")):
37
- conn.execute(
38
- """
39
- INSERT INTO workspace_memberships (id, workspace_id, user_id, role, status, created_at, updated_at)
40
- VALUES (?, ?, ?, ?, ?, ?, ?)
41
- """,
42
- (f"wm-{uid}", self.workspace_id, uid, role, "active", now, now),
43
- )
44
- conn.commit()
45
- finally:
46
- conn.close()
47
-
48
- def tearDown(self) -> None:
49
- server.DATA_DIR = self._old_data_dir
50
- server.AUTH_DB_PATH = self._old_auth_db_path
51
-
52
- def test_new_space_adds_all_workspace_members(self) -> None:
53
- status, payload = server._create_room(
54
- self.workspace_id,
55
- {"name": "Engineering", "slug": "engineering", "room_type": "channel"},
56
- self.owner_id,
57
- )
58
- self.assertEqual(status, 201)
59
- room_id = payload["room"]["id"]
60
-
61
- conn = server._workframe_db()
62
- try:
63
- self.assertTrue(server._user_can_access_room(conn, room_id, self.owner_id))
64
- self.assertTrue(server._user_can_access_room(conn, room_id, self.member_id))
65
- finally:
66
- conn.close()
67
-
68
- def test_onboard_joins_existing_spaces(self) -> None:
69
- status, payload = server._create_room(
70
- self.workspace_id,
71
- {"name": "Engineering", "slug": "engineering", "room_type": "channel"},
72
- self.owner_id,
73
- )
74
- self.assertEqual(status, 201)
75
- room_id = payload["room"]["id"]
76
-
77
- late_user = "user-late"
78
- conn = server._workframe_db()
79
- try:
80
- conn.execute(
81
- "INSERT INTO users (id, display_name, role, status, created_at, updated_at) VALUES (?,?,?,?,?,?)",
82
- (late_user, "Late", "member", "active", "2", "2"),
83
- )
84
- conn.execute(
85
- """
86
- INSERT INTO workspace_memberships (id, workspace_id, user_id, role, status, created_at, updated_at)
87
- VALUES (?, ?, ?, ?, ?, ?, ?)
88
- """,
89
- ("wm-late", self.workspace_id, late_user, "member", "active", "2", "2"),
90
- )
91
- server._onboard_workspace_member_rooms(conn, self.workspace_id, late_user)
92
- conn.commit()
93
- self.assertTrue(server._user_can_access_room(conn, room_id, late_user))
94
- finally:
95
- conn.close()
96
-
97
-
98
- if __name__ == "__main__":
99
- unittest.main()
@@ -1,37 +0,0 @@
1
- import tempfile
2
- import unittest
3
- from pathlib import Path
4
-
5
- import stack_config
6
-
7
-
8
- class StripeStackConfigTests(unittest.TestCase):
9
- def setUp(self) -> None:
10
- self._tmpdir = tempfile.TemporaryDirectory()
11
- stack_config.DATA_DIR = Path(self._tmpdir.name)
12
- stack_config.CONFIG_PATH = stack_config.DATA_DIR / "stack_config.json"
13
-
14
- def tearDown(self) -> None:
15
- self._tmpdir.cleanup()
16
-
17
- def test_stripe_connect_patch_and_public_payload(self) -> None:
18
- stack_config.patch_stack_config(
19
- {
20
- "stripe_connect": {
21
- "client_id": "ca_test123",
22
- "client_secret": "sk_test_secret",
23
- },
24
- },
25
- )
26
- resolved = stack_config.resolved_stripe_connect()
27
- self.assertEqual(resolved["client_id"], "ca_test123")
28
- self.assertEqual(resolved["client_secret"], "sk_test_secret")
29
- public = stack_config.get_stack_config()["stripe_connect"]
30
- self.assertEqual(public["client_id"], "ca_test123")
31
- self.assertTrue(public["has_secret"])
32
- self.assertTrue(public["enabled"])
33
- self.assertNotIn("client_secret", public)
34
-
35
-
36
- if __name__ == "__main__":
37
- unittest.main()
@@ -1,52 +0,0 @@
1
- import importlib.util
2
- import unittest
3
- from pathlib import Path
4
- from unittest.mock import patch
5
-
6
- ROOT = Path(__file__).resolve().parents[2]
7
- SUPERVISOR = ROOT / "workframe-supervisor" / "server.py"
8
- API = ROOT / "workframe-api" / "server.py"
9
-
10
-
11
- def _load(path: Path, name: str):
12
- spec = importlib.util.spec_from_file_location(name, path)
13
- mod = importlib.util.module_from_spec(spec)
14
- assert spec and spec.loader
15
- spec.loader.exec_module(mod)
16
- return mod
17
-
18
-
19
- class SupervisorAuthTest(unittest.TestCase):
20
- def test_secrets_compare(self) -> None:
21
- sup = _load(SUPERVISOR, "workframe_supervisor")
22
- self.assertTrue(sup.secrets_compare("abc", "abc"))
23
- self.assertFalse(sup.secrets_compare("abc", "abd"))
24
-
25
-
26
- class SecureModeLifecycleTest(unittest.TestCase):
27
- def test_secure_mode_delegates_start_to_supervisor(self) -> None:
28
- api = _load(API, "workframe_api")
29
- with patch.object(api, "SECURE_MODE", True), patch.object(
30
- api, "_primary_profile", return_value="workframe-agent"
31
- ), patch.object(api, "resolve_validated_profile", return_value="architect"), patch.object(
32
- api, "_supervisor_profile_lifecycle", return_value={"ok": True, "profile": "architect"}
33
- ) as delegate:
34
- out = api.profile_gateway_lifecycle("architect", "start")
35
- delegate.assert_called_once_with("architect", "start")
36
- self.assertTrue(out["ok"])
37
-
38
- def test_secure_mode_gateway_exec_uses_supervisor(self) -> None:
39
- api = _load(API, "workframe_api")
40
- with patch.object(api, "SECURE_MODE", True), patch.object(
41
- api, "resolve_validated_profile", return_value="architect"
42
- ), patch.object(
43
- api, "_supervisor_gateway_exec", return_value=(0, "ok")
44
- ) as delegate:
45
- code, out = api._gateway_exec("architect", ["gateway", "start"])
46
- delegate.assert_called_once_with("architect", ["gateway", "start"])
47
- self.assertEqual(code, 0)
48
- self.assertEqual(out, "ok")
49
-
50
-
51
- if __name__ == "__main__":
52
- unittest.main()
@@ -1,125 +0,0 @@
1
- """Per-run credential vault + lease tests."""
2
-
3
- import tempfile
4
- import unittest
5
- from pathlib import Path
6
- from unittest.mock import patch
7
-
8
- import credential_vault
9
- import server
10
- import turn_credentials
11
- import vault_kek
12
- from db_setup import ensure_workframe_schemas
13
-
14
-
15
- class TurnCredentialVaultTests(unittest.TestCase):
16
- def setUp(self) -> None:
17
- self._tmp = tempfile.TemporaryDirectory()
18
- self.addCleanup(self._tmp.cleanup)
19
- self._old_data = server.DATA_DIR
20
- self._old_auth = server.AUTH_DB_PATH
21
- self._old_hermes = server.HERMES_DATA
22
- server.DATA_DIR = Path(self._tmp.name) / "api-data"
23
- server.DATA_DIR.mkdir(parents=True)
24
- server.AUTH_DB_PATH = server.DATA_DIR / "auth.db"
25
- server.HERMES_DATA = Path(self._tmp.name) / "hermes"
26
- (server.HERMES_DATA / "profiles").mkdir(parents=True)
27
- (server._profile_dir("workframe-agent")).mkdir(parents=True, exist_ok=True)
28
- credential_vault.DATA_DIR = server.DATA_DIR
29
- credential_vault.VAULT_DB = server.DATA_DIR / "credential_vault.db"
30
- vault_kek.DATA_DIR = server.DATA_DIR
31
- vault_kek.VAULT_KEK_FILE = server.DATA_DIR / ".vault_kek"
32
- turn_credentials.DATA_DIR = server.DATA_DIR
33
- turn_credentials.WORKFRAME_DB = server.DATA_DIR / "workframe.db"
34
- ensure_workframe_schemas()
35
- credential_vault.ensure_schema()
36
- credential_vault.unseal_for_tests()
37
- turn_credentials.ensure_schema()
38
-
39
- def tearDown(self) -> None:
40
- server.DATA_DIR = self._old_data
41
- server.AUTH_DB_PATH = self._old_auth
42
- server.HERMES_DATA = self._old_hermes
43
-
44
- def test_store_user_credential_uses_vault_not_agents_env(self) -> None:
45
- user_id = "user-vault-1"
46
- payload = server._store_user_credential(
47
- user_id, "openrouter", "api_key", "sk-secret", "OPENROUTER_API_KEY", "test",
48
- )
49
- self.assertTrue(str(payload["credential_ref"]).startswith("vault:"))
50
- env_path = server._user_hermes_env_path(user_id)
51
- if env_path.is_file():
52
- self.assertNotIn("sk-secret", env_path.read_text(encoding="utf-8"))
53
- bid = credential_vault.parse_vault_ref(payload["credential_ref"])
54
- self.assertEqual(credential_vault.read_secret(bid), "sk-secret")
55
-
56
- @patch.object(server, "_wait_profile_api_healthy", return_value=True)
57
- def test_overlay_writes_lease_token_not_raw_secret(self, _wait_mock) -> None:
58
- user_id = "user-vault-overlay-1"
59
- workspace_id = "ws-vault-test"
60
- self.workspace_id = workspace_id
61
- self.user_a = user_id
62
- conn = server._workframe_db()
63
- try:
64
- now = "1"
65
- conn.execute(
66
- "INSERT INTO workspaces (id, slug, display_name, owner_id, status, created_at, updated_at) VALUES (?,?,?,?,?,?,?)",
67
- (workspace_id, "ws-vault-test", "Iso", user_id, "active", now, now),
68
- )
69
- conn.execute(
70
- "INSERT INTO users (id, display_name, role, status, created_at, updated_at) VALUES (?,?,?,?,?,?)",
71
- (user_id, "Fab", "member", "active", now, now),
72
- )
73
- conn.execute(
74
- """
75
- INSERT INTO workspace_memberships (id, workspace_id, user_id, role, status, created_at, updated_at)
76
- VALUES (?, ?, ?, 'member', 'active', ?, ?)
77
- """,
78
- ("wm-1", workspace_id, user_id, now, now),
79
- )
80
- conn.execute(
81
- """
82
- INSERT INTO agent_profiles (id, workspace_id, slug, display_name, is_native, status, created_at, updated_at)
83
- VALUES (?, ?, ?, ?, ?, 'available', ?, ?)
84
- """,
85
- ("ap-arch", workspace_id, "architect", "Architect", 0, now, now),
86
- )
87
- cred_id = "cred-or-1"
88
- conn.execute(
89
- """
90
- INSERT INTO credential_bindings (
91
- id, workspace_id, user_id, agent_profile_id, provider,
92
- credential_type, credential_ref, label, is_active,
93
- expires_at, created_by, created_at, updated_at, deleted_at
94
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
95
- """,
96
- (
97
- cred_id, None, user_id, None, "openrouter",
98
- "api_key", credential_vault.vault_ref(cred_id), "OR", 1, None, user_id,
99
- now, now, None,
100
- ),
101
- )
102
- conn.commit()
103
- finally:
104
- conn.close()
105
- credential_vault.store_secret(cred_id, "sk-fab-only", env_var="OPENROUTER_API_KEY", provider="openrouter", user_id=user_id)
106
- runtime = server._runtime_profile_slug(user_id, "architect")
107
- prof_dir = server._profile_dir(runtime)
108
- prof_dir.mkdir(parents=True, exist_ok=True)
109
- run_id = "run-test-1"
110
- server._overlay_turn_provider_env(runtime, user_id, workspace_id, "openrouter", run_id)
111
- text = (prof_dir / ".env").read_text(encoding="utf-8")
112
- self.assertIn("wf_rt_", text)
113
- self.assertNotIn("sk-fab-only", text)
114
- lease_line = [ln for ln in text.splitlines() if "OPENROUTER_API_KEY" in ln][0]
115
- token = lease_line.split("=", 1)[1].strip().strip("\"'")
116
- meta = turn_credentials.validate_lease(token)
117
- self.assertIsNotNone(meta)
118
- assert meta is not None
119
- self.assertEqual(meta["run_id"], run_id)
120
- server._revoke_turn_credential_lease(run_id, runtime)
121
- self.assertIsNone(turn_credentials.validate_lease(token))
122
-
123
-
124
- if __name__ == "__main__":
125
- unittest.main()
@@ -1,176 +0,0 @@
1
- """Stack update checks — version compare + safe apply hooks."""
2
- import json
3
- import os
4
- import tempfile
5
- import unittest
6
- from pathlib import Path
7
- from unittest import mock
8
-
9
- import updates
10
-
11
-
12
- class UpdatesModuleTests(unittest.TestCase):
13
- def test_version_lt(self) -> None:
14
- self.assertTrue(updates._version_lt("0.1.0", "0.1.1"))
15
- self.assertFalse(updates._version_lt("0.1.0", "0.1.0"))
16
- self.assertFalse(updates._version_lt("0.2.0", "0.1.9"))
17
-
18
- def test_parse_hermes_version_output(self) -> None:
19
- self.assertEqual(
20
- updates.parse_hermes_version_output("Hermes Agent v0.17.0 (2026.6.19) · upstream 50386786"),
21
- "0.17.0",
22
- )
23
- self.assertEqual(updates.parse_hermes_version_output(""), "")
24
-
25
- @mock.patch.object(updates, "_http_json")
26
- def test_docker_hub_digest_prefers_tag_digest_over_first_image(self, http_json: mock.MagicMock) -> None:
27
- http_json.return_value = {
28
- "digest": "sha256:tag-level",
29
- "images": [{"digest": "sha256:arm64-first", "architecture": "arm64", "os": "linux"}],
30
- }
31
- self.assertEqual(updates._docker_hub_digest("nousresearch/hermes-agent", "latest"), "sha256:tag-level")
32
-
33
- @mock.patch.object(updates, "_http_json")
34
- def test_docker_hub_digest_prefers_amd64_when_no_tag_digest(self, http_json: mock.MagicMock) -> None:
35
- http_json.return_value = {
36
- "images": [
37
- {"digest": "sha256:arm64", "architecture": "arm64", "os": "linux"},
38
- {"digest": "sha256:amd64", "architecture": "amd64", "os": "linux"},
39
- ],
40
- }
41
- self.assertEqual(updates._docker_hub_digest("repo", "latest"), "sha256:amd64")
42
-
43
- @mock.patch.object(updates, "_npm_latest_version", return_value="0.1.1")
44
- @mock.patch.object(updates, "_container_image_digest", return_value=("digest-a", "nousresearch/hermes-agent:latest"))
45
- @mock.patch.object(updates, "_docker_hub_digest", return_value="digest-b")
46
- @mock.patch.object(updates.Path, "exists", return_value=True)
47
- def test_updates_available_flags_workframe_and_hermes(
48
- self,
49
- _exists: mock.MagicMock,
50
- _hub: mock.MagicMock,
51
- _container: mock.MagicMock,
52
- _npm: mock.MagicMock,
53
- ) -> None:
54
- with tempfile.TemporaryDirectory() as tmp:
55
- root = Path(tmp)
56
- (root / "workframe-manifest.json").write_text(
57
- json.dumps({"package_version": "0.1.0"}),
58
- encoding="utf-8",
59
- )
60
- (root / "docker-compose.yml").write_text("services: {}\n", encoding="utf-8")
61
- with mock.patch.dict(
62
- os.environ,
63
- {
64
- "WORKFRAME_PROJECT_ROOT": str(root),
65
- "WORKFRAME_COMPOSE_DIR": str(root),
66
- "WORKFRAME_API_VERSION": "0.1.0",
67
- },
68
- clear=False,
69
- ):
70
- status = updates.updates_available(desktop_version="0.0.9")
71
- self.assertTrue(status["workframe"]["update_available"])
72
- self.assertTrue(status["hermes"]["update_available"])
73
- self.assertTrue(status["desktop"]["update_available"])
74
- self.assertFalse(status["workframe"]["can_update"])
75
- self.assertFalse(status["hermes"]["can_update"])
76
- self.assertIn("WORKFRAME_HOST_COMPOSE_DIR", status["workframe"]["reason"] or "")
77
-
78
- @mock.patch.object(updates, "_npm_latest_version", return_value="0.1.1")
79
- @mock.patch.object(updates, "_container_image_digest", return_value=("digest-a", "nousresearch/hermes-agent:latest"))
80
- @mock.patch.object(updates, "_docker_hub_digest", return_value="digest-b")
81
- @mock.patch.object(updates, "_script_path")
82
- @mock.patch.object(updates.Path, "exists", return_value=True)
83
- def test_updates_available_can_update_when_host_compose_ready(
84
- self,
85
- _exists: mock.MagicMock,
86
- script_path: mock.MagicMock,
87
- _hub: mock.MagicMock,
88
- _container: mock.MagicMock,
89
- _npm: mock.MagicMock,
90
- ) -> None:
91
- script_path.side_effect = lambda name: Path(f"/tmp/{name}")
92
- with tempfile.TemporaryDirectory() as tmp:
93
- root = Path(tmp)
94
- (root / "workframe-manifest.json").write_text(
95
- json.dumps({"package_version": "0.1.0"}),
96
- encoding="utf-8",
97
- )
98
- (root / "docker-compose.yml").write_text("services: {}\n", encoding="utf-8")
99
- with mock.patch.dict(
100
- os.environ,
101
- {
102
- "WORKFRAME_PROJECT_ROOT": str(root),
103
- "WORKFRAME_COMPOSE_DIR": str(root),
104
- "WORKFRAME_HOST_COMPOSE_DIR": str(root),
105
- "WORKFRAME_API_VERSION": "0.1.0",
106
- },
107
- clear=False,
108
- ):
109
- status = updates.updates_available()
110
- self.assertTrue(status["hermes"]["can_update"])
111
- self.assertTrue(status["hermes"]["can_restart_gateway"])
112
- self.assertEqual(status["hermes"]["state"], "available")
113
-
114
- @mock.patch.object(updates, "_npm_latest_version", return_value="0.1.0")
115
- @mock.patch.object(updates, "_container_image_digest", return_value=("digest-a", "img"))
116
- @mock.patch.object(updates, "_docker_hub_digest", return_value="digest-a")
117
- @mock.patch.object(updates.Path, "exists", return_value=True)
118
- def test_updates_available_when_digest_unknown_no_hermes_flag(
119
- self,
120
- _exists: mock.MagicMock,
121
- _hub: mock.MagicMock,
122
- _container: mock.MagicMock,
123
- _npm: mock.MagicMock,
124
- ) -> None:
125
- with tempfile.TemporaryDirectory() as tmp:
126
- root = Path(tmp)
127
- (root / "workframe-manifest.json").write_text(
128
- json.dumps({"package_version": "0.1.0"}),
129
- encoding="utf-8",
130
- )
131
- with mock.patch.dict(
132
- os.environ,
133
- {"WORKFRAME_PROJECT_ROOT": str(root), "WORKFRAME_COMPOSE_DIR": str(root)},
134
- clear=False,
135
- ):
136
- status = updates.updates_available()
137
- self.assertFalse(status["hermes"]["update_available"])
138
- self.assertFalse(status["workframe"]["update_available"])
139
-
140
- @mock.patch.object(updates.subprocess, "run")
141
- @mock.patch.object(updates, "_script_path")
142
- @mock.patch.object(updates.Path, "exists", return_value=True)
143
- def test_apply_update_runs_scripts(
144
- self,
145
- _exists: mock.MagicMock,
146
- script_path: mock.MagicMock,
147
- run: mock.MagicMock,
148
- ) -> None:
149
- script_path.side_effect = lambda name: Path(f"/tmp/{name}")
150
- run.return_value = mock.Mock(returncode=0, stdout="ok", stderr="")
151
- with mock.patch.dict(os.environ, {"WORKFRAME_COMPOSE_DIR": "/tmp", "WORKFRAME_PROJECT_ROOT": "/tmp", "WORKFRAME_ENABLE_ADMIN_UPDATES": "1"}, clear=False):
152
- result = updates.apply_update("hermes")
153
- self.assertTrue(result["ok"])
154
- run.assert_called_once()
155
-
156
- @mock.patch.object(updates.subprocess, "run")
157
- @mock.patch.object(updates, "_script_path")
158
- @mock.patch.object(updates, "_docker_apply_ready", return_value=(True, None))
159
- @mock.patch.object(updates.Path, "exists", return_value=True)
160
- def test_restart_gateway_runs_script(
161
- self,
162
- _exists: mock.MagicMock,
163
- _ready: mock.MagicMock,
164
- script_path: mock.MagicMock,
165
- run: mock.MagicMock,
166
- ) -> None:
167
- script_path.return_value = Path("/tmp/restart-gateway-hermes.sh")
168
- run.return_value = mock.Mock(returncode=0, stdout="ok", stderr="")
169
- with mock.patch.dict(os.environ, {"WORKFRAME_COMPOSE_DIR": "/tmp", "WORKFRAME_PROJECT_ROOT": "/tmp", "WORKFRAME_ENABLE_ADMIN_UPDATES": "1"}, clear=False):
170
- result = updates.restart_gateway()
171
- self.assertTrue(result["ok"])
172
- run.assert_called_once()
173
-
174
-
175
- if __name__ == "__main__":
176
- unittest.main()