@agent-native/dispatch 0.6.1 → 0.8.0

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 (341) hide show
  1. package/README.md +38 -1
  2. package/dist/actions/apply-dream-proposal.d.ts +3 -0
  3. package/dist/actions/apply-dream-proposal.d.ts.map +1 -0
  4. package/dist/actions/apply-dream-proposal.js +11 -0
  5. package/dist/actions/apply-dream-proposal.js.map +1 -0
  6. package/dist/actions/create-dream-report.d.ts +3 -0
  7. package/dist/actions/create-dream-report.d.ts.map +1 -0
  8. package/dist/actions/create-dream-report.js +67 -0
  9. package/dist/actions/create-dream-report.js.map +1 -0
  10. package/dist/actions/create-pylon-ticket.d.ts +3 -0
  11. package/dist/actions/create-pylon-ticket.d.ts.map +1 -0
  12. package/dist/actions/create-pylon-ticket.js +94 -0
  13. package/dist/actions/create-pylon-ticket.js.map +1 -0
  14. package/dist/actions/create-vault-grant.js +1 -1
  15. package/dist/actions/create-vault-grant.js.map +1 -1
  16. package/dist/actions/create-vault-secret.d.ts.map +1 -1
  17. package/dist/actions/create-vault-secret.js +4 -3
  18. package/dist/actions/create-vault-secret.js.map +1 -1
  19. package/dist/actions/create-workspace-resource.js +3 -3
  20. package/dist/actions/create-workspace-resource.js.map +1 -1
  21. package/dist/actions/delete-workspace-resource.js +1 -1
  22. package/dist/actions/delete-workspace-resource.js.map +1 -1
  23. package/dist/actions/ensure-dream-job.d.ts +3 -0
  24. package/dist/actions/ensure-dream-job.d.ts.map +1 -0
  25. package/dist/actions/ensure-dream-job.js +73 -0
  26. package/dist/actions/ensure-dream-job.js.map +1 -0
  27. package/dist/actions/get-dream-settings.d.ts +3 -0
  28. package/dist/actions/get-dream-settings.d.ts.map +1 -0
  29. package/dist/actions/get-dream-settings.js +11 -0
  30. package/dist/actions/get-dream-settings.js.map +1 -0
  31. package/dist/actions/get-dream.d.ts +3 -0
  32. package/dist/actions/get-dream.d.ts.map +1 -0
  33. package/dist/actions/get-dream.js +13 -0
  34. package/dist/actions/get-dream.js.map +1 -0
  35. package/dist/actions/get-vault-access-settings.d.ts +3 -0
  36. package/dist/actions/get-vault-access-settings.d.ts.map +1 -0
  37. package/dist/actions/get-vault-access-settings.js +10 -0
  38. package/dist/actions/get-vault-access-settings.js.map +1 -0
  39. package/dist/actions/get-workspace-resource-effective-context.d.ts +3 -0
  40. package/dist/actions/get-workspace-resource-effective-context.d.ts.map +1 -0
  41. package/dist/actions/get-workspace-resource-effective-context.js +27 -0
  42. package/dist/actions/get-workspace-resource-effective-context.js.map +1 -0
  43. package/dist/actions/grant-vault-secrets-to-app.js +1 -1
  44. package/dist/actions/grant-vault-secrets-to-app.js.map +1 -1
  45. package/dist/actions/index.d.ts.map +1 -1
  46. package/dist/actions/index.js +38 -4
  47. package/dist/actions/index.js.map +1 -1
  48. package/dist/actions/list-dream-candidates.d.ts +3 -0
  49. package/dist/actions/list-dream-candidates.d.ts.map +1 -0
  50. package/dist/actions/list-dream-candidates.js +68 -0
  51. package/dist/actions/list-dream-candidates.js.map +1 -0
  52. package/dist/actions/list-dreams.d.ts +3 -0
  53. package/dist/actions/list-dreams.d.ts.map +1 -0
  54. package/dist/actions/list-dreams.js +17 -0
  55. package/dist/actions/list-dreams.js.map +1 -0
  56. package/dist/actions/list-integrations-catalog.js +1 -1
  57. package/dist/actions/list-integrations-catalog.js.map +1 -1
  58. package/dist/actions/list-vault-grants.js +1 -1
  59. package/dist/actions/list-vault-grants.js.map +1 -1
  60. package/dist/actions/list-workspace-apps.d.ts.map +1 -1
  61. package/dist/actions/list-workspace-apps.js +5 -1
  62. package/dist/actions/list-workspace-apps.js.map +1 -1
  63. package/dist/actions/list-workspace-resources-for-app.d.ts +3 -0
  64. package/dist/actions/list-workspace-resources-for-app.d.ts.map +1 -0
  65. package/dist/actions/list-workspace-resources-for-app.js +12 -0
  66. package/dist/actions/list-workspace-resources-for-app.js.map +1 -0
  67. package/dist/actions/list-workspace-resources.js +1 -1
  68. package/dist/actions/list-workspace-resources.js.map +1 -1
  69. package/dist/actions/navigate.d.ts +1 -0
  70. package/dist/actions/navigate.d.ts.map +1 -1
  71. package/dist/actions/navigate.js +2 -1
  72. package/dist/actions/navigate.js.map +1 -1
  73. package/dist/actions/preview-dream-proposal.d.ts +3 -0
  74. package/dist/actions/preview-dream-proposal.d.ts.map +1 -0
  75. package/dist/actions/preview-dream-proposal.js +13 -0
  76. package/dist/actions/preview-dream-proposal.js.map +1 -0
  77. package/dist/actions/preview-workspace-resource-change.d.ts +3 -0
  78. package/dist/actions/preview-workspace-resource-change.d.ts.map +1 -0
  79. package/dist/actions/preview-workspace-resource-change.js +24 -0
  80. package/dist/actions/preview-workspace-resource-change.js.map +1 -0
  81. package/dist/actions/reject-dream-proposal.d.ts +3 -0
  82. package/dist/actions/reject-dream-proposal.d.ts.map +1 -0
  83. package/dist/actions/reject-dream-proposal.js +12 -0
  84. package/dist/actions/reject-dream-proposal.js.map +1 -0
  85. package/dist/actions/restore-starter-workspace-resources.d.ts +3 -0
  86. package/dist/actions/restore-starter-workspace-resources.d.ts.map +1 -0
  87. package/dist/actions/restore-starter-workspace-resources.js +14 -0
  88. package/dist/actions/restore-starter-workspace-resources.js.map +1 -0
  89. package/dist/actions/send-code-agent-remote-command.d.ts +3 -0
  90. package/dist/actions/send-code-agent-remote-command.d.ts.map +1 -0
  91. package/dist/actions/send-code-agent-remote-command.js +53 -0
  92. package/dist/actions/send-code-agent-remote-command.js.map +1 -0
  93. package/dist/actions/set-dream-settings.d.ts +3 -0
  94. package/dist/actions/set-dream-settings.d.ts.map +1 -0
  95. package/dist/actions/set-dream-settings.js +41 -0
  96. package/dist/actions/set-dream-settings.js.map +1 -0
  97. package/dist/actions/set-vault-access-settings.d.ts +3 -0
  98. package/dist/actions/set-vault-access-settings.d.ts.map +1 -0
  99. package/dist/actions/set-vault-access-settings.js +13 -0
  100. package/dist/actions/set-vault-access-settings.js.map +1 -0
  101. package/dist/actions/start-workspace-app-creation.d.ts.map +1 -1
  102. package/dist/actions/start-workspace-app-creation.js +6 -0
  103. package/dist/actions/start-workspace-app-creation.js.map +1 -1
  104. package/dist/actions/sync-vault-to-app.js +1 -1
  105. package/dist/actions/sync-vault-to-app.js.map +1 -1
  106. package/dist/actions/update-workspace-app-metadata.d.ts +3 -0
  107. package/dist/actions/update-workspace-app-metadata.d.ts.map +1 -0
  108. package/dist/actions/update-workspace-app-metadata.js +30 -0
  109. package/dist/actions/update-workspace-app-metadata.js.map +1 -0
  110. package/dist/actions/update-workspace-resource.js +1 -1
  111. package/dist/actions/update-workspace-resource.js.map +1 -1
  112. package/dist/actions/view-screen.d.ts.map +1 -1
  113. package/dist/actions/view-screen.js +77 -4
  114. package/dist/actions/view-screen.js.map +1 -1
  115. package/dist/components/app-keys-popover.js +16 -5
  116. package/dist/components/app-keys-popover.js.map +1 -1
  117. package/dist/components/approval-value-block.d.ts +7 -0
  118. package/dist/components/approval-value-block.d.ts.map +1 -0
  119. package/dist/components/approval-value-block.js +22 -0
  120. package/dist/components/approval-value-block.js.map +1 -0
  121. package/dist/components/create-app-popover.d.ts.map +1 -1
  122. package/dist/components/create-app-popover.js +41 -16
  123. package/dist/components/create-app-popover.js.map +1 -1
  124. package/dist/components/dispatch-shell.d.ts +4 -4
  125. package/dist/components/dispatch-shell.d.ts.map +1 -1
  126. package/dist/components/dispatch-shell.js +6 -6
  127. package/dist/components/dispatch-shell.js.map +1 -1
  128. package/dist/components/layout/Layout.d.ts.map +1 -1
  129. package/dist/components/layout/Layout.js +18 -4
  130. package/dist/components/layout/Layout.js.map +1 -1
  131. package/dist/components/messaging-setup-panel.d.ts.map +1 -1
  132. package/dist/components/messaging-setup-panel.js +2 -2
  133. package/dist/components/messaging-setup-panel.js.map +1 -1
  134. package/dist/components/ui/chart.d.ts +1 -1
  135. package/dist/components/workspace-app-card.d.ts.map +1 -1
  136. package/dist/components/workspace-app-card.js +63 -3
  137. package/dist/components/workspace-app-card.js.map +1 -1
  138. package/dist/components/workspace-resource-effective-stack.d.ts +11 -0
  139. package/dist/components/workspace-resource-effective-stack.d.ts.map +1 -0
  140. package/dist/components/workspace-resource-effective-stack.js +59 -0
  141. package/dist/components/workspace-resource-effective-stack.js.map +1 -0
  142. package/dist/components/workspace-resource-impact-preview.d.ts +9 -0
  143. package/dist/components/workspace-resource-impact-preview.d.ts.map +1 -0
  144. package/dist/components/workspace-resource-impact-preview.js +39 -0
  145. package/dist/components/workspace-resource-impact-preview.js.map +1 -0
  146. package/dist/db/migrations.d.ts.map +1 -1
  147. package/dist/db/migrations.js +59 -0
  148. package/dist/db/migrations.js.map +1 -1
  149. package/dist/db/schema.d.ts +714 -0
  150. package/dist/db/schema.d.ts.map +1 -1
  151. package/dist/db/schema.js +44 -2
  152. package/dist/db/schema.js.map +1 -1
  153. package/dist/hooks/use-navigation-state.d.ts +3 -0
  154. package/dist/hooks/use-navigation-state.d.ts.map +1 -1
  155. package/dist/hooks/use-navigation-state.js +35 -8
  156. package/dist/hooks/use-navigation-state.js.map +1 -1
  157. package/dist/lib/catch-all-target.d.ts +2 -0
  158. package/dist/lib/catch-all-target.d.ts.map +1 -0
  159. package/dist/lib/catch-all-target.js +95 -0
  160. package/dist/lib/catch-all-target.js.map +1 -0
  161. package/dist/lib/utils.d.ts +2 -1
  162. package/dist/lib/utils.d.ts.map +1 -1
  163. package/dist/lib/utils.js +5 -1
  164. package/dist/lib/utils.js.map +1 -1
  165. package/dist/lib/workspace-apps.d.ts +9 -0
  166. package/dist/lib/workspace-apps.d.ts.map +1 -1
  167. package/dist/lib/workspace-apps.js.map +1 -1
  168. package/dist/routes/index.d.ts.map +1 -1
  169. package/dist/routes/index.js +1 -0
  170. package/dist/routes/index.js.map +1 -1
  171. package/dist/routes/pages/$appId.d.ts +2 -2
  172. package/dist/routes/pages/$appId.d.ts.map +1 -1
  173. package/dist/routes/pages/$appId.js +17 -8
  174. package/dist/routes/pages/$appId.js.map +1 -1
  175. package/dist/routes/pages/approval.d.ts.map +1 -1
  176. package/dist/routes/pages/approval.js +4 -1
  177. package/dist/routes/pages/approval.js.map +1 -1
  178. package/dist/routes/pages/approvals.js +1 -1
  179. package/dist/routes/pages/approvals.js.map +1 -1
  180. package/dist/routes/pages/dream-settings.d.ts +34 -0
  181. package/dist/routes/pages/dream-settings.d.ts.map +1 -0
  182. package/dist/routes/pages/dream-settings.js +68 -0
  183. package/dist/routes/pages/dream-settings.js.map +1 -0
  184. package/dist/routes/pages/dreams.d.ts +5 -0
  185. package/dist/routes/pages/dreams.d.ts.map +1 -0
  186. package/dist/routes/pages/dreams.js +435 -0
  187. package/dist/routes/pages/dreams.js.map +1 -0
  188. package/dist/routes/pages/integrations.d.ts.map +1 -1
  189. package/dist/routes/pages/integrations.js +20 -15
  190. package/dist/routes/pages/integrations.js.map +1 -1
  191. package/dist/routes/pages/new-app.js +1 -1
  192. package/dist/routes/pages/new-app.js.map +1 -1
  193. package/dist/routes/pages/overview.d.ts.map +1 -1
  194. package/dist/routes/pages/overview.js +5 -1
  195. package/dist/routes/pages/overview.js.map +1 -1
  196. package/dist/routes/pages/vault.d.ts.map +1 -1
  197. package/dist/routes/pages/vault.js +23 -5
  198. package/dist/routes/pages/vault.js.map +1 -1
  199. package/dist/routes/pages/workspace.d.ts.map +1 -1
  200. package/dist/routes/pages/workspace.js +187 -35
  201. package/dist/routes/pages/workspace.js.map +1 -1
  202. package/dist/server/lib/app-creation-store.d.ts +13 -0
  203. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  204. package/dist/server/lib/app-creation-store.js +298 -11
  205. package/dist/server/lib/app-creation-store.js.map +1 -1
  206. package/dist/server/lib/dispatch-integrations.d.ts +1 -1
  207. package/dist/server/lib/dispatch-integrations.d.ts.map +1 -1
  208. package/dist/server/lib/dispatch-integrations.js +9 -4
  209. package/dist/server/lib/dispatch-integrations.js.map +1 -1
  210. package/dist/server/lib/dispatch-remote-commands.d.ts +83 -0
  211. package/dist/server/lib/dispatch-remote-commands.d.ts.map +1 -0
  212. package/dist/server/lib/dispatch-remote-commands.js +256 -0
  213. package/dist/server/lib/dispatch-remote-commands.js.map +1 -0
  214. package/dist/server/lib/dispatch-store.d.ts +26 -0
  215. package/dist/server/lib/dispatch-store.d.ts.map +1 -1
  216. package/dist/server/lib/dispatch-store.js +17 -1
  217. package/dist/server/lib/dispatch-store.js.map +1 -1
  218. package/dist/server/lib/dreams-store.d.ts +398 -0
  219. package/dist/server/lib/dreams-store.d.ts.map +1 -0
  220. package/dist/server/lib/dreams-store.js +2330 -0
  221. package/dist/server/lib/dreams-store.js.map +1 -0
  222. package/dist/server/lib/env-config.d.ts.map +1 -1
  223. package/dist/server/lib/env-config.js +5 -0
  224. package/dist/server/lib/env-config.js.map +1 -1
  225. package/dist/server/lib/onboarding-steps.d.ts +12 -0
  226. package/dist/server/lib/onboarding-steps.d.ts.map +1 -0
  227. package/dist/server/lib/onboarding-steps.js +47 -0
  228. package/dist/server/lib/onboarding-steps.js.map +1 -0
  229. package/dist/server/lib/thread-debug-store.d.ts +2 -2
  230. package/dist/server/lib/vault-store.d.ts +55 -0
  231. package/dist/server/lib/vault-store.d.ts.map +1 -1
  232. package/dist/server/lib/vault-store.js +210 -41
  233. package/dist/server/lib/vault-store.js.map +1 -1
  234. package/dist/server/lib/workspace-resources-store.d.ts +181 -17
  235. package/dist/server/lib/workspace-resources-store.d.ts.map +1 -1
  236. package/dist/server/lib/workspace-resources-store.js +737 -108
  237. package/dist/server/lib/workspace-resources-store.js.map +1 -1
  238. package/dist/server/plugins/agent-chat.d.ts.map +1 -1
  239. package/dist/server/plugins/agent-chat.js +2 -1
  240. package/dist/server/plugins/agent-chat.js.map +1 -1
  241. package/dist/server/plugins/core-routes.d.ts.map +1 -1
  242. package/dist/server/plugins/core-routes.js +4 -0
  243. package/dist/server/plugins/core-routes.js.map +1 -1
  244. package/dist/server/plugins/integrations.js +2 -2
  245. package/dist/server/plugins/integrations.js.map +1 -1
  246. package/package.json +15 -11
  247. package/src/actions/apply-dream-proposal.ts +12 -0
  248. package/src/actions/create-dream-report.ts +76 -0
  249. package/src/actions/create-pylon-ticket.ts +109 -0
  250. package/src/actions/create-vault-grant.ts +1 -1
  251. package/src/actions/create-vault-secret.ts +4 -3
  252. package/src/actions/create-workspace-resource.ts +3 -3
  253. package/src/actions/delete-workspace-resource.ts +1 -1
  254. package/src/actions/ensure-dream-job.ts +76 -0
  255. package/src/actions/get-dream-settings.ts +12 -0
  256. package/src/actions/get-dream.ts +14 -0
  257. package/src/actions/get-vault-access-settings.ts +11 -0
  258. package/src/actions/get-workspace-resource-effective-context.ts +34 -0
  259. package/src/actions/grant-vault-secrets-to-app.ts +1 -1
  260. package/src/actions/index.spec.ts +26 -0
  261. package/src/actions/index.ts +39 -4
  262. package/src/actions/list-dream-candidates.ts +77 -0
  263. package/src/actions/list-dreams.ts +17 -0
  264. package/src/actions/list-integrations-catalog.ts +1 -1
  265. package/src/actions/list-vault-grants.ts +1 -1
  266. package/src/actions/list-workspace-apps.ts +5 -1
  267. package/src/actions/list-workspace-resources-for-app.ts +13 -0
  268. package/src/actions/list-workspace-resources.ts +1 -1
  269. package/src/actions/navigate.ts +2 -1
  270. package/src/actions/preview-dream-proposal.ts +14 -0
  271. package/src/actions/preview-workspace-resource-change.ts +25 -0
  272. package/src/actions/reject-dream-proposal.ts +12 -0
  273. package/src/actions/restore-starter-workspace-resources.ts +17 -0
  274. package/src/actions/send-code-agent-remote-command.ts +59 -0
  275. package/src/actions/set-dream-settings.spec.ts +81 -0
  276. package/src/actions/set-dream-settings.ts +44 -0
  277. package/src/actions/set-vault-access-settings.ts +16 -0
  278. package/src/actions/start-workspace-app-creation.ts +8 -0
  279. package/src/actions/sync-vault-to-app.ts +1 -1
  280. package/src/actions/update-workspace-app-metadata.ts +32 -0
  281. package/src/actions/update-workspace-resource.ts +1 -1
  282. package/src/actions/view-screen.ts +94 -3
  283. package/src/components/app-keys-popover.tsx +23 -7
  284. package/src/components/approval-value-block.spec.tsx +59 -0
  285. package/src/components/approval-value-block.tsx +33 -0
  286. package/src/components/create-app-popover.tsx +50 -16
  287. package/src/components/dispatch-shell.tsx +16 -15
  288. package/src/components/layout/Layout.tsx +19 -5
  289. package/src/components/messaging-setup-panel.tsx +54 -39
  290. package/src/components/workspace-app-card.tsx +268 -1
  291. package/src/components/workspace-resource-effective-stack.spec.tsx +125 -0
  292. package/src/components/workspace-resource-effective-stack.tsx +141 -0
  293. package/src/components/workspace-resource-impact-preview.spec.tsx +147 -0
  294. package/src/components/workspace-resource-impact-preview.tsx +116 -0
  295. package/src/db/migrations.ts +59 -0
  296. package/src/db/schema.ts +46 -2
  297. package/src/hooks/use-navigation-state.ts +34 -9
  298. package/src/lib/catch-all-target.spec.ts +218 -0
  299. package/src/lib/catch-all-target.ts +99 -0
  300. package/src/lib/utils.ts +6 -1
  301. package/src/lib/workspace-apps.ts +9 -0
  302. package/src/routes/index.ts +1 -0
  303. package/src/routes/pages/$appId.tsx +21 -8
  304. package/src/routes/pages/approval.tsx +14 -1
  305. package/src/routes/pages/approvals.tsx +1 -1
  306. package/src/routes/pages/dream-settings.spec.ts +130 -0
  307. package/src/routes/pages/dream-settings.ts +103 -0
  308. package/src/routes/pages/dreams.tsx +1828 -0
  309. package/src/routes/pages/integrations.tsx +57 -18
  310. package/src/routes/pages/new-app.tsx +1 -1
  311. package/src/routes/pages/overview.tsx +11 -3
  312. package/src/routes/pages/vault.tsx +76 -9
  313. package/src/routes/pages/workspace.tsx +577 -97
  314. package/src/server/lib/app-creation-store.spec.ts +61 -2
  315. package/src/server/lib/app-creation-store.ts +389 -13
  316. package/src/server/lib/dispatch-integrations.ts +10 -3
  317. package/src/server/lib/dispatch-remote-commands.spec.ts +167 -0
  318. package/src/server/lib/dispatch-remote-commands.ts +375 -0
  319. package/src/server/lib/dispatch-store.ts +37 -1
  320. package/src/server/lib/dreams-store.spec.ts +1492 -0
  321. package/src/server/lib/dreams-store.ts +3168 -0
  322. package/src/server/lib/env-config.ts +5 -0
  323. package/src/server/lib/onboarding-steps.ts +49 -0
  324. package/src/server/lib/vault-store.spec.ts +69 -0
  325. package/src/server/lib/vault-store.ts +266 -49
  326. package/src/server/lib/workspace-resource-approval-lifecycle.spec.ts +236 -0
  327. package/src/server/lib/workspace-resources-store.spec.ts +1106 -0
  328. package/src/server/lib/workspace-resources-store.ts +1001 -134
  329. package/src/server/plugins/agent-chat.ts +2 -1
  330. package/src/server/plugins/core-routes.ts +5 -0
  331. package/src/server/plugins/integrations.ts +2 -2
  332. package/dist/actions/sync-workspace-resources-to-all.d.ts +0 -3
  333. package/dist/actions/sync-workspace-resources-to-all.d.ts.map +0 -1
  334. package/dist/actions/sync-workspace-resources-to-all.js +0 -9
  335. package/dist/actions/sync-workspace-resources-to-all.js.map +0 -1
  336. package/dist/actions/sync-workspace-resources-to-app.d.ts +0 -3
  337. package/dist/actions/sync-workspace-resources-to-app.d.ts.map +0 -1
  338. package/dist/actions/sync-workspace-resources-to-app.js +0 -11
  339. package/dist/actions/sync-workspace-resources-to-app.js.map +0 -1
  340. package/src/actions/sync-workspace-resources-to-all.ts +0 -10
  341. package/src/actions/sync-workspace-resources-to-app.ts +0 -12
