@a5c-ai/kradle 5.0.1-staging.3abdf9534c25

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 (295) hide show
  1. package/Dockerfile +31 -0
  2. package/README.md +187 -0
  3. package/bin/kradle-demo.mjs +23 -0
  4. package/bin/kradle-server.mjs +14 -0
  5. package/dist/kradle-controller-ui.json +3482 -0
  6. package/dist/kradle-lifecycle.json +201 -0
  7. package/dist/kradle-runtime-snapshot.json +3125 -0
  8. package/dist/kradle-summary.json +724 -0
  9. package/docs/README.md +61 -0
  10. package/docs/agents/README.md +83 -0
  11. package/docs/agents/acceptance-test-matrix.md +193 -0
  12. package/docs/agents/agent-mux-adapter-contract.md +167 -0
  13. package/docs/agents/agent-mux-source-map.md +310 -0
  14. package/docs/agents/agent-run-memory-import-spec.md +256 -0
  15. package/docs/agents/agent-stack-management-spec.md +421 -0
  16. package/docs/agents/api-contract-spec.md +309 -0
  17. package/docs/agents/artifacts-writeback-spec.md +145 -0
  18. package/docs/agents/chart-packaging-spec.md +128 -0
  19. package/docs/agents/ci-orchestration-spec.md +140 -0
  20. package/docs/agents/context-assembly-spec.md +219 -0
  21. package/docs/agents/controller-reconciliation-spec.md +255 -0
  22. package/docs/agents/crd-schema-spec.md +315 -0
  23. package/docs/agents/decision-log-open-questions.md +169 -0
  24. package/docs/agents/developer-implementation-checklist.md +329 -0
  25. package/docs/agents/dispatching-design.md +262 -0
  26. package/docs/agents/gaps-agent-mux-to-kradle-crds.md +298 -0
  27. package/docs/agents/glossary.md +66 -0
  28. package/docs/agents/implementation-blueprint.md +324 -0
  29. package/docs/agents/implementation-rollout-slices.md +251 -0
  30. package/docs/agents/memory-context-integration-spec.md +194 -0
  31. package/docs/agents/memory-ontology-schema-spec.md +253 -0
  32. package/docs/agents/memory-operations-runbook.md +121 -0
  33. package/docs/agents/mvp-vertical-slice-spec.md +146 -0
  34. package/docs/agents/observability-audit-spec.md +265 -0
  35. package/docs/agents/operator-runbook.md +174 -0
  36. package/docs/agents/org-memory-api-payload-examples.md +333 -0
  37. package/docs/agents/org-memory-controller-sequence-spec.md +181 -0
  38. package/docs/agents/org-memory-e2e-fixture-plan.md +161 -0
  39. package/docs/agents/org-memory-ui-implementation-map.md +114 -0
  40. package/docs/agents/org-memory-vertical-slice-spec.md +168 -0
  41. package/docs/agents/org-resource-model-delta-spec.md +111 -0
  42. package/docs/agents/org-route-resource-model-spec.md +183 -0
  43. package/docs/agents/org-scoping-namespace-spec.md +114 -0
  44. package/docs/agents/rbac-secrets-management-spec.md +406 -0
  45. package/docs/agents/repository-page-integration-spec.md +255 -0
  46. package/docs/agents/resource-contract-examples.md +808 -0
  47. package/docs/agents/resource-relationship-map.md +190 -0
  48. package/docs/agents/security-threat-model.md +188 -0
  49. package/docs/agents/shared-memory-company-brain-spec.md +358 -0
  50. package/docs/agents/storage-migration-spec.md +168 -0
  51. package/docs/agents/subagent-orchestration-spec.md +152 -0
  52. package/docs/agents/system-overview.md +88 -0
  53. package/docs/agents/tools-mcp-skills-spec.md +189 -0
  54. package/docs/agents/traceability-matrix.md +79 -0
  55. package/docs/agents/ui-flow-spec.md +211 -0
  56. package/docs/agents/ui-ux-system-spec.md +426 -0
  57. package/docs/agents/workspace-lifecycle-spec.md +166 -0
  58. package/docs/architecture-spec.md +78 -0
  59. package/docs/architecture-v2.md +2759 -0
  60. package/docs/components/control-plane.md +78 -0
  61. package/docs/components/data-plane.md +69 -0
  62. package/docs/components/hooks-events.md +67 -0
  63. package/docs/components/identity-rbac-policy.md +73 -0
  64. package/docs/components/kubevela-oam.md +70 -0
  65. package/docs/components/operations-publishing.md +81 -0
  66. package/docs/components/runners-ci.md +66 -0
  67. package/docs/components/web-ui.md +94 -0
  68. package/docs/crd-behaviors-and-relationships.md +3926 -0
  69. package/docs/external/README.md +47 -0
  70. package/docs/external/bidirectional-sync-design.md +134 -0
  71. package/docs/external/cicd-interface.md +64 -0
  72. package/docs/external/external-backend-controllers.md +170 -0
  73. package/docs/external/external-backend-crds.md +234 -0
  74. package/docs/external/external-backend-ui-spec.md +151 -0
  75. package/docs/external/external-backend-ux-flows.md +115 -0
  76. package/docs/external/external-object-mapping.md +125 -0
  77. package/docs/external/git-forge-interface.md +68 -0
  78. package/docs/external/github-integration-design.md +151 -0
  79. package/docs/external/issue-tracking-interface.md +66 -0
  80. package/docs/external/provider-capability-manifests.md +204 -0
  81. package/docs/external/provider-catalog.md +139 -0
  82. package/docs/external/provider-rollout-testing.md +78 -0
  83. package/docs/external/research-results.md +48 -0
  84. package/docs/external/security-auth-permissions.md +81 -0
  85. package/docs/external/sync-state-machines.md +108 -0
  86. package/docs/external/unified-external-backend-model.md +107 -0
  87. package/docs/external/user-facing-changes.md +67 -0
  88. package/docs/gaps.md +161 -0
  89. package/docs/install.md +94 -0
  90. package/docs/integration-and-design-decisions.md +1530 -0
  91. package/docs/kradle-design.md +334 -0
  92. package/docs/local-minikube.md +55 -0
  93. package/docs/ontology/README.md +32 -0
  94. package/docs/ontology/bounded-contexts.md +29 -0
  95. package/docs/ontology/events-and-hooks.md +32 -0
  96. package/docs/ontology/oam-kubevela.md +32 -0
  97. package/docs/ontology/operations-and-release.md +25 -0
  98. package/docs/ontology/personas-and-actors.md +32 -0
  99. package/docs/ontology/policies-and-invariants.md +33 -0
  100. package/docs/ontology/problem-space.md +30 -0
  101. package/docs/ontology/resource-contracts.md +40 -0
  102. package/docs/ontology/resource-taxonomy.md +42 -0
  103. package/docs/ontology/runners-and-ci.md +29 -0
  104. package/docs/ontology/solution-space.md +24 -0
  105. package/docs/ontology/storage-and-data-boundaries.md +29 -0
  106. package/docs/ontology/validation-matrix.md +24 -0
  107. package/docs/ontology/web-ui-excellent-flows.md +32 -0
  108. package/docs/ontology/workflows.md +39 -0
  109. package/docs/ontology/world.md +35 -0
  110. package/docs/openapi.yaml +1291 -0
  111. package/docs/product-requirements.md +62 -0
  112. package/docs/requirements-v2.md +235 -0
  113. package/docs/roadmap-mvp.md +87 -0
  114. package/docs/sdk-api-reference.md +1108 -0
  115. package/docs/system-requirements.md +90 -0
  116. package/docs/system-spec-v2.md +1230 -0
  117. package/docs/tests/README.md +53 -0
  118. package/docs/tests/agent-qa-plan.md +63 -0
  119. package/docs/tests/browser-ui-tests.md +62 -0
  120. package/docs/tests/ci-quality-gates.md +48 -0
  121. package/docs/tests/coverage-model.md +64 -0
  122. package/docs/tests/e2e-scenario-tests.md +53 -0
  123. package/docs/tests/fixtures-test-data.md +63 -0
  124. package/docs/tests/observability-reliability-tests.md +54 -0
  125. package/docs/tests/product-test-matrix.md +145 -0
  126. package/docs/tests/qa-adoption-roadmap.md +130 -0
  127. package/docs/tests/qa-automation-plan.md +101 -0
  128. package/docs/tests/security-compliance-tests.md +57 -0
  129. package/docs/tests/test-framework-tools.md +88 -0
  130. package/docs/tests/test-suite-layout.md +121 -0
  131. package/docs/tests/unit-integration-tests.md +48 -0
  132. package/docs/todo-kyverno +714 -0
  133. package/docs/todos.md +4 -0
  134. package/docs/user-stories.md +78 -0
  135. package/docs/web-console-spec.md +533 -0
  136. package/examples/minikube-demo.yaml +190 -0
  137. package/examples/oam-application.yaml +23 -0
  138. package/examples/policy-kyverno-pr-title.yaml +18 -0
  139. package/package.json +66 -0
  140. package/scripts/build.mjs +29 -0
  141. package/scripts/setup-minikube.mjs +65 -0
  142. package/scripts/smoke.mjs +37 -0
  143. package/scripts/validate-doc-coverage.mjs +152 -0
  144. package/scripts/validate-package.mjs +95 -0
  145. package/scripts/validate-ui.mjs +305 -0
  146. package/src/agent-adapter-controller.js +169 -0
  147. package/src/agent-approval-controller.js +170 -0
  148. package/src/agent-context-bundles.js +242 -0
  149. package/src/agent-dispatch-controller.js +549 -0
  150. package/src/agent-gateway-config-controller.js +147 -0
  151. package/src/agent-identity-migration.js +115 -0
  152. package/src/agent-memory-controller.js +357 -0
  153. package/src/agent-memory-import.js +327 -0
  154. package/src/agent-memory-query.js +292 -0
  155. package/src/agent-memory-repository-source-controller.js +255 -0
  156. package/src/agent-mux-client.js +589 -0
  157. package/src/agent-permission-review.js +250 -0
  158. package/src/agent-persona-controller.js +135 -0
  159. package/src/agent-project-controller.js +117 -0
  160. package/src/agent-prompt-composition.js +55 -0
  161. package/src/agent-provider-config-controller.js +151 -0
  162. package/src/agent-secret-config-grant-controller.js +282 -0
  163. package/src/agent-session-transcript-controller.js +189 -0
  164. package/src/agent-stack-controller.js +421 -0
  165. package/src/agent-subagent-controller.js +160 -0
  166. package/src/agent-transport-binding-controller.js +121 -0
  167. package/src/agent-trigger-controller.js +387 -0
  168. package/src/agent-workspace-controller.js +702 -0
  169. package/src/agent-writeback-controller.js +302 -0
  170. package/src/api-controller.js +621 -0
  171. package/src/argocd-gitops.js +43 -0
  172. package/src/artifact-registry-controller.js +542 -0
  173. package/src/assistant-runtime.js +284 -0
  174. package/src/async-controller.js +207 -0
  175. package/src/audit-controller.js +191 -0
  176. package/src/auth.js +310 -0
  177. package/src/component-catalog.js +41 -0
  178. package/src/control-plane.js +136 -0
  179. package/src/controller-client.js +112 -0
  180. package/src/controller-ui.js +620 -0
  181. package/src/data-plane.js +179 -0
  182. package/src/event-bus.js +397 -0
  183. package/src/external/conflict-controller.js +225 -0
  184. package/src/external/github/auth.js +96 -0
  185. package/src/external/github/cicd.js +180 -0
  186. package/src/external/github/git-forge.js +240 -0
  187. package/src/external/github/index.js +144 -0
  188. package/src/external/github/issue-tracking.js +163 -0
  189. package/src/external/provider-adapter.js +161 -0
  190. package/src/external/provider-resource-factory.js +221 -0
  191. package/src/external/sync-controller.js +235 -0
  192. package/src/external/webhook-controller.js +144 -0
  193. package/src/external/write-controller.js +283 -0
  194. package/src/gitea-backend.js +131 -0
  195. package/src/gitea-service.js +173 -0
  196. package/src/handoff.js +98 -0
  197. package/src/health-probes.js +134 -0
  198. package/src/hooks-events.js +63 -0
  199. package/src/hooks-lifecycle.js +117 -0
  200. package/src/http-server.js +409 -0
  201. package/src/identity-policy.js +86 -0
  202. package/src/index.js +71 -0
  203. package/src/jitsi-agent-bridge.js +141 -0
  204. package/src/jitsi-meeting-controller.js +291 -0
  205. package/src/jitsi-sync-controller.js +198 -0
  206. package/src/kradle-inference-service-controller.js +246 -0
  207. package/src/kubernetes-controller-async.js +531 -0
  208. package/src/kubernetes-controller.js +904 -0
  209. package/src/kubernetes-resource-gateway.js +48 -0
  210. package/src/model-route-controller.js +364 -0
  211. package/src/notification-controller.js +178 -0
  212. package/src/operations.js +112 -0
  213. package/src/org-scoping.js +5 -0
  214. package/src/resource-model.js +282 -0
  215. package/src/runner-controller.js +272 -0
  216. package/src/runners-ci.js +48 -0
  217. package/src/runtime.js +196 -0
  218. package/src/snapshot-cache.js +157 -0
  219. package/src/virtual-model-controller.js +538 -0
  220. package/src/virtual-model-hook-bridge.js +200 -0
  221. package/src/web-ui.js +40 -0
  222. package/tests/agent-adapter-controller.test.js +361 -0
  223. package/tests/agent-approval-controller.test.js +173 -0
  224. package/tests/agent-context-bundles.test.js +278 -0
  225. package/tests/agent-dispatch-controller.test.js +679 -0
  226. package/tests/agent-gateway-config-controller.test.js +386 -0
  227. package/tests/agent-identity-migration.test.js +87 -0
  228. package/tests/agent-memory-controller.test.js +461 -0
  229. package/tests/agent-memory-import-snapshot.test.js +477 -0
  230. package/tests/agent-memory-query.test.js +404 -0
  231. package/tests/agent-memory-repository-source.test.js +514 -0
  232. package/tests/agent-mux-client.test.js +389 -0
  233. package/tests/agent-mux-integration.test.js +971 -0
  234. package/tests/agent-permission-review-v2.test.js +317 -0
  235. package/tests/agent-permission-review.test.js +209 -0
  236. package/tests/agent-persona-controller.test.js +127 -0
  237. package/tests/agent-project-controller.test.js +302 -0
  238. package/tests/agent-prompt-composition.test.js +76 -0
  239. package/tests/agent-provider-config-controller.test.js +376 -0
  240. package/tests/agent-resources.test.js +303 -0
  241. package/tests/agent-secret-config-grant.test.js +231 -0
  242. package/tests/agent-session-transcript-controller.test.js +499 -0
  243. package/tests/agent-stack-controller.test.js +283 -0
  244. package/tests/agent-subagent-controller.test.js +201 -0
  245. package/tests/agent-transport-binding-controller.test.js +294 -0
  246. package/tests/agent-trigger-controller.test.js +271 -0
  247. package/tests/agent-trigger-routes.test.js +190 -0
  248. package/tests/agent-trigger-sources.test.js +245 -0
  249. package/tests/agent-workspace-controller.test.js +181 -0
  250. package/tests/agent-writeback.test.js +292 -0
  251. package/tests/approval-persistence.test.js +171 -0
  252. package/tests/artifact-registry.test.js +511 -0
  253. package/tests/assistant-runtime.test.js +506 -0
  254. package/tests/async-controller.test.js +252 -0
  255. package/tests/audit-controller.test.js +227 -0
  256. package/tests/codespace-controller.test.js +318 -0
  257. package/tests/controller-client.test.js +133 -0
  258. package/tests/deployment.test.js +527 -0
  259. package/tests/e2e/lifecycle.test.js +120 -0
  260. package/tests/event-bus-integration.test.js +355 -0
  261. package/tests/external-github-forge.test.js +560 -0
  262. package/tests/external-github-issues-cicd.test.js +520 -0
  263. package/tests/external-integration.test.js +470 -0
  264. package/tests/external-persistence.test.js +415 -0
  265. package/tests/external-provider-adapter.test.js +365 -0
  266. package/tests/external-resource-model.test.js +223 -0
  267. package/tests/external-webhook-sync.test.js +287 -0
  268. package/tests/external-write-conflict.test.js +353 -0
  269. package/tests/gitea-service.test.js +253 -0
  270. package/tests/health-check-real.test.js +165 -0
  271. package/tests/health-probes.test.js +90 -0
  272. package/tests/hooks-lifecycle.test.js +364 -0
  273. package/tests/integration/full-flow.test.js +266 -0
  274. package/tests/jitsi-agent-bridge.test.js +119 -0
  275. package/tests/jitsi-helm-integration.test.js +77 -0
  276. package/tests/jitsi-meeting-controller.test.js +170 -0
  277. package/tests/jitsi-resource-model.test.js +73 -0
  278. package/tests/jitsi-sync-controller.test.js +112 -0
  279. package/tests/kradle-inference-service.test.js +689 -0
  280. package/tests/kradle.test.js +779 -0
  281. package/tests/memory-search-wiring.test.js +270 -0
  282. package/tests/model-route-controller.test.js +733 -0
  283. package/tests/notification-controller.test.js +196 -0
  284. package/tests/notification-integration.test.js +179 -0
  285. package/tests/org-scoping.test.js +687 -0
  286. package/tests/runner-controller.test.js +327 -0
  287. package/tests/runner-integration.test.js +231 -0
  288. package/tests/session-cookie-hmac.test.js +151 -0
  289. package/tests/snapshot-performance.test.js +315 -0
  290. package/tests/sse-events.test.js +107 -0
  291. package/tests/virtual-model-controller.test.js +877 -0
  292. package/tests/virtual-model-hook-bridge.test.js +384 -0
  293. package/tests/webhook-trigger.test.js +198 -0
  294. package/tests/workspace-volumes.test.js +312 -0
  295. package/tests/writeback-persistence.test.js +207 -0
