@owlmeans/server-planning 0.1.18-rc.8 → 0.1.18

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 (438) hide show
  1. package/README.md +92 -19
  2. package/agent-meta/manifest.json +105 -4
  3. package/agent-meta/skills/server-planning/SKILL.md +89 -138
  4. package/agent-meta/skills/server-planning/parts/auxiliary.md +54 -0
  5. package/agent-meta/skills/server-planning/parts/executor.md +82 -0
  6. package/agent-meta/skills/server-planning/parts/schemas.md +26 -0
  7. package/agent-meta/skills/server-planning/parts/serving.md +88 -0
  8. package/agent-meta/skills/server-planning/parts/stores.md +91 -0
  9. package/build/actions/apply-schema-request.d.ts +8 -0
  10. package/build/actions/apply-schema-request.d.ts.map +1 -0
  11. package/build/actions/apply-schema-request.js +33 -0
  12. package/build/actions/apply-schema-request.js.map +1 -0
  13. package/build/actions/commit.d.ts +3 -3
  14. package/build/actions/commit.d.ts.map +1 -1
  15. package/build/actions/commit.js +5 -4
  16. package/build/actions/commit.js.map +1 -1
  17. package/build/actions/define-schemas.d.ts +13 -0
  18. package/build/actions/define-schemas.d.ts.map +1 -0
  19. package/build/actions/define-schemas.js +28 -0
  20. package/build/actions/define-schemas.js.map +1 -0
  21. package/build/actions/execute.d.ts +11 -10
  22. package/build/actions/execute.d.ts.map +1 -1
  23. package/build/actions/execute.js +17 -27
  24. package/build/actions/execute.js.map +1 -1
  25. package/build/actions/execution/types.d.ts +34 -0
  26. package/build/actions/execution/types.d.ts.map +1 -0
  27. package/build/actions/execution/types.js +2 -0
  28. package/build/actions/execution/types.js.map +1 -0
  29. package/build/actions/execution.d.ts +4 -0
  30. package/build/actions/execution.d.ts.map +1 -0
  31. package/build/actions/execution.js +88 -0
  32. package/build/actions/execution.js.map +1 -0
  33. package/build/actions/get-card.d.ts +6 -0
  34. package/build/actions/get-card.d.ts.map +1 -0
  35. package/build/actions/get-card.js +9 -0
  36. package/build/actions/get-card.js.map +1 -0
  37. package/build/actions/get-specification.d.ts +6 -0
  38. package/build/actions/get-specification.d.ts.map +1 -0
  39. package/build/actions/get-specification.js +9 -0
  40. package/build/actions/get-specification.js.map +1 -0
  41. package/build/actions/index.d.ts +12 -3
  42. package/build/actions/index.d.ts.map +1 -1
  43. package/build/actions/index.js +11 -3
  44. package/build/actions/index.js.map +1 -1
  45. package/build/actions/links.d.ts +3 -3
  46. package/build/actions/links.d.ts.map +1 -1
  47. package/build/actions/links.js +6 -5
  48. package/build/actions/links.js.map +1 -1
  49. package/build/actions/list-card-specifications.d.ts +6 -0
  50. package/build/actions/list-card-specifications.d.ts.map +1 -0
  51. package/build/actions/list-card-specifications.js +11 -0
  52. package/build/actions/list-card-specifications.js.map +1 -0
  53. package/build/actions/list-card-transitions.d.ts +6 -0
  54. package/build/actions/list-card-transitions.d.ts.map +1 -0
  55. package/build/actions/list-card-transitions.js +11 -0
  56. package/build/actions/list-card-transitions.js.map +1 -0
  57. package/build/actions/list-cards.d.ts +6 -0
  58. package/build/actions/list-cards.d.ts.map +1 -0
  59. package/build/actions/list-cards.js +10 -0
  60. package/build/actions/list-cards.js.map +1 -0
  61. package/build/actions/list-schemas.d.ts +13 -0
  62. package/build/actions/list-schemas.d.ts.map +1 -0
  63. package/build/actions/list-schemas.js +20 -0
  64. package/build/actions/list-schemas.js.map +1 -0
  65. package/build/actions/list-specification-revisions.d.ts +6 -0
  66. package/build/actions/list-specification-revisions.d.ts.map +1 -0
  67. package/build/actions/list-specification-revisions.js +10 -0
  68. package/build/actions/list-specification-revisions.js.map +1 -0
  69. package/build/actions/resources.d.ts +5 -0
  70. package/build/actions/resources.d.ts.map +1 -0
  71. package/build/actions/resources.js +111 -0
  72. package/build/actions/resources.js.map +1 -0
  73. package/build/actions/summarize-cards.d.ts +6 -0
  74. package/build/actions/summarize-cards.d.ts.map +1 -0
  75. package/build/actions/summarize-cards.js +11 -0
  76. package/build/actions/summarize-cards.js.map +1 -0
  77. package/build/actions/transitions.d.ts +3 -3
  78. package/build/actions/transitions.d.ts.map +1 -1
  79. package/build/actions/transitions.js +4 -3
  80. package/build/actions/transitions.js.map +1 -1
  81. package/build/actions/types.d.ts +6 -0
  82. package/build/actions/types.d.ts.map +1 -0
  83. package/build/actions/types.js +2 -0
  84. package/build/actions/types.js.map +1 -0
  85. package/build/actions/watch.d.ts +2 -2
  86. package/build/actions/watch.d.ts.map +1 -1
  87. package/build/actions/watch.js +8 -6
  88. package/build/actions/watch.js.map +1 -1
  89. package/build/conformance/assert/types.d.ts +19 -0
  90. package/build/conformance/assert/types.d.ts.map +1 -0
  91. package/build/conformance/assert/types.js +2 -0
  92. package/build/conformance/assert/types.js.map +1 -0
  93. package/build/conformance/assert.d.ts +15 -0
  94. package/build/conformance/assert.d.ts.map +1 -0
  95. package/build/conformance/assert.js +52 -0
  96. package/build/conformance/assert.js.map +1 -0
  97. package/build/conformance/cases.d.ts +7 -0
  98. package/build/conformance/cases.d.ts.map +1 -0
  99. package/build/conformance/cases.js +544 -0
  100. package/build/conformance/cases.js.map +1 -0
  101. package/build/conformance/clock.d.ts +6 -0
  102. package/build/conformance/clock.d.ts.map +1 -0
  103. package/build/conformance/clock.js +12 -0
  104. package/build/conformance/clock.js.map +1 -0
  105. package/build/conformance/consts.d.ts +30 -0
  106. package/build/conformance/consts.d.ts.map +1 -0
  107. package/build/conformance/consts.js +159 -0
  108. package/build/conformance/consts.js.map +1 -0
  109. package/build/conformance/errors.d.ts +5 -0
  110. package/build/conformance/errors.d.ts.map +1 -0
  111. package/build/conformance/errors.js +8 -0
  112. package/build/conformance/errors.js.map +1 -0
  113. package/build/conformance/fixtures/types.d.ts +13 -0
  114. package/build/conformance/fixtures/types.d.ts.map +1 -0
  115. package/build/conformance/fixtures/types.js +2 -0
  116. package/build/conformance/fixtures/types.js.map +1 -0
  117. package/build/conformance/fixtures.d.ts +14 -0
  118. package/build/conformance/fixtures.d.ts.map +1 -0
  119. package/build/conformance/fixtures.js +26 -0
  120. package/build/conformance/fixtures.js.map +1 -0
  121. package/build/conformance/index.d.ts +15 -0
  122. package/build/conformance/index.d.ts.map +1 -0
  123. package/build/conformance/index.js +12 -0
  124. package/build/conformance/index.js.map +1 -0
  125. package/build/conformance/resources.d.ts +4 -0
  126. package/build/conformance/resources.d.ts.map +1 -0
  127. package/build/conformance/resources.js +228 -0
  128. package/build/conformance/resources.js.map +1 -0
  129. package/build/conformance/types.d.ts +18 -0
  130. package/build/conformance/types.d.ts.map +1 -0
  131. package/build/conformance/types.js +2 -0
  132. package/build/conformance/types.js.map +1 -0
  133. package/build/consts.d.ts +5 -0
  134. package/build/consts.d.ts.map +1 -1
  135. package/build/consts.js +5 -0
  136. package/build/consts.js.map +1 -1
  137. package/build/definitions.d.ts +18 -0
  138. package/build/definitions.d.ts.map +1 -0
  139. package/build/definitions.js +225 -0
  140. package/build/definitions.js.map +1 -0
  141. package/build/executor/changes/types.d.ts +36 -0
  142. package/build/executor/changes/types.d.ts.map +1 -0
  143. package/build/executor/changes/types.js +2 -0
  144. package/build/executor/changes/types.js.map +1 -0
  145. package/build/executor/changes.d.ts +3 -30
  146. package/build/executor/changes.d.ts.map +1 -1
  147. package/build/executor/changes.js +42 -51
  148. package/build/executor/changes.js.map +1 -1
  149. package/build/executor/code/types.d.ts +17 -0
  150. package/build/executor/code/types.d.ts.map +1 -0
  151. package/build/executor/code/types.js +2 -0
  152. package/build/executor/code/types.js.map +1 -0
  153. package/build/executor/code.d.ts +3 -14
  154. package/build/executor/code.d.ts.map +1 -1
  155. package/build/executor/code.js +50 -54
  156. package/build/executor/code.js.map +1 -1
  157. package/build/executor/consts.local.d.ts +5 -0
  158. package/build/executor/consts.local.d.ts.map +1 -0
  159. package/build/executor/consts.local.js +6 -0
  160. package/build/executor/consts.local.js.map +1 -0
  161. package/build/executor/creator/types.d.ts +33 -0
  162. package/build/executor/creator/types.d.ts.map +1 -0
  163. package/build/executor/creator/types.js +2 -0
  164. package/build/executor/creator/types.js.map +1 -0
  165. package/build/executor/creator.d.ts +11 -0
  166. package/build/executor/creator.d.ts.map +1 -0
  167. package/build/executor/creator.js +32 -0
  168. package/build/executor/creator.js.map +1 -0
  169. package/build/executor/resolve/types.d.ts +30 -0
  170. package/build/executor/resolve/types.d.ts.map +1 -0
  171. package/build/executor/resolve/types.js +2 -0
  172. package/build/executor/resolve/types.js.map +1 -0
  173. package/build/executor/resolve.d.ts +5 -34
  174. package/build/executor/resolve.d.ts.map +1 -1
  175. package/build/executor/resolve.js +169 -110
  176. package/build/executor/resolve.js.map +1 -1
  177. package/build/executor/types.d.ts +23 -0
  178. package/build/executor/types.d.ts.map +1 -0
  179. package/build/executor/types.js +2 -0
  180. package/build/executor/types.js.map +1 -0
  181. package/build/executor/validate.d.ts +6 -5
  182. package/build/executor/validate.d.ts.map +1 -1
  183. package/build/executor/validate.js +38 -27
  184. package/build/executor/validate.js.map +1 -1
  185. package/build/executor.d.ts +11 -7
  186. package/build/executor.d.ts.map +1 -1
  187. package/build/executor.js +27 -21
  188. package/build/executor.js.map +1 -1
  189. package/build/facade.d.ts +20 -2
  190. package/build/facade.d.ts.map +1 -1
  191. package/build/facade.js +144 -22
  192. package/build/facade.js.map +1 -1
  193. package/build/helper.d.ts +5 -9
  194. package/build/helper.d.ts.map +1 -1
  195. package/build/helper.js +12 -11
  196. package/build/helper.js.map +1 -1
  197. package/build/hierarchy/types.d.ts +20 -0
  198. package/build/hierarchy/types.d.ts.map +1 -0
  199. package/build/hierarchy/types.js +2 -0
  200. package/build/hierarchy/types.js.map +1 -0
  201. package/build/hierarchy.d.ts +5 -0
  202. package/build/hierarchy.d.ts.map +1 -0
  203. package/build/hierarchy.js +133 -0
  204. package/build/hierarchy.js.map +1 -0
  205. package/build/index.d.ts +1 -0
  206. package/build/index.d.ts.map +1 -1
  207. package/build/index.js +1 -0
  208. package/build/index.js.map +1 -1
  209. package/build/invariants/types.d.ts +6 -0
  210. package/build/invariants/types.d.ts.map +1 -0
  211. package/build/invariants/types.js +2 -0
  212. package/build/invariants/types.js.map +1 -0
  213. package/build/invariants.d.ts +5 -0
  214. package/build/invariants.d.ts.map +1 -0
  215. package/build/invariants.js +126 -0
  216. package/build/invariants.js.map +1 -0
  217. package/build/projection.d.ts +4 -10
  218. package/build/projection.d.ts.map +1 -1
  219. package/build/projection.js +7 -5
  220. package/build/projection.js.map +1 -1
  221. package/build/queue/consts.d.ts +5 -0
  222. package/build/queue/consts.d.ts.map +1 -0
  223. package/build/queue/consts.js +5 -0
  224. package/build/queue/consts.js.map +1 -0
  225. package/build/queue/declare.d.ts +5 -0
  226. package/build/queue/declare.d.ts.map +1 -0
  227. package/build/queue/declare.js +5 -0
  228. package/build/queue/declare.js.map +1 -0
  229. package/build/queue/index.d.ts +3 -18
  230. package/build/queue/index.d.ts.map +1 -1
  231. package/build/queue/index.js +2 -7
  232. package/build/queue/index.js.map +1 -1
  233. package/build/queue/types.d.ts +13 -0
  234. package/build/queue/types.d.ts.map +1 -0
  235. package/build/queue/types.js +2 -0
  236. package/build/queue/types.js.map +1 -0
  237. package/build/registry.d.ts +5 -22
  238. package/build/registry.d.ts.map +1 -1
  239. package/build/registry.js +16 -10
  240. package/build/registry.js.map +1 -1
  241. package/build/resources.d.ts +5 -0
  242. package/build/resources.d.ts.map +1 -0
  243. package/build/resources.js +287 -0
  244. package/build/resources.js.map +1 -0
  245. package/build/service.d.ts +3 -16
  246. package/build/service.d.ts.map +1 -1
  247. package/build/service.js +12 -0
  248. package/build/service.js.map +1 -1
  249. package/build/store/commits.d.ts.map +1 -1
  250. package/build/store/commits.js +3 -1
  251. package/build/store/commits.js.map +1 -1
  252. package/build/store/composite.d.ts +1 -0
  253. package/build/store/composite.d.ts.map +1 -1
  254. package/build/store/composite.js +7 -2
  255. package/build/store/composite.js.map +1 -1
  256. package/build/store/consts.local.d.ts +2 -0
  257. package/build/store/consts.local.d.ts.map +1 -0
  258. package/build/store/consts.local.js +2 -0
  259. package/build/store/consts.local.js.map +1 -0
  260. package/build/store/fold/types.d.ts +43 -0
  261. package/build/store/fold/types.d.ts.map +1 -0
  262. package/build/store/fold/types.js +2 -0
  263. package/build/store/fold/types.js.map +1 -0
  264. package/build/store/fold.d.ts +8 -30
  265. package/build/store/fold.d.ts.map +1 -1
  266. package/build/store/fold.js +190 -198
  267. package/build/store/fold.js.map +1 -1
  268. package/build/store/index.d.ts +1 -0
  269. package/build/store/index.d.ts.map +1 -1
  270. package/build/store/index.js.map +1 -1
  271. package/build/store/memory.d.ts +12 -0
  272. package/build/store/memory.d.ts.map +1 -1
  273. package/build/store/memory.js +0 -0
  274. package/build/store/memory.js.map +1 -1
  275. package/build/store/types.d.ts +15 -1
  276. package/build/store/types.d.ts.map +1 -1
  277. package/build/store/types.local.d.ts +5 -0
  278. package/build/store/types.local.d.ts.map +1 -0
  279. package/build/store/types.local.js +2 -0
  280. package/build/store/types.local.js.map +1 -0
  281. package/build/types.d.ts +126 -2
  282. package/build/types.d.ts.map +1 -1
  283. package/build/types.local.d.ts +7 -0
  284. package/build/types.local.d.ts.map +1 -0
  285. package/build/types.local.js +2 -0
  286. package/build/types.local.js.map +1 -0
  287. package/build/utils/access/types.d.ts +30 -0
  288. package/build/utils/access/types.d.ts.map +1 -0
  289. package/build/utils/access/types.js +2 -0
  290. package/build/utils/access/types.js.map +1 -0
  291. package/build/utils/access.d.ts +4 -0
  292. package/build/utils/access.d.ts.map +1 -0
  293. package/build/utils/access.js +28 -0
  294. package/build/utils/access.js.map +1 -0
  295. package/build/utils/guard/types.d.ts +23 -0
  296. package/build/utils/guard/types.d.ts.map +1 -0
  297. package/build/utils/guard/types.js +2 -0
  298. package/build/utils/guard/types.js.map +1 -0
  299. package/build/utils/guard.d.ts +7 -10
  300. package/build/utils/guard.d.ts.map +1 -1
  301. package/build/utils/guard.js +36 -32
  302. package/build/utils/guard.js.map +1 -1
  303. package/build/utils/handler/types.d.ts +26 -0
  304. package/build/utils/handler/types.d.ts.map +1 -0
  305. package/build/utils/handler/types.js +2 -0
  306. package/build/utils/handler/types.js.map +1 -0
  307. package/build/utils/handler.d.ts +6 -0
  308. package/build/utils/handler.d.ts.map +1 -0
  309. package/build/utils/handler.js +21 -0
  310. package/build/utils/handler.js.map +1 -0
  311. package/build/utils/index.d.ts +8 -0
  312. package/build/utils/index.d.ts.map +1 -1
  313. package/build/utils/index.js +3 -0
  314. package/build/utils/index.js.map +1 -1
  315. package/build/utils/request/types.d.ts +23 -0
  316. package/build/utils/request/types.d.ts.map +1 -0
  317. package/build/utils/request/types.js +2 -0
  318. package/build/utils/request/types.js.map +1 -0
  319. package/build/utils/request.d.ts +4 -0
  320. package/build/utils/request.d.ts.map +1 -0
  321. package/build/utils/request.js +27 -0
  322. package/build/utils/request.js.map +1 -0
  323. package/build/utils/scope.d.ts +17 -13
  324. package/build/utils/scope.d.ts.map +1 -1
  325. package/build/utils/scope.js +21 -35
  326. package/build/utils/scope.js.map +1 -1
  327. package/build/utils/types.d.ts +8 -0
  328. package/build/utils/types.d.ts.map +1 -0
  329. package/build/utils/types.js +2 -0
  330. package/build/utils/types.js.map +1 -0
  331. package/package.json +30 -22
  332. package/src/actions/apply-schema-request.ts +36 -0
  333. package/src/actions/commit.ts +7 -5
  334. package/src/actions/define-schemas.ts +34 -0
  335. package/src/actions/execute.ts +32 -34
  336. package/src/actions/execution/types.ts +34 -0
  337. package/src/actions/execution.ts +100 -0
  338. package/src/actions/get-card.ts +14 -0
  339. package/src/actions/get-specification.ts +14 -0
  340. package/src/actions/index.ts +13 -3
  341. package/src/actions/links.ts +8 -6
  342. package/src/actions/list-card-specifications.ts +15 -0
  343. package/src/actions/list-card-transitions.ts +15 -0
  344. package/src/actions/list-cards.ts +14 -0
  345. package/src/actions/list-schemas.ts +29 -0
  346. package/src/actions/list-specification-revisions.ts +15 -0
  347. package/src/actions/resources.ts +79 -0
  348. package/src/actions/summarize-cards.ts +15 -0
  349. package/src/actions/transitions.ts +6 -4
  350. package/src/actions/types.ts +5 -0
  351. package/src/actions/watch.ts +11 -7
  352. package/src/conformance/assert/types.ts +16 -0
  353. package/src/conformance/assert.ts +67 -0
  354. package/src/conformance/cases.ts +631 -0
  355. package/src/conformance/clock.ts +11 -0
  356. package/src/conformance/consts.ts +165 -0
  357. package/src/conformance/errors.ts +7 -0
  358. package/src/conformance/fixtures/types.ts +11 -0
  359. package/src/conformance/fixtures.ts +46 -0
  360. package/src/conformance/index.ts +14 -0
  361. package/src/conformance/resources.ts +227 -0
  362. package/src/conformance/types.ts +20 -0
  363. package/src/consts.ts +6 -0
  364. package/src/definitions.ts +264 -0
  365. package/src/executor/changes/types.ts +38 -0
  366. package/src/executor/changes.ts +49 -68
  367. package/src/executor/code/types.ts +23 -0
  368. package/src/executor/code.ts +62 -69
  369. package/src/executor/consts.local.ts +8 -0
  370. package/src/executor/creator/types.ts +33 -0
  371. package/src/executor/creator.ts +43 -0
  372. package/src/executor/resolve/types.ts +30 -0
  373. package/src/executor/resolve.ts +181 -139
  374. package/src/executor/types.ts +23 -0
  375. package/src/executor/validate.ts +40 -38
  376. package/src/executor.ts +33 -27
  377. package/src/facade.ts +132 -29
  378. package/src/helper.ts +12 -11
  379. package/src/hierarchy/types.ts +20 -0
  380. package/src/hierarchy.ts +113 -0
  381. package/src/index.ts +1 -0
  382. package/src/invariants/types.ts +6 -0
  383. package/src/invariants.ts +106 -0
  384. package/src/projection.ts +12 -18
  385. package/src/queue/consts.ts +5 -0
  386. package/src/queue/declare.ts +7 -0
  387. package/src/queue/index.ts +3 -25
  388. package/src/queue/types.ts +14 -0
  389. package/src/registry.ts +18 -36
  390. package/src/resources.ts +256 -0
  391. package/src/service.ts +16 -20
  392. package/src/store/commits.ts +5 -4
  393. package/src/store/composite.ts +7 -2
  394. package/src/store/consts.local.ts +1 -0
  395. package/src/store/fold/types.ts +44 -0
  396. package/src/store/fold.ts +214 -212
  397. package/src/store/index.ts +1 -0
  398. package/src/store/memory.ts +0 -0
  399. package/src/store/types.local.ts +3 -0
  400. package/src/store/types.ts +16 -1
  401. package/src/types.local.ts +7 -0
  402. package/src/types.ts +139 -6
  403. package/src/utils/access/types.ts +30 -0
  404. package/src/utils/access.ts +33 -0
  405. package/src/utils/guard/types.ts +21 -0
  406. package/src/utils/guard.ts +44 -31
  407. package/src/utils/handler/types.ts +28 -0
  408. package/src/utils/handler.ts +35 -0
  409. package/src/utils/index.ts +9 -0
  410. package/src/utils/request/types.ts +23 -0
  411. package/src/utils/request.ts +40 -0
  412. package/src/utils/scope.ts +39 -42
  413. package/src/utils/types.ts +8 -0
  414. package/tests/access.spec.ts +160 -0
  415. package/tests/app.spec.ts +112 -0
  416. package/tests/conformance.spec.ts +47 -0
  417. package/tests/create-example.spec.ts +202 -0
  418. package/tests/creator.spec.ts +257 -0
  419. package/tests/entrypoints.spec.ts +5 -0
  420. package/tests/memory-store.spec.ts +52 -0
  421. package/tests/narrowing.spec.ts +94 -0
  422. package/tests/plugins.spec.ts +4 -3
  423. package/tests/writes.spec.ts +140 -0
  424. package/build/actions/cards.d.ts +0 -16
  425. package/build/actions/cards.d.ts.map +0 -1
  426. package/build/actions/cards.js +0 -32
  427. package/build/actions/cards.js.map +0 -1
  428. package/build/actions/schemas.d.ts +0 -6
  429. package/build/actions/schemas.d.ts.map +0 -1
  430. package/build/actions/schemas.js +0 -9
  431. package/build/actions/schemas.js.map +0 -1
  432. package/build/actions/specs.d.ts +0 -10
  433. package/build/actions/specs.d.ts.map +0 -1
  434. package/build/actions/specs.js +0 -14
  435. package/build/actions/specs.js.map +0 -1
  436. package/src/actions/cards.ts +0 -52
  437. package/src/actions/schemas.ts +0 -15
  438. package/src/actions/specs.ts +0 -23
@@ -0,0 +1,257 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { CommitState, makePlanningProtocols, PlanningError, TransitionAction, WorkcardKind } from '@owlmeans/planning'
3
+ import type { PlanningFacade, Transition, TransitionExecution, WorkcardChanges, WorkcardDraft } from '@owlmeans/planning'
4
+ import { executePlanning, executionHelper } from '../src/actions/index.js'
5
+ import { createProject, createTask, ENTITY, invoke, makeTestPlanning, PROFILE, PROJECT, session, TASK } from './context.js'
6
+ import { creatorHelper } from '../src/executor.js'
7
+
8
+ const tree = makePlanningProtocols({ base: { alias: 'test:planning' }, guards: 'test-guard' })
9
+
10
+ const draftOf = (exec: TransitionExecution): WorkcardDraft => exec.card as WorkcardDraft
11
+
12
+ describe('@owlmeans/server-planning — a create\'s createdBy', () => {
13
+ test('an in-process create is stamped with the scope\'s profile, on the card and in the log', async () => {
14
+ const { facade } = await makeTestPlanning()
15
+ const planning = facade()
16
+ const project = await createProject(planning)
17
+ const receipt = await createTask(planning, project.id!)
18
+
19
+ expect(project.createdBy).toBe(PROFILE)
20
+ expect(receipt.card!.createdBy).toBe(PROFILE)
21
+ expect(receipt.transition.changes?.createdBy).toBe(PROFILE)
22
+ expect((await planning.cards.get(receipt.card!.id!)).createdBy).toBe(PROFILE)
23
+ })
24
+
25
+ test('an explicit createdBy wins over the scope', async () => {
26
+ const { facade } = await makeTestPlanning()
27
+ const planning = facade()
28
+ const project = await createProject(planning, 'Delegated', { createdBy: 'owner-7' })
29
+
30
+ expect(project.createdBy).toBe('owner-7')
31
+ expect((await createTask(planning, project.id!, 'Theirs', { createdBy: 'owner-8' })).card!.createdBy).toBe('owner-8')
32
+ })
33
+
34
+ test('the subject falls back to the userId, then to the scope\'s actor', async () => {
35
+ const { service } = await makeTestPlanning()
36
+
37
+ const byUser = await createProject(service.for({ entityId: ENTITY, userId: 'user-9' }))
38
+ const byActor = await createProject(service.for({ entityId: ENTITY, actor: { profileId: 'agent-profile' } }))
39
+
40
+ expect(byUser.createdBy).toBe('user-9')
41
+ expect(byActor.createdBy).toBe('agent-profile')
42
+ })
43
+
44
+ test('a scope that names nobody leaves createdBy unset', async () => {
45
+ const { service } = await makeTestPlanning()
46
+
47
+ const system = await createProject(service.for({ entityId: ENTITY, service: 'importer', channel: 'job' }))
48
+
49
+ expect(system.createdBy).toBeUndefined()
50
+ expect(Object.keys(system)).not.toContain('createdBy')
51
+ })
52
+
53
+ test('later writes by another profile never move it, and the caller\'s execution is not touched', async () => {
54
+ const { service, facade } = await makeTestPlanning()
55
+ const exec: TransitionExecution = {
56
+ card: { kind: WorkcardKind.Project, type: PROJECT, title: 'Kept' }, action: TransitionAction.Create,
57
+ }
58
+ const project = (await facade().execute(exec, { wait: true })).card!
59
+ const other = service.for({ entityId: ENTITY, profileId: 'profile-2' })
60
+
61
+ await other.execute({ card: project.id!, action: TransitionAction.Update, changes: { title: 'Renamed' } }, { wait: true })
62
+ await other.execute({ card: project.id!, action: TransitionAction.Transit, transition: 'activate' }, { wait: true })
63
+
64
+ expect((await other.cards.get(project.id!)).createdBy).toBe(PROFILE)
65
+ expect(draftOf(exec).createdBy).toBeUndefined()
66
+ })
67
+
68
+ test('a before plugin already sees the stamped createdBy', async () => {
69
+ const seen: (string | undefined)[] = []
70
+ const { facade } = await makeTestPlanning({
71
+ plugins: [{
72
+ name: 'owner-check', order: 10,
73
+ before: async exec => { seen.push(typeof exec.card === 'object' ? exec.card.createdBy : 'update') },
74
+ }],
75
+ })
76
+
77
+ await createProject(facade())
78
+
79
+ expect(seen).toEqual([PROFILE])
80
+ })
81
+
82
+ test('the wire action is unchanged: the body\'s claim is replaced by the request\'s subject', async () => {
83
+ const { context } = await makeTestPlanning()
84
+ const execute = executePlanning(tree.execute)
85
+ const body = { card: { kind: WorkcardKind.Project, type: PROJECT, title: 'Wire', createdBy: 'intruder' }, action: TransitionAction.Create, wait: true }
86
+
87
+ const signed = await invoke(execute, context, { ...session(), body })
88
+ expect(signed.card.createdBy).toBe(PROFILE)
89
+
90
+ const byUser = await invoke(execute, context, {
91
+ ...session(ENTITY, { auth: { type: 'test', userId: 'user-1', entitySlug: ENTITY, scopes: ['*'] } }), body,
92
+ })
93
+ expect(byUser.card.createdBy).toBe('user-1')
94
+
95
+ const anonymous = await invoke(execute, context, {
96
+ ...session(ENTITY, { auth: { type: 'test', entitySlug: ENTITY, scopes: ['*'] } }), body,
97
+ })
98
+ expect(anonymous.card.createdBy).toBeUndefined()
99
+ })
100
+
101
+ test('wireExecution still answers the execution the executor will run', () => {
102
+ const body = { card: { kind: WorkcardKind.Project, type: PROJECT, title: 'W', createdBy: 'intruder' }, action: TransitionAction.Create }
103
+
104
+ expect(draftOf(executionHelper.wireExecution(body, { entityId: ENTITY, profileId: PROFILE, userId: 'user-1' })).createdBy).toBe(PROFILE)
105
+ expect(draftOf(executionHelper.wireExecution(body, { entityId: ENTITY, userId: 'user-1' })).createdBy).toBe('user-1')
106
+ expect(draftOf(executionHelper.wireExecution(body, { entityId: ENTITY })).createdBy).toBeUndefined()
107
+ })
108
+
109
+ test('creatorOf and withCreator', () => {
110
+ expect(creatorHelper.creatorOf({ entityId: ENTITY, profileId: 'p', userId: 'u', actor: { profileId: 'a' } })).toBe('p')
111
+ expect(creatorHelper.creatorOf({ entityId: ENTITY, profileId: '', userId: 'u' })).toBe('u')
112
+ expect(creatorHelper.creatorOf({ entityId: ENTITY, actor: { userId: 'au', service: 's' } })).toBe('au')
113
+ expect(creatorHelper.creatorOf({ entityId: ENTITY, service: 's' })).toBeUndefined()
114
+
115
+ const update: TransitionExecution = { card: 'card-1', action: TransitionAction.Update, changes: { title: 'T' } }
116
+ expect(creatorHelper.withCreator(update, { entityId: ENTITY, profileId: 'p' })).toBe(update)
117
+ const blank: TransitionExecution = { card: { kind: WorkcardKind.Project, type: PROJECT, title: 'T', createdBy: '' }, action: TransitionAction.Create }
118
+ expect(draftOf(creatorHelper.withCreator(blank, { entityId: ENTITY, profileId: 'p' })).createdBy).toBe('p')
119
+ })
120
+ })
121
+
122
+ /** The refusal of a write that names a provenance field, and the log it left untouched. */
123
+ const refusedOf = async (run: Promise<unknown>): Promise<string> => {
124
+ const error = await run.then(() => null, (error: Error) => error)
125
+ expect(error).toBeInstanceOf(PlanningError)
126
+ return error!.message
127
+ }
128
+
129
+ const logOf = async (planning: PlanningFacade, card: string): Promise<number> =>
130
+ (await planning.transitions.list({ card })).total
131
+
132
+ describe('@owlmeans/server-planning — createdBy never moves after the create', () => {
133
+ test('an in-process update, transit, unset or create overlay naming it is refused, and nothing is appended', async () => {
134
+ const { facade } = await makeTestPlanning()
135
+ const planning = facade()
136
+ const project = await createProject(planning)
137
+ const task = (await createTask(planning, project.id!)).card!
138
+ const attempts: TransitionExecution[] = [
139
+ { card: task.id!, action: TransitionAction.Update, changes: { createdBy: 'intruder' } },
140
+ { card: task.id!, action: TransitionAction.Update, changes: { title: 'Mine now', createdBy: 'intruder' } },
141
+ { card: task.id!, action: TransitionAction.Update, changes: { createdBy: null } as unknown as WorkcardChanges },
142
+ { card: task.id!, action: TransitionAction.Update, unset: ['createdBy'] },
143
+ { card: task.id!, action: TransitionAction.Transit, transition: 'start', changes: { createdBy: 'intruder' } },
144
+ { card: { kind: WorkcardKind.Card, type: TASK, parent: project.id!, title: 'Overlay' }, action: TransitionAction.Create, changes: { createdBy: 'intruder' } },
145
+ ]
146
+
147
+ for (const exec of attempts) {
148
+ expect(await refusedOf(planning.execute(exec, { wait: true }))).toBe('planning:immutable:createdBy')
149
+ }
150
+ expect(await refusedOf(planning.execute({ card: task.id!, action: TransitionAction.Update, changes: { createdAt: '2001-01-01T00:00:00.000Z' } as WorkcardChanges })))
151
+ .toBe('planning:immutable:createdAt')
152
+ expect(await refusedOf(planning.execute({ card: task.id!, action: TransitionAction.Update, unset: ['createdAt'] })))
153
+ .toBe('planning:immutable:createdAt')
154
+
155
+ const card = await planning.cards.get(task.id!)
156
+ expect([card.createdBy, card.title, card.status, card.seq]).toEqual([PROFILE, 'A task', 'todo', 1])
157
+ expect(await logOf(planning, task.id!)).toBe(1)
158
+ expect((await planning.cards.list({ parent: project.id })).total).toBe(1)
159
+ })
160
+
161
+ test('a before plugin cannot move it either', async () => {
162
+ const { facade } = await makeTestPlanning({
163
+ plugins: [{
164
+ name: 'reassign', order: 10,
165
+ before: async exec => exec.action === TransitionAction.Update
166
+ ? { ...exec, changes: { ...exec.changes, createdBy: 'plugin-owner' } }
167
+ : undefined,
168
+ }],
169
+ })
170
+ const planning = facade()
171
+ const project = await createProject(planning)
172
+
173
+ expect(await refusedOf(planning.execute({ card: project.id!, action: TransitionAction.Update, changes: { title: 'Renamed' } })))
174
+ .toBe('planning:immutable:createdBy')
175
+ expect((await planning.cards.get(project.id!)).createdBy).toBe(PROFILE)
176
+ })
177
+
178
+ test('a trusted in-process create names it; the owner then moves it no more than anyone else', async () => {
179
+ const { facade } = await makeTestPlanning()
180
+ const planning = facade()
181
+ const project = await createProject(planning, 'On behalf', { createdBy: 'owner-7' })
182
+ await planning.execute({ card: project.id!, action: TransitionAction.Update, changes: { title: 'Still theirs' } }, { wait: true })
183
+
184
+ expect(await refusedOf(planning.execute({ card: project.id!, action: TransitionAction.Update, changes: { createdBy: PROFILE } })))
185
+ .toBe('planning:immutable:createdBy')
186
+ expect((await planning.cards.get(project.id!))).toMatchObject({ title: 'Still theirs', createdBy: 'owner-7', seq: 2 })
187
+ })
188
+
189
+ test('the wire refuses it on an update, an unset and a create\'s changes; the draft claim is still dropped', async () => {
190
+ const { context, facade } = await makeTestPlanning()
191
+ const execute = executePlanning(tree.execute)
192
+ const created = await invoke(execute, context, {
193
+ ...session(), body: { card: { kind: WorkcardKind.Project, type: PROJECT, title: 'Wire', createdBy: 'intruder' }, action: TransitionAction.Create, wait: true },
194
+ })
195
+ const id = created.card.id as string
196
+ const bodies = [
197
+ { card: id, action: TransitionAction.Update, changes: { createdBy: 'intruder' }, wait: true },
198
+ { card: id, action: TransitionAction.Update, changes: { title: 'Hijacked', createdBy: 'intruder' }, wait: true },
199
+ { card: id, action: TransitionAction.Update, unset: ['createdBy'], wait: true },
200
+ { card: id, action: TransitionAction.Transit, transition: 'activate', changes: { createdBy: 'intruder' }, wait: true },
201
+ { card: { kind: WorkcardKind.Project, type: PROJECT, title: 'Overlay' }, action: TransitionAction.Create, changes: { createdBy: 'intruder' }, wait: true },
202
+ ]
203
+
204
+ for (const body of bodies) {
205
+ expect(await refusedOf(invoke(execute, context, { ...session(), body }))).toBe('planning:immutable:createdBy')
206
+ }
207
+ expect(await refusedOf(invoke(execute, context, { ...session(), body: { card: id, action: TransitionAction.Update, unset: ['createdAt'] } })))
208
+ .toBe('planning:immutable:createdAt')
209
+
210
+ expect(created.card.createdBy).toBe(PROFILE)
211
+ const card = await facade().cards.get(id)
212
+ expect([card.createdBy, card.title, card.status, card.seq]).toEqual([PROFILE, 'Wire', 'open', 1])
213
+ expect(await logOf(facade(), id)).toBe(1)
214
+ expect((await facade().cards.list({ kind: WorkcardKind.Project })).total).toBe(1)
215
+ })
216
+
217
+ test('a legacy transition that named it still folds, and the card keeps committing past it', async () => {
218
+ const { facade, store } = await makeTestPlanning()
219
+ const planning = facade()
220
+ const project = await createProject(planning)
221
+ const task = (await createTask(planning, project.id!)).card!
222
+ const legacy = async (patch: Partial<Transition>) => {
223
+ const seq = await store.transitions!.nextSeq(task.id!, null)
224
+ return await store.transitions!.append({
225
+ entityId: task.entityId, card: task.id!, kind: task.kind, type: task.type, project: project.id!, seq,
226
+ action: TransitionAction.Update, changes: {}, actor: { profileId: 'intruder' },
227
+ at: '2001-01-01T00:00:00.000Z', commit: { state: CommitState.Pending }, ...patch,
228
+ })
229
+ }
230
+
231
+ const renamed = await legacy({ changes: { title: 'Legacy', createdBy: 'intruder' } })
232
+ const cleared = await legacy({ unset: ['createdBy'] })
233
+ await store.cards.project(task.id!)
234
+
235
+ expect((await planning.commits.status(renamed.id!)).state).toBe(CommitState.Committed)
236
+ expect((await planning.commits.status(cleared.id!)).state).toBe(CommitState.Committed)
237
+ const folded = await planning.cards.get(task.id!)
238
+ expect([folded.title, folded.seq, folded.createdBy]).toEqual(['Legacy', 3, undefined])
239
+
240
+ await planning.execute({ card: task.id!, action: TransitionAction.Update, changes: { title: 'After' } }, { wait: true })
241
+ expect(await refusedOf(planning.execute({ card: task.id!, action: TransitionAction.Update, changes: { createdBy: PROFILE } })))
242
+ .toBe('planning:immutable:createdBy')
243
+ expect((await planning.cards.get(task.id!))).toMatchObject({ title: 'After', seq: 4 })
244
+ })
245
+
246
+ test('assertCreatorFixed', () => {
247
+ const draft = { kind: WorkcardKind.Project, type: PROJECT, title: 'T', createdBy: 'owner' }
248
+
249
+ expect(() => creatorHelper.assertCreatorFixed({ card: draft, action: TransitionAction.Create })).not.toThrow()
250
+ expect(() => creatorHelper.assertCreatorFixed({ card: 'c', action: TransitionAction.Update, changes: { title: 'T', createdBy: undefined } })).not.toThrow()
251
+ expect(() => creatorHelper.assertCreatorFixed({ card: 'c', action: TransitionAction.Update, unset: ['fields.createdBy'] })).not.toThrow()
252
+ expect(() => creatorHelper.assertCreatorFixed({ card: 'c', action: TransitionAction.Update, changes: { fields: { createdBy: 'x' } } })).not.toThrow()
253
+ expect(() => creatorHelper.assertCreatorFixed({ card: 'c', action: TransitionAction.Update, changes: { createdBy: 'x' } })).toThrow('planning:immutable:createdBy')
254
+ expect(() => creatorHelper.assertCreatorFixed({ card: 'c', action: TransitionAction.Delete, unset: ['createdBy'] })).toThrow('planning:immutable:createdBy')
255
+ expect(() => creatorHelper.assertCreatorFixed({ card: draft, action: TransitionAction.Create, changes: { createdBy: 'x' } })).toThrow('planning:immutable:createdBy')
256
+ })
257
+ })
@@ -16,6 +16,11 @@ describe('@owlmeans/server-planning — entrypoints', () => {
16
16
 
17
17
  expect(bindings.length).toBe(declared.length)
18
18
  expect(new Set(bindings.map(binding => binding.alias))).toEqual(new Set(declared.map(protocol => protocol.alias)))
19
+
20
+ const scoped = makePlanningProtocols({ base: { alias: 'test:planning' }, guards: 'test-guard', definitions: true })
21
+ const all = protocols(scoped as never)
22
+ expect(servePlanningEntrypoints(scoped).map(binding => binding.alias).sort()).toEqual(all.map(protocol => protocol.alias).sort())
23
+ expect(all.length).toBe(declared.length + 1)
19
24
  })
20
25
 
21
26
  test('execute takes the actor and createdBy from the request, never from the body', async () => {
@@ -52,6 +52,58 @@ describe('@owlmeans/server-planning — the memory store', () => {
52
52
  expect(await store.cards.purge(kept.id!, ENTITY)).toBe(4)
53
53
  })
54
54
 
55
+ test('deleting a card takes its own documents with their log, edges and comments, and keeps its own delete', async () => {
56
+ const { facade, store } = await makeTestPlanning()
57
+ const planning = facade()
58
+ const project = await createProject(planning)
59
+ const task = (await createTask(planning, project.id!, 'Documented')).card!
60
+ const other = (await createTask(planning, project.id!, 'Other')).card!
61
+ const design = (await planning.execute({
62
+ card: { kind: WorkcardKind.Specification, type: SPEC, parent: task.id!, title: 'design', category: 'design', format: SpecificationFormat.Json, body: '{}' },
63
+ action: TransitionAction.Create,
64
+ }, { wait: true })).card!
65
+ // A document of the document, an edge into it and a comment with its mention — no fixture
66
+ // type declares them, so they are written to the store directly.
67
+ const at = new Date().toISOString()
68
+ await store.cards.put({ ...design, id: 'nested-document', parent: design.id!, parents: [design.id!], title: 'nested' })
69
+ await store.links.put({ entityId: ENTITY, type: 'blocks', from: other.id!, to: design.id!, createdAt: at })
70
+ const comment = await store.comments!.put({ entityId: ENTITY, card: design.id!, author: 'author-1', body: 'Looks right', version: 1, createdAt: at })
71
+ const mention = await store.mentions!.put({ entityId: ENTITY, comment: comment.id!, card: design.id!, assignee: 'author-1', revision: 1, version: 1, createdAt: at })
72
+
73
+ const deleted = await planning.execute({ card: task.id!, action: TransitionAction.Delete }, { wait: true })
74
+
75
+ expect((await planning.commits.status(deleted.transition.id!)).state).toBe(CommitState.Committed)
76
+ for (const id of [task.id!, design.id!, 'nested-document']) {
77
+ expect(await planning.cards.load(id)).toBeNull()
78
+ }
79
+ expect((await planning.cards.list({ kind: WorkcardKind.Specification })).total).toBe(0)
80
+ expect((await store.transitions.list({ entityId: ENTITY, card: design.id! })).total).toBe(0)
81
+ expect((await store.transitions.list({ entityId: ENTITY, card: task.id! })).items.map(row => row.action))
82
+ .toEqual([TransitionAction.Create, TransitionAction.Delete])
83
+ expect((await store.links.list({ entityId: ENTITY, to: design.id! })).total).toBe(0)
84
+ expect(await store.comments!.get(comment.id!, ENTITY)).toBeNull()
85
+ expect(await store.mentions!.get(mention.id!, ENTITY)).toBeNull()
86
+ expect(await planning.cards.load(other.id!)).not.toBeNull()
87
+ })
88
+
89
+ test('a card with a child card is refused its delete, documents or not', async () => {
90
+ const { facade, store } = await makeTestPlanning()
91
+ const planning = facade()
92
+ const project = await createProject(planning)
93
+ const task = (await createTask(planning, project.id!, 'Parent')).card!
94
+ await planning.execute({
95
+ card: { kind: WorkcardKind.Specification, type: SPEC, parent: task.id!, title: 'design', category: 'design', format: SpecificationFormat.Json, body: '{}' },
96
+ action: TransitionAction.Create,
97
+ }, { wait: true })
98
+ // No fixture type admits a child card under a task, so the child is written to the store directly.
99
+ await store.cards.put({ ...task, id: 'child-card', code: undefined, parent: task.id!, parents: [task.id!], title: 'Child' })
100
+
101
+ await expect(planning.execute({ card: task.id!, action: TransitionAction.Delete }, { wait: true }))
102
+ .rejects.toThrow(`hierarchy:parent-in-use:${task.id}`)
103
+ expect(await planning.cards.load(task.id!)).not.toBeNull()
104
+ expect((await planning.specifications.current(task.id!, 'design'))?.body).toBe('{}')
105
+ })
106
+
55
107
  test('byKey answers the first transition under that key, within its entity only', async () => {
56
108
  const { facade, store } = await makeTestPlanning()
57
109
  const planning = facade()
@@ -0,0 +1,94 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { WorkcardKind, WorkcardNotFound } from '@owlmeans/planning'
3
+ import type { PlanningFacade, Workcard } from '@owlmeans/planning'
4
+ import { createProject, createTask, ENTITY, makeTestPlanning, PROFILE } from './context.js'
5
+
6
+ /**
7
+ * Two projects with a task each, a design document on each task and a plan on the first project —
8
+ * a specification's `parents` hold only its own card, so a scope narrowed to one project sees the
9
+ * task's document through the task.
10
+ */
11
+ const fixture = async () => {
12
+ const planning = await makeTestPlanning()
13
+ const owner = planning.facade()
14
+ const granted = await createProject(owner, 'Granted')
15
+ const hidden = await createProject(owner, 'Hidden')
16
+ const seen = (await createTask(owner, granted.id!, 'Seen')).card!
17
+ const unseen = (await createTask(owner, hidden.id!, 'Unseen')).card!
18
+ const write = async (card: Workcard, category: string, body: string): Promise<Workcard> =>
19
+ (await (await owner.model(card)).write(category, body, { wait: true })).card!
20
+ const design = await write(seen, 'design', '{"steps":[1]}')
21
+ const hiddenDesign = await write(unseen, 'design', '{"steps":[2]}')
22
+ const plan = await write(granted, 'plan', '{"steps":[3]}')
23
+ const scoped = (projects: string[]): PlanningFacade =>
24
+ planning.service.for({ entityId: ENTITY, profileId: PROFILE, channel: 'test', projects })
25
+
26
+ return { ...planning, owner, granted, hidden, seen, unseen, design, hiddenDesign, plan, scoped }
27
+ }
28
+
29
+ const ids = (items: Workcard[]): string[] => items.map(card => card.id!).sort()
30
+
31
+ describe('@owlmeans/server-planning — a narrowed scope lists what it reads', () => {
32
+ test('a granted card\'s specification is listed and counted as it is read', async () => {
33
+ const { seen, unseen, design, hiddenDesign, plan, granted, scoped } = await fixture()
34
+ const member = scoped([granted.id!])
35
+
36
+ expect((await member.cards.get(design.id!)).id).toBe(design.id!)
37
+ await expect(member.cards.get(hiddenDesign.id!)).rejects.toBeInstanceOf(WorkcardNotFound)
38
+
39
+ const underCard = await member.cards.list({ parent: seen.id!, kind: WorkcardKind.Specification })
40
+ expect(ids(underCard.items)).toEqual([design.id!])
41
+ expect(underCard.total).toBe(1)
42
+ expect(await member.cards.count({ parent: seen.id!, kind: WorkcardKind.Specification })).toBe(1)
43
+ expect((await member.cards.list({ parent: unseen.id!, kind: WorkcardKind.Specification })).items).toEqual([])
44
+ expect(await member.cards.count({ parent: unseen.id! })).toBe(0)
45
+
46
+ const everyDocument = await member.cards.list({ kind: WorkcardKind.Specification })
47
+ expect(ids(everyDocument.items)).toEqual([design.id!, plan.id!].sort())
48
+ expect(await member.cards.count({ kind: WorkcardKind.Specification })).toBe(2)
49
+ expect(ids((await member.cards.list({ kind: WorkcardKind.Card })).items)).toEqual([seen.id!])
50
+
51
+ expect(ids((await member.cards.list({ parent: seen.id!, category: 'design' })).items)).toEqual([design.id!])
52
+ expect((await member.specifications.list(seen.id!)).total).toBe(1)
53
+ // A summary counts a parent's cards, never its documents.
54
+ expect(Object.keys(await member.cards.summary([granted.id!, seen.id!, unseen.id!]))).toEqual([granted.id!])
55
+ })
56
+
57
+ test('every card the narrowed scope reads is the one it lists, and no other', async () => {
58
+ const { owner, granted, scoped } = await fixture()
59
+ const member = scoped([granted.id!])
60
+ const every = { size: 0, kind: [WorkcardKind.Project, WorkcardKind.Card, WorkcardKind.Specification] }
61
+ const everything = (await owner.cards.list(every)).items
62
+ const listed = new Set(ids((await member.cards.list(every)).items))
63
+
64
+ expect(everything.length).toBe(7)
65
+ for (const card of everything) {
66
+ expect([card.id, (await member.cards.load(card.id!)) != null]).toEqual([card.id, listed.has(card.id!)])
67
+ }
68
+ expect(listed.size).toBe(4)
69
+ expect(await member.cards.count(every)).toBe(listed.size)
70
+ })
71
+
72
+ test('a scope with no project, or another one, sees none of the granted project\'s documents', async () => {
73
+ const { seen, design, plan, hidden, scoped } = await fixture()
74
+
75
+ for (const outsider of [scoped([]), scoped([hidden.id!])]) {
76
+ await expect(outsider.cards.get(design.id!)).rejects.toBeInstanceOf(WorkcardNotFound)
77
+ expect((await outsider.cards.list({ parent: seen.id!, kind: WorkcardKind.Specification })).items).toEqual([])
78
+ expect(await outsider.cards.count({ parent: seen.id! })).toBe(0)
79
+ const documents = ids((await outsider.cards.list({ kind: WorkcardKind.Specification })).items)
80
+ expect(documents).not.toContain(design.id!)
81
+ expect(documents).not.toContain(plan.id!)
82
+ }
83
+ expect((await scoped([]).cards.list({ size: 0, kind: [WorkcardKind.Card, WorkcardKind.Specification] })).total).toBe(0)
84
+ })
85
+
86
+ test('an unnarrowed scope sees every document of the organization', async () => {
87
+ const { owner, unseen, design, hiddenDesign, plan } = await fixture()
88
+
89
+ expect(ids((await owner.cards.list({ kind: WorkcardKind.Specification })).items))
90
+ .toEqual([design.id!, hiddenDesign.id!, plan.id!].sort())
91
+ expect(await owner.cards.count({ kind: WorkcardKind.Specification })).toBe(3)
92
+ expect(ids((await owner.cards.list({ parent: unseen.id!, kind: WorkcardKind.Specification })).items)).toEqual([hiddenDesign.id!])
93
+ })
94
+ })
@@ -1,3 +1,4 @@
1
+ import { configureLog, logConfig } from '@owlmeans/log'
1
2
  import { describe, expect, test } from 'bun:test'
2
3
  import { CommitState, PlanningRefused, TransitionAction, WorkcardKind } from '@owlmeans/planning'
3
4
  import { makeMemoryPlanningStore } from '../src/store/memory.js'
@@ -66,8 +67,8 @@ describe('@owlmeans/server-planning — plugins', () => {
66
67
  ],
67
68
  }, { sync: false })
68
69
  const planning = facade()
69
- const errors = console.error
70
- console.error = () => undefined
70
+ const { level } = logConfig()
71
+ configureLog({ level: 'silent' })
71
72
  try {
72
73
  const receipt = await planning.execute({ card: { kind: WorkcardKind.Project, type: PROJECT, title: 'Later' }, action: TransitionAction.Create })
73
74
  expect(receipt.transition.commit.state).toBe(CommitState.Pending)
@@ -80,7 +81,7 @@ describe('@owlmeans/server-planning — plugins', () => {
80
81
  expect(seen).toEqual([`${receipt.transition.card}:committed:test`])
81
82
  expect((await receipt.committed({ timeout: 100 }))!.title).toBe('Later')
82
83
  } finally {
83
- console.error = errors
84
+ configureLog({ level })
84
85
  }
85
86
  })
86
87
  })
@@ -0,0 +1,140 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import {
3
+ makePlanningProtocols, PlanningForbidden, SchemaOrigin, SpecificationFormat, TransitionAction, WorkcardKind,
4
+ } from '@owlmeans/planning'
5
+ import type { SchemaDefineReply, ScopedSchemaBundle, Specification } from '@owlmeans/planning'
6
+ import { defineSchemas, executePlanning, getCard, listCards, listSchemas } from '../src/actions/index.js'
7
+ import type { PlanningAccess, PlanningHandlerOptions } from '../src/types.js'
8
+ import { createProject, createTask, ENTITY, invoke, makeTestPlanning, PROJECT, session, TASK } from './context.js'
9
+
10
+ const tree = makePlanningProtocols({ base: { alias: 'test:planning' }, guards: 'test-guard', definitions: true })
11
+
12
+ const withAccess = (access: PlanningAccess): PlanningHandlerOptions => ({ access: async () => access })
13
+
14
+ const flow = { id: 'test:rota', version: 1, statuses: [{ key: 'due', intrinsic: 'planned' }], transitions: [] }
15
+
16
+ /** Two projects: the caller views both and writes only in `written`. */
17
+ const setup = async () => {
18
+ const planning = await makeTestPlanning({}, { schemas: true })
19
+ const { facade } = planning
20
+ const viewed = await createProject(facade(), 'Viewed')
21
+ const written = await createProject(facade(), 'Written')
22
+ const viewedTask = (await createTask(facade(), viewed.id!, 'Viewed task')).card!
23
+ const writtenTask = (await createTask(facade(), written.id!, 'Written task')).card!
24
+ const access: PlanningAccess = { entityId: ENTITY, projects: [viewed.id!, written.id!], writes: [written.id!] }
25
+
26
+ return { ...planning, viewed, written, viewedTask, writtenTask, access, opts: withAccess(access) }
27
+ }
28
+
29
+ describe('@owlmeans/server-planning — per-project writes', () => {
30
+ test('a viewer reads a project it may not write in', async () => {
31
+ const { context, viewed, viewedTask, opts } = await setup()
32
+
33
+ expect((await invoke(getCard(tree.card.get, opts), context, { ...session(), params: { id: viewedTask.id } })).id).toBe(viewedTask.id)
34
+ const listed = await invoke(listCards(tree.card.list, opts), context, { ...session(), query: { parent: viewed.id } })
35
+ expect(listed.items.map((card: { id: string }) => card.id)).toEqual([viewedTask.id])
36
+ const bundle: ScopedSchemaBundle = await invoke(listSchemas(tree.schema.list), context, { ...session(), query: { project: viewed.id } })
37
+ expect(bundle.types.some(type => type.type === TASK)).toBe(true)
38
+ })
39
+
40
+ test('a viewer\'s execute answers 403 on a card, a project, a create and a link; nothing is appended', async () => {
41
+ const { context, facade, viewed, viewedTask, writtenTask, opts } = await setup()
42
+ const execute = executePlanning(tree.execute, opts)
43
+ const refused = async (body: Record<string, unknown>) =>
44
+ await expect(invoke(execute, context, { ...session(), body })).rejects.toBeInstanceOf(PlanningForbidden)
45
+
46
+ expect(PlanningForbidden.httpStatus).toBe(403)
47
+ await refused({ card: viewedTask.id, action: TransitionAction.Update, changes: { title: 'Renamed' } })
48
+ await refused({ card: viewedTask.id, action: TransitionAction.Transit, transition: 'start' })
49
+ await refused({ card: viewed.id, action: TransitionAction.Update, changes: { title: 'Renamed' } })
50
+ await refused({ card: viewed.id, action: TransitionAction.Delete })
51
+ await refused({ card: { kind: WorkcardKind.Card, type: TASK, parent: viewed.id, title: 'Sneaky' }, action: TransitionAction.Create })
52
+ await refused({ card: { kind: WorkcardKind.Card, type: TASK, parent: writtenTask.id, parents: [viewed.id], title: 'Sneaky' }, action: TransitionAction.Create })
53
+ await refused({ card: viewedTask.id, action: TransitionAction.Link, link: { type: 'blocks', to: writtenTask.id } })
54
+
55
+ expect((await facade().cards.get(viewedTask.id!)).title).toBe('Viewed task')
56
+ expect((await facade().cards.get(viewed.id!)).title).toBe('Viewed')
57
+ expect((await facade().cards.list({ parent: viewed.id })).items.map(card => card.id)).toEqual([viewedTask.id])
58
+ })
59
+
60
+ test('a specification is written through its parent card', async () => {
61
+ const { context, facade, viewedTask, writtenTask, opts } = await setup()
62
+ const execute = executePlanning(tree.execute, opts)
63
+ const design = async (task: string) => ((await (await facade().model(await facade().cards.get(task)))
64
+ .write('design', '{"v":1}', { wait: true })).card as Specification)
65
+ const viewedDesign = await design(viewedTask.id!)
66
+ const writtenDesign = await design(writtenTask.id!)
67
+
68
+ await expect(invoke(execute, context, {
69
+ ...session(), body: { card: viewedDesign.id, action: TransitionAction.Update, changes: { body: '{"v":2}' } },
70
+ })).rejects.toBeInstanceOf(PlanningForbidden)
71
+ await expect(invoke(execute, context, {
72
+ ...session(), body: {
73
+ card: { kind: WorkcardKind.Specification, type: viewedDesign.type, parent: viewedTask.id, category: 'design', format: SpecificationFormat.Json, title: 'd', body: '{}' },
74
+ action: TransitionAction.Create,
75
+ },
76
+ })).rejects.toBeInstanceOf(PlanningForbidden)
77
+ const updated = await invoke(execute, context, {
78
+ ...session(), body: { card: writtenDesign.id, action: TransitionAction.Update, changes: { body: '{"v":2}' }, wait: true },
79
+ })
80
+ expect(updated.card.body).toBe('{"v":2}')
81
+ })
82
+
83
+ test('a writer writes in its project: create, transit, link, and its project layer', async () => {
84
+ const { context, facade, written, writtenTask, viewedTask, opts } = await setup()
85
+ const execute = executePlanning(tree.execute, opts)
86
+
87
+ const made = await invoke(execute, context, {
88
+ ...session(), body: { card: { kind: WorkcardKind.Card, type: TASK, parent: written.id, title: 'Allowed' }, action: TransitionAction.Create, wait: true },
89
+ })
90
+ expect(made.card.parent).toBe(written.id)
91
+ await invoke(execute, context, { ...session(), body: { card: writtenTask.id, action: TransitionAction.Transit, transition: 'start', wait: true } })
92
+ expect((await facade().cards.get(writtenTask.id!)).status).toBe('doing')
93
+ // A link FROM a writable card may point at a card the caller only views.
94
+ await invoke(execute, context, {
95
+ ...session(), body: { card: writtenTask.id, action: TransitionAction.Link, link: { type: 'blocks', to: viewedTask.id }, wait: true },
96
+ })
97
+ await invoke(execute, context, { ...session(), body: { card: written.id, action: TransitionAction.Update, changes: { title: 'Renamed' }, wait: true } })
98
+ expect((await facade().cards.get(written.id!)).title).toBe('Renamed')
99
+
100
+ const layer: SchemaDefineReply = await invoke(defineSchemas(tree.schema.define!, opts), context, {
101
+ ...session(), body: { project: written.id, flows: [flow] },
102
+ })
103
+ expect(layer.bundle.origins?.flows['test:rota']).toBe(SchemaOrigin.Project)
104
+ })
105
+
106
+ test('a viewer\'s project-layer schema define answers 403; writes never reach the root', async () => {
107
+ const { context, viewed, access, opts } = await setup()
108
+
109
+ await expect(invoke(defineSchemas(tree.schema.define!, opts), context, {
110
+ ...session(), body: { project: viewed.id, flows: [flow] },
111
+ })).rejects.toBeInstanceOf(PlanningForbidden)
112
+ // `writes` does not gate the organization layer (that is `grants.defineSchemas`), nor a project create.
113
+ const project = await invoke(executePlanning(tree.execute, opts), context, {
114
+ ...session(), body: { card: { kind: WorkcardKind.Project, type: PROJECT, title: 'New' }, action: TransitionAction.Create, wait: true },
115
+ })
116
+ expect(project.card.kind).toBe(WorkcardKind.Project)
117
+ await expect(invoke(defineSchemas(tree.schema.define!, withAccess({ ...access, grants: { defineSchemas: [viewed.id!] } })), context, {
118
+ ...session(), body: { project: viewed.id, flows: [flow] },
119
+ })).rejects.toBeInstanceOf(PlanningForbidden)
120
+ // A project create stays `createProjects`' decision when grants are present.
121
+ await expect(invoke(executePlanning(tree.execute, withAccess({ ...access, grants: {} })), context, {
122
+ ...session(), body: { card: { kind: WorkcardKind.Project, type: PROJECT, title: 'Refused' }, action: TransitionAction.Create },
123
+ })).rejects.toBeInstanceOf(PlanningForbidden)
124
+ })
125
+
126
+ test('absent writes keep the old behaviour: every visible project is writable', async () => {
127
+ const { context, facade, viewed, viewedTask, access } = await setup()
128
+ const { writes: _writes, ...open } = access
129
+ const opts = withAccess(open)
130
+
131
+ await invoke(executePlanning(tree.execute, opts), context, {
132
+ ...session(), body: { card: viewedTask.id, action: TransitionAction.Update, changes: { title: 'Renamed' }, wait: true },
133
+ })
134
+ expect((await facade().cards.get(viewedTask.id!)).title).toBe('Renamed')
135
+ const layer: SchemaDefineReply = await invoke(defineSchemas(tree.schema.define!, opts), context, {
136
+ ...session(), body: { project: viewed.id, flows: [flow] },
137
+ })
138
+ expect(layer.records.map(record => record.key)).toEqual(['test:rota'])
139
+ })
140
+ })
@@ -1,16 +0,0 @@
1
- import { handlers } from '@owlmeans/server-api';
2
- import type { PlanningProtocols } from '@owlmeans/planning';
3
- import type { Context, PlanningHandlerOptions } from '../types.js';
4
- type RequestHandler = ReturnType<ReturnType<typeof handlers<Context>>['request']>;
5
- /** Cards of the caller's entity; the query arrives in its wire shape and is decoded here. */
6
- export declare const listCards: (protocol: PlanningProtocols['card']['list'], opts?: PlanningHandlerOptions) => RequestHandler;
7
- /** Intrinsic counts of DIRECT children per parent. */
8
- export declare const summarizeCards: (protocol: PlanningProtocols['card']['summary'], opts?: PlanningHandlerOptions) => RequestHandler;
9
- /** @throws {WorkcardNotFound} for an absent card and for another entity's alike */
10
- export declare const getCard: (protocol: PlanningProtocols['card']['get'], opts?: PlanningHandlerOptions) => RequestHandler;
11
- /** One card's log. */
12
- export declare const listCardTransitions: (protocol: PlanningProtocols['card']['transitions'], opts?: PlanningHandlerOptions) => RequestHandler;
13
- /** One card's specifications — the current document per category unless `all`. */
14
- export declare const listCardSpecifications: (protocol: PlanningProtocols['card']['specifications'], opts?: PlanningHandlerOptions) => RequestHandler;
15
- export {};
16
- //# sourceMappingURL=cards.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cards.d.ts","sourceRoot":"","sources":["../../src/actions/cards.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAI/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,KAAK,EAAE,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAGlE,KAAK,cAAc,GAAG,UAAU,CAAC,UAAU,CAAC,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAA;AAEjF,6FAA6F;AAC7F,eAAO,MAAM,SAAS,aAAc,iBAAiB,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,SAAS,sBAAsB,KAAG,cAKlG,CAAA;AAEL,sDAAsD;AACtD,eAAO,MAAM,cAAc,aAAc,iBAAiB,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,SAAS,sBAAsB,KAAG,cAM1G,CAAA;AAEL,mFAAmF;AACnF,eAAO,MAAM,OAAO,aAAc,iBAAiB,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,SAAS,sBAAsB,KAAG,cAK/F,CAAA;AAEL,sBAAsB;AACtB,eAAO,MAAM,mBAAmB,aAAc,iBAAiB,CAAC,MAAM,CAAC,CAAC,aAAa,CAAC,SAAS,sBAAsB,KAAG,cAMnH,CAAA;AAEL,kFAAkF;AAClF,eAAO,MAAM,sBAAsB,aAAc,iBAAiB,CAAC,MAAM,CAAC,CAAC,gBAAgB,CAAC,SAAS,sBAAsB,KAAG,cAMzH,CAAA"}
@@ -1,32 +0,0 @@
1
- import { handlers } from '@owlmeans/server-api';
2
- import { decodeSpecificationQuery, decodeSummaryQuery, decodeTransitionQuery, decodeWorkcardQuery, } from '@owlmeans/planning';
3
- import { concealed, handlerFacade } from '../utils/index.js';
4
- /** Cards of the caller's entity; the query arrives in its wire shape and is decoded here. */
5
- export const listCards = (protocol, opts) => handlers().request(protocol, async (req, ctx) => concealed(async () => {
6
- const facade = await handlerFacade(ctx, req, opts);
7
- return await facade.cards.list(decodeWorkcardQuery(req.query));
8
- }));
9
- /** Intrinsic counts of DIRECT children per parent. */
10
- export const summarizeCards = (protocol, opts) => handlers().request(protocol, async (req, ctx) => concealed(async () => {
11
- const facade = await handlerFacade(ctx, req, opts);
12
- const { parents, ...query } = decodeSummaryQuery(req.query);
13
- return await facade.cards.summary(parents, query);
14
- }));
15
- /** @throws {WorkcardNotFound} for an absent card and for another entity's alike */
16
- export const getCard = (protocol, opts) => handlers().request(protocol, async (req, ctx) => concealed(async () => {
17
- const facade = await handlerFacade(ctx, req, opts);
18
- return await facade.cards.get(`${req.params.id}`);
19
- }));
20
- /** One card's log. */
21
- export const listCardTransitions = (protocol, opts) => handlers().request(protocol, async (req, ctx) => concealed(async () => {
22
- const facade = await handlerFacade(ctx, req, opts);
23
- const card = await facade.cards.get(`${req.params.id}`);
24
- return await facade.transitions.list({ ...decodeTransitionQuery(req.query), card: card.id });
25
- }));
26
- /** One card's specifications — the current document per category unless `all`. */
27
- export const listCardSpecifications = (protocol, opts) => handlers().request(protocol, async (req, ctx) => concealed(async () => {
28
- const facade = await handlerFacade(ctx, req, opts);
29
- const card = await facade.cards.get(`${req.params.id}`);
30
- return await facade.specifications.list(card.id, decodeSpecificationQuery(req.query));
31
- }));
32
- //# sourceMappingURL=cards.js.map