@@ -0,0 +1,1106 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const mocks = vi.hoisted(() => ({
4
+ getDb: vi.fn(),
5
+ getDbExec: vi.fn(),
6
+ isPostgres: vi.fn(() => false),
7
+ currentOwnerEmail: vi.fn(() => "owner@example.test"),
8
+ currentOrgId: vi.fn(() => "org_123"),
9
+ getApprovalPolicy: vi.fn(async () => ({
10
+ enabled: false,
11
+ approverEmails: [],
12
+ })),
13
+ createApprovalRequest: vi.fn(async (input: any) => ({
14
+ id: "approval_1",
15
+ status: "pending",
16
+ ...input,
17
+ })),
18
+ recordAudit: vi.fn(async () => undefined),
19
+ resourcePut: vi.fn(async () => undefined),
20
+ resourceGetByPath: vi.fn(async () => null),
21
+ resourceListAllOwners: vi.fn(async () => []),
22
+ resourceEffectiveContext: vi.fn(async (_userEmail: string, path: string) => ({
23
+ path,
24
+ effectiveScope: "workspace",
25
+ effectiveResource: {
26
+ id: "resource_meta_1",
27
+ path,
28
+ owner: "__workspace__",
29
+ mimeType: "text/markdown",
30
+ size: 10,
31
+ createdAt: 1,
32
+ updatedAt: 2,
33
+ createdBy: "system",
34
+ visibility: "workspace",
35
+ threadId: null,
36
+ runId: null,
37
+ expiresAt: null,
38
+ metadata: null,
39
+ },
40
+ layers: [
41
+ {
42
+ scope: "workspace",
43
+ label: "Workspace default",
44
+ owner: "__workspace__",
45
+ resource: {
46
+ id: "resource_meta_1",
47
+ path,
48
+ owner: "__workspace__",
49
+ mimeType: "text/markdown",
50
+ size: 10,
51
+ createdAt: 1,
52
+ updatedAt: 2,
53
+ createdBy: "system",
54
+ visibility: "workspace",
55
+ threadId: null,
56
+ runId: null,
57
+ expiresAt: null,
58
+ metadata: null,
59
+ },
60
+ exists: true,
61
+ effective: true,
62
+ overridden: false,
63
+ canWrite: false,
64
+ },
65
+ {
66
+ scope: "shared",
67
+ label: "Organization/app override",
68
+ owner: "__shared__",
69
+ resource: null,
70
+ exists: false,
71
+ effective: false,
72
+ overridden: false,
73
+ canWrite: true,
74
+ },
75
+ {
76
+ scope: "personal",
77
+ label: "Personal override",
78
+ owner: "owner@example.test",
79
+ resource: null,
80
+ exists: false,
81
+ effective: false,
82
+ overridden: false,
83
+ canWrite: true,
84
+ },
85
+ ],
86
+ })),
87
+ resourceDeleteByPath: vi.fn(async () => undefined),
88
+ getOrgSetting: vi.fn(async () => null),
89
+ getUserSetting: vi.fn(async () => null),
90
+ putOrgSetting: vi.fn(async () => undefined),
91
+ putUserSetting: vi.fn(async () => undefined),
92
+ }));
93
+
94
+ vi.mock("@agent-native/core/db", () => ({
95
+ getDbExec: () => mocks.getDbExec(),
96
+ isPostgres: () => mocks.isPostgres(),
97
+ }));
98
+
99
+ vi.mock("../../db/index.js", async () => {
100
+ const schema = await import("../../db/schema.js");
101
+ return {
102
+ ...schema,
103
+ schema,
104
+ getDb: () => mocks.getDb(),
105
+ };
106
+ });
107
+
108
+ vi.mock("./dispatch-store.js", () => ({
109
+ createApprovalRequest: (...args: any[]) =>
110
+ mocks.createApprovalRequest(...args),
111
+ currentOwnerEmail: () => mocks.currentOwnerEmail(),
112
+ currentOrgId: () => mocks.currentOrgId(),
113
+ getApprovalPolicy: () => mocks.getApprovalPolicy(),
114
+ recordAudit: (...args: any[]) => mocks.recordAudit(...args),
115
+ }));
116
+
117
+ vi.mock("@agent-native/core/resources/store", () => ({
118
+ SHARED_OWNER: "__shared__",
119
+ WORKSPACE_OWNER: "__workspace__",
120
+ resourcePut: (...args: any[]) => mocks.resourcePut(...args),
121
+ resourceGetByPath: (...args: any[]) => mocks.resourceGetByPath(...args),
122
+ resourceListAllOwners: (...args: any[]) =>
123
+ mocks.resourceListAllOwners(...args),
124
+ resourceEffectiveContext: (...args: any[]) =>
125
+ mocks.resourceEffectiveContext(...args),
126
+ resourceDeleteByPath: (...args: any[]) => mocks.resourceDeleteByPath(...args),
127
+ }));
128
+
129
+ vi.mock("@agent-native/core/settings", () => ({
130
+ getOrgSetting: (...args: any[]) => mocks.getOrgSetting(...args),
131
+ getUserSetting: (...args: any[]) => mocks.getUserSetting(...args),
132
+ putOrgSetting: (...args: any[]) => mocks.putOrgSetting(...args),
133
+ putUserSetting: (...args: any[]) => mocks.putUserSetting(...args),
134
+ }));
135
+
136
+ vi.mock("@agent-native/core/server/agent-discovery", () => ({
137
+ discoverAgents: vi.fn(async () => []),
138
+ }));
139
+
140
+ import {
141
+ ensureStarterWorkspaceResources,
142
+ createWorkspaceResource,
143
+ deleteWorkspaceResource,
144
+ getWorkspaceResourceEffectiveContext,
145
+ listWorkspaceResourcesForApp,
146
+ previewWorkspaceResourceChange,
147
+ restoreStarterWorkspaceResources,
148
+ STARTER_GLOBAL_WORKSPACE_RESOURCES,
149
+ updateWorkspaceResource,
150
+ } from "./workspace-resources-store.js";
151
+
152
+ interface ResourceRow {
153
+ id: string;
154
+ ownerEmail: string;
155
+ orgId: string | null;
156
+ kind: string;
157
+ name: string;
158
+ description: string | null;
159
+ path: string;
160
+ content: string;
161
+ scope: string;
162
+ createdBy: string;
163
+ createdAt: number;
164
+ updatedAt: number;
165
+ }
166
+
167
+ interface GrantRow {
168
+ id: string;
169
+ ownerEmail: string;
170
+ orgId: string | null;
171
+ resourceId: string;
172
+ appId: string;
173
+ status: string;
174
+ syncedAt: number | null;
175
+ createdAt: number;
176
+ updatedAt: number;
177
+ }
178
+
179
+ function createFakeDb(state: {
180
+ resources: ResourceRow[];
181
+ grants?: GrantRow[];
182
+ }) {
183
+ const latestResource = () => state.resources.at(-1);
184
+
185
+ return {
186
+ insert: vi.fn(() => ({
187
+ values: vi.fn(async (values: ResourceRow | GrantRow) => {
188
+ if ("kind" in values) state.resources.push(values);
189
+ }),
190
+ })),
191
+ select: vi.fn(() => ({
192
+ from: vi.fn((table: any) => {
193
+ const tableName = table?.[Symbol.for("drizzle:Name")] ?? "";
194
+ const rows =
195
+ tableName === "workspace_resource_grants"
196
+ ? (state.grants ?? [])
197
+ : state.resources;
198
+ return {
199
+ where: vi.fn(() => ({
200
+ limit: vi.fn(async () => {
201
+ const resource = latestResource();
202
+ return resource ? [resource] : [];
203
+ }),
204
+ orderBy: vi.fn(async () => rows),
205
+ })),
206
+ orderBy: vi.fn(async () => rows),
207
+ };
208
+ }),
209
+ })),
210
+ update: vi.fn(() => ({
211
+ set: vi.fn((updates: Partial<ResourceRow | GrantRow>) => ({
212
+ where: vi.fn(async () => {
213
+ const resource = latestResource();
214
+ if (resource) Object.assign(resource, updates);
215
+ }),
216
+ })),
217
+ })),
218
+ delete: vi.fn(() => ({
219
+ where: vi.fn(async () => {
220
+ state.resources.pop();
221
+ }),
222
+ })),
223
+ };
224
+ }
225
+
226
+ function createStarterFakeDb(state: { resources: ResourceRow[] }) {
227
+ let limitCall = 0;
228
+ return {
229
+ select: vi.fn(() => ({
230
+ from: vi.fn(() => ({
231
+ where: vi.fn(() => ({
232
+ limit: vi.fn(async () => {
233
+ limitCall += 1;
234
+ return limitCall % 2 === 0 && state.resources.length > 0
235
+ ? [state.resources.at(-1)]
236
+ : [];
237
+ }),
238
+ })),
239
+ })),
240
+ })),
241
+ };
242
+ }
243
+
244
+ function createStarterExec(state: { resources: ResourceRow[] }) {
245
+ return {
246
+ execute: vi.fn(async (query: { args?: unknown[] }) => {
247
+ const args = query.args ?? [];
248
+ state.resources.push({
249
+ id: String(args[0]),
250
+ ownerEmail: String(args[1]),
251
+ orgId: args[2] as string | null,
252
+ kind: String(args[3]),
253
+ name: String(args[4]),
254
+ description: args[5] as string | null,
255
+ path: String(args[6]),
256
+ content: String(args[7]),
257
+ scope: String(args[8]),
258
+ createdBy: String(args[9]),
259
+ createdAt: Number(args[10]),
260
+ updatedAt: Number(args[11]),
261
+ });
262
+ return { rows: [], rowsAffected: 1 };
263
+ }),
264
+ };
265
+ }
266
+
267
+ function dispatchMetadata(resource: Pick<ResourceRow, "id">) {
268
+ return JSON.stringify({
269
+ source: "dispatch-workspace-resource",
270
+ resourceId: resource.id,
271
+ });
272
+ }
273
+
274
+ beforeEach(() => {
275
+ mocks.getDb.mockReturnValue(createFakeDb({ resources: [] }));
276
+ mocks.getDbExec.mockReturnValue({ execute: vi.fn() });
277
+ mocks.isPostgres.mockReturnValue(false);
278
+ mocks.getApprovalPolicy.mockResolvedValue({
279
+ enabled: false,
280
+ approverEmails: [],
281
+ });
282
+ mocks.createApprovalRequest.mockImplementation(async (input: any) => ({
283
+ id: "approval_1",
284
+ status: "pending",
285
+ ...input,
286
+ }));
287
+ mocks.resourceListAllOwners.mockResolvedValue([]);
288
+ mocks.getOrgSetting.mockResolvedValue(null);
289
+ mocks.getUserSetting.mockResolvedValue(null);
290
+ mocks.putOrgSetting.mockResolvedValue(undefined);
291
+ mocks.putUserSetting.mockResolvedValue(undefined);
292
+ });
293
+
294
+ afterEach(() => {
295
+ vi.clearAllMocks();
296
+ });
297
+
298
+ describe("workspace resource materialization", () => {
299
+ it("seeds the starter global workspace resources once per org scope", async () => {
300
+ const state = { resources: [] as ResourceRow[] };
301
+ mocks.getDb.mockReturnValue(createStarterFakeDb(state));
302
+ mocks.getDbExec.mockReturnValue(createStarterExec(state));
303
+
304
+ await ensureStarterWorkspaceResources({
305
+ ownerEmail: "owner@example.test",
306
+ orgId: "org_123",
307
+ });
308
+
309
+ expect(state.resources.map((resource) => resource.path)).toEqual(
310
+ STARTER_GLOBAL_WORKSPACE_RESOURCES.map((resource) => resource.path),
311
+ );
312
+ expect(state.resources.every((resource) => resource.scope === "all")).toBe(
313
+ true,
314
+ );
315
+ expect(mocks.resourcePut).toHaveBeenCalledTimes(
316
+ STARTER_GLOBAL_WORKSPACE_RESOURCES.length,
317
+ );
318
+ expect(mocks.resourcePut).toHaveBeenCalledWith(
319
+ "__workspace__",
320
+ "context/company.md",
321
+ expect.stringContaining("# Company Profile"),
322
+ "text/markdown",
323
+ expect.objectContaining({ createdBy: "system" }),
324
+ );
325
+ expect(mocks.putOrgSetting).toHaveBeenCalledWith(
326
+ "org_123",
327
+ "dispatch-starter-workspace-resources",
328
+ expect.objectContaining({
329
+ version: 2,
330
+ resources: STARTER_GLOBAL_WORKSPACE_RESOURCES.map((resource) => ({
331
+ path: resource.path,
332
+ kind: resource.kind,
333
+ scope: resource.scope,
334
+ })),
335
+ }),
336
+ );
337
+ });
338
+
339
+ it("skips starter seeding after the scope marker exists", async () => {
340
+ mocks.getOrgSetting.mockResolvedValueOnce({ version: 2 });
341
+
342
+ await ensureStarterWorkspaceResources({
343
+ ownerEmail: "owner@example.test",
344
+ orgId: "org_123",
345
+ });
346
+
347
+ expect(mocks.getDbExec).not.toHaveBeenCalled();
348
+ expect(mocks.resourcePut).not.toHaveBeenCalled();
349
+ expect(mocks.putOrgSetting).not.toHaveBeenCalled();
350
+ });
351
+
352
+ it("restores a missing starter resource without rerunning automatic seeding", async () => {
353
+ mocks.getOrgSetting.mockResolvedValue({ version: 2 });
354
+ const state = { resources: [] as ResourceRow[] };
355
+ mocks.getDb.mockReturnValue(createStarterFakeDb(state));
356
+ mocks.getDbExec.mockReturnValue(createStarterExec(state));
357
+
358
+ const result = await restoreStarterWorkspaceResources({
359
+ paths: ["context/brand.md"],
360
+ });
361
+
362
+ expect(result.restored.map((resource) => resource.path)).toEqual([
363
+ "context/brand.md",
364
+ ]);
365
+ expect(state.resources).toHaveLength(1);
366
+ expect(state.resources[0]).toEqual(
367
+ expect.objectContaining({
368
+ path: "context/brand.md",
369
+ scope: "all",
370
+ }),
371
+ );
372
+ expect(mocks.putOrgSetting).not.toHaveBeenCalled();
373
+ expect(mocks.resourcePut).toHaveBeenCalledWith(
374
+ "__workspace__",
375
+ "context/brand.md",
376
+ expect.stringContaining("# Brand Guidelines"),
377
+ "text/markdown",
378
+ expect.objectContaining({ createdBy: "system" }),
379
+ );
380
+
381
+ mocks.getDb.mockReturnValue(createFakeDb({ resources: state.resources }));
382
+ const received = await listWorkspaceResourcesForApp("mail");
383
+
384
+ expect(received.resources).toEqual([
385
+ expect.objectContaining({
386
+ path: "context/brand.md",
387
+ source: "workspace",
388
+ }),
389
+ ]);
390
+ });
391
+
392
+ it("materializes scope=all starter resources into the core workspace resource store", async () => {
393
+ const created = await createWorkspaceResource({
394
+ kind: "instruction",
395
+ name: "Starter guardrails",
396
+ description: "Always-on workspace instructions",
397
+ path: "instructions/starter.md",
398
+ content: "# Starter\nUse the shared workspace context.",
399
+ scope: "all",
400
+ });
401
+
402
+ expect(created?.scope).toBe("all");
403
+ expect(mocks.resourcePut).toHaveBeenCalledWith(
404
+ "__workspace__",
405
+ "instructions/starter.md",
406
+ "# Starter\nUse the shared workspace context.",
407
+ "text/markdown",
408
+ {
409
+ createdBy: "system",
410
+ metadata: expect.objectContaining({
411
+ source: "dispatch-workspace-resource",
412
+ resourceId: created?.id,
413
+ kind: "instruction",
414
+ name: "Starter guardrails",
415
+ description: "Always-on workspace instructions",
416
+ updatedAt: created?.updatedAt,
417
+ }),
418
+ },
419
+ );
420
+ });
421
+
422
+ it("uses application/json when materializing global agent profile resources", async () => {
423
+ await createWorkspaceResource({
424
+ kind: "agent",
425
+ name: "Research agent",
426
+ path: "remote-agents/research.json",
427
+ content: '{"name":"Research"}',
428
+ scope: "all",
429
+ });
430
+
431
+ expect(mocks.resourcePut).toHaveBeenCalledWith(
432
+ "__workspace__",
433
+ "remote-agents/research.json",
434
+ '{"name":"Research"}',
435
+ "application/json",
436
+ expect.any(Object),
437
+ );
438
+ });
439
+
440
+ it("does not materialize selected-only resources", async () => {
441
+ await createWorkspaceResource({
442
+ kind: "knowledge",
443
+ name: "Launch notes",
444
+ path: "context/launch.md",
445
+ content: "# Launch",
446
+ scope: "selected",
447
+ });
448
+
449
+ expect(mocks.resourcePut).not.toHaveBeenCalled();
450
+ expect(mocks.resourceDeleteByPath).not.toHaveBeenCalled();
451
+ });
452
+
453
+ it("removes a previously materialized global resource when it becomes selected-only", async () => {
454
+ const state = {
455
+ resources: [
456
+ {
457
+ id: "resource_1",
458
+ ownerEmail: "owner@example.test",
459
+ orgId: "org_123",
460
+ kind: "instruction",
461
+ name: "Starter guardrails",
462
+ description: null,
463
+ path: "instructions/starter.md",
464
+ content: "# Starter",
465
+ scope: "all",
466
+ createdBy: "owner@example.test",
467
+ createdAt: 1,
468
+ updatedAt: 1,
469
+ },
470
+ ],
471
+ };
472
+ mocks.getDb.mockReturnValue(createFakeDb(state));
473
+ mocks.resourceGetByPath.mockResolvedValueOnce({
474
+ id: "shared_1",
475
+ owner: "__workspace__",
476
+ path: "instructions/starter.md",
477
+ metadata: dispatchMetadata(state.resources[0]),
478
+ });
479
+
480
+ await updateWorkspaceResource("resource_1", { scope: "selected" });
481
+
482
+ expect(mocks.resourceDeleteByPath).toHaveBeenCalledWith(
483
+ "__workspace__",
484
+ "instructions/starter.md",
485
+ );
486
+ expect(mocks.resourcePut).not.toHaveBeenCalled();
487
+ });
488
+
489
+ it("leaves shared resources alone when metadata does not match Dispatch ownership", async () => {
490
+ const state = {
491
+ resources: [
492
+ {
493
+ id: "resource_1",
494
+ ownerEmail: "owner@example.test",
495
+ orgId: "org_123",
496
+ kind: "instruction",
497
+ name: "Starter guardrails",
498
+ description: null,
499
+ path: "instructions/starter.md",
500
+ content: "# Starter",
501
+ scope: "all",
502
+ createdBy: "owner@example.test",
503
+ createdAt: 1,
504
+ updatedAt: 1,
505
+ },
506
+ ],
507
+ };
508
+ mocks.getDb.mockReturnValue(createFakeDb(state));
509
+ mocks.resourceGetByPath.mockResolvedValueOnce({
510
+ id: "shared_1",
511
+ owner: "__workspace__",
512
+ path: "instructions/starter.md",
513
+ metadata: JSON.stringify({
514
+ source: "manual",
515
+ resourceId: "resource_1",
516
+ }),
517
+ });
518
+
519
+ await updateWorkspaceResource("resource_1", { scope: "selected" });
520
+
521
+ expect(mocks.resourceDeleteByPath).not.toHaveBeenCalled();
522
+ });
523
+
524
+ it("removes a materialized global resource before deleting the workspace resource", async () => {
525
+ const state = {
526
+ resources: [
527
+ {
528
+ id: "resource_1",
529
+ ownerEmail: "owner@example.test",
530
+ orgId: "org_123",
531
+ kind: "knowledge",
532
+ name: "Positioning",
533
+ description: null,
534
+ path: "context/positioning.md",
535
+ content: "# Positioning",
536
+ scope: "all",
537
+ createdBy: "owner@example.test",
538
+ createdAt: 1,
539
+ updatedAt: 1,
540
+ },
541
+ ],
542
+ grants: [],
543
+ };
544
+ mocks.getDb.mockReturnValue(createFakeDb(state));
545
+ mocks.resourceGetByPath.mockResolvedValueOnce({
546
+ id: "shared_1",
547
+ owner: "__workspace__",
548
+ path: "context/positioning.md",
549
+ metadata: dispatchMetadata(state.resources[0]),
550
+ });
551
+
552
+ await deleteWorkspaceResource("resource_1");
553
+
554
+ expect(mocks.resourceDeleteByPath).toHaveBeenCalledWith(
555
+ "__workspace__",
556
+ "context/positioning.md",
557
+ );
558
+ });
559
+
560
+ it("lists the inherited and granted workspace resources an app receives", async () => {
561
+ mocks.getOrgSetting.mockResolvedValue({ version: 2 });
562
+ const state = {
563
+ resources: [
564
+ {
565
+ id: "global_1",
566
+ ownerEmail: "owner@example.test",
567
+ orgId: "org_123",
568
+ kind: "instruction",
569
+ name: "Guardrails",
570
+ description: null,
571
+ path: "instructions/guardrails.md",
572
+ content: "# Guardrails",
573
+ scope: "all",
574
+ createdBy: "owner@example.test",
575
+ createdAt: 1,
576
+ updatedAt: 2,
577
+ },
578
+ {
579
+ id: "selected_1",
580
+ ownerEmail: "owner@example.test",
581
+ orgId: "org_123",
582
+ kind: "knowledge",
583
+ name: "Analytics Messaging",
584
+ description: null,
585
+ path: "context/analytics.md",
586
+ content: "# Analytics",
587
+ scope: "selected",
588
+ createdBy: "owner@example.test",
589
+ createdAt: 1,
590
+ updatedAt: 3,
591
+ },
592
+ {
593
+ id: "selected_2",
594
+ ownerEmail: "owner@example.test",
595
+ orgId: "org_123",
596
+ kind: "knowledge",
597
+ name: "Mail Messaging",
598
+ description: null,
599
+ path: "context/mail.md",
600
+ content: "# Mail",
601
+ scope: "selected",
602
+ createdBy: "owner@example.test",
603
+ createdAt: 1,
604
+ updatedAt: 4,
605
+ },
606
+ ],
607
+ grants: [
608
+ {
609
+ id: "grant_1",
610
+ ownerEmail: "owner@example.test",
611
+ orgId: "org_123",
612
+ resourceId: "selected_1",
613
+ appId: "analytics",
614
+ status: "active",
615
+ syncedAt: 123,
616
+ createdAt: 1,
617
+ updatedAt: 1,
618
+ },
619
+ {
620
+ id: "grant_2",
621
+ ownerEmail: "owner@example.test",
622
+ orgId: "org_123",
623
+ resourceId: "selected_2",
624
+ appId: "analytics",
625
+ status: "revoked",
626
+ syncedAt: null,
627
+ createdAt: 1,
628
+ updatedAt: 1,
629
+ },
630
+ ],
631
+ };
632
+ mocks.getDb.mockReturnValue(createFakeDb(state));
633
+
634
+ const result = await listWorkspaceResourcesForApp("analytics");
635
+
636
+ expect(result.counts).toEqual({
637
+ total: 2,
638
+ workspace: 1,
639
+ global: 1,
640
+ granted: 1,
641
+ autoLoaded: 1,
642
+ });
643
+ expect(result.resources.map((resource) => resource.path)).toEqual([
644
+ "instructions/guardrails.md",
645
+ "context/analytics.md",
646
+ ]);
647
+ expect(result.resources[0]).toEqual(
648
+ expect.objectContaining({
649
+ source: "workspace",
650
+ autoLoaded: true,
651
+ grantId: null,
652
+ }),
653
+ );
654
+ expect(result.resources[1]).toEqual(
655
+ expect.objectContaining({
656
+ source: "grant",
657
+ autoLoaded: false,
658
+ grantId: "grant_1",
659
+ }),
660
+ );
661
+ expect(result.resources[1]).not.toHaveProperty("syncedAt");
662
+ });
663
+
664
+ it("previews all-app effective context without requiring a grant or sync", async () => {
665
+ mocks.getOrgSetting.mockResolvedValue({ version: 2 });
666
+ const state = {
667
+ resources: [
668
+ {
669
+ id: "global_1",
670
+ ownerEmail: "owner@example.test",
671
+ orgId: "org_123",
672
+ kind: "knowledge",
673
+ name: "Brand",
674
+ description: "Brand guidance",
675
+ path: "context/brand.md",
676
+ content: "# Brand",
677
+ scope: "all",
678
+ createdBy: "owner@example.test",
679
+ createdAt: 1,
680
+ updatedAt: 2,
681
+ },
682
+ ],
683
+ grants: [],
684
+ };
685
+ mocks.getDb.mockReturnValue(createFakeDb(state));
686
+
687
+ const result = await getWorkspaceResourceEffectiveContext({
688
+ resourceId: "global_1",
689
+ appId: "analytics",
690
+ userEmail: "person@example.test",
691
+ });
692
+
693
+ expect(result.availability).toBe("all-apps");
694
+ expect(result.availableToApp).toBe(true);
695
+ expect(result.path).toBe("context/brand.md");
696
+ expect(result.workspaceResource).toEqual(
697
+ expect.objectContaining({
698
+ id: "global_1",
699
+ path: "context/brand.md",
700
+ scope: "all",
701
+ }),
702
+ );
703
+ expect(mocks.resourceEffectiveContext).toHaveBeenCalledWith(
704
+ "person@example.test",
705
+ "context/brand.md",
706
+ );
707
+ expect(mocks.resourcePut).toHaveBeenCalledWith(
708
+ "__workspace__",
709
+ "context/brand.md",
710
+ "# Brand",
711
+ "text/markdown",
712
+ expect.objectContaining({ createdBy: "system" }),
713
+ );
714
+ });
715
+
716
+ it("returns the winning layer from the runtime effective context stack", async () => {
717
+ mocks.getOrgSetting.mockResolvedValue({ version: 2 });
718
+ mocks.resourceEffectiveContext.mockResolvedValueOnce({
719
+ path: "instructions/guardrails.md",
720
+ effectiveScope: "personal",
721
+ effectiveResource: {
722
+ id: "personal_meta",
723
+ path: "instructions/guardrails.md",
724
+ owner: "person@example.test",
725
+ mimeType: "text/markdown",
726
+ size: 24,
727
+ createdAt: 1,
728
+ updatedAt: 4,
729
+ createdBy: "user",
730
+ visibility: "workspace",
731
+ threadId: null,
732
+ runId: null,
733
+ expiresAt: null,
734
+ metadata: null,
735
+ },
736
+ layers: [
737
+ {
738
+ scope: "workspace",
739
+ label: "Workspace default",
740
+ owner: "__workspace__",
741
+ resource: {
742
+ id: "workspace_meta",
743
+ path: "instructions/guardrails.md",
744
+ owner: "__workspace__",
745
+ mimeType: "text/markdown",
746
+ size: 16,
747
+ createdAt: 1,
748
+ updatedAt: 2,
749
+ createdBy: "system",
750
+ visibility: "workspace",
751
+ threadId: null,
752
+ runId: null,
753
+ expiresAt: null,
754
+ metadata: null,
755
+ },
756
+ exists: true,
757
+ effective: false,
758
+ overridden: true,
759
+ canWrite: false,
760
+ },
761
+ {
762
+ scope: "shared",
763
+ label: "Organization/app override",
764
+ owner: "__shared__",
765
+ resource: {
766
+ id: "shared_meta",
767
+ path: "instructions/guardrails.md",
768
+ owner: "__shared__",
769
+ mimeType: "text/markdown",
770
+ size: 20,
771
+ createdAt: 1,
772
+ updatedAt: 3,
773
+ createdBy: "user",
774
+ visibility: "workspace",
775
+ threadId: null,
776
+ runId: null,
777
+ expiresAt: null,
778
+ metadata: null,
779
+ },
780
+ exists: true,
781
+ effective: false,
782
+ overridden: true,
783
+ canWrite: true,
784
+ },
785
+ {
786
+ scope: "personal",
787
+ label: "Personal override",
788
+ owner: "person@example.test",
789
+ resource: {
790
+ id: "personal_meta",
791
+ path: "instructions/guardrails.md",
792
+ owner: "person@example.test",
793
+ mimeType: "text/markdown",
794
+ size: 24,
795
+ createdAt: 1,
796
+ updatedAt: 4,
797
+ createdBy: "user",
798
+ visibility: "workspace",
799
+ threadId: null,
800
+ runId: null,
801
+ expiresAt: null,
802
+ metadata: null,
803
+ },
804
+ exists: true,
805
+ effective: true,
806
+ overridden: false,
807
+ canWrite: true,
808
+ },
809
+ ],
810
+ });
811
+ const state = {
812
+ resources: [
813
+ {
814
+ id: "global_1",
815
+ ownerEmail: "owner@example.test",
816
+ orgId: "org_123",
817
+ kind: "instruction",
818
+ name: "Guardrails",
819
+ description: null,
820
+ path: "instructions/guardrails.md",
821
+ content: "# Workspace guardrails",
822
+ scope: "all",
823
+ createdBy: "owner@example.test",
824
+ createdAt: 1,
825
+ updatedAt: 2,
826
+ },
827
+ ],
828
+ grants: [],
829
+ };
830
+ mocks.getDb.mockReturnValue(createFakeDb(state));
831
+
832
+ const result = await getWorkspaceResourceEffectiveContext({
833
+ resourceId: "global_1",
834
+ appId: "analytics",
835
+ userEmail: "person@example.test",
836
+ });
837
+
838
+ expect(result.availability).toBe("all-apps");
839
+ expect(result.effectiveScope).toBe("personal");
840
+ expect(result.effectiveResource).toEqual(
841
+ expect.objectContaining({
842
+ id: "personal_meta",
843
+ owner: "person@example.test",
844
+ }),
845
+ );
846
+ expect(
847
+ result.layers.map((layer) => [layer.scope, layer.effective]),
848
+ ).toEqual([
849
+ ["workspace", false],
850
+ ["shared", false],
851
+ ["personal", true],
852
+ ]);
853
+ });
854
+
855
+ it("reports selected resources as app-specific exceptions", async () => {
856
+ mocks.getOrgSetting.mockResolvedValue({ version: 2 });
857
+ const state = {
858
+ resources: [
859
+ {
860
+ id: "selected_1",
861
+ ownerEmail: "owner@example.test",
862
+ orgId: "org_123",
863
+ kind: "knowledge",
864
+ name: "Analytics launch",
865
+ description: null,
866
+ path: "context/analytics-launch.md",
867
+ content: "# Launch",
868
+ scope: "selected",
869
+ createdBy: "owner@example.test",
870
+ createdAt: 1,
871
+ updatedAt: 2,
872
+ },
873
+ ],
874
+ grants: [
875
+ {
876
+ id: "grant_1",
877
+ ownerEmail: "owner@example.test",
878
+ orgId: "org_123",
879
+ resourceId: "selected_1",
880
+ appId: "analytics",
881
+ status: "active",
882
+ syncedAt: null,
883
+ createdAt: 1,
884
+ updatedAt: 1,
885
+ },
886
+ ],
887
+ };
888
+ mocks.getDb.mockReturnValue(createFakeDb(state));
889
+
890
+ const result = await getWorkspaceResourceEffectiveContext({
891
+ resourceId: "selected_1",
892
+ appId: "analytics",
893
+ });
894
+
895
+ expect(result.availability).toBe("selected-granted");
896
+ expect(result.availableToApp).toBe(true);
897
+ expect(result.activeGrantId).toBe("grant_1");
898
+ expect(mocks.resourcePut).not.toHaveBeenCalled();
899
+ expect(mocks.resourceEffectiveContext).toHaveBeenCalledWith(
900
+ "owner@example.test",
901
+ "context/analytics-launch.md",
902
+ );
903
+ });
904
+
905
+ it("marks selected resources unavailable to apps without an active grant", async () => {
906
+ mocks.getOrgSetting.mockResolvedValue({ version: 2 });
907
+ const state = {
908
+ resources: [
909
+ {
910
+ id: "selected_1",
911
+ ownerEmail: "owner@example.test",
912
+ orgId: "org_123",
913
+ kind: "knowledge",
914
+ name: "Analytics launch",
915
+ description: null,
916
+ path: "context/analytics-launch.md",
917
+ content: "# Launch",
918
+ scope: "selected",
919
+ createdBy: "owner@example.test",
920
+ createdAt: 1,
921
+ updatedAt: 2,
922
+ },
923
+ ],
924
+ grants: [],
925
+ };
926
+ mocks.getDb.mockReturnValue(createFakeDb(state));
927
+
928
+ const result = await getWorkspaceResourceEffectiveContext({
929
+ resourceId: "selected_1",
930
+ appId: "analytics",
931
+ });
932
+
933
+ expect(result.availability).toBe("selected-not-granted");
934
+ expect(result.availableToApp).toBe(false);
935
+ expect(result.activeGrantId).toBeNull();
936
+ });
937
+
938
+ it("queues All-app updates for approval when approval policy is enabled", async () => {
939
+ mocks.getOrgSetting.mockResolvedValue({ version: 2 });
940
+ mocks.getApprovalPolicy.mockResolvedValue({
941
+ enabled: true,
942
+ approverEmails: ["admin@example.test"],
943
+ });
944
+ const state = {
945
+ resources: [
946
+ {
947
+ id: "global_1",
948
+ ownerEmail: "owner@example.test",
949
+ orgId: "org_123",
950
+ kind: "instruction",
951
+ name: "Guardrails",
952
+ description: null,
953
+ path: "instructions/guardrails.md",
954
+ content: "# Guardrails",
955
+ scope: "all",
956
+ createdBy: "owner@example.test",
957
+ createdAt: 1,
958
+ updatedAt: 2,
959
+ },
960
+ ],
961
+ grants: [],
962
+ };
963
+ mocks.getDb.mockReturnValue(createFakeDb(state));
964
+
965
+ const result = await updateWorkspaceResource("global_1", {
966
+ content: "# Updated guardrails",
967
+ });
968
+
969
+ expect(result).toEqual(
970
+ expect.objectContaining({
971
+ id: "approval_1",
972
+ status: "pending",
973
+ changeType: "workspace-resource.update",
974
+ targetType: "workspace-instruction",
975
+ targetId: "global_1",
976
+ }),
977
+ );
978
+ expect(mocks.createApprovalRequest).toHaveBeenCalledWith(
979
+ expect.objectContaining({
980
+ changeType: "workspace-resource.update",
981
+ beforeValue: expect.objectContaining({
982
+ content: "# Guardrails",
983
+ scope: "all",
984
+ }),
985
+ afterValue: expect.objectContaining({
986
+ content: "# Updated guardrails",
987
+ scope: "all",
988
+ }),
989
+ }),
990
+ );
991
+ expect(state.resources[0].content).toBe("# Guardrails");
992
+ expect(mocks.resourcePut).not.toHaveBeenCalled();
993
+ });
994
+
995
+ it("updates selected-only resources directly when approval policy is enabled", async () => {
996
+ mocks.getOrgSetting.mockResolvedValue({ version: 2 });
997
+ mocks.getApprovalPolicy.mockResolvedValue({
998
+ enabled: true,
999
+ approverEmails: ["admin@example.test"],
1000
+ });
1001
+ const state = {
1002
+ resources: [
1003
+ {
1004
+ id: "selected_1",
1005
+ ownerEmail: "owner@example.test",
1006
+ orgId: "org_123",
1007
+ kind: "knowledge",
1008
+ name: "Launch",
1009
+ description: null,
1010
+ path: "context/launch.md",
1011
+ content: "# Launch",
1012
+ scope: "selected",
1013
+ createdBy: "owner@example.test",
1014
+ createdAt: 1,
1015
+ updatedAt: 2,
1016
+ },
1017
+ ],
1018
+ grants: [],
1019
+ };
1020
+ mocks.getDb.mockReturnValue(createFakeDb(state));
1021
+
1022
+ const result = await updateWorkspaceResource("selected_1", {
1023
+ content: "# Updated launch",
1024
+ });
1025
+
1026
+ expect(result).toEqual(
1027
+ expect.objectContaining({
1028
+ id: "selected_1",
1029
+ content: "# Updated launch",
1030
+ }),
1031
+ );
1032
+ expect(mocks.createApprovalRequest).not.toHaveBeenCalled();
1033
+ expect(mocks.resourcePut).not.toHaveBeenCalled();
1034
+ });
1035
+
1036
+ it("previews All-app impact, approval behavior, and overrides", async () => {
1037
+ mocks.getOrgSetting.mockResolvedValue({ version: 2 });
1038
+ mocks.getApprovalPolicy.mockResolvedValue({
1039
+ enabled: true,
1040
+ approverEmails: ["admin@example.test"],
1041
+ });
1042
+ mocks.resourceListAllOwners.mockResolvedValue([
1043
+ {
1044
+ id: "workspace_meta",
1045
+ owner: "__workspace__",
1046
+ path: "context/brand.md",
1047
+ updatedAt: 1,
1048
+ },
1049
+ {
1050
+ id: "shared_meta",
1051
+ owner: "__shared__",
1052
+ path: "context/brand.md",
1053
+ updatedAt: 2,
1054
+ },
1055
+ {
1056
+ id: "personal_meta",
1057
+ owner: "person@example.test",
1058
+ path: "context/brand.md",
1059
+ updatedAt: 3,
1060
+ },
1061
+ ]);
1062
+ const state = {
1063
+ resources: [
1064
+ {
1065
+ id: "global_1",
1066
+ ownerEmail: "owner@example.test",
1067
+ orgId: "org_123",
1068
+ kind: "knowledge",
1069
+ name: "Brand",
1070
+ description: null,
1071
+ path: "context/brand.md",
1072
+ content: "# Brand",
1073
+ scope: "all",
1074
+ createdBy: "owner@example.test",
1075
+ createdAt: 1,
1076
+ updatedAt: 2,
1077
+ },
1078
+ ],
1079
+ grants: [],
1080
+ };
1081
+ mocks.getDb.mockReturnValue(createFakeDb(state));
1082
+
1083
+ const result = await previewWorkspaceResourceChange({
1084
+ operation: "update",
1085
+ resourceId: "global_1",
1086
+ scope: "all",
1087
+ });
1088
+
1089
+ expect(result.affectsAllApps).toBe(true);
1090
+ expect(result.approval).toEqual({
1091
+ policyEnabled: true,
1092
+ willRequestApproval: true,
1093
+ });
1094
+ expect(result.overrides).toEqual(
1095
+ expect.objectContaining({
1096
+ count: 2,
1097
+ sharedCount: 1,
1098
+ personalCount: 1,
1099
+ }),
1100
+ );
1101
+ expect(result.overrides.items.map((item) => item.owner)).toEqual([
1102
+ "__shared__",
1103
+ "person@example.test",
1104
+ ]);
1105
+ });
1106
+ });