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,701 +0,0 @@
1
- import tempfile
2
- import unittest
3
- from pathlib import Path
4
- from unittest.mock import patch
5
-
6
- import server
7
- from db_setup import ensure_workframe_schemas
8
-
9
-
10
- class RoomTenancyTests(unittest.TestCase):
11
- def setUp(self) -> None:
12
- self._tmp = tempfile.TemporaryDirectory()
13
- self.addCleanup(self._tmp.cleanup)
14
- self._old_data_dir = server.DATA_DIR
15
- self._old_auth_db_path = server.AUTH_DB_PATH
16
- server.DATA_DIR = Path(self._tmp.name)
17
- server.AUTH_DB_PATH = Path(self._tmp.name) / "auth.db"
18
- ensure_workframe_schemas()
19
- self.workspace_id = "workspace-a"
20
- self.user_a = "user-a"
21
- self.user_b = "user-b"
22
- self.agent_profile_id = "agent-a"
23
- self.agent_slug = "agent-a"
24
- self.room_a = "room-a"
25
- self.room_b = "room-b"
26
- conn = server._workframe_db()
27
- try:
28
- now = "1"
29
- for uid, name in ((self.user_a, "User A"), (self.user_b, "User B")):
30
- conn.execute(
31
- "INSERT INTO users (id, display_name, role, status, created_at, updated_at) VALUES (?,?,?,?,?,?)",
32
- (uid, name, "member", "active", now, now),
33
- )
34
- conn.execute(
35
- "INSERT INTO workspaces (id, slug, display_name, owner_id, status, created_at, updated_at) VALUES (?,?,?,?,?,?,?)",
36
- (self.workspace_id, "workspace-a", "Workspace A", self.user_a, "active", now, now),
37
- )
38
- conn.execute(
39
- """
40
- INSERT INTO agent_profiles (id, workspace_id, slug, display_name, status, created_at, updated_at)
41
- VALUES (?, ?, ?, ?, ?, ?, ?)
42
- """,
43
- (self.agent_profile_id, self.workspace_id, self.agent_slug, "Agent A", "available", now, now),
44
- )
45
- for room_id, user_id in ((self.room_a, self.user_a), (self.room_b, self.user_b)):
46
- conn.execute(
47
- """
48
- INSERT INTO rooms (
49
- id, workspace_id, agent_profile_id, name, slug, room_type, status, created_at, updated_at
50
- ) VALUES (?, ?, ?, ?, ?, 'direct', 'active', ?, ?)
51
- """,
52
- (room_id, self.workspace_id, self.agent_profile_id, f"DM {user_id}", f"dm-{user_id}", now, now),
53
- )
54
- conn.execute(
55
- """
56
- INSERT INTO room_memberships (id, room_id, user_id, role, status, joined_at, updated_at)
57
- VALUES (?, ?, ?, 'member', 'active', ?, ?)
58
- """,
59
- (f"rm-{room_id}", room_id, user_id, now, now),
60
- )
61
- conn.commit()
62
- finally:
63
- conn.close()
64
-
65
- def tearDown(self) -> None:
66
- server.DATA_DIR = self._old_data_dir
67
- server.AUTH_DB_PATH = self._old_auth_db_path
68
-
69
- def test_channel_room_clears_agent_on_migration(self) -> None:
70
- conn = server._workframe_db()
71
- try:
72
- conn.execute(
73
- """
74
- INSERT INTO rooms (
75
- id, workspace_id, agent_profile_id, name, slug, room_type, status, created_at, updated_at
76
- ) VALUES ('general', ?, ?, 'General', 'general', 'channel', 'active', '1', '1')
77
- """,
78
- (self.workspace_id, self.agent_profile_id),
79
- )
80
- conn.execute("DELETE FROM schema_migrations WHERE version = '6'")
81
- conn.commit()
82
- finally:
83
- conn.close()
84
- server._ensure_workframe_db_schema()
85
- conn = server._workframe_db()
86
- try:
87
- row = conn.execute("SELECT agent_profile_id FROM rooms WHERE id = 'general'").fetchone()
88
- finally:
89
- conn.close()
90
- self.assertIsNone(row["agent_profile_id"])
91
-
92
- @patch.object(server, "_runtime_profile_on_disk", return_value=True)
93
- @patch.object(server, "_overlay_turn_user_env")
94
- @patch.object(server, "_overlay_turn_provider_env")
95
- @patch.object(server, "ensure_runtime_profile")
96
- @patch.object(server, "resolve_validated_profile", side_effect=lambda slug: slug)
97
- @patch.object(server, "ensure_profile_api", return_value={"api_port": 1})
98
- @patch.object(server, "_create_profile_session_via_api")
99
- @patch.object(server, "_session_exists", return_value=False)
100
- def test_distinct_rooms_get_distinct_sessions(
101
- self,
102
- _session_exists,
103
- create_session,
104
- _ensure_api,
105
- _resolve,
106
- _ensure_runtime,
107
- _overlay_llm,
108
- _overlay_user,
109
- _on_disk,
110
- ) -> None:
111
- sessions: dict[str, str] = {}
112
-
113
- def _create(prof: str, sid: str, title: str):
114
- sessions[sid] = prof
115
- return 200, {}, title
116
-
117
- create_session.side_effect = _create
118
-
119
- first = server.profile_chat_session(
120
- self.agent_slug,
121
- {"room_id": self.room_a, "source_id": "ui", "client_id": "c1"},
122
- self.user_a,
123
- )
124
- second = server.profile_chat_session(
125
- self.agent_slug,
126
- {"room_id": self.room_b, "source_id": "ui", "client_id": "c1"},
127
- self.user_b,
128
- )
129
- self.assertNotEqual(first["session_id"], second["session_id"])
130
- self.assertEqual(first["profile"], "u-user-a-agent-a")
131
- self.assertEqual(second["profile"], "u-user-b-agent-a")
132
- self.assertEqual(sessions[first["session_id"]], "u-user-a-agent-a")
133
- self.assertEqual(sessions[second["session_id"]], "u-user-b-agent-a")
134
-
135
- conn = server._workframe_db()
136
- try:
137
- row_a = server._get_active_room_session(conn, self.room_a, self.agent_profile_id)
138
- row_b = server._get_active_room_session(conn, self.room_b, self.agent_profile_id)
139
- finally:
140
- conn.close()
141
- self.assertEqual(row_a["session_id"], first["session_id"])
142
- self.assertEqual(row_b["session_id"], second["session_id"])
143
-
144
- @patch.object(server, "_runtime_profile_on_disk", return_value=True)
145
- @patch.object(server, "_overlay_turn_user_env")
146
- @patch.object(server, "_overlay_turn_provider_env")
147
- @patch.object(server, "ensure_runtime_profile")
148
- @patch.object(server, "resolve_validated_profile", side_effect=lambda slug: slug)
149
- @patch.object(server, "ensure_profile_api", return_value={"api_port": 1})
150
- @patch.object(server, "_create_profile_session_via_api")
151
- def test_reuses_latest_valid_room_session(
152
- self,
153
- create_session,
154
- _ensure_api,
155
- _resolve,
156
- _ensure_runtime,
157
- _overlay_llm,
158
- _overlay_user,
159
- _on_disk,
160
- ) -> None:
161
- runtime_prof = "u-user-a-agent-a"
162
- create_session.return_value = (200, {}, "should-not-create")
163
- conn = server._workframe_db()
164
- try:
165
- now = "99"
166
- conn.execute(
167
- """
168
- INSERT INTO room_sessions (
169
- id, room_id, agent_profile_id, session_id, gateway_session_id,
170
- title, status, created_by, created_at, updated_at
171
- ) VALUES (?, ?, ?, ?, ?, ?, 'active', ?, ?, ?)
172
- """,
173
- (
174
- "rs-old",
175
- self.room_a,
176
- self.agent_profile_id,
177
- "wf_room_old_session",
178
- f"api:{runtime_prof}:wf_room_old_session",
179
- "Old",
180
- self.user_a,
181
- now,
182
- now,
183
- ),
184
- )
185
- conn.commit()
186
- finally:
187
- conn.close()
188
-
189
- with patch.object(server, "_session_exists", side_effect=lambda _p, sid: sid == "wf_room_old_session"):
190
- result = server.profile_chat_session(
191
- self.agent_slug,
192
- {"room_id": self.room_a, "source_id": "ui", "client_id": "c1"},
193
- self.user_a,
194
- )
195
-
196
- self.assertEqual(result["session_id"], "wf_room_old_session")
197
- self.assertEqual(result["profile"], runtime_prof)
198
- self.assertFalse(result.get("created"))
199
- create_session.assert_not_called()
200
- conn = server._workframe_db()
201
- try:
202
- row = server._get_active_room_session(conn, self.room_a, self.agent_profile_id)
203
- finally:
204
- conn.close()
205
- self.assertEqual(row["session_id"], "wf_room_old_session")
206
- self.assertEqual(row["status"], "active")
207
-
208
- def test_runtime_profile_slug_format(self) -> None:
209
- self.assertEqual(
210
- server._runtime_profile_slug("user-a", "agent-a"),
211
- "u-user-a-agent-a",
212
- )
213
-
214
- @patch.object(server, "_list_profiles", return_value=["workframe-agent", "architect"])
215
- def test_runtime_template_slug_resolves_template(self, _list_profiles) -> None:
216
- self.assertEqual(
217
- server._runtime_template_slug("u-cb6a2db4-ac86-4c49-8-workframe-agent"),
218
- "workframe-agent",
219
- )
220
-
221
- @patch.object(server, "load_agent_registry", return_value={"workframe-agent": {"display_name": "Workframe Agent"}})
222
- @patch.object(server, "_list_profiles", return_value=["workframe-agent"])
223
- def test_profile_display_name_runtime_uses_registry(self, _list_profiles, _registry) -> None:
224
- self.assertEqual(
225
- server._profile_display_name("u-cb6a2db4-ac86-4c49-8-workframe-agent"),
226
- "Workframe Agent",
227
- )
228
-
229
- def test_profile_display_name_runtime_without_registry_titlecases_template(self) -> None:
230
- with patch.object(server, "_runtime_template_slug", return_value="workframe-agent"):
231
- with patch.object(server, "_agent_db_display_name", return_value=""):
232
- with patch.object(server, "_agent_registry_row", return_value={}):
233
- with patch.object(server, "_is_native_profile", return_value=True):
234
- self.assertEqual(
235
- server._profile_display_name("u-user-workframe-agent"),
236
- server._native_display_name(),
237
- )
238
-
239
- @patch.object(server, "ensure_user_agent_cohort", return_value=[])
240
- @patch.object(server, "resolve_validated_profile", side_effect=lambda slug: slug)
241
- @patch.object(server, "_runtime_profile_on_disk", side_effect=[False, True])
242
- @patch.object(server, "ensure_runtime_profile")
243
- def test_resolve_chat_hermes_profile_provisions_runtime_lazily(
244
- self, ensure_runtime, _on_disk, _resolve, _cohort,
245
- ) -> None:
246
- prof = server._resolve_chat_hermes_profile(
247
- self.agent_slug,
248
- self.user_a,
249
- self.room_a,
250
- self.workspace_id,
251
- )
252
- self.assertEqual(prof, "u-user-a-agent-a")
253
- ensure_runtime.assert_called_once_with(
254
- "u-user-a-agent-a",
255
- self.agent_slug,
256
- self.user_a,
257
- self.workspace_id,
258
- )
259
-
260
- @patch.object(server, "ensure_user_agent_cohort", return_value=[])
261
- @patch.object(server, "resolve_validated_profile", side_effect=lambda slug: slug)
262
- @patch.object(server, "_runtime_profile_on_disk", return_value=False)
263
- @patch.object(server, "ensure_runtime_profile")
264
- def test_resolve_chat_hermes_profile_still_missing_after_provision(
265
- self, _ensure_runtime, _on_disk, _resolve, _cohort,
266
- ) -> None:
267
- with self.assertRaises(ValueError) as ctx:
268
- server._resolve_chat_hermes_profile(
269
- self.agent_slug,
270
- self.user_a,
271
- self.room_a,
272
- self.workspace_id,
273
- )
274
- self.assertEqual(str(ctx.exception), "runtime_profile_not_provisioned")
275
-
276
- @patch.object(server, "ensure_user_agent_cohort", return_value=[])
277
- @patch.object(server, "resolve_validated_profile", side_effect=lambda slug: slug)
278
- @patch.object(server, "_runtime_profile_on_disk", return_value=True)
279
- @patch.object(server, "ensure_runtime_profile")
280
- def test_resolve_chat_hermes_profile_skips_ensure_when_runtime_on_disk(
281
- self, _ensure_runtime, _on_disk, _resolve, _cohort,
282
- ) -> None:
283
- prof = server._resolve_chat_hermes_profile(
284
- self.agent_slug,
285
- self.user_a,
286
- self.room_a,
287
- self.workspace_id,
288
- )
289
- self.assertEqual(prof, "u-user-a-agent-a")
290
- _ensure_runtime.assert_not_called()
291
-
292
- @patch.object(server, "_overlay_turn_user_env")
293
- @patch.object(server, "_try_overlay_turn_provider_env", return_value=False)
294
- @patch.object(server, "_user_has_llm_provider", return_value=False)
295
- @patch.object(server, "ensure_runtime_profile")
296
- @patch.object(server, "resolve_validated_profile", side_effect=lambda slug: slug)
297
- def test_room_access_denied_for_non_member(self, _resolve, _ensure_runtime, _has_llm, _overlay_llm, _overlay_user) -> None:
298
- with patch.object(server, "ensure_profile_api", return_value={"api_port": 1}):
299
- with self.assertRaises(ValueError) as ctx:
300
- server.profile_chat_session(
301
- self.agent_slug,
302
- {"room_id": self.room_a, "source_id": "ui", "client_id": "c1"},
303
- self.user_b,
304
- )
305
- self.assertEqual(str(ctx.exception), "room_access_denied")
306
-
307
- @patch.object(server, "_runtime_profile_on_disk", return_value=True)
308
- @patch.object(server, "_overlay_turn_user_env")
309
- @patch.object(server, "_user_has_llm_provider", return_value=False)
310
- @patch.object(server, "ensure_runtime_profile")
311
- @patch.object(server, "resolve_validated_profile", side_effect=lambda slug: slug)
312
- @patch.object(server, "ensure_profile_api", return_value={"api_port": 1})
313
- @patch.object(server, "_create_profile_session_via_api", return_value=(200, {}, "session"))
314
- @patch.object(server, "_session_exists", return_value=False)
315
- def test_bootstrap_succeeds_without_llm_provider(
316
- self,
317
- _session_exists,
318
- create_session,
319
- _ensure_api,
320
- _resolve,
321
- _ensure_runtime,
322
- _has_llm,
323
- _overlay_user,
324
- _on_disk,
325
- ) -> None:
326
- result = server.profile_chat_session(
327
- self.agent_slug,
328
- {"room_id": self.room_a, "source_id": "ui", "client_id": "c1"},
329
- self.user_a,
330
- )
331
- self.assertTrue(result.get("session_id"))
332
- self.assertFalse(result.get("llm_ready"))
333
-
334
- @patch.object(server, "resolve_validated_profile", side_effect=lambda slug: slug)
335
- @patch.object(server, "_runtime_profile_on_disk", return_value=True)
336
- @patch.object(server, "chat_messages", return_value={"messages": [], "session": {}})
337
- @patch.object(server, "ensure_profile_api", return_value={"api_port": 1})
338
- @patch.object(server, "_create_profile_session_via_api", return_value=(200, {}, "session"))
339
- @patch.object(server, "_session_exists", return_value=False)
340
- def test_room_chat_bind_ignores_url_profile(
341
- self,
342
- _session_exists,
343
- _create_session,
344
- _ensure_api,
345
- _chat_messages,
346
- _on_disk,
347
- _resolve,
348
- ) -> None:
349
- result = server.room_chat_bind(
350
- self.room_a,
351
- {"source_id": "ui", "client_id": "c1"},
352
- self.user_a,
353
- )
354
- self.assertEqual(result["template_profile"], self.agent_slug)
355
- self.assertEqual(result["profile"], "u-user-a-agent-a")
356
-
357
- def test_parse_room_mentions_resolves_agent_slug(self) -> None:
358
- agents = [{"id": "a1", "slug": "architect", "display_name": "Architect"}]
359
- users = [{"id": "u1", "handle": "alan", "display_name": "Alan"}]
360
- parsed = server._parse_room_mentions("@architect please review", agents, users)
361
- self.assertEqual([a["slug"] for a in parsed["agents"]], ["architect"])
362
- self.assertEqual(parsed["users"], [])
363
-
364
- def test_parse_room_mentions_ignores_plain_text(self) -> None:
365
- agents = [{"id": "a1", "slug": "architect", "display_name": "Architect"}]
366
- parsed = server._parse_room_mentions("hello team", agents, [])
367
- self.assertEqual(parsed["agents"], [])
368
-
369
- def test_sanitize_space_room_clears_agent_binding(self) -> None:
370
- room = {
371
- "room_type": "channel",
372
- "agent_profile_id": "agent-a",
373
- "hermes_profile": "architect",
374
- }
375
- server._sanitize_space_room_agent_fields(room)
376
- self.assertIsNone(room["agent_profile_id"])
377
- self.assertIsNone(room["hermes_profile"])
378
-
379
- @patch.object(server, "_bootstrap_profile_providers", return_value=False)
380
- @patch.object(server, "_read_model_block", return_value={
381
- "default": "openrouter/owl-alpha",
382
- "provider": "openrouter",
383
- "base_url": "",
384
- "fallback_chain": [],
385
- })
386
- @patch.object(server, "_user_llm_providers_for_picker", return_value={"openrouter"})
387
- @patch.object(server, "profile_exists", return_value=True)
388
- def test_hermes_models_accepts_runtime_profile_slug(
389
- self,
390
- _exists,
391
- _picker,
392
- _block,
393
- _bootstrap,
394
- ) -> None:
395
- result = server.hermes_models("u-user-a-agent-a", self.user_a, self.workspace_id)
396
- self.assertTrue(result["has_llm_provider"])
397
- self.assertEqual(result["primary"], "openrouter/owl-alpha")
398
-
399
- def test_room_messages_query_returns_latest_window(self) -> None:
400
- conn = server._workframe_db()
401
- space_id = "space-general"
402
- try:
403
- conn.execute(
404
- """
405
- INSERT INTO rooms (
406
- id, workspace_id, name, slug, room_type, status, created_at, updated_at
407
- ) VALUES (?, ?, 'General', 'general', 'channel', 'active', '1', '1')
408
- """,
409
- (space_id, self.workspace_id),
410
- )
411
- conn.execute(
412
- """
413
- INSERT INTO room_memberships (id, room_id, user_id, role, status, joined_at, updated_at)
414
- VALUES ('rm-space', ?, ?, 'member', 'active', '1', '1')
415
- """,
416
- (space_id, self.user_a),
417
- )
418
- for i in range(55):
419
- ts = str(1000 + i)
420
- conn.execute(
421
- """
422
- INSERT INTO messages (
423
- id, room_id, sender_user_id, content, content_type, is_edited, created_at, updated_at
424
- ) VALUES (?, ?, ?, ?, 'text', 0, ?, ?)
425
- """,
426
- (f"m{i}", space_id, self.user_a, f"msg-{i}", ts, ts),
427
- )
428
- conn.commit()
429
- limit = 50
430
- rows = conn.execute(
431
- """
432
- SELECT id FROM (
433
- SELECT id, created_at FROM messages
434
- WHERE room_id = ? AND deleted_at IS NULL
435
- ORDER BY created_at DESC
436
- LIMIT ? OFFSET 0
437
- ) AS recent
438
- ORDER BY created_at ASC
439
- """,
440
- (space_id, limit),
441
- ).fetchall()
442
- self.assertEqual(len(rows), 50)
443
- self.assertEqual(rows[0]["id"], "m5")
444
- self.assertEqual(rows[-1]["id"], "m54")
445
- finally:
446
- conn.close()
447
-
448
- @patch.object(server, "_overlay_turn_user_env")
449
- @patch.object(server, "_try_overlay_turn_provider_env", return_value=False)
450
- @patch.object(server, "_user_has_llm_provider", return_value=True)
451
- @patch.object(server, "ensure_runtime_profile")
452
- @patch.object(server, "resolve_validated_profile", side_effect=lambda slug: slug)
453
- @patch.object(server, "ensure_profile_api", return_value={"api_port": 1})
454
- @patch.object(server, "_create_profile_session_via_api", return_value=(200, {}, "title"))
455
- @patch.object(server, "_session_exists", return_value=False)
456
- def test_space_profile_chat_session_allows_channel_room(
457
- self,
458
- _session_exists,
459
- create_session,
460
- _ensure_api,
461
- _resolve,
462
- _ensure_runtime,
463
- _has_llm,
464
- _overlay_llm,
465
- _overlay_user,
466
- ) -> None:
467
- conn = server._workframe_db()
468
- space_id = "space-general"
469
- try:
470
- conn.execute(
471
- """
472
- INSERT INTO rooms (
473
- id, workspace_id, name, slug, room_type, status, created_at, updated_at
474
- ) VALUES (?, ?, 'General', 'general', 'channel', 'active', '1', '1')
475
- """,
476
- (space_id, self.workspace_id),
477
- )
478
- conn.execute(
479
- """
480
- INSERT INTO room_memberships (id, room_id, user_id, role, status, joined_at, updated_at)
481
- VALUES ('rm-space', ?, ?, 'member', 'active', '1', '1')
482
- """,
483
- (space_id, self.user_a),
484
- )
485
- conn.commit()
486
- finally:
487
- conn.close()
488
- create_session.return_value = (200, {}, "space session")
489
- result = server.profile_chat_session(
490
- self.agent_slug,
491
- {"room_id": space_id, "source_id": "room", "client_id": space_id},
492
- self.user_a,
493
- )
494
- self.assertTrue(result.get("session_id"))
495
- self.assertEqual(result.get("profile"), self.agent_slug)
496
- _ensure_runtime.assert_not_called()
497
-
498
- @patch.object(server, "_runtime_profile_on_disk", return_value=True)
499
- @patch.object(server, "_runtime_template_slug", return_value="agent-a")
500
- @patch.object(server, "ensure_runtime_profile")
501
- @patch.object(server, "resolve_validated_profile", side_effect=lambda slug: slug)
502
- @patch.object(server, "ensure_profile_api", return_value={"api_port": 1})
503
- @patch.object(server, "_create_profile_session_via_api", return_value=(200, {}, "title"))
504
- @patch.object(server, "_session_exists", return_value=False)
505
- def test_space_profile_chat_session_accepts_runtime_slug(
506
- self,
507
- _session_exists,
508
- create_session,
509
- _ensure_api,
510
- _resolve,
511
- _ensure_runtime,
512
- _runtime_template,
513
- _runtime_on_disk,
514
- ) -> None:
515
- conn = server._workframe_db()
516
- space_id = "space-runtime"
517
- runtime = "u-user-a-agent-a"
518
- try:
519
- conn.execute(
520
- """
521
- INSERT INTO rooms (
522
- id, workspace_id, name, slug, room_type, status, created_at, updated_at
523
- ) VALUES (?, ?, 'General', 'general', 'channel', 'active', '1', '1')
524
- """,
525
- (space_id, self.workspace_id),
526
- )
527
- conn.execute(
528
- """
529
- INSERT INTO room_memberships (id, room_id, user_id, role, status, joined_at, updated_at)
530
- VALUES ('rm-space-rt', ?, ?, 'member', 'active', '1', '1')
531
- """,
532
- (space_id, self.user_a),
533
- )
534
- conn.commit()
535
- finally:
536
- conn.close()
537
- create_session.return_value = (200, {}, "space session")
538
- result = server.profile_chat_session(
539
- runtime,
540
- {"room_id": space_id, "source_id": "room", "client_id": space_id},
541
- self.user_a,
542
- )
543
- self.assertTrue(result.get("session_id"))
544
- self.assertEqual(result.get("profile"), runtime)
545
- self.assertEqual(result.get("template_profile"), self.agent_slug)
546
-
547
- def test_profile_turn_payload_includes_space_transcript(self) -> None:
548
- conn = server._workframe_db()
549
- space_id = "space-prompt"
550
- try:
551
- conn.execute(
552
- """
553
- INSERT INTO rooms (
554
- id, workspace_id, name, slug, room_type, status, created_at, updated_at
555
- ) VALUES (?, ?, 'General', 'general', 'channel', 'active', '1', '1')
556
- """,
557
- (space_id, self.workspace_id),
558
- )
559
- conn.execute(
560
- """
561
- INSERT INTO messages (
562
- id, room_id, sender_user_id, content, content_type, is_edited, created_at, updated_at
563
- ) VALUES ('m1', ?, ?, '@agent-a hello', 'text', 0, '1', '1')
564
- """,
565
- (space_id, self.user_a),
566
- )
567
- conn.commit()
568
- finally:
569
- conn.close()
570
- payload = server._profile_turn_payload(self.agent_slug, "@agent-a hello", space_id)
571
- self.assertIn("group chat", payload["message"])
572
- self.assertIn("@agent-a hello", payload["message"])
573
-
574
- @patch.object(server.threading, "Thread")
575
- def test_process_space_mentions_can_skip_server_invoke(self, thread_cls) -> None:
576
- conn = server._workframe_db()
577
- space_id = "space-mentions"
578
- try:
579
- conn.execute(
580
- """
581
- INSERT INTO rooms (
582
- id, workspace_id, name, slug, room_type, status, created_at, updated_at
583
- ) VALUES (?, ?, 'General', 'general', 'channel', 'active', '1', '1')
584
- """,
585
- (space_id, self.workspace_id),
586
- )
587
- conn.execute(
588
- """
589
- INSERT INTO room_memberships (id, room_id, agent_profile_id, role, status, joined_at, updated_at)
590
- VALUES ('rm-agent', ?, ?, 'agent', 'active', '1', '1')
591
- """,
592
- (space_id, self.agent_profile_id),
593
- )
594
- conn.commit()
595
- finally:
596
- conn.close()
597
- result = server._process_space_message_mentions(
598
- space_id,
599
- self.workspace_id,
600
- self.user_a,
601
- "@agent-a ping",
602
- "parent-1",
603
- invoke_agents=False,
604
- )
605
- self.assertEqual(result["agent_mentions"], [self.agent_slug])
606
- thread_cls.assert_not_called()
607
-
608
- def test_resolve_latest_room_session_prefers_active(self) -> None:
609
- conn = server._workframe_db()
610
- profile = "u-user-a-agent-a"
611
- sid_archived = "sess-archived"
612
- sid_active = "sess-active"
613
- try:
614
- conn.execute(
615
- """
616
- INSERT INTO room_sessions (
617
- id, room_id, agent_profile_id, session_id, gateway_session_id,
618
- title, status, created_by, created_at, updated_at
619
- ) VALUES (?, ?, ?, ?, ?, '', ?, ?, ?, ?)
620
- """,
621
- (
622
- "rs-archived",
623
- self.room_a,
624
- self.agent_profile_id,
625
- sid_archived,
626
- f"api:{profile}:{sid_archived}",
627
- "archived",
628
- self.user_a,
629
- "100",
630
- "300",
631
- ),
632
- )
633
- conn.execute(
634
- """
635
- INSERT INTO room_sessions (
636
- id, room_id, agent_profile_id, session_id, gateway_session_id,
637
- title, status, created_by, created_at, updated_at
638
- ) VALUES (?, ?, ?, ?, ?, '', ?, ?, ?, ?)
639
- """,
640
- (
641
- "rs-active",
642
- self.room_a,
643
- self.agent_profile_id,
644
- sid_active,
645
- f"api:{profile}:{sid_active}",
646
- "active",
647
- self.user_a,
648
- "200",
649
- "200",
650
- ),
651
- )
652
- conn.commit()
653
- finally:
654
- conn.close()
655
-
656
- with patch.object(server, "_session_exists", return_value=True):
657
- conn = server._workframe_db()
658
- try:
659
- row = server._resolve_latest_room_session(
660
- conn, profile, self.room_a, self.agent_profile_id,
661
- )
662
- self.assertIsNotNone(row)
663
- self.assertEqual(str(row["session_id"]), sid_active)
664
- finally:
665
- conn.close()
666
-
667
- def test_room_member_manage_requires_admin_or_workspace_owner(self) -> None:
668
- conn = server._workframe_db()
669
- try:
670
- self.assertTrue(server._user_can_access_room(conn, self.room_a, self.user_a))
671
- self.assertFalse(server._user_can_manage_room_members(conn, self.room_a, self.user_b))
672
- self.assertFalse(server._user_can_manage_room_members(conn, self.room_a, self.user_a))
673
- conn.execute(
674
- "UPDATE room_memberships SET role = 'admin' WHERE room_id = ? AND user_id = ?",
675
- (self.room_a, self.user_a),
676
- )
677
- conn.commit()
678
- self.assertTrue(server._user_can_manage_room_members(conn, self.room_a, self.user_a))
679
- finally:
680
- conn.close()
681
-
682
- def test_workspace_owner_can_manage_room_members_without_room_admin(self) -> None:
683
- conn = server._workframe_db()
684
- try:
685
- now = "1"
686
- conn.execute(
687
- """
688
- INSERT INTO workspace_memberships (
689
- id, workspace_id, user_id, role, status, created_at, updated_at
690
- ) VALUES (?, ?, ?, 'owner', 'active', ?, ?)
691
- """,
692
- ("wm-owner", self.workspace_id, self.user_a, now, now),
693
- )
694
- conn.commit()
695
- self.assertTrue(server._user_can_manage_room_members(conn, self.room_a, self.user_a))
696
- finally:
697
- conn.close()
698
-
699
-
700
- if __name__ == "__main__":
701
- unittest.main()