create-workframe 0.1.0 → 0.1.2

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 (198) hide show
  1. package/LICENSE +201 -201
  2. package/NOTICE +12 -12
  3. package/README.md +8 -92
  4. package/SECURITY.md +38 -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/bundle-workframe-ui.mjs +3 -3
  28. package/scripts/ensure-compose-host-paths.mjs +51 -51
  29. package/scripts/lib/install-identity.mjs +212 -212
  30. package/scripts/set-compose-public-url.mjs +92 -92
  31. package/scripts/sync-canonical-to-package.mjs +27 -9
  32. package/shared/WORKFRAME_AGENT_LIBRARY.md +17 -17
  33. package/shared/WORKFRAME_AGENT_OPERATIONS.md +15 -15
  34. package/shared/WORKFRAME_AGENT_PACKS.json +18 -18
  35. package/shared/WORKFRAME_AGENT_PACKS.yaml +8 -8
  36. package/shared/WORKFRAME_SKILL_CURATION.md +4 -4
  37. package/workframe-api/README.md +26 -28
  38. package/workframe-api/action_proxy.py +131 -131
  39. package/workframe-api/auth_rate_limit.py +49 -49
  40. package/workframe-api/credential_vault.py +445 -445
  41. package/workframe-api/data/avatar-catalog.json +41 -41
  42. package/workframe-api/email_sender.py +220 -220
  43. package/workframe-api/google_auth.py +90 -90
  44. package/workframe-api/install_api.py +359 -359
  45. package/workframe-api/internal_proxy_auth.py +150 -150
  46. package/workframe-api/llm_proxy.py +277 -277
  47. package/workframe-api/oidc_jwt.py +108 -108
  48. package/workframe-api/package.json +12 -13
  49. package/workframe-api/public/assets/index-DPXu_lGn.css +1 -1
  50. package/workframe-api/public/assets/index-DYnLrCZZ.js +8 -8
  51. package/workframe-api/requirements.txt +2 -2
  52. package/workframe-api/site_meta.py +271 -271
  53. package/workframe-api/stack_config.py +427 -427
  54. package/workframe-api/time-bind-chat.py +99 -99
  55. package/workframe-api/turn_credentials.py +226 -226
  56. package/workframe-api/updates.py +417 -417
  57. package/workframe-api/vault_kek.py +159 -159
  58. package/workframe-api/zk_auth.py +633 -633
  59. package/workframe-supervisor/Dockerfile +11 -11
  60. package/workframe-supervisor/server.py +787 -787
  61. package/workframe-ui/docker/nginx.conf +85 -85
  62. package/workframe-ui/public/assets/{arc-CBDYvkAF.js → arc-COAT3laO.js} +1 -1
  63. package/workframe-ui/public/assets/architecture-7EHR7CIX-DUyH3hWG.js +1 -0
  64. package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-XnBRKeW0.js → architectureDiagram-3BPJPVTR-BFjWV24l.js} +1 -1
  65. package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-VYHUfVhd.js → blockDiagram-GPEHLZMM-DSQLPfrj.js} +1 -1
  66. package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-BTjUcJpm.js → c4Diagram-AAUBKEIU-DKEHv1t2.js} +1 -1
  67. package/workframe-ui/public/assets/channel-g7r_RGaY.js +1 -0
  68. package/workframe-ui/public/assets/{chunk-2J33WTMH-w7uu7R-b.js → chunk-2J33WTMH-DHZg-DUi.js} +1 -1
  69. package/workframe-ui/public/assets/{chunk-3OPIFGDE-Cb9LtnDX.js → chunk-3OPIFGDE-BB-OYTfp.js} +1 -1
  70. package/workframe-ui/public/assets/{chunk-4BX2VUAB-DiQ-qCwH.js → chunk-4BX2VUAB-C93q0YIm.js} +1 -1
  71. package/workframe-ui/public/assets/{chunk-55IACEB6-C-mLFr7z.js → chunk-55IACEB6-MAYniqik.js} +1 -1
  72. package/workframe-ui/public/assets/{chunk-5ZQYHXKU-DOesfiCI.js → chunk-5ZQYHXKU-ChgN6YJs.js} +1 -1
  73. package/workframe-ui/public/assets/{chunk-727SXJPM-BJ3oBZuz.js → chunk-727SXJPM-B_FYwdAv.js} +1 -1
  74. package/workframe-ui/public/assets/{chunk-AQP2D5EJ-CCA6xpGs.js → chunk-AQP2D5EJ-1_Hw_h1A.js} +1 -1
  75. package/workframe-ui/public/assets/{chunk-BSJP7CBP-a0cMNFb2.js → chunk-BSJP7CBP-CFiDQ1Rv.js} +1 -1
  76. package/workframe-ui/public/assets/{chunk-CSCIHK7Q-kuqN8EIY.js → chunk-CSCIHK7Q-DZ9UMTlB.js} +1 -1
  77. package/workframe-ui/public/assets/{chunk-FMBD7UC4-DyPgYHCg.js → chunk-FMBD7UC4-DlMlyFgw.js} +1 -1
  78. package/workframe-ui/public/assets/{chunk-KSCS5N6A-CdUuvR0V.js → chunk-KSCS5N6A-DHXtQ_Hf.js} +1 -1
  79. package/workframe-ui/public/assets/{chunk-L5ZTLDWV-Dq9NoWmK.js → chunk-L5ZTLDWV-CuQzg-QG.js} +1 -1
  80. package/workframe-ui/public/assets/{chunk-LZXEDZCA-p74rddlO.js → chunk-LZXEDZCA-BHzjzCGg.js} +2 -2
  81. package/workframe-ui/public/assets/{chunk-ND2GUHAM-DBD2u1Gz.js → chunk-ND2GUHAM-DHXx05n2.js} +1 -1
  82. package/workframe-ui/public/assets/{chunk-NZK2D7GU-BeIeYFnd.js → chunk-NZK2D7GU-CV5pmDM_.js} +1 -1
  83. package/workframe-ui/public/assets/{chunk-O5CBEL6O-ClHc56ib.js → chunk-O5CBEL6O-6tkCHxsV.js} +1 -1
  84. package/workframe-ui/public/assets/chunk-QZHKN3VN-C5UQehWY.js +1 -0
  85. package/workframe-ui/public/assets/chunk-WU5MYG2G-DhWllrI8.js +1 -0
  86. package/workframe-ui/public/assets/{chunk-XPW4576I-EFr8R_1p.js → chunk-XPW4576I-BClwIiCp.js} +1 -1
  87. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BBM_8T8E.js +1 -0
  88. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BBM_8T8E.js +1 -0
  89. package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-C7aPBODd.js → cose-bilkent-S5V4N54A-DOrGV6DQ.js} +1 -1
  90. package/workframe-ui/public/assets/{dagre-BM42HDAG-BdU1Rv-H.js → dagre-BM42HDAG-DXTPvJkX.js} +1 -1
  91. package/workframe-ui/public/assets/{diagram-2AECGRRQ-DWowSo85.js → diagram-2AECGRRQ-xX_v-pbf.js} +1 -1
  92. package/workframe-ui/public/assets/{diagram-5GNKFQAL-MnxBbceO.js → diagram-5GNKFQAL-Cd2pXbBe.js} +1 -1
  93. package/workframe-ui/public/assets/{diagram-KO2AKTUF-DQaLRXFf.js → diagram-KO2AKTUF-Df3XvUtk.js} +1 -1
  94. package/workframe-ui/public/assets/{diagram-LMA3HP47-CQaBud9k.js → diagram-LMA3HP47-CsijIPaD.js} +1 -1
  95. package/workframe-ui/public/assets/{diagram-OG6HWLK6-D8bAXbY9.js → diagram-OG6HWLK6-aq5fmfHd.js} +1 -1
  96. package/workframe-ui/public/assets/{dist-DGpTLHr_.js → dist-D1c0mkbB.js} +1 -1
  97. package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-1E-xSvBK.js → erDiagram-TEJ5UH35-DnFysVRY.js} +1 -1
  98. package/workframe-ui/public/assets/eventmodeling-FCH6USID-Ci8mdb44.js +1 -0
  99. package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-CgOVD5hu.js → flowDiagram-I6XJVG4X-C6Ebi3su.js} +1 -1
  100. package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-JFYAIauo.js → ganttDiagram-6RSMTGT7-BQXQtUpa.js} +1 -1
  101. package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-B9REenIl.js → gitGraph-WXDBUCRP-Dt0zIs_M.js} +1 -1
  102. package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-BQ7NcMSn.js → gitGraphDiagram-PVQCEYII-BF8gHzRn.js} +1 -1
  103. package/workframe-ui/public/assets/index-DpoUZAxh.css +1 -0
  104. package/workframe-ui/public/assets/{index-Dnw6vjqb.js → index-lRpzpNPT.js} +2 -2
  105. package/workframe-ui/public/assets/{info-J43DQDTF-CL6-eTjH.js → info-J43DQDTF-CSmszQJT.js} +1 -1
  106. package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-LJTODW4W.js → infoDiagram-5YYISTIA-CVTKGW6p.js} +1 -1
  107. package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-bchrQVuo.js → ishikawaDiagram-YF4QCWOH-Z8pT09Lv.js} +1 -1
  108. package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-DkrvYuxP.js → journeyDiagram-JHISSGLW-r3wD68_T.js} +1 -1
  109. package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-DFRbj0IG.js → kanban-definition-UN3LZRKU-Il8VglqN.js} +1 -1
  110. package/workframe-ui/public/assets/{line-Vd48P7-O.js → line-oyjpfz2A.js} +1 -1
  111. package/workframe-ui/public/assets/{linear-Ckizh2G7.js → linear-Cf7p5tVp.js} +1 -1
  112. package/workframe-ui/public/assets/{mermaid-parser.core-Bkimsnqj.js → mermaid-parser.core-YmbZ-AfY.js} +2 -2
  113. package/workframe-ui/public/assets/{mermaid.core-x0TvVuPo.js → mermaid.core-BFdCAqCo.js} +3 -3
  114. package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-6ykAFPEz.js → mindmap-definition-RKZ34NQL-Cy2iCtEl.js} +1 -1
  115. package/workframe-ui/public/assets/{packet-YPE3B663-Dw3xgMDt.js → packet-YPE3B663-DwOBZL6K.js} +1 -1
  116. package/workframe-ui/public/assets/{pie-LRSECV5Y-DATysawG.js → pie-LRSECV5Y-04PPhnKK.js} +1 -1
  117. package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-SJKD1S0S.js → pieDiagram-4H26LBE5-LxIpgHqi.js} +1 -1
  118. package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-BrYDZX8q.js → quadrantDiagram-W4KKPZXB-0nBYfYm4.js} +1 -1
  119. package/workframe-ui/public/assets/{radar-GUYGQ44K-BmWYPCds.js → radar-GUYGQ44K-D2-vBqps.js} +1 -1
  120. package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-DwL9Mc8e.js → requirementDiagram-4Y6WPE33-DbuU0nlu.js} +1 -1
  121. package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-DYIFsL8h.js → sankeyDiagram-5OEKKPKP-B2hQ6B2x.js} +1 -1
  122. package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-0-FPkFk8.js → sequenceDiagram-3UESZ5HK-BBrU30e1.js} +1 -1
  123. package/workframe-ui/public/assets/{src-B_od6b6h.js → src-BJEDmV70.js} +1 -1
  124. package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BQCiBk6u.js → stateDiagram-AJRCARHV-7FGO4kkH.js} +1 -1
  125. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-DLTSizMg.js +1 -0
  126. package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-DS3tFcXj.js → timeline-definition-PNZ67QCA-ptDm4rCN.js} +1 -1
  127. package/workframe-ui/public/assets/{treeView-BLDUP644-DSyUCKLY.js → treeView-BLDUP644-CS6Z-0q8.js} +1 -1
  128. package/workframe-ui/public/assets/{treemap-LRROVOQU-CEZaNh5Y.js → treemap-LRROVOQU-DqV4Y2VA.js} +1 -1
  129. package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-CD-Vc9NF.js → vennDiagram-CIIHVFJN-C0UrZJYt.js} +1 -1
  130. package/workframe-ui/public/assets/{wardley-L42UT6IY-Drq5w1Mc.js → wardley-L42UT6IY-bNDN3_Sa.js} +1 -1
  131. package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DouXDJoF.js → wardleyDiagram-YWT4CUSO-jWiJsefM.js} +1 -1
  132. package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-DDf_Lol5.js → xychartDiagram-2RQKCTM6-Dsh_fLCy.js} +1 -1
  133. package/workframe-ui/public/favicon.svg +7 -7
  134. package/workframe-ui/public/index.html +50 -50
  135. package/workframe-ui/public/workframe-config.json +3 -3
  136. package/scripts/security_audit.py +0 -156
  137. package/scripts/test-scaffold.mjs +0 -390
  138. package/workframe-api/tests/__init__.py +0 -0
  139. package/workframe-api/tests/db_setup.py +0 -13
  140. package/workframe-api/tests/test_admin_updates_gated.py +0 -30
  141. package/workframe-api/tests/test_agent_dm_bootstrap.py +0 -196
  142. package/workframe-api/tests/test_agent_profile_sync.py +0 -76
  143. package/workframe-api/tests/test_auth_email.py +0 -222
  144. package/workframe-api/tests/test_auth_hole_fix_selfcheck.py +0 -99
  145. package/workframe-api/tests/test_auth_rate_limit.py +0 -19
  146. package/workframe-api/tests/test_avatar_resolve.py +0 -77
  147. package/workframe-api/tests/test_child_soul_template.py +0 -71
  148. package/workframe-api/tests/test_credential_canary.py +0 -135
  149. package/workframe-api/tests/test_credential_isolation.py +0 -448
  150. package/workframe-api/tests/test_credential_resolution.py +0 -206
  151. package/workframe-api/tests/test_device_oauth.py +0 -108
  152. package/workframe-api/tests/test_doctor_repair.py +0 -103
  153. package/workframe-api/tests/test_ensure_profile_api.py +0 -77
  154. package/workframe-api/tests/test_gateway_compose_security.py +0 -136
  155. package/workframe-api/tests/test_install_secure_host.py +0 -39
  156. package/workframe-api/tests/test_internal_proxy_auth.py +0 -125
  157. package/workframe-api/tests/test_invite_runtime_bootstrap.py +0 -72
  158. package/workframe-api/tests/test_kanban_delegation.py +0 -185
  159. package/workframe-api/tests/test_llm_proxy.py +0 -155
  160. package/workframe-api/tests/test_login_access_policy.py +0 -183
  161. package/workframe-api/tests/test_mvp_model_bootstrap.py +0 -75
  162. package/workframe-api/tests/test_onboarding_bootstrap.py +0 -248
  163. package/workframe-api/tests/test_platform_auth.py +0 -47
  164. package/workframe-api/tests/test_profile_config_path.py +0 -56
  165. package/workframe-api/tests/test_profile_config_yaml_repair.py +0 -63
  166. package/workframe-api/tests/test_profile_create.py +0 -72
  167. package/workframe-api/tests/test_profile_identity_overlay.py +0 -61
  168. package/workframe-api/tests/test_profile_install_health.py +0 -45
  169. package/workframe-api/tests/test_profile_secret_policy.py +0 -57
  170. package/workframe-api/tests/test_profile_workspace_cwd.py +0 -34
  171. package/workframe-api/tests/test_provider_bootstrap.py +0 -75
  172. package/workframe-api/tests/test_provider_connect.py +0 -54
  173. package/workframe-api/tests/test_room_crud.py +0 -192
  174. package/workframe-api/tests/test_room_tenancy.py +0 -701
  175. package/workframe-api/tests/test_runtime_identity_backfill.py +0 -34
  176. package/workframe-api/tests/test_site_meta.py +0 -81
  177. package/workframe-api/tests/test_soul_stub.py +0 -42
  178. package/workframe-api/tests/test_space_member_sync.py +0 -99
  179. package/workframe-api/tests/test_stripe_stack_config.py +0 -37
  180. package/workframe-api/tests/test_supervisor_lifecycle.py +0 -52
  181. package/workframe-api/tests/test_turn_credential_vault.py +0 -125
  182. package/workframe-api/tests/test_updates.py +0 -176
  183. package/workframe-api/tests/test_user_cohort.py +0 -113
  184. package/workframe-api/tests/test_vault_envelope.py +0 -110
  185. package/workframe-api/tests/test_workspace_members.py +0 -183
  186. package/workframe-api/tests/test_workspace_messaging_sync.py +0 -125
  187. package/workframe-api/tests/test_workspace_provider_list.py +0 -57
  188. package/workframe-supervisor/tests/test_exec_guard.py +0 -42
  189. package/workframe-supervisor/tests/test_server_import.py +0 -21
  190. package/workframe-ui/public/assets/architecture-7EHR7CIX-CtbQKTuT.js +0 -1
  191. package/workframe-ui/public/assets/channel-Dy4Z4-jn.js +0 -1
  192. package/workframe-ui/public/assets/chunk-QZHKN3VN-CtBEchFK.js +0 -1
  193. package/workframe-ui/public/assets/chunk-WU5MYG2G-B9pBtriN.js +0 -1
  194. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BMAEA8jI.js +0 -1
  195. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BMAEA8jI.js +0 -1
  196. package/workframe-ui/public/assets/eventmodeling-FCH6USID-D75cstNT.js +0 -1
  197. package/workframe-ui/public/assets/index-DpAGxump.css +0 -1
  198. 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()