@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.
- package/Dockerfile +31 -0
- package/README.md +187 -0
- package/bin/kradle-demo.mjs +23 -0
- package/bin/kradle-server.mjs +14 -0
- package/dist/kradle-controller-ui.json +3482 -0
- package/dist/kradle-lifecycle.json +201 -0
- package/dist/kradle-runtime-snapshot.json +3125 -0
- package/dist/kradle-summary.json +724 -0
- package/docs/README.md +61 -0
- package/docs/agents/README.md +83 -0
- package/docs/agents/acceptance-test-matrix.md +193 -0
- package/docs/agents/agent-mux-adapter-contract.md +167 -0
- package/docs/agents/agent-mux-source-map.md +310 -0
- package/docs/agents/agent-run-memory-import-spec.md +256 -0
- package/docs/agents/agent-stack-management-spec.md +421 -0
- package/docs/agents/api-contract-spec.md +309 -0
- package/docs/agents/artifacts-writeback-spec.md +145 -0
- package/docs/agents/chart-packaging-spec.md +128 -0
- package/docs/agents/ci-orchestration-spec.md +140 -0
- package/docs/agents/context-assembly-spec.md +219 -0
- package/docs/agents/controller-reconciliation-spec.md +255 -0
- package/docs/agents/crd-schema-spec.md +315 -0
- package/docs/agents/decision-log-open-questions.md +169 -0
- package/docs/agents/developer-implementation-checklist.md +329 -0
- package/docs/agents/dispatching-design.md +262 -0
- package/docs/agents/gaps-agent-mux-to-kradle-crds.md +298 -0
- package/docs/agents/glossary.md +66 -0
- package/docs/agents/implementation-blueprint.md +324 -0
- package/docs/agents/implementation-rollout-slices.md +251 -0
- package/docs/agents/memory-context-integration-spec.md +194 -0
- package/docs/agents/memory-ontology-schema-spec.md +253 -0
- package/docs/agents/memory-operations-runbook.md +121 -0
- package/docs/agents/mvp-vertical-slice-spec.md +146 -0
- package/docs/agents/observability-audit-spec.md +265 -0
- package/docs/agents/operator-runbook.md +174 -0
- package/docs/agents/org-memory-api-payload-examples.md +333 -0
- package/docs/agents/org-memory-controller-sequence-spec.md +181 -0
- package/docs/agents/org-memory-e2e-fixture-plan.md +161 -0
- package/docs/agents/org-memory-ui-implementation-map.md +114 -0
- package/docs/agents/org-memory-vertical-slice-spec.md +168 -0
- package/docs/agents/org-resource-model-delta-spec.md +111 -0
- package/docs/agents/org-route-resource-model-spec.md +183 -0
- package/docs/agents/org-scoping-namespace-spec.md +114 -0
- package/docs/agents/rbac-secrets-management-spec.md +406 -0
- package/docs/agents/repository-page-integration-spec.md +255 -0
- package/docs/agents/resource-contract-examples.md +808 -0
- package/docs/agents/resource-relationship-map.md +190 -0
- package/docs/agents/security-threat-model.md +188 -0
- package/docs/agents/shared-memory-company-brain-spec.md +358 -0
- package/docs/agents/storage-migration-spec.md +168 -0
- package/docs/agents/subagent-orchestration-spec.md +152 -0
- package/docs/agents/system-overview.md +88 -0
- package/docs/agents/tools-mcp-skills-spec.md +189 -0
- package/docs/agents/traceability-matrix.md +79 -0
- package/docs/agents/ui-flow-spec.md +211 -0
- package/docs/agents/ui-ux-system-spec.md +426 -0
- package/docs/agents/workspace-lifecycle-spec.md +166 -0
- package/docs/architecture-spec.md +78 -0
- package/docs/architecture-v2.md +2759 -0
- package/docs/components/control-plane.md +78 -0
- package/docs/components/data-plane.md +69 -0
- package/docs/components/hooks-events.md +67 -0
- package/docs/components/identity-rbac-policy.md +73 -0
- package/docs/components/kubevela-oam.md +70 -0
- package/docs/components/operations-publishing.md +81 -0
- package/docs/components/runners-ci.md +66 -0
- package/docs/components/web-ui.md +94 -0
- package/docs/crd-behaviors-and-relationships.md +3926 -0
- package/docs/external/README.md +47 -0
- package/docs/external/bidirectional-sync-design.md +134 -0
- package/docs/external/cicd-interface.md +64 -0
- package/docs/external/external-backend-controllers.md +170 -0
- package/docs/external/external-backend-crds.md +234 -0
- package/docs/external/external-backend-ui-spec.md +151 -0
- package/docs/external/external-backend-ux-flows.md +115 -0
- package/docs/external/external-object-mapping.md +125 -0
- package/docs/external/git-forge-interface.md +68 -0
- package/docs/external/github-integration-design.md +151 -0
- package/docs/external/issue-tracking-interface.md +66 -0
- package/docs/external/provider-capability-manifests.md +204 -0
- package/docs/external/provider-catalog.md +139 -0
- package/docs/external/provider-rollout-testing.md +78 -0
- package/docs/external/research-results.md +48 -0
- package/docs/external/security-auth-permissions.md +81 -0
- package/docs/external/sync-state-machines.md +108 -0
- package/docs/external/unified-external-backend-model.md +107 -0
- package/docs/external/user-facing-changes.md +67 -0
- package/docs/gaps.md +161 -0
- package/docs/install.md +94 -0
- package/docs/integration-and-design-decisions.md +1530 -0
- package/docs/kradle-design.md +334 -0
- package/docs/local-minikube.md +55 -0
- package/docs/ontology/README.md +32 -0
- package/docs/ontology/bounded-contexts.md +29 -0
- package/docs/ontology/events-and-hooks.md +32 -0
- package/docs/ontology/oam-kubevela.md +32 -0
- package/docs/ontology/operations-and-release.md +25 -0
- package/docs/ontology/personas-and-actors.md +32 -0
- package/docs/ontology/policies-and-invariants.md +33 -0
- package/docs/ontology/problem-space.md +30 -0
- package/docs/ontology/resource-contracts.md +40 -0
- package/docs/ontology/resource-taxonomy.md +42 -0
- package/docs/ontology/runners-and-ci.md +29 -0
- package/docs/ontology/solution-space.md +24 -0
- package/docs/ontology/storage-and-data-boundaries.md +29 -0
- package/docs/ontology/validation-matrix.md +24 -0
- package/docs/ontology/web-ui-excellent-flows.md +32 -0
- package/docs/ontology/workflows.md +39 -0
- package/docs/ontology/world.md +35 -0
- package/docs/openapi.yaml +1291 -0
- package/docs/product-requirements.md +62 -0
- package/docs/requirements-v2.md +235 -0
- package/docs/roadmap-mvp.md +87 -0
- package/docs/sdk-api-reference.md +1108 -0
- package/docs/system-requirements.md +90 -0
- package/docs/system-spec-v2.md +1230 -0
- package/docs/tests/README.md +53 -0
- package/docs/tests/agent-qa-plan.md +63 -0
- package/docs/tests/browser-ui-tests.md +62 -0
- package/docs/tests/ci-quality-gates.md +48 -0
- package/docs/tests/coverage-model.md +64 -0
- package/docs/tests/e2e-scenario-tests.md +53 -0
- package/docs/tests/fixtures-test-data.md +63 -0
- package/docs/tests/observability-reliability-tests.md +54 -0
- package/docs/tests/product-test-matrix.md +145 -0
- package/docs/tests/qa-adoption-roadmap.md +130 -0
- package/docs/tests/qa-automation-plan.md +101 -0
- package/docs/tests/security-compliance-tests.md +57 -0
- package/docs/tests/test-framework-tools.md +88 -0
- package/docs/tests/test-suite-layout.md +121 -0
- package/docs/tests/unit-integration-tests.md +48 -0
- package/docs/todo-kyverno +714 -0
- package/docs/todos.md +4 -0
- package/docs/user-stories.md +78 -0
- package/docs/web-console-spec.md +533 -0
- package/examples/minikube-demo.yaml +190 -0
- package/examples/oam-application.yaml +23 -0
- package/examples/policy-kyverno-pr-title.yaml +18 -0
- package/package.json +66 -0
- package/scripts/build.mjs +29 -0
- package/scripts/setup-minikube.mjs +65 -0
- package/scripts/smoke.mjs +37 -0
- package/scripts/validate-doc-coverage.mjs +152 -0
- package/scripts/validate-package.mjs +95 -0
- package/scripts/validate-ui.mjs +305 -0
- package/src/agent-adapter-controller.js +169 -0
- package/src/agent-approval-controller.js +170 -0
- package/src/agent-context-bundles.js +242 -0
- package/src/agent-dispatch-controller.js +549 -0
- package/src/agent-gateway-config-controller.js +147 -0
- package/src/agent-identity-migration.js +115 -0
- package/src/agent-memory-controller.js +357 -0
- package/src/agent-memory-import.js +327 -0
- package/src/agent-memory-query.js +292 -0
- package/src/agent-memory-repository-source-controller.js +255 -0
- package/src/agent-mux-client.js +589 -0
- package/src/agent-permission-review.js +250 -0
- package/src/agent-persona-controller.js +135 -0
- package/src/agent-project-controller.js +117 -0
- package/src/agent-prompt-composition.js +55 -0
- package/src/agent-provider-config-controller.js +151 -0
- package/src/agent-secret-config-grant-controller.js +282 -0
- package/src/agent-session-transcript-controller.js +189 -0
- package/src/agent-stack-controller.js +421 -0
- package/src/agent-subagent-controller.js +160 -0
- package/src/agent-transport-binding-controller.js +121 -0
- package/src/agent-trigger-controller.js +387 -0
- package/src/agent-workspace-controller.js +702 -0
- package/src/agent-writeback-controller.js +302 -0
- package/src/api-controller.js +621 -0
- package/src/argocd-gitops.js +43 -0
- package/src/artifact-registry-controller.js +542 -0
- package/src/assistant-runtime.js +284 -0
- package/src/async-controller.js +207 -0
- package/src/audit-controller.js +191 -0
- package/src/auth.js +310 -0
- package/src/component-catalog.js +41 -0
- package/src/control-plane.js +136 -0
- package/src/controller-client.js +112 -0
- package/src/controller-ui.js +620 -0
- package/src/data-plane.js +179 -0
- package/src/event-bus.js +397 -0
- package/src/external/conflict-controller.js +225 -0
- package/src/external/github/auth.js +96 -0
- package/src/external/github/cicd.js +180 -0
- package/src/external/github/git-forge.js +240 -0
- package/src/external/github/index.js +144 -0
- package/src/external/github/issue-tracking.js +163 -0
- package/src/external/provider-adapter.js +161 -0
- package/src/external/provider-resource-factory.js +221 -0
- package/src/external/sync-controller.js +235 -0
- package/src/external/webhook-controller.js +144 -0
- package/src/external/write-controller.js +283 -0
- package/src/gitea-backend.js +131 -0
- package/src/gitea-service.js +173 -0
- package/src/handoff.js +98 -0
- package/src/health-probes.js +134 -0
- package/src/hooks-events.js +63 -0
- package/src/hooks-lifecycle.js +117 -0
- package/src/http-server.js +409 -0
- package/src/identity-policy.js +86 -0
- package/src/index.js +71 -0
- package/src/jitsi-agent-bridge.js +141 -0
- package/src/jitsi-meeting-controller.js +291 -0
- package/src/jitsi-sync-controller.js +198 -0
- package/src/kradle-inference-service-controller.js +246 -0
- package/src/kubernetes-controller-async.js +531 -0
- package/src/kubernetes-controller.js +904 -0
- package/src/kubernetes-resource-gateway.js +48 -0
- package/src/model-route-controller.js +364 -0
- package/src/notification-controller.js +178 -0
- package/src/operations.js +112 -0
- package/src/org-scoping.js +5 -0
- package/src/resource-model.js +282 -0
- package/src/runner-controller.js +272 -0
- package/src/runners-ci.js +48 -0
- package/src/runtime.js +196 -0
- package/src/snapshot-cache.js +157 -0
- package/src/virtual-model-controller.js +538 -0
- package/src/virtual-model-hook-bridge.js +200 -0
- package/src/web-ui.js +40 -0
- package/tests/agent-adapter-controller.test.js +361 -0
- package/tests/agent-approval-controller.test.js +173 -0
- package/tests/agent-context-bundles.test.js +278 -0
- package/tests/agent-dispatch-controller.test.js +679 -0
- package/tests/agent-gateway-config-controller.test.js +386 -0
- package/tests/agent-identity-migration.test.js +87 -0
- package/tests/agent-memory-controller.test.js +461 -0
- package/tests/agent-memory-import-snapshot.test.js +477 -0
- package/tests/agent-memory-query.test.js +404 -0
- package/tests/agent-memory-repository-source.test.js +514 -0
- package/tests/agent-mux-client.test.js +389 -0
- package/tests/agent-mux-integration.test.js +971 -0
- package/tests/agent-permission-review-v2.test.js +317 -0
- package/tests/agent-permission-review.test.js +209 -0
- package/tests/agent-persona-controller.test.js +127 -0
- package/tests/agent-project-controller.test.js +302 -0
- package/tests/agent-prompt-composition.test.js +76 -0
- package/tests/agent-provider-config-controller.test.js +376 -0
- package/tests/agent-resources.test.js +303 -0
- package/tests/agent-secret-config-grant.test.js +231 -0
- package/tests/agent-session-transcript-controller.test.js +499 -0
- package/tests/agent-stack-controller.test.js +283 -0
- package/tests/agent-subagent-controller.test.js +201 -0
- package/tests/agent-transport-binding-controller.test.js +294 -0
- package/tests/agent-trigger-controller.test.js +271 -0
- package/tests/agent-trigger-routes.test.js +190 -0
- package/tests/agent-trigger-sources.test.js +245 -0
- package/tests/agent-workspace-controller.test.js +181 -0
- package/tests/agent-writeback.test.js +292 -0
- package/tests/approval-persistence.test.js +171 -0
- package/tests/artifact-registry.test.js +511 -0
- package/tests/assistant-runtime.test.js +506 -0
- package/tests/async-controller.test.js +252 -0
- package/tests/audit-controller.test.js +227 -0
- package/tests/codespace-controller.test.js +318 -0
- package/tests/controller-client.test.js +133 -0
- package/tests/deployment.test.js +527 -0
- package/tests/e2e/lifecycle.test.js +120 -0
- package/tests/event-bus-integration.test.js +355 -0
- package/tests/external-github-forge.test.js +560 -0
- package/tests/external-github-issues-cicd.test.js +520 -0
- package/tests/external-integration.test.js +470 -0
- package/tests/external-persistence.test.js +415 -0
- package/tests/external-provider-adapter.test.js +365 -0
- package/tests/external-resource-model.test.js +223 -0
- package/tests/external-webhook-sync.test.js +287 -0
- package/tests/external-write-conflict.test.js +353 -0
- package/tests/gitea-service.test.js +253 -0
- package/tests/health-check-real.test.js +165 -0
- package/tests/health-probes.test.js +90 -0
- package/tests/hooks-lifecycle.test.js +364 -0
- package/tests/integration/full-flow.test.js +266 -0
- package/tests/jitsi-agent-bridge.test.js +119 -0
- package/tests/jitsi-helm-integration.test.js +77 -0
- package/tests/jitsi-meeting-controller.test.js +170 -0
- package/tests/jitsi-resource-model.test.js +73 -0
- package/tests/jitsi-sync-controller.test.js +112 -0
- package/tests/kradle-inference-service.test.js +689 -0
- package/tests/kradle.test.js +779 -0
- package/tests/memory-search-wiring.test.js +270 -0
- package/tests/model-route-controller.test.js +733 -0
- package/tests/notification-controller.test.js +196 -0
- package/tests/notification-integration.test.js +179 -0
- package/tests/org-scoping.test.js +687 -0
- package/tests/runner-controller.test.js +327 -0
- package/tests/runner-integration.test.js +231 -0
- package/tests/session-cookie-hmac.test.js +151 -0
- package/tests/snapshot-performance.test.js +315 -0
- package/tests/sse-events.test.js +107 -0
- package/tests/virtual-model-controller.test.js +877 -0
- package/tests/virtual-model-hook-bridge.test.js +384 -0
- package/tests/webhook-trigger.test.js +198 -0
- package/tests/workspace-volumes.test.js +312 -0
- 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
|
+
});
|