create-flowdular 0.2.3 → 0.2.5

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 (271) hide show
  1. package/README.md +11 -0
  2. package/agent-template/.agents/skills/agent-tool-design/SKILL.md +203 -0
  3. package/agent-template/.agents/skills/auth-security-review/SKILL.md +90 -0
  4. package/agent-template/.agents/skills/auto-review/SKILL.md +103 -0
  5. package/agent-template/.agents/skills/bug-hunt/SKILL.md +104 -0
  6. package/agent-template/.agents/skills/business-agent-design/SKILL.md +182 -0
  7. package/agent-template/.agents/skills/cli-extension/SKILL.md +108 -0
  8. package/agent-template/.agents/skills/core-extend/SKILL.md +99 -0
  9. package/agent-template/.agents/skills/database-adapter/SKILL.md +198 -0
  10. package/agent-template/.agents/skills/database-adapter/references/first-run-and-matrix.md +105 -0
  11. package/agent-template/.agents/skills/migration-authoring/SKILL.md +161 -0
  12. package/agent-template/.agents/skills/module-new/SKILL.md +171 -0
  13. package/agent-template/.agents/skills/module-update/SKILL.md +91 -0
  14. package/agent-template/.agents/skills/perf-audit/SKILL.md +98 -0
  15. package/agent-template/.agents/skills/release-eject-pr/SKILL.md +107 -0
  16. package/agent-template/.agents/skills/spec-approval/SKILL.md +106 -0
  17. package/agent-template/.agents/skills/test-hardening/SKILL.md +79 -0
  18. package/agent-template/.agents/skills/translations-i18n/SKILL.md +78 -0
  19. package/agent-template/.agents/skills/ux-design/SKILL.md +92 -0
  20. package/agent-template/.agents/skills/variables/SKILL.md +156 -0
  21. package/agent-template/.agents/skills/workflow-development/SKILL.md +192 -0
  22. package/agent-template/.ai/README.md +62 -0
  23. package/agent-template/.ai/agents/README.md +27 -0
  24. package/agent-template/.ai/agents/module-executor.md +36 -0
  25. package/agent-template/.ai/agents/reviewer.md +23 -0
  26. package/agent-template/.ai/agents/sandbox/agentic-engineer.md +31 -0
  27. package/agent-template/.ai/agents/sandbox/backend-engineer.md +36 -0
  28. package/agent-template/.ai/agents/sandbox/business-manager.md +23 -0
  29. package/agent-template/.ai/agents/sandbox/frontend-engineer.md +27 -0
  30. package/agent-template/.ai/agents/sandbox/ux-designer.md +23 -0
  31. package/agent-template/.ai/agents/spec-author.md +29 -0
  32. package/agent-template/.ai/blueprints/add-migration/README.md +5 -0
  33. package/agent-template/.ai/blueprints/add-migration/allowed-paths.yaml +23 -0
  34. package/agent-template/.ai/blueprints/add-migration/blueprint.json +14 -0
  35. package/agent-template/.ai/blueprints/add-migration/examples/invalid/input-destructive.json +6 -0
  36. package/agent-template/.ai/blueprints/add-migration/examples/invalid/plan-unnumbered-file.json +9 -0
  37. package/agent-template/.ai/blueprints/add-migration/examples/valid/input.json +6 -0
  38. package/agent-template/.ai/blueprints/add-migration/examples/valid/plan.json +9 -0
  39. package/agent-template/.ai/blueprints/add-migration/gates.yaml +30 -0
  40. package/agent-template/.ai/blueprints/add-migration/input.schema.json +23 -0
  41. package/agent-template/.ai/blueprints/add-migration/plan.schema.json +54 -0
  42. package/agent-template/.ai/blueprints/add-migration/required-files.yaml +18 -0
  43. package/agent-template/.ai/blueprints/add-migration/spec-requirements.yaml +13 -0
  44. package/agent-template/.ai/blueprints/add-migration/steps.yaml +62 -0
  45. package/agent-template/.ai/blueprints/author-spec/README.md +5 -0
  46. package/agent-template/.ai/blueprints/author-spec/allowed-paths.yaml +7 -0
  47. package/agent-template/.ai/blueprints/author-spec/blueprint.json +14 -0
  48. package/agent-template/.ai/blueprints/author-spec/examples/invalid/input-missing-outcome.json +5 -0
  49. package/agent-template/.ai/blueprints/author-spec/examples/valid/input.json +6 -0
  50. package/agent-template/.ai/blueprints/author-spec/gates.yaml +13 -0
  51. package/agent-template/.ai/blueprints/author-spec/input.schema.json +20 -0
  52. package/agent-template/.ai/blueprints/author-spec/plan.schema.json +14 -0
  53. package/agent-template/.ai/blueprints/author-spec/required-files.yaml +6 -0
  54. package/agent-template/.ai/blueprints/author-spec/spec-requirements.yaml +35 -0
  55. package/agent-template/.ai/blueprints/author-spec/steps.yaml +28 -0
  56. package/agent-template/.ai/blueprints/author-spec/templates/module.yaml +45 -0
  57. package/agent-template/.ai/blueprints/bug-fix/README.md +5 -0
  58. package/agent-template/.ai/blueprints/bug-fix/allowed-paths.yaml +29 -0
  59. package/agent-template/.ai/blueprints/bug-fix/blueprint.json +14 -0
  60. package/agent-template/.ai/blueprints/bug-fix/examples/invalid/input-no-symptom.json +4 -0
  61. package/agent-template/.ai/blueprints/bug-fix/examples/invalid/plan-no-test.json +8 -0
  62. package/agent-template/.ai/blueprints/bug-fix/examples/valid/input.json +6 -0
  63. package/agent-template/.ai/blueprints/bug-fix/examples/valid/plan.json +8 -0
  64. package/agent-template/.ai/blueprints/bug-fix/gates.yaml +30 -0
  65. package/agent-template/.ai/blueprints/bug-fix/input.schema.json +25 -0
  66. package/agent-template/.ai/blueprints/bug-fix/plan.schema.json +53 -0
  67. package/agent-template/.ai/blueprints/bug-fix/required-files.yaml +7 -0
  68. package/agent-template/.ai/blueprints/bug-fix/spec-requirements.yaml +7 -0
  69. package/agent-template/.ai/blueprints/bug-fix/steps.yaml +51 -0
  70. package/agent-template/.ai/blueprints/core-extend/README.md +5 -0
  71. package/agent-template/.ai/blueprints/core-extend/allowed-paths.yaml +49 -0
  72. package/agent-template/.ai/blueprints/core-extend/blueprint.json +14 -0
  73. package/agent-template/.ai/blueprints/core-extend/examples/invalid/input-unknown-package.json +5 -0
  74. package/agent-template/.ai/blueprints/core-extend/examples/invalid/plan-missing-gates.json +7 -0
  75. package/agent-template/.ai/blueprints/core-extend/examples/valid/input.json +6 -0
  76. package/agent-template/.ai/blueprints/core-extend/examples/valid/plan.json +10 -0
  77. package/agent-template/.ai/blueprints/core-extend/gates.yaml +16 -0
  78. package/agent-template/.ai/blueprints/core-extend/input.schema.json +54 -0
  79. package/agent-template/.ai/blueprints/core-extend/plan.schema.json +39 -0
  80. package/agent-template/.ai/blueprints/core-extend/required-files.yaml +36 -0
  81. package/agent-template/.ai/blueprints/core-extend/spec-requirements.yaml +17 -0
  82. package/agent-template/.ai/blueprints/core-extend/steps.yaml +54 -0
  83. package/agent-template/.ai/blueprints/edit-module/README.md +9 -0
  84. package/agent-template/.ai/blueprints/edit-module/allowed-paths.yaml +27 -0
  85. package/agent-template/.ai/blueprints/edit-module/blueprint.json +20 -0
  86. package/agent-template/.ai/blueprints/edit-module/examples/invalid/input-unknown-change.json +5 -0
  87. package/agent-template/.ai/blueprints/edit-module/examples/invalid/plan-touches-platform.json +15 -0
  88. package/agent-template/.ai/blueprints/edit-module/examples/valid/input.json +5 -0
  89. package/agent-template/.ai/blueprints/edit-module/examples/valid/plan.json +25 -0
  90. package/agent-template/.ai/blueprints/edit-module/gates.yaml +30 -0
  91. package/agent-template/.ai/blueprints/edit-module/input.schema.json +31 -0
  92. package/agent-template/.ai/blueprints/edit-module/plan.schema.json +65 -0
  93. package/agent-template/.ai/blueprints/edit-module/required-files.yaml +80 -0
  94. package/agent-template/.ai/blueprints/edit-module/spec-requirements.yaml +15 -0
  95. package/agent-template/.ai/blueprints/edit-module/steps.yaml +115 -0
  96. package/agent-template/.ai/blueprints/new-module/README.md +7 -0
  97. package/agent-template/.ai/blueprints/new-module/allowed-paths.yaml +27 -0
  98. package/agent-template/.ai/blueprints/new-module/blueprint.json +20 -0
  99. package/agent-template/.ai/blueprints/new-module/examples/invalid/input-spec-outside-modules.json +4 -0
  100. package/agent-template/.ai/blueprints/new-module/examples/invalid/plan-unknown-gate.json +8 -0
  101. package/agent-template/.ai/blueprints/new-module/examples/valid/input.json +5 -0
  102. package/agent-template/.ai/blueprints/new-module/examples/valid/plan.json +22 -0
  103. package/agent-template/.ai/blueprints/new-module/gates.yaml +30 -0
  104. package/agent-template/.ai/blueprints/new-module/input.schema.json +21 -0
  105. package/agent-template/.ai/blueprints/new-module/plan.schema.json +58 -0
  106. package/agent-template/.ai/blueprints/new-module/required-files.yaml +73 -0
  107. package/agent-template/.ai/blueprints/new-module/spec-requirements.yaml +30 -0
  108. package/agent-template/.ai/blueprints/new-module/steps.yaml +138 -0
  109. package/agent-template/.ai/blueprints/release/README.md +5 -0
  110. package/agent-template/.ai/blueprints/release/allowed-paths.yaml +19 -0
  111. package/agent-template/.ai/blueprints/release/blueprint.json +14 -0
  112. package/agent-template/.ai/blueprints/release/examples/invalid/input-bad-version.json +4 -0
  113. package/agent-template/.ai/blueprints/release/examples/invalid/plan-bad-branch.json +7 -0
  114. package/agent-template/.ai/blueprints/release/examples/valid/input.json +5 -0
  115. package/agent-template/.ai/blueprints/release/examples/valid/plan.json +20 -0
  116. package/agent-template/.ai/blueprints/release/gates.yaml +20 -0
  117. package/agent-template/.ai/blueprints/release/input.schema.json +24 -0
  118. package/agent-template/.ai/blueprints/release/plan.schema.json +46 -0
  119. package/agent-template/.ai/blueprints/release/required-files.yaml +19 -0
  120. package/agent-template/.ai/blueprints/release/spec-requirements.yaml +8 -0
  121. package/agent-template/.ai/blueprints/release/steps.yaml +47 -0
  122. package/agent-template/.ai/blueprints/security-review/README.md +5 -0
  123. package/agent-template/.ai/blueprints/security-review/allowed-paths.yaml +6 -0
  124. package/agent-template/.ai/blueprints/security-review/blueprint.json +14 -0
  125. package/agent-template/.ai/blueprints/security-review/examples/invalid/input-unknown-kind.json +4 -0
  126. package/agent-template/.ai/blueprints/security-review/examples/invalid/plan-finding-without-scenario.json +14 -0
  127. package/agent-template/.ai/blueprints/security-review/examples/valid/input.json +4 -0
  128. package/agent-template/.ai/blueprints/security-review/examples/valid/plan.json +19 -0
  129. package/agent-template/.ai/blueprints/security-review/gates.yaml +22 -0
  130. package/agent-template/.ai/blueprints/security-review/input.schema.json +20 -0
  131. package/agent-template/.ai/blueprints/security-review/plan.schema.json +65 -0
  132. package/agent-template/.ai/blueprints/security-review/required-files.yaml +6 -0
  133. package/agent-template/.ai/blueprints/security-review/spec-requirements.yaml +9 -0
  134. package/agent-template/.ai/blueprints/security-review/steps.yaml +38 -0
  135. package/agent-template/.ai/examples/README.md +8 -0
  136. package/agent-template/.ai/examples/bad/client-imports-server/README.md +20 -0
  137. package/agent-template/.ai/examples/bad/client-imports-server/api.ts +12 -0
  138. package/agent-template/.ai/examples/bad/missing-acl/README.md +23 -0
  139. package/agent-template/.ai/examples/bad/missing-acl/endpoints.ts +12 -0
  140. package/agent-template/.ai/examples/bad/tenant-from-body/README.md +19 -0
  141. package/agent-template/.ai/examples/bad/tenant-from-body/endpoints.ts +33 -0
  142. package/agent-template/.ai/examples/client-contribution/CustomerListView.tsrx +34 -0
  143. package/agent-template/.ai/examples/client-contribution/README.md +11 -0
  144. package/agent-template/.ai/examples/client-contribution/contribution.tsrx +48 -0
  145. package/agent-template/.ai/examples/client-contribution/index.ts +20 -0
  146. package/agent-template/.ai/examples/client-contribution/permissions.ts +8 -0
  147. package/agent-template/.ai/examples/customer-cli-extension/README.md +14 -0
  148. package/agent-template/.ai/examples/customer-cli-extension/commands.json +17 -0
  149. package/agent-template/.ai/examples/customer-cli-extension/index.ts +36 -0
  150. package/agent-template/.ai/examples/module-create/task-packet.json +11 -0
  151. package/agent-template/.ai/guides/application-development.md +97 -0
  152. package/agent-template/.ai/policies/capabilities.yaml +164 -0
  153. package/agent-template/.ai/policies/model-routing.yaml +72 -0
  154. package/agent-template/.ai/policies/path-ownership.yaml +65 -0
  155. package/agent-template/.ai/policies/task-budgets.yaml +37 -0
  156. package/agent-template/.ai/references/catalog/LICENSE +21 -0
  157. package/agent-template/.ai/references/catalog/migrations/0001_catalog_core.down.sql +2 -0
  158. package/agent-template/.ai/references/catalog/migrations/0001_catalog_core.up.sql +21 -0
  159. package/agent-template/.ai/references/catalog/migrations/0002_catalog_history.down.sql +3 -0
  160. package/agent-template/.ai/references/catalog/migrations/0002_catalog_history.up.sql +20 -0
  161. package/agent-template/.ai/references/catalog/migrations/0003_catalog_history_service_actors.down.sql +3 -0
  162. package/agent-template/.ai/references/catalog/migrations/0003_catalog_history_service_actors.up.sql +36 -0
  163. package/agent-template/.ai/references/catalog/migrations/0004_catalog_idempotency_ledger.down.sql +3 -0
  164. package/agent-template/.ai/references/catalog/migrations/0004_catalog_idempotency_ledger.up.sql +19 -0
  165. package/agent-template/.ai/references/catalog/migrations/README.md +3 -0
  166. package/agent-template/.ai/references/catalog/module.json +27 -0
  167. package/agent-template/.ai/references/catalog/package.json +49 -0
  168. package/agent-template/.ai/references/catalog/spec/module.yaml +86 -0
  169. package/agent-template/.ai/references/catalog/src/acl/permissions.ts +6 -0
  170. package/agent-template/.ai/references/catalog/src/agent/tools.ts +164 -0
  171. package/agent-template/.ai/references/catalog/src/api/endpoints.ts +243 -0
  172. package/agent-template/.ai/references/catalog/src/client/CatalogHistoryDrawer.tsrx +123 -0
  173. package/agent-template/.ai/references/catalog/src/client/CatalogItemForm.tsrx +190 -0
  174. package/agent-template/.ai/references/catalog/src/client/CatalogView.tsrx +473 -0
  175. package/agent-template/.ai/references/catalog/src/client/api.ts +111 -0
  176. package/agent-template/.ai/references/catalog/src/client/contribution.tsrx +61 -0
  177. package/agent-template/.ai/references/catalog/src/client/index.ts +18 -0
  178. package/agent-template/.ai/references/catalog/src/client/navigation-copy.ts +9 -0
  179. package/agent-template/.ai/references/catalog/src/client/state.ts +24 -0
  180. package/agent-template/.ai/references/catalog/src/domain/types.ts +32 -0
  181. package/agent-template/.ai/references/catalog/src/domain/variables.ts +111 -0
  182. package/agent-template/.ai/references/catalog/src/index.ts +31 -0
  183. package/agent-template/.ai/references/catalog/src/platform.ts +35 -0
  184. package/agent-template/.ai/references/catalog/src/server/index.ts +4 -0
  185. package/agent-template/.ai/references/catalog/src/server/runtime.ts +86 -0
  186. package/agent-template/.ai/references/catalog/src/services/catalog-service.ts +306 -0
  187. package/agent-template/.ai/references/catalog/src/services/database-repository.ts +440 -0
  188. package/agent-template/.ai/references/catalog/src/services/index.ts +4 -0
  189. package/agent-template/.ai/references/catalog/src/services/migration.ts +171 -0
  190. package/agent-template/.ai/references/catalog/src/services/repository.ts +36 -0
  191. package/agent-template/.ai/references/catalog/src/services/target-idempotency.ts +59 -0
  192. package/agent-template/.ai/references/catalog/tests/agent-tools.test.ts +277 -0
  193. package/agent-template/.ai/references/catalog/tests/endpoints.test.ts +320 -0
  194. package/agent-template/.ai/references/catalog/tests/idempotency.test.ts +297 -0
  195. package/agent-template/.ai/references/catalog/tests/migrations.test.ts +149 -0
  196. package/agent-template/.ai/references/catalog/tests/module.test.ts +271 -0
  197. package/agent-template/.ai/references/catalog/tests/support/database.ts +76 -0
  198. package/agent-template/.ai/references/catalog/translations/en.json +101 -0
  199. package/agent-template/.ai/references/catalog/translations/pl.json +101 -0
  200. package/agent-template/.ai/references/catalog/tsconfig.json +15 -0
  201. package/agent-template/.ai/references/catalog/vitest.config.ts +16 -0
  202. package/agent-template/.ai/references/catalog.provenance.json +55 -0
  203. package/agent-template/.ai/rules/flowdular.md +86 -0
  204. package/agent-template/.ai/skills/README.md +36 -0
  205. package/agent-template/.ai/skills/agent-tool-design/SKILL.md +209 -0
  206. package/agent-template/.ai/skills/auth-security-review/SKILL.md +96 -0
  207. package/agent-template/.ai/skills/auto-review/SKILL.md +112 -0
  208. package/agent-template/.ai/skills/bug-hunt/SKILL.md +110 -0
  209. package/agent-template/.ai/skills/business-agent-design/SKILL.md +188 -0
  210. package/agent-template/.ai/skills/cli-extension/SKILL.md +114 -0
  211. package/agent-template/.ai/skills/core-extend/SKILL.md +104 -0
  212. package/agent-template/.ai/skills/database-adapter/SKILL.md +204 -0
  213. package/agent-template/.ai/skills/database-adapter/references/first-run-and-matrix.md +105 -0
  214. package/agent-template/.ai/skills/migration-authoring/SKILL.md +167 -0
  215. package/agent-template/.ai/skills/module-new/SKILL.md +180 -0
  216. package/agent-template/.ai/skills/module-update/SKILL.md +100 -0
  217. package/agent-template/.ai/skills/perf-audit/SKILL.md +105 -0
  218. package/agent-template/.ai/skills/release-eject-pr/SKILL.md +113 -0
  219. package/agent-template/.ai/skills/spec-approval/SKILL.md +112 -0
  220. package/agent-template/.ai/skills/test-hardening/SKILL.md +86 -0
  221. package/agent-template/.ai/skills/translations-i18n/SKILL.md +85 -0
  222. package/agent-template/.ai/skills/ux-design/SKILL.md +97 -0
  223. package/agent-template/.ai/skills/variables/SKILL.md +164 -0
  224. package/agent-template/.ai/skills/workflow-development/SKILL.md +199 -0
  225. package/agent-template/.claude/skills/agent-tool-design/SKILL.md +203 -0
  226. package/agent-template/.claude/skills/auth-security-review/SKILL.md +90 -0
  227. package/agent-template/.claude/skills/auto-review/SKILL.md +103 -0
  228. package/agent-template/.claude/skills/bug-hunt/SKILL.md +104 -0
  229. package/agent-template/.claude/skills/business-agent-design/SKILL.md +182 -0
  230. package/agent-template/.claude/skills/cli-extension/SKILL.md +108 -0
  231. package/agent-template/.claude/skills/core-extend/SKILL.md +99 -0
  232. package/agent-template/.claude/skills/database-adapter/SKILL.md +198 -0
  233. package/agent-template/.claude/skills/database-adapter/references/first-run-and-matrix.md +105 -0
  234. package/agent-template/.claude/skills/migration-authoring/SKILL.md +161 -0
  235. package/agent-template/.claude/skills/module-new/SKILL.md +171 -0
  236. package/agent-template/.claude/skills/module-update/SKILL.md +91 -0
  237. package/agent-template/.claude/skills/perf-audit/SKILL.md +98 -0
  238. package/agent-template/.claude/skills/release-eject-pr/SKILL.md +107 -0
  239. package/agent-template/.claude/skills/spec-approval/SKILL.md +106 -0
  240. package/agent-template/.claude/skills/test-hardening/SKILL.md +79 -0
  241. package/agent-template/.claude/skills/translations-i18n/SKILL.md +78 -0
  242. package/agent-template/.claude/skills/ux-design/SKILL.md +92 -0
  243. package/agent-template/.claude/skills/variables/SKILL.md +156 -0
  244. package/agent-template/.claude/skills/workflow-development/SKILL.md +192 -0
  245. package/agent-template/AGENTS.md +77 -0
  246. package/agent-template/CLAUDE.md +77 -0
  247. package/agent-template/docs/adr/0001-development-reload.md +16 -0
  248. package/agent-template/docs/adr/0002-durable-agent-execution.md +21 -0
  249. package/agent-template/docs/adr/0003-module-settings.md +22 -0
  250. package/agent-template/docs/adr/0004-enterprise-access-and-audit.md +36 -0
  251. package/agent-template/docs/adr/0005-sandbox-runtime-and-coding-agents.md +81 -0
  252. package/agent-template/docs/adr/0006-agentic-workflows.md +1702 -0
  253. package/agent-template/docs/adr/0007-module-owned-agents.md +429 -0
  254. package/agent-template/docs/adr/0008-database-adapter-contract.md +90 -0
  255. package/agent-template/docs/agent-contract.md +45 -0
  256. package/agent-template/docs/configuration.md +122 -0
  257. package/agent-template/docs/database-adapters.md +346 -0
  258. package/agent-template/docs/design-system.md +217 -0
  259. package/agent-template/docs/modules.md +146 -0
  260. package/agent-template/platform/scripts/build.mjs +38 -0
  261. package/agent-template/rulesync.jsonc +11 -0
  262. package/dist/bin.js +3 -1
  263. package/package.json +3 -2
  264. package/template/default/.prettierignore +9 -0
  265. package/template/default/README.md +12 -0
  266. package/template/default/flowdular.json +3 -3
  267. package/template/default/modules/example/package.json +1 -1
  268. package/template/default/package.json +6 -2
  269. package/template/default/platform/octane.config.ts +17 -6
  270. package/template/default/platform/package.json +3 -2
  271. package/template/default/platform/scripts/dev.mjs +39 -6
@@ -0,0 +1,277 @@
1
+ import type { DatabaseProvider } from '@flowdular/sdk/database';
2
+ import { validateToolInput, validateToolOutput } from '@flowdular/sdk/harness';
3
+ import type { AgentToolContext } from '@flowdular/sdk/harness/runtime';
4
+ import { afterAll, afterEach, describe, expect, it } from 'vitest';
5
+ import { catalogAgentTools } from '../src/agent/tools.ts';
6
+ import type { CatalogItem } from '../src/domain/types.ts';
7
+ import {
8
+ createCatalogRuntime,
9
+ type CatalogRuntime,
10
+ } from '../src/server/runtime.ts';
11
+ import {
12
+ agentActor,
13
+ createPlatformVariableRegistry,
14
+ } from '@flowdular/sdk/kernel';
15
+ import { CATALOG_PERMISSIONS } from '../src/acl/permissions.ts';
16
+ import { registerCatalogVariableSource } from '../src/domain/variables.ts';
17
+ import {
18
+ catalogTestProvider,
19
+ closeCatalogTestDatabases,
20
+ } from './support/database.ts';
21
+
22
+ const runtimes = new Set<CatalogRuntime>();
23
+
24
+ afterEach(async () => {
25
+ await Promise.all([...runtimes].map((runtime) => runtime.dispose()));
26
+ runtimes.clear();
27
+ });
28
+
29
+ afterAll(closeCatalogTestDatabases);
30
+
31
+ function catalogRuntime(databases: DatabaseProvider): CatalogRuntime {
32
+ const runtime = createCatalogRuntime({ databases, purpose: 'test' });
33
+ runtimes.add(runtime);
34
+ return runtime;
35
+ }
36
+
37
+ /* execute never reads context.permissions: RBAC is enforced by the harness
38
+ before it calls the tool, and that path is proven in modules/agents. These
39
+ tests cover what the tool itself owns: tenant from context, reused service
40
+ validation, and bounded output. */
41
+ function toolContext(
42
+ tenantId: string,
43
+ overrides: Partial<AgentToolContext> = {},
44
+ ): AgentToolContext {
45
+ return {
46
+ runId: 'run-1',
47
+ tenantId,
48
+ requestedBy: 'account-1',
49
+ idempotencyKey: 'direct-tool-call-1',
50
+ permissions: new Set<string>(),
51
+ signal: new AbortController().signal,
52
+ ...overrides,
53
+ };
54
+ }
55
+
56
+ const item = (sku: string) => ({
57
+ sku,
58
+ name: `Item ${sku}`,
59
+ kind: 'product' as const,
60
+ unit: 'ea',
61
+ basePriceMinor: 1000,
62
+ currency: 'EUR',
63
+ });
64
+
65
+ describe('catalog agent tools', () => {
66
+ it('registers the two catalog tools with the right permissions', async () => {
67
+ expect(
68
+ catalogAgentTools(catalogRuntime(await catalogTestProvider())).map(
69
+ (tool) => ({
70
+ id: tool.id,
71
+ permissions: tool.requiredPermissions,
72
+ version: tool.contractVersion,
73
+ risk: tool.risk,
74
+ idempotency: tool.idempotency,
75
+ idempotencyProtection: tool.idempotencyProtection,
76
+ cancellation: tool.cancellation,
77
+ hasInput: tool.inputSchema !== undefined,
78
+ hasOutput: tool.outputSchema !== undefined,
79
+ }),
80
+ ),
81
+ ).toEqual([
82
+ {
83
+ id: 'catalog.item.list',
84
+ permissions: ['catalog.items.read'],
85
+ version: 1,
86
+ risk: 'read',
87
+ idempotency: 'required',
88
+ idempotencyProtection: undefined,
89
+ cancellation: 'cooperative',
90
+ hasInput: true,
91
+ hasOutput: true,
92
+ },
93
+ {
94
+ id: 'catalog.item.create',
95
+ permissions: ['catalog.items.manage'],
96
+ version: 1,
97
+ risk: 'workspace-write',
98
+ idempotency: 'required',
99
+ idempotencyProtection: 'target-ledger',
100
+ cancellation: 'cooperative',
101
+ hasInput: true,
102
+ hasOutput: true,
103
+ },
104
+ ]);
105
+ });
106
+
107
+ it('cooperates with cancellation before reading or writing', async () => {
108
+ const controller = new AbortController();
109
+ controller.abort('workflow-cancelled');
110
+ const [list, create] = catalogAgentTools(
111
+ catalogRuntime(await catalogTestProvider()),
112
+ );
113
+ await expect(
114
+ list!.execute({}, toolContext('tenant-a', { signal: controller.signal })),
115
+ ).rejects.toBeDefined();
116
+ await expect(
117
+ create!.execute(
118
+ item('CANCELLED-1'),
119
+ toolContext('tenant-a', { signal: controller.signal }),
120
+ ),
121
+ ).rejects.toBeDefined();
122
+ });
123
+
124
+ it('refuses a mutating call without a durable idempotency key', async () => {
125
+ const create = catalogAgentTools(
126
+ catalogRuntime(await catalogTestProvider()),
127
+ )[1]!;
128
+ const { idempotencyKey: _ignored, ...withoutKey } = toolContext('tenant-a');
129
+ await expect(
130
+ create.execute(item('NO-KEY'), withoutKey as AgentToolContext),
131
+ ).rejects.toMatchObject({ code: 'TOOL_IDEMPOTENCY_KEY_REQUIRED' });
132
+ });
133
+
134
+ it('validates workflow action input and output against the published schemas', async () => {
135
+ const [list, create] = catalogAgentTools(
136
+ catalogRuntime(await catalogTestProvider()),
137
+ );
138
+ expect(() =>
139
+ validateToolInput(create!.inputSchema, {
140
+ ...item('INVALID-TENANT'),
141
+ tenantId: 'tenant-b',
142
+ }),
143
+ ).toThrowError(/tenantId/);
144
+ const listed = await list!.execute({}, toolContext('tenant-a'));
145
+ expect(() => validateToolOutput(list!.outputSchema, listed)).not.toThrow();
146
+ const created = await create!.execute(
147
+ item('SCHEMA-1'),
148
+ toolContext('tenant-a'),
149
+ );
150
+ expect(() =>
151
+ validateToolOutput(create!.outputSchema, created),
152
+ ).not.toThrow();
153
+ });
154
+
155
+ it('creates under the run tenant and ignores a tenant named in the input', async () => {
156
+ const runtime = catalogRuntime(await catalogTestProvider());
157
+ const [, create] = catalogAgentTools(runtime);
158
+ const created = (await create!.execute(
159
+ { ...item('SKU-1'), tenantId: 'tenant-b' },
160
+ toolContext('tenant-a'),
161
+ )) as CatalogItem;
162
+ expect(created.tenantId).toBe('tenant-a');
163
+ await expect(
164
+ (await runtime.service()).list('tenant-a'),
165
+ ).resolves.toHaveLength(1);
166
+ await expect(
167
+ (await runtime.service()).list('tenant-b'),
168
+ ).resolves.toHaveLength(0);
169
+ });
170
+
171
+ it('preserves a workflow service actor and its configuring user in history', async () => {
172
+ const runtime = catalogRuntime(await catalogTestProvider());
173
+ const [, create] = catalogAgentTools(runtime);
174
+ const actor = {
175
+ kind: 'service',
176
+ id: 'workflow:catalog-sync',
177
+ label: 'Catalog sync',
178
+ configuredBy: {
179
+ kind: 'user',
180
+ id: 'account-owner',
181
+ label: 'Workspace owner',
182
+ },
183
+ } as const;
184
+ const created = (await create!.execute(
185
+ item('WORKFLOW-1'),
186
+ toolContext('tenant-a', {
187
+ actor,
188
+ idempotencyKey: 'workflow-run:node:attempt-1',
189
+ }),
190
+ )) as CatalogItem;
191
+
192
+ expect(
193
+ (
194
+ await (
195
+ await runtime.service()
196
+ ).history('tenant-a', {
197
+ recordId: created.id,
198
+ limit: 10,
199
+ cursor: null,
200
+ })
201
+ ).entries[0]?.actor,
202
+ ).toEqual(actor);
203
+ await expect((await runtime.service()).list('tenant-b')).resolves.toEqual(
204
+ [],
205
+ );
206
+ });
207
+
208
+ it('reuses the service validation so a tool cannot persist a bad currency', async () => {
209
+ const runtime = catalogRuntime(await catalogTestProvider());
210
+ const [, create] = catalogAgentTools(runtime);
211
+ await expect(
212
+ create!.execute(
213
+ { ...item('SKU-2'), currency: 'US' },
214
+ toolContext('tenant-a'),
215
+ ),
216
+ ).rejects.toMatchObject({ code: 'INVALID_INPUT' });
217
+ });
218
+
219
+ it('bounds list output at the page cap and filters by query', async () => {
220
+ const runtime = catalogRuntime(await catalogTestProvider());
221
+ const [list] = catalogAgentTools(runtime);
222
+ for (let index = 0; index < 205; index += 1) {
223
+ const context = toolContext('tenant-a');
224
+ await (
225
+ await runtime.service()
226
+ ).create(
227
+ 'tenant-a',
228
+ item(`SKU-${String(index).padStart(3, '0')}`),
229
+ agentActor(context),
230
+ );
231
+ }
232
+ expect(
233
+ (await list!.execute(
234
+ {},
235
+ toolContext('tenant-a'),
236
+ )) as readonly CatalogItem[],
237
+ ).toHaveLength(200);
238
+
239
+ const filtered = (await list!.execute(
240
+ { query: 'sku-004' },
241
+ toolContext('tenant-a'),
242
+ )) as readonly CatalogItem[];
243
+ expect(filtered.map((entry) => entry.sku)).toEqual(['SKU-004']);
244
+ });
245
+
246
+ it('resolves catalog variables through the tenant-scoped read tool', async () => {
247
+ const runtime = catalogRuntime(await catalogTestProvider());
248
+ const tools = catalogAgentTools(runtime);
249
+ const created = (await tools[1]!.execute(
250
+ item('LINKED-1'),
251
+ toolContext('tenant-a'),
252
+ )) as CatalogItem;
253
+ const registry = createPlatformVariableRegistry();
254
+ registerCatalogVariableSource(registry, tools);
255
+ const request = (tenantId: string) => ({
256
+ tenantId,
257
+ actor: {
258
+ kind: 'user' as const,
259
+ id: 'account-1',
260
+ label: 'Account 1',
261
+ },
262
+ permissionSnapshot: [CATALOG_PERMISSIONS.read],
263
+ signal: new AbortController().signal,
264
+ bindings: { catalogItemId: created.id },
265
+ });
266
+
267
+ await expect(
268
+ registry.resolve(
269
+ '{{ catalogItem.sku }}: {{ catalogItem.name }}',
270
+ request('tenant-a'),
271
+ ),
272
+ ).resolves.toBe('LINKED-1: Item LINKED-1');
273
+ await expect(
274
+ registry.resolve('{{ catalogItem.name }}', request('tenant-b')),
275
+ ).rejects.toMatchObject({ code: 'VARIABLE_VALUE_UNAVAILABLE' });
276
+ });
277
+ });
@@ -0,0 +1,320 @@
1
+ import type { DatabaseProvider } from '@flowdular/sdk/database';
2
+ import type { AuthPrincipal } from '@flowdular/sdk/modules/auth';
3
+ import type { AuthRuntime } from '@flowdular/sdk/modules/auth/server';
4
+ import { AUTH_PRINCIPAL_STATE_KEY } from '@flowdular/sdk/modules/auth/server';
5
+ import { afterAll, afterEach, describe, expect, it } from 'vitest';
6
+ import { createCatalogRoutes } from '../src/api/endpoints.ts';
7
+ import { CATALOG_PERMISSIONS } from '../src/acl/permissions.ts';
8
+ import {
9
+ createCatalogRuntime,
10
+ type CatalogRuntime,
11
+ } from '../src/server/runtime.ts';
12
+ import {
13
+ catalogTestProvider,
14
+ closeCatalogTestDatabases,
15
+ } from './support/database.ts';
16
+
17
+ /* auth.core resolves the browser session in its middleware and publishes it in
18
+ context state; sessionMutationDenial reads it from there. The key is not yet
19
+ exported from @flowdular/sdk/modules/auth/server, so this double mirrors it. */
20
+ const AUTH_SESSION_STATE_KEY = 'flowdular.auth.session';
21
+
22
+ type CatalogRoute = ReturnType<typeof createCatalogRoutes>[number];
23
+ type CatalogRouteContext = Parameters<CatalogRoute['handler']>[0];
24
+
25
+ const runtimes = new Set<CatalogRuntime>();
26
+
27
+ afterEach(async () => {
28
+ await Promise.all([...runtimes].map((runtime) => runtime.dispose()));
29
+ runtimes.clear();
30
+ });
31
+
32
+ afterAll(closeCatalogTestDatabases);
33
+
34
+ function catalogRuntime(databases: DatabaseProvider): CatalogRuntime {
35
+ const runtime = createCatalogRuntime({ databases, purpose: 'test' });
36
+ runtimes.add(runtime);
37
+ return runtime;
38
+ }
39
+
40
+ function principal(
41
+ scopes: readonly string[],
42
+ tenantId = 'tenant-a',
43
+ ): AuthPrincipal {
44
+ return {
45
+ accountId: 'account-a',
46
+ tenantId,
47
+ email: 'owner@example.test',
48
+ displayName: 'Owner',
49
+ role: 'owner',
50
+ scopes,
51
+ tenants: [],
52
+ };
53
+ }
54
+
55
+ function authRuntime(actor: AuthPrincipal): AuthRuntime {
56
+ const session = {
57
+ principal: actor,
58
+ csrfToken: 'csrf-token',
59
+ expiresAt: Date.now() + 60_000,
60
+ sessionId: 'session-id',
61
+ passwordChangeRequired: false,
62
+ };
63
+ return {
64
+ cookie: { name: 'test-session', secure: false, maxAgeSeconds: 3_600 },
65
+ authorizeAgentToolAccess: () => [],
66
+ service: () => ({
67
+ resolveSession: (token: string) => (token === 'token' ? session : null),
68
+ }),
69
+ } as unknown as AuthRuntime;
70
+ }
71
+
72
+ function context(request: Request, actor?: AuthPrincipal): CatalogRouteContext {
73
+ const state = new Map<string, unknown>();
74
+ if (actor) {
75
+ state.set(AUTH_PRINCIPAL_STATE_KEY, actor);
76
+ state.set(AUTH_SESSION_STATE_KEY, {
77
+ principal: actor,
78
+ csrfToken: 'csrf-token',
79
+ expiresAt: Date.now() + 60_000,
80
+ sessionId: 'session-id',
81
+ passwordChangeRequired: false,
82
+ });
83
+ }
84
+ return {
85
+ request,
86
+ url: new URL(request.url),
87
+ state,
88
+ } as unknown as CatalogRouteContext;
89
+ }
90
+
91
+ function mutation(path: string, body: unknown, csrf = 'csrf-token'): Request {
92
+ return new Request(`https://erp.example${path}`, {
93
+ method: 'POST',
94
+ headers: {
95
+ 'content-type': 'application/json',
96
+ origin: 'https://erp.example',
97
+ cookie: 'test-session=token',
98
+ 'x-csrf-token': csrf,
99
+ },
100
+ body: JSON.stringify(body),
101
+ });
102
+ }
103
+
104
+ function route(
105
+ routes: readonly CatalogRoute[],
106
+ path: string,
107
+ method: string,
108
+ ): CatalogRoute {
109
+ const result = routes.find(
110
+ (entry) => entry.path === path && entry.methods.includes(method),
111
+ );
112
+ if (!result) throw new Error(`Missing ${method} ${path}.`);
113
+ return result;
114
+ }
115
+
116
+ describe('catalog lifecycle and history endpoints', () => {
117
+ it('returns 401 and 403 for every endpoint before access to tenant data', async () => {
118
+ const routes = createCatalogRoutes(
119
+ { authorizeAgentToolAccess: () => [] } as unknown as AuthRuntime,
120
+ catalogRuntime(await catalogTestProvider()),
121
+ );
122
+ for (const endpoint of routes) {
123
+ const method = endpoint.methods.includes('GET') ? 'GET' : 'POST';
124
+ const request = new Request(`https://erp.example${endpoint.path}`, {
125
+ method,
126
+ ...(method === 'POST'
127
+ ? { headers: { 'content-type': 'application/json' }, body: '{}' }
128
+ : {}),
129
+ });
130
+ const unauthenticated = await endpoint.handler(context(request));
131
+ expect(unauthenticated.status, `${method} ${endpoint.path}`).toBe(401);
132
+ const forbidden = await endpoint.handler(context(request, principal([])));
133
+ expect(forbidden.status, `${method} ${endpoint.path}`).toBe(403);
134
+ }
135
+ });
136
+
137
+ it('records the user actor for a protected lifecycle transition', async () => {
138
+ const actor = principal([
139
+ CATALOG_PERMISSIONS.read,
140
+ CATALOG_PERMISSIONS.manage,
141
+ ]);
142
+ const runtime = catalogRuntime(await catalogTestProvider());
143
+ const routes = createCatalogRoutes(authRuntime(actor), runtime);
144
+ const created = await route(routes, '/api/catalog/items', 'POST').handler(
145
+ context(
146
+ mutation('/api/catalog/items', {
147
+ sku: 'A-1',
148
+ name: 'Alpha',
149
+ kind: 'product',
150
+ unit: 'each',
151
+ basePriceMinor: 100,
152
+ currency: 'EUR',
153
+ }),
154
+ actor,
155
+ ),
156
+ );
157
+ const item = ((await created.json()) as { item: { id: string } }).item;
158
+ const archived = await route(
159
+ routes,
160
+ '/api/catalog/items/archive',
161
+ 'POST',
162
+ ).handler(
163
+ context(mutation('/api/catalog/items/archive', { id: item.id }), actor),
164
+ );
165
+ expect(archived.status).toBe(200);
166
+
167
+ const history = await route(
168
+ routes,
169
+ '/api/catalog/items/history',
170
+ 'GET',
171
+ ).handler(
172
+ context(
173
+ new Request(
174
+ `https://erp.example/api/catalog/items/history?recordId=${item.id}`,
175
+ ),
176
+ actor,
177
+ ),
178
+ );
179
+ expect(history.status).toBe(200);
180
+ expect(await history.json()).toMatchObject({
181
+ entries: [
182
+ { action: 'archived', actor: { kind: 'user', id: 'account-a' } },
183
+ { action: 'created', actor: { kind: 'user', id: 'account-a' } },
184
+ ],
185
+ });
186
+ });
187
+
188
+ it('updates, restores, and permanently deletes only an archived item in the active tenant', async () => {
189
+ const actor = principal([
190
+ CATALOG_PERMISSIONS.read,
191
+ CATALOG_PERMISSIONS.manage,
192
+ ]);
193
+ const runtime = catalogRuntime(await catalogTestProvider());
194
+ const routes = createCatalogRoutes(authRuntime(actor), runtime);
195
+ const created = await route(routes, '/api/catalog/items', 'POST').handler(
196
+ context(
197
+ mutation('/api/catalog/items', {
198
+ sku: 'LIFECYCLE-1',
199
+ name: 'Lifecycle item',
200
+ kind: 'product',
201
+ unit: 'each',
202
+ basePriceMinor: 100,
203
+ currency: 'EUR',
204
+ }),
205
+ actor,
206
+ ),
207
+ );
208
+ const item = ((await created.json()) as { item: { id: string } }).item;
209
+
210
+ const updated = await route(
211
+ routes,
212
+ '/api/catalog/items/update',
213
+ 'POST',
214
+ ).handler(
215
+ context(
216
+ mutation('/api/catalog/items/update', {
217
+ id: item.id,
218
+ name: 'Lifecycle item revised',
219
+ kind: 'service',
220
+ unit: 'hour',
221
+ basePriceMinor: 250,
222
+ currency: 'PLN',
223
+ }),
224
+ actor,
225
+ ),
226
+ );
227
+ expect(updated.status).toBe(200);
228
+ expect(await updated.json()).toMatchObject({
229
+ item: { id: item.id, name: 'Lifecycle item revised', currency: 'PLN' },
230
+ });
231
+
232
+ const activeDelete = await route(
233
+ routes,
234
+ '/api/catalog/items/delete',
235
+ 'POST',
236
+ ).handler(
237
+ context(mutation('/api/catalog/items/delete', { id: item.id }), actor),
238
+ );
239
+ expect(activeDelete.status).toBe(409);
240
+ expect(await activeDelete.json()).toMatchObject({
241
+ error: { code: 'CATALOG_ITEM_NOT_ARCHIVED' },
242
+ });
243
+
244
+ const foreignActor = principal(
245
+ [CATALOG_PERMISSIONS.read, CATALOG_PERMISSIONS.manage],
246
+ 'tenant-b',
247
+ );
248
+ const foreignArchive = await route(
249
+ createCatalogRoutes(authRuntime(foreignActor), runtime),
250
+ '/api/catalog/items/archive',
251
+ 'POST',
252
+ ).handler(
253
+ context(
254
+ mutation('/api/catalog/items/archive', { id: item.id }),
255
+ foreignActor,
256
+ ),
257
+ );
258
+ expect(foreignArchive.status).toBe(404);
259
+
260
+ for (const action of ['archive', 'restore', 'archive'] as const) {
261
+ const response = await route(
262
+ routes,
263
+ `/api/catalog/items/${action}`,
264
+ 'POST',
265
+ ).handler(
266
+ context(
267
+ mutation(`/api/catalog/items/${action}`, { id: item.id }),
268
+ actor,
269
+ ),
270
+ );
271
+ expect(response.status, action).toBe(200);
272
+ }
273
+
274
+ const removed = await route(
275
+ routes,
276
+ '/api/catalog/items/delete',
277
+ 'POST',
278
+ ).handler(
279
+ context(mutation('/api/catalog/items/delete', { id: item.id }), actor),
280
+ );
281
+ expect(removed.status).toBe(200);
282
+ expect(await removed.json()).toEqual({ deleted: true });
283
+ await expect(
284
+ (await runtime.service()).get('tenant-a', item.id),
285
+ ).resolves.toBeNull();
286
+ });
287
+
288
+ it('rejects a lifecycle mutation without a valid CSRF token', async () => {
289
+ const actor = principal([CATALOG_PERMISSIONS.manage]);
290
+ const runtime = catalogRuntime(await catalogTestProvider());
291
+ const item = await (
292
+ await runtime.service()
293
+ ).create(
294
+ 'tenant-a',
295
+ {
296
+ sku: 'A-1',
297
+ name: 'Alpha',
298
+ kind: 'product',
299
+ unit: 'each',
300
+ basePriceMinor: 100,
301
+ currency: 'EUR',
302
+ },
303
+ { kind: 'user', id: 'account-a', label: 'Owner' },
304
+ );
305
+ const response = await route(
306
+ createCatalogRoutes(authRuntime(actor), runtime),
307
+ '/api/catalog/items/archive',
308
+ 'POST',
309
+ ).handler(
310
+ context(
311
+ mutation('/api/catalog/items/archive', { id: item.id }, 'bad'),
312
+ actor,
313
+ ),
314
+ );
315
+ expect(response.status).toBe(403);
316
+ expect(
317
+ (await (await runtime.service()).get('tenant-a', item.id))?.status,
318
+ ).toBe('active');
319
+ });
320
+ });