@@ -0,0 +1,384 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import {
4
+ createVirtualModelHookBridge,
5
+ VIRTUAL_MODEL_HOOK_TYPES,
6
+ VIRTUAL_MODEL_HOOK_BRIDGE_BOUNDARY,
7
+ createVirtualModelController,
8
+ createResource,
9
+ createEventBus,
10
+ } from '../src/index.js';
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Virtual Model Hook Bridge Tests
14
+ //
15
+ // Verifies the bridge between KradleVirtualModel CRD hooks and the agent-mux
16
+ // hook dispatcher, including model matching, hook type mapping, and result
17
+ // format conversion.
18
+ // ---------------------------------------------------------------------------
19
+
20
+ function makeVirtualModel(name, overrides = {}) {
21
+ return createResource('KradleVirtualModel', { name, namespace: 'kradle-org-default' }, {
22
+ organizationRef: 'default',
23
+ modelName: `model-${name}`,
24
+ routes: [
25
+ { modelRouteRef: 'route-primary', weight: 80 },
26
+ { modelRouteRef: 'route-secondary', weight: 20 }
27
+ ],
28
+ ...overrides
29
+ });
30
+ }
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // 1. Factory and shape
34
+ // ---------------------------------------------------------------------------
35
+
36
+ test('createVirtualModelHookBridge returns bridge with expected methods', () => {
37
+ const bridge = createVirtualModelHookBridge();
38
+ assert.ok(bridge, 'bridge must be truthy');
39
+ assert.equal(typeof bridge.matchVirtualModel, 'function', 'must expose matchVirtualModel');
40
+ assert.equal(typeof bridge.handleHook, 'function', 'must expose handleHook');
41
+ assert.ok(Array.isArray(bridge.hookTypes), 'must expose hookTypes array');
42
+ assert.equal(bridge.role, 'virtual-model-hook-bridge', 'must declare its role');
43
+ });
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // 2. VIRTUAL_MODEL_HOOK_BRIDGE_BOUNDARY exported correctly
47
+ // ---------------------------------------------------------------------------
48
+
49
+ test('VIRTUAL_MODEL_HOOK_BRIDGE_BOUNDARY is exported with correct role', () => {
50
+ assert.ok(VIRTUAL_MODEL_HOOK_BRIDGE_BOUNDARY, 'BOUNDARY must be exported');
51
+ assert.equal(VIRTUAL_MODEL_HOOK_BRIDGE_BOUNDARY.role, 'virtual-model-hook-bridge');
52
+ assert.ok(Array.isArray(VIRTUAL_MODEL_HOOK_BRIDGE_BOUNDARY.owns));
53
+ assert.ok(VIRTUAL_MODEL_HOOK_BRIDGE_BOUNDARY.owns.includes('virtual model matching'));
54
+ assert.ok(Array.isArray(VIRTUAL_MODEL_HOOK_BRIDGE_BOUNDARY.mustNotOwn));
55
+ assert.ok(VIRTUAL_MODEL_HOOK_BRIDGE_BOUNDARY.mustNotOwn.includes('hook dispatch orchestration'));
56
+ });
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // 3. VIRTUAL_MODEL_HOOK_TYPES exported correctly
60
+ // ---------------------------------------------------------------------------
61
+
62
+ test('VIRTUAL_MODEL_HOOK_TYPES contains all expected hook types', () => {
63
+ assert.ok(Array.isArray(VIRTUAL_MODEL_HOOK_TYPES), 'must be an array');
64
+ assert.equal(VIRTUAL_MODEL_HOOK_TYPES.length, 11, 'must have 11 hook types');
65
+ assert.ok(VIRTUAL_MODEL_HOOK_TYPES.includes('VirtualModel.PreModelResolution'));
66
+ assert.ok(VIRTUAL_MODEL_HOOK_TYPES.includes('VirtualModel.PreCompletion'));
67
+ assert.ok(VIRTUAL_MODEL_HOOK_TYPES.includes('VirtualModel.PostCompletion'));
68
+ assert.ok(VIRTUAL_MODEL_HOOK_TYPES.includes('VirtualModel.PreToolUse'));
69
+ assert.ok(VIRTUAL_MODEL_HOOK_TYPES.includes('VirtualModel.PostToolUse'));
70
+ assert.ok(VIRTUAL_MODEL_HOOK_TYPES.includes('VirtualModel.TurnEnd'));
71
+ assert.ok(VIRTUAL_MODEL_HOOK_TYPES.includes('VirtualModel.SessionStart'));
72
+ assert.ok(VIRTUAL_MODEL_HOOK_TYPES.includes('VirtualModel.SessionEnd'));
73
+ assert.ok(VIRTUAL_MODEL_HOOK_TYPES.includes('VirtualModel.UserPromptSubmit'));
74
+ assert.ok(VIRTUAL_MODEL_HOOK_TYPES.includes('VirtualModel.OnError'));
75
+ assert.ok(VIRTUAL_MODEL_HOOK_TYPES.includes('VirtualModel.OnCompact'));
76
+ });
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // 4. matchVirtualModel finds by modelName
80
+ // ---------------------------------------------------------------------------
81
+
82
+ test('matchVirtualModel finds virtual model by spec.modelName', () => {
83
+ const bridge = createVirtualModelHookBridge();
84
+ const vm1 = makeVirtualModel('alpha');
85
+ const vm2 = makeVirtualModel('beta');
86
+ const matched = bridge.matchVirtualModel('model-beta', [vm1, vm2]);
87
+ assert.ok(matched, 'should find a match');
88
+ assert.equal(matched.metadata.name, 'beta');
89
+ });
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // 5. matchVirtualModel returns null for non-matching
93
+ // ---------------------------------------------------------------------------
94
+
95
+ test('matchVirtualModel returns null when no model matches', () => {
96
+ const bridge = createVirtualModelHookBridge();
97
+ const vm1 = makeVirtualModel('alpha');
98
+ const matched = bridge.matchVirtualModel('model-missing', [vm1]);
99
+ assert.equal(matched, null, 'should return null for non-matching');
100
+ });
101
+
102
+ test('matchVirtualModel returns null for empty array', () => {
103
+ const bridge = createVirtualModelHookBridge();
104
+ const matched = bridge.matchVirtualModel('model-alpha', []);
105
+ assert.equal(matched, null, 'should return null for empty array');
106
+ });
107
+
108
+ test('matchVirtualModel returns null for null inputs', () => {
109
+ const bridge = createVirtualModelHookBridge();
110
+ assert.equal(bridge.matchVirtualModel(null, []), null);
111
+ assert.equal(bridge.matchVirtualModel('model', null), null);
112
+ });
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // 6. handleHook PreModelResolution with rules returns modify
116
+ // ---------------------------------------------------------------------------
117
+
118
+ test('handleHook PreModelResolution with rules returns modify decision', () => {
119
+ const bridge = createVirtualModelHookBridge();
120
+ const vm = makeVirtualModel('routed', {
121
+ rules: [
122
+ {
123
+ name: 'region-rule',
124
+ conditions: [{ field: 'region', operator: 'eq', value: 'us-east' }],
125
+ action: { route: 'route-us-east' }
126
+ }
127
+ ]
128
+ });
129
+ const result = bridge.handleHook('VirtualModel.PreModelResolution', {
130
+ data: { requestContext: { region: 'us-east' } }
131
+ }, vm);
132
+ assert.equal(result.decision, 'modify');
133
+ assert.equal(result.modifiedInput.modelId, 'route-us-east');
134
+ });
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // 7. handleHook PreCompletion with transform hook returns modified request
138
+ // ---------------------------------------------------------------------------
139
+
140
+ test('handleHook PreCompletion with transform hook returns modified request', () => {
141
+ const bridge = createVirtualModelHookBridge();
142
+ const vm = makeVirtualModel('transform', {
143
+ hooks: {
144
+ requestTransform: 'return { ...args.request, injected: true };'
145
+ }
146
+ });
147
+ const result = bridge.handleHook('VirtualModel.PreCompletion', {
148
+ data: { request: { model: 'gpt-4', prompt: 'hello' }, context: {} }
149
+ }, vm);
150
+ assert.equal(result.decision, 'modify');
151
+ assert.ok(result.modifiedInput.request.injected, 'should have injected field');
152
+ assert.equal(result.modifiedInput.request.model, 'gpt-4');
153
+ });
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // 8. handleHook PostCompletion with transform hook returns modified response
157
+ // ---------------------------------------------------------------------------
158
+
159
+ test('handleHook PostCompletion with transform hook returns modified response', () => {
160
+ const bridge = createVirtualModelHookBridge();
161
+ const vm = makeVirtualModel('resp-transform', {
162
+ hooks: {
163
+ responseTransform: 'return { ...args.response, filtered: true };'
164
+ }
165
+ });
166
+ const result = bridge.handleHook('VirtualModel.PostCompletion', {
167
+ data: { response: { text: 'hello world' }, context: {} }
168
+ }, vm);
169
+ assert.equal(result.decision, 'modify');
170
+ assert.ok(result.modifiedInput.response.filtered, 'should have filtered field');
171
+ assert.equal(result.modifiedInput.response.text, 'hello world');
172
+ });
173
+
174
+ // ---------------------------------------------------------------------------
175
+ // 9. handleHook PreToolUse deny via hook
176
+ // ---------------------------------------------------------------------------
177
+
178
+ test('handleHook PreToolUse deny via hook', () => {
179
+ const bridge = createVirtualModelHookBridge();
180
+ const vm = makeVirtualModel('tool-deny', {
181
+ hooks: {
182
+ onPreToolUse: 'return { allow: false };'
183
+ }
184
+ });
185
+ const result = bridge.handleHook('VirtualModel.PreToolUse', {
186
+ data: { toolCall: { name: 'dangerous_tool' }, session: {} }
187
+ }, vm);
188
+ assert.equal(result.decision, 'deny');
189
+ });
190
+
191
+ // ---------------------------------------------------------------------------
192
+ // 10. handleHook PreToolUse allow by default
193
+ // ---------------------------------------------------------------------------
194
+
195
+ test('handleHook PreToolUse allow by default (no hook defined)', () => {
196
+ const bridge = createVirtualModelHookBridge();
197
+ const vm = makeVirtualModel('tool-allow');
198
+ const result = bridge.handleHook('VirtualModel.PreToolUse', {
199
+ data: { toolCall: { name: 'safe_tool' }, session: {} }
200
+ }, vm);
201
+ assert.equal(result.decision, 'allow');
202
+ });
203
+
204
+ // ---------------------------------------------------------------------------
205
+ // 11. handleHook UserPromptSubmit deny via hook
206
+ // ---------------------------------------------------------------------------
207
+
208
+ test('handleHook UserPromptSubmit deny via hook', () => {
209
+ const bridge = createVirtualModelHookBridge();
210
+ const vm = makeVirtualModel('prompt-deny', {
211
+ hooks: {
212
+ onUserPromptSubmit: 'return { block: true };'
213
+ }
214
+ });
215
+ const result = bridge.handleHook('VirtualModel.UserPromptSubmit', {
216
+ data: { prompt: 'DROP TABLE users;', session: {} }
217
+ }, vm);
218
+ assert.equal(result.decision, 'deny');
219
+ assert.ok(result.message, 'should include a deny message');
220
+ });
221
+
222
+ // ---------------------------------------------------------------------------
223
+ // 12. handleHook UserPromptSubmit modify via hook
224
+ // ---------------------------------------------------------------------------
225
+
226
+ test('handleHook UserPromptSubmit modify via hook', () => {
227
+ const bridge = createVirtualModelHookBridge();
228
+ const vm = makeVirtualModel('prompt-modify', {
229
+ hooks: {
230
+ onUserPromptSubmit: 'return { block: false, modified: args.prompt + " [sanitized]" };'
231
+ }
232
+ });
233
+ const result = bridge.handleHook('VirtualModel.UserPromptSubmit', {
234
+ data: { prompt: 'hello', session: {} }
235
+ }, vm);
236
+ assert.equal(result.decision, 'modify');
237
+ assert.equal(result.modifiedInput.prompt, 'hello [sanitized]');
238
+ });
239
+
240
+ // ---------------------------------------------------------------------------
241
+ // 13. handleHook OnError returns retry
242
+ // ---------------------------------------------------------------------------
243
+
244
+ test('handleHook OnError returns retry with fallback route', () => {
245
+ const bridge = createVirtualModelHookBridge();
246
+ const vm = makeVirtualModel('error-retry', {
247
+ hooks: {
248
+ onError: 'return { retry: true, fallbackRoute: "route-fallback" };'
249
+ }
250
+ });
251
+ const result = bridge.handleHook('VirtualModel.OnError', {
252
+ data: { error: { code: 503, message: 'Service unavailable' }, session: {} }
253
+ }, vm);
254
+ assert.equal(result.decision, 'modify');
255
+ assert.equal(result.modifiedInput.retry, true);
256
+ assert.equal(result.modifiedInput.fallbackRoute, 'route-fallback');
257
+ });
258
+
259
+ // ---------------------------------------------------------------------------
260
+ // 14. handleHook SessionStart/End return allow
261
+ // ---------------------------------------------------------------------------
262
+
263
+ test('handleHook SessionStart returns allow', () => {
264
+ const eventBus = createEventBus();
265
+ const bridge = createVirtualModelHookBridge({ eventBus });
266
+ const vm = makeVirtualModel('session-start');
267
+ const result = bridge.handleHook('VirtualModel.SessionStart', {
268
+ data: { session: { id: 'sess-1' } }
269
+ }, vm);
270
+ assert.equal(result.decision, 'allow');
271
+ });
272
+
273
+ test('handleHook SessionEnd returns allow', () => {
274
+ const eventBus = createEventBus();
275
+ const bridge = createVirtualModelHookBridge({ eventBus });
276
+ const vm = makeVirtualModel('session-end');
277
+ const result = bridge.handleHook('VirtualModel.SessionEnd', {
278
+ data: { session: { id: 'sess-1', turnCount: 5 } }
279
+ }, vm);
280
+ assert.equal(result.decision, 'allow');
281
+ });
282
+
283
+ // ---------------------------------------------------------------------------
284
+ // 15. handleHook with unknown type returns allow
285
+ // ---------------------------------------------------------------------------
286
+
287
+ test('handleHook with unknown type returns allow', () => {
288
+ const bridge = createVirtualModelHookBridge();
289
+ const vm = makeVirtualModel('unknown');
290
+ const result = bridge.handleHook('VirtualModel.UnknownHook', {
291
+ data: {}
292
+ }, vm);
293
+ assert.equal(result.decision, 'allow');
294
+ });
295
+
296
+ // ---------------------------------------------------------------------------
297
+ // 16. handleHook with null virtual model returns allow
298
+ // ---------------------------------------------------------------------------
299
+
300
+ test('handleHook with null virtual model returns allow', () => {
301
+ const bridge = createVirtualModelHookBridge();
302
+ const result = bridge.handleHook('VirtualModel.PreCompletion', {
303
+ data: { request: { model: 'test' } }
304
+ }, null);
305
+ assert.equal(result.decision, 'allow');
306
+ });
307
+
308
+ // ---------------------------------------------------------------------------
309
+ // 17. handleHook TurnEnd returns action
310
+ // ---------------------------------------------------------------------------
311
+
312
+ test('handleHook TurnEnd returns allow with action', () => {
313
+ const bridge = createVirtualModelHookBridge();
314
+ const vm = makeVirtualModel('turn-end', {
315
+ hooks: {
316
+ onTurnEnd: 'return { action: "escalate" };'
317
+ }
318
+ });
319
+ const result = bridge.handleHook('VirtualModel.TurnEnd', {
320
+ data: { turn: { index: 3 }, session: { turnCount: 3 } }
321
+ }, vm);
322
+ assert.equal(result.decision, 'allow');
323
+ assert.equal(result.modifiedInput.action, 'escalate');
324
+ });
325
+
326
+ // ---------------------------------------------------------------------------
327
+ // 18. handleHook OnCompact with modification
328
+ // ---------------------------------------------------------------------------
329
+
330
+ test('handleHook OnCompact with modification returns modify', () => {
331
+ const bridge = createVirtualModelHookBridge();
332
+ const vm = makeVirtualModel('compact-mod', {
333
+ hooks: {
334
+ onCompact: 'return { modified: args.summary + " [compacted]" };'
335
+ }
336
+ });
337
+ const result = bridge.handleHook('VirtualModel.OnCompact', {
338
+ data: { summary: 'conversation so far', session: {} }
339
+ }, vm);
340
+ assert.equal(result.decision, 'modify');
341
+ assert.equal(result.modifiedInput.summary, 'conversation so far [compacted]');
342
+ });
343
+
344
+ // ---------------------------------------------------------------------------
345
+ // 19. handleHook OnCompact without modification returns allow
346
+ // ---------------------------------------------------------------------------
347
+
348
+ test('handleHook OnCompact without modification returns allow', () => {
349
+ const bridge = createVirtualModelHookBridge();
350
+ const vm = makeVirtualModel('compact-noop');
351
+ const result = bridge.handleHook('VirtualModel.OnCompact', {
352
+ data: { summary: 'conversation', session: {} }
353
+ }, vm);
354
+ assert.equal(result.decision, 'allow');
355
+ });
356
+
357
+ // ---------------------------------------------------------------------------
358
+ // 20. handleHook PostToolUse with modification
359
+ // ---------------------------------------------------------------------------
360
+
361
+ test('handleHook PostToolUse with modification returns modify', () => {
362
+ const bridge = createVirtualModelHookBridge();
363
+ const vm = makeVirtualModel('post-tool-mod', {
364
+ hooks: {
365
+ onPostToolUse: 'return { modified: "sanitized output" };'
366
+ }
367
+ });
368
+ const result = bridge.handleHook('VirtualModel.PostToolUse', {
369
+ data: { toolCall: { name: 'read' }, toolResult: 'raw output', session: {} }
370
+ }, vm);
371
+ assert.equal(result.decision, 'modify');
372
+ assert.equal(result.modifiedInput.result, 'sanitized output');
373
+ });
374
+
375
+ // ---------------------------------------------------------------------------
376
+ // 21. Bridge accepts custom controller
377
+ // ---------------------------------------------------------------------------
378
+
379
+ test('createVirtualModelHookBridge accepts custom controller', () => {
380
+ const controller = createVirtualModelController({ eventBus: createEventBus() });
381
+ const bridge = createVirtualModelHookBridge({ controller });
382
+ assert.ok(bridge, 'bridge should be created with custom controller');
383
+ assert.equal(bridge.role, 'virtual-model-hook-bridge');
384
+ });
@@ -0,0 +1,198 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { createAgentTriggerController, createResource } from '../src/index.js';
4
+
5
+ // ─── helpers ─────────────────────────────────────────────────────────────────
6
+
7
+ function makeWebhookRule(name, webhookTrigger = {}, extra = {}) {
8
+ return createResource('AgentTriggerRule', { name, namespace: 'kradle-org-default' }, {
9
+ organizationRef: 'default',
10
+ agentStack: 'ci-stack',
11
+ taskKind: 'diagnostic',
12
+ webhookTrigger,
13
+ ...extra,
14
+ });
15
+ }
16
+
17
+ function makeWebhookEvent(overrides = {}) {
18
+ return {
19
+ eventType: 'push',
20
+ repository: 'owner/repo',
21
+ ref: 'refs/heads/main',
22
+ action: null,
23
+ provider: 'github',
24
+ ...overrides,
25
+ };
26
+ }
27
+
28
+ // ─── tests ───────────────────────────────────────────────────────────────────
29
+
30
+ // 1. evaluateWebhookEvent matches push event to a push trigger rule
31
+ test('evaluateWebhookEvent: matches push event to push trigger rule', () => {
32
+ const ctrl = createAgentTriggerController();
33
+ const rule = makeWebhookRule('push-rule', { events: ['push'] });
34
+ const event = makeWebhookEvent({ eventType: 'push' });
35
+
36
+ const { matchingRules, dispatchIntents } = ctrl.evaluateWebhookEvent(event, [rule]);
37
+
38
+ assert.equal(matchingRules.length, 1);
39
+ assert.equal(matchingRules[0].metadata.name, 'push-rule');
40
+ assert.equal(dispatchIntents.length, 1);
41
+ assert.equal(dispatchIntents[0].agentStack, 'ci-stack');
42
+ assert.equal(dispatchIntents[0].taskKind, 'diagnostic');
43
+ });
44
+
45
+ // 2. evaluateWebhookEvent matches pull_request event to pull_request trigger rule
46
+ test('evaluateWebhookEvent: matches pull_request event to pull_request trigger rule', () => {
47
+ const ctrl = createAgentTriggerController();
48
+ const rule = makeWebhookRule('pr-rule', { events: ['pull_request'] });
49
+ const event = makeWebhookEvent({ eventType: 'pull_request', action: 'opened' });
50
+
51
+ const { matchingRules, dispatchIntents } = ctrl.evaluateWebhookEvent(event, [rule]);
52
+
53
+ assert.equal(matchingRules.length, 1);
54
+ assert.equal(matchingRules[0].metadata.name, 'pr-rule');
55
+ assert.equal(dispatchIntents[0].taskKind, 'diagnostic');
56
+ });
57
+
58
+ // 3. evaluateWebhookEvent returns empty when no rules match event type
59
+ test('evaluateWebhookEvent: returns empty when no rules match event type', () => {
60
+ const ctrl = createAgentTriggerController();
61
+ const rule = makeWebhookRule('push-only', { events: ['push'] });
62
+ const event = makeWebhookEvent({ eventType: 'issues' });
63
+
64
+ const { matchingRules, dispatchIntents } = ctrl.evaluateWebhookEvent(event, [rule]);
65
+
66
+ assert.equal(matchingRules.length, 0);
67
+ assert.equal(dispatchIntents.length, 0);
68
+ });
69
+
70
+ // 4. evaluateWebhookEvent filters by repository — matches when equal
71
+ test('evaluateWebhookEvent: filters by repository — matches when equal', () => {
72
+ const ctrl = createAgentTriggerController();
73
+ const rule = makeWebhookRule('repo-rule', { events: ['push'], repository: 'owner/repo' });
74
+ const event = makeWebhookEvent({ eventType: 'push', repository: 'owner/repo' });
75
+
76
+ const { matchingRules } = ctrl.evaluateWebhookEvent(event, [rule]);
77
+
78
+ assert.equal(matchingRules.length, 1);
79
+ });
80
+
81
+ // 5. evaluateWebhookEvent filters by repository — no match when different
82
+ test('evaluateWebhookEvent: filters by repository — no match when different', () => {
83
+ const ctrl = createAgentTriggerController();
84
+ const rule = makeWebhookRule('repo-rule', { events: ['push'], repository: 'owner/repo' });
85
+ const event = makeWebhookEvent({ eventType: 'push', repository: 'other/repo' });
86
+
87
+ const { matchingRules } = ctrl.evaluateWebhookEvent(event, [rule]);
88
+
89
+ assert.equal(matchingRules.length, 0);
90
+ });
91
+
92
+ // 6. evaluateWebhookEvent respects enabled: false — disabled rules are excluded
93
+ test('evaluateWebhookEvent: respects enabled: false — disabled rules are excluded', () => {
94
+ const ctrl = createAgentTriggerController();
95
+ const rule = makeWebhookRule('disabled-rule', { events: ['push'] }, { enabled: false });
96
+ const event = makeWebhookEvent({ eventType: 'push' });
97
+
98
+ const { matchingRules } = ctrl.evaluateWebhookEvent(event, [rule]);
99
+
100
+ assert.equal(matchingRules.length, 0);
101
+ });
102
+
103
+ // 7. evaluateWebhookEvent deduplicates rules with the same name
104
+ test('evaluateWebhookEvent: deduplicates rules with the same name', () => {
105
+ const ctrl = createAgentTriggerController();
106
+ const rule1 = makeWebhookRule('dup-rule', { events: ['push'] });
107
+ const rule2 = makeWebhookRule('dup-rule', { events: ['push'] }); // same name
108
+ const event = makeWebhookEvent({ eventType: 'push' });
109
+
110
+ const { matchingRules } = ctrl.evaluateWebhookEvent(event, [rule1, rule2]);
111
+
112
+ assert.equal(matchingRules.length, 1);
113
+ });
114
+
115
+ // 8. evaluateWebhookEvent handles empty rules array
116
+ test('evaluateWebhookEvent: handles empty rules array', () => {
117
+ const ctrl = createAgentTriggerController();
118
+ const event = makeWebhookEvent();
119
+
120
+ const { matchingRules, dispatchIntents } = ctrl.evaluateWebhookEvent(event, []);
121
+
122
+ assert.equal(matchingRules.length, 0);
123
+ assert.equal(dispatchIntents.length, 0);
124
+ });
125
+
126
+ // 9. evaluateWebhookEvent handles undefined/null rules gracefully
127
+ test('evaluateWebhookEvent: handles undefined rules gracefully', () => {
128
+ const ctrl = createAgentTriggerController();
129
+ const event = makeWebhookEvent();
130
+
131
+ const resultUndefined = ctrl.evaluateWebhookEvent(event, undefined);
132
+ assert.equal(resultUndefined.matchingRules.length, 0);
133
+ assert.equal(resultUndefined.dispatchIntents.length, 0);
134
+
135
+ const resultNull = ctrl.evaluateWebhookEvent(event, null);
136
+ assert.equal(resultNull.matchingRules.length, 0);
137
+ assert.equal(resultNull.dispatchIntents.length, 0);
138
+ });
139
+
140
+ // 10. evaluateWebhookEvent matches wildcard events array ['*']
141
+ test('evaluateWebhookEvent: matches wildcard events array [\'*\']', () => {
142
+ const ctrl = createAgentTriggerController();
143
+ const rule = makeWebhookRule('wildcard-rule', { events: ['*'] });
144
+ const events = ['push', 'pull_request', 'issues', 'workflow_run', 'ping'];
145
+
146
+ for (const eventType of events) {
147
+ const { matchingRules } = ctrl.evaluateWebhookEvent(makeWebhookEvent({ eventType }), [rule]);
148
+ assert.equal(matchingRules.length, 1, `Should match event type '${eventType}' with wildcard`);
149
+ }
150
+ });
151
+
152
+ // 11. evaluateWebhookEvent filters by action — matches when equal
153
+ test('evaluateWebhookEvent: filters by action — matches when equal', () => {
154
+ const ctrl = createAgentTriggerController();
155
+ const rule = makeWebhookRule('pr-opened-rule', { events: ['pull_request'], action: 'opened' });
156
+ const event = makeWebhookEvent({ eventType: 'pull_request', action: 'opened' });
157
+
158
+ const { matchingRules } = ctrl.evaluateWebhookEvent(event, [rule]);
159
+
160
+ assert.equal(matchingRules.length, 1);
161
+ });
162
+
163
+ // 12. evaluateWebhookEvent filters by action — no match when action differs
164
+ test('evaluateWebhookEvent: filters by action — no match when action differs', () => {
165
+ const ctrl = createAgentTriggerController();
166
+ const rule = makeWebhookRule('pr-opened-rule', { events: ['pull_request'], action: 'opened' });
167
+ const event = makeWebhookEvent({ eventType: 'pull_request', action: 'closed' });
168
+
169
+ const { matchingRules } = ctrl.evaluateWebhookEvent(event, [rule]);
170
+
171
+ assert.equal(matchingRules.length, 0);
172
+ });
173
+
174
+ // 13. evaluateWebhookEvent matches when events array is absent (match all)
175
+ test('evaluateWebhookEvent: matches when events array is absent (match all)', () => {
176
+ const ctrl = createAgentTriggerController();
177
+ // webhookTrigger present but no events filter
178
+ const rule = makeWebhookRule('no-events-filter', {});
179
+ const event = makeWebhookEvent({ eventType: 'workflow_run' });
180
+
181
+ const { matchingRules } = ctrl.evaluateWebhookEvent(event, [rule]);
182
+
183
+ assert.equal(matchingRules.length, 1);
184
+ });
185
+
186
+ // 14. dispatchIntent includes correct agentStack and uses custom taskKind
187
+ test('evaluateWebhookEvent: dispatchIntent includes agentStack and custom taskKind', () => {
188
+ const ctrl = createAgentTriggerController();
189
+ const rule = makeWebhookRule('custom-kind-rule', { events: ['push'] }, { taskKind: 'repair', agentStack: 'my-stack' });
190
+ const event = makeWebhookEvent({ eventType: 'push' });
191
+
192
+ const { dispatchIntents } = ctrl.evaluateWebhookEvent(event, [rule]);
193
+
194
+ assert.equal(dispatchIntents.length, 1);
195
+ assert.equal(dispatchIntents[0].agentStack, 'my-stack');
196
+ assert.equal(dispatchIntents[0].taskKind, 'repair');
197
+ assert.deepEqual(dispatchIntents[0].event, event);
198
+ });