@a5c-ai/krate 5.0.1-staging.04a3db697
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 +183 -0
- package/bin/krate-demo.mjs +23 -0
- package/bin/krate-server.mjs +14 -0
- package/dist/krate-controller-ui.json +3067 -0
- package/dist/krate-lifecycle.json +201 -0
- package/dist/krate-runtime-snapshot.json +2955 -0
- package/dist/krate-summary.json +722 -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-krate-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/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/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/krate-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/product-requirements.md +62 -0
- package/docs/roadmap-mvp.md +87 -0
- package/docs/system-requirements.md +90 -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/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 +63 -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 +93 -0
- package/scripts/validate-ui.mjs +236 -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 +209 -0
- package/src/agent-gateway-config-controller.js +147 -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 +280 -0
- package/src/agent-permission-review.js +250 -0
- package/src/agent-project-controller.js +117 -0
- package/src/agent-provider-config-controller.js +150 -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 +347 -0
- package/src/agent-subagent-controller.js +160 -0
- package/src/agent-transport-binding-controller.js +121 -0
- package/src/agent-trigger-controller.js +321 -0
- package/src/agent-workspace-controller.js +447 -0
- package/src/agent-writeback-controller.js +302 -0
- package/src/api-controller.js +541 -0
- package/src/argocd-gitops.js +43 -0
- package/src/async-controller.js +207 -0
- package/src/audit-controller.js +191 -0
- package/src/auth.js +307 -0
- package/src/component-catalog.js +41 -0
- package/src/control-plane.js +136 -0
- package/src/controller-client.js +50 -0
- package/src/controller-ui.js +551 -0
- package/src/data-plane.js +178 -0
- package/src/event-bus.js +61 -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 +161 -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 +95 -0
- package/src/gitea-service.js +173 -0
- package/src/handoff.js +98 -0
- package/src/hooks-events.js +63 -0
- package/src/http-server.js +377 -0
- package/src/identity-policy.js +86 -0
- package/src/index.js +55 -0
- package/src/kubernetes-controller-async.js +511 -0
- package/src/kubernetes-controller.js +878 -0
- package/src/kubernetes-resource-gateway.js +48 -0
- package/src/operations.js +112 -0
- package/src/org-scoping.js +5 -0
- package/src/resource-model.js +221 -0
- package/src/runners-ci.js +48 -0
- package/src/runtime.js +196 -0
- package/src/snapshot-cache.js +157 -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 +315 -0
- package/tests/agent-gateway-config-controller.test.js +386 -0
- package/tests/agent-memory-controller.test.js +308 -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 +204 -0
- package/tests/agent-permission-review-v2.test.js +317 -0
- package/tests/agent-permission-review.test.js +209 -0
- package/tests/agent-project-controller.test.js +302 -0
- package/tests/agent-provider-config-controller.test.js +376 -0
- package/tests/agent-resources.test.js +228 -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 +221 -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 +211 -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/async-controller.test.js +252 -0
- package/tests/audit-controller.test.js +227 -0
- package/tests/deployment.test.js +396 -0
- package/tests/e2e/lifecycle.test.js +117 -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 +340 -0
- package/tests/external-provider-adapter.test.js +365 -0
- package/tests/external-resource-model.test.js +215 -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/integration/full-flow.test.js +266 -0
- package/tests/krate.test.js +727 -0
- package/tests/memory-search-wiring.test.js +270 -0
- package/tests/org-scoping.test.js +687 -0
- package/tests/session-cookie-hmac.test.js +151 -0
- package/tests/snapshot-performance.test.js +247 -0
- package/tests/sse-events.test.js +107 -0
- package/tests/workspace-volumes.test.js +312 -0
- package/tests/writeback-persistence.test.js +207 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External Integration Tests — Wire external controllers into runtime
|
|
3
|
+
*
|
|
4
|
+
* Tests that the api-controller properly delegates to sync-controller,
|
|
5
|
+
* write-controller, conflict-controller, and webhook-controller.
|
|
6
|
+
*/
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
import test from 'node:test';
|
|
9
|
+
import { createKrateApiController } from '../src/api-controller.js';
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Helpers
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
/** Minimal resource gateway stub for testing */
|
|
16
|
+
function makeGateway(overrides = {}) {
|
|
17
|
+
const store = new Map();
|
|
18
|
+
return {
|
|
19
|
+
namespace: 'krate-system',
|
|
20
|
+
resourceDefinitions: {},
|
|
21
|
+
async snapshot() { return { resources: {}, namespace: 'krate-system' }; },
|
|
22
|
+
async list(kind) { return { items: [] }; },
|
|
23
|
+
async get(kind, name) { return null; },
|
|
24
|
+
async apply(resource) {
|
|
25
|
+
store.set(resource.metadata?.name, resource);
|
|
26
|
+
return { operation: 'apply', resource };
|
|
27
|
+
},
|
|
28
|
+
async delete(kind, name) { return { operation: 'delete' }; },
|
|
29
|
+
watch() { return { close: () => {} }; },
|
|
30
|
+
...overrides
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function makeController(gatewayOverrides = {}) {
|
|
35
|
+
return createKrateApiController({ resourceGateway: makeGateway(gatewayOverrides) });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Test 1: syncExternalBinding exists on controller
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
test('api-controller exposes syncExternalBinding method', () => {
|
|
43
|
+
const controller = makeController();
|
|
44
|
+
assert.equal(typeof controller.syncExternalBinding, 'function',
|
|
45
|
+
'controller must expose syncExternalBinding');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Test 2: syncExternalBinding creates sync controller and runs sync
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
test('syncExternalBinding creates sync controller and upserts resource', async () => {
|
|
53
|
+
const controller = makeController();
|
|
54
|
+
|
|
55
|
+
const result = await controller.syncExternalBinding('github-binding', {
|
|
56
|
+
kind: 'Repository',
|
|
57
|
+
localName: 'my-repo',
|
|
58
|
+
namespace: 'default',
|
|
59
|
+
spec: { organizationRef: 'default' },
|
|
60
|
+
externalEnvelope: {
|
|
61
|
+
nativeId: 'github-repo-42',
|
|
62
|
+
url: 'https://github.com/org/repo',
|
|
63
|
+
etag: '"abc"',
|
|
64
|
+
providerRef: 'github-provider'
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
assert.ok(result, 'syncExternalBinding must return a result');
|
|
69
|
+
assert.ok(result.resource, 'result must include the upserted resource');
|
|
70
|
+
assert.equal(result.resource.status?.external?.nativeId, 'github-repo-42',
|
|
71
|
+
'upserted resource must have correct nativeId');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Test 3: syncExternalBinding calls persistFn (applyResource) for watermark
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
test('syncExternalBinding with watermark timestamp persists watermark via applyResource', async () => {
|
|
79
|
+
const applied = [];
|
|
80
|
+
const controller = createKrateApiController({
|
|
81
|
+
resourceGateway: makeGateway({
|
|
82
|
+
async apply(resource) {
|
|
83
|
+
applied.push(JSON.parse(JSON.stringify(resource)));
|
|
84
|
+
return { operation: 'apply', resource };
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await controller.syncExternalBinding('github-binding-wm', {
|
|
90
|
+
kind: 'Repository',
|
|
91
|
+
localName: 'wm-repo',
|
|
92
|
+
namespace: 'default',
|
|
93
|
+
spec: {},
|
|
94
|
+
externalEnvelope: { nativeId: 'id-1', url: 'https://example.com', etag: '"v1"', providerRef: 'github' },
|
|
95
|
+
watermark: '2024-01-01T10:00:00.000Z'
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Allow async persist tick
|
|
99
|
+
await new Promise((r) => setImmediate(r));
|
|
100
|
+
|
|
101
|
+
assert.ok(applied.length >= 1, 'applyResource must be called at least once for the upserted resource');
|
|
102
|
+
const kinds = applied.map((r) => r.kind);
|
|
103
|
+
assert.ok(kinds.some((k) => k), 'applied resources must have kind fields');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Test 4: writeController called from api-controller creates and persists intent
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
test('api-controller exposes approveExternalWriteIntent method', () => {
|
|
111
|
+
const controller = makeController();
|
|
112
|
+
assert.equal(typeof controller.approveExternalWriteIntent, 'function',
|
|
113
|
+
'controller must expose approveExternalWriteIntent');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Test 5: createExternalWriteIntent creates a WriteIntent via write controller
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
test('api-controller exposes createExternalWriteIntent method', () => {
|
|
121
|
+
const controller = makeController();
|
|
122
|
+
assert.equal(typeof controller.createExternalWriteIntent, 'function',
|
|
123
|
+
'controller must expose createExternalWriteIntent');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('createExternalWriteIntent creates intent with correct phase', async () => {
|
|
127
|
+
const controller = makeController();
|
|
128
|
+
const result = await controller.createExternalWriteIntent({
|
|
129
|
+
interfaceKey: 'issueTracking',
|
|
130
|
+
operation: 'createIssue',
|
|
131
|
+
payload: { title: 'Bug fix' },
|
|
132
|
+
resourceRef: 'org/repo#1',
|
|
133
|
+
requiresApproval: false
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
assert.ok(result, 'createExternalWriteIntent must return a result');
|
|
137
|
+
assert.ok(result.intent, 'result must include the intent');
|
|
138
|
+
assert.equal(result.intent.status.phase, 'ReadyToSend',
|
|
139
|
+
'intent without approval must start as ReadyToSend');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Test 6: conflictController detects divergence when sync finds changed resource
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
test('api-controller exposes detectExternalConflict method', () => {
|
|
147
|
+
const controller = makeController();
|
|
148
|
+
assert.equal(typeof controller.detectExternalConflict, 'function',
|
|
149
|
+
'controller must expose detectExternalConflict');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('detectExternalConflict creates conflict when local and external values differ', async () => {
|
|
153
|
+
const controller = makeController();
|
|
154
|
+
const result = await controller.detectExternalConflict({
|
|
155
|
+
resourceRef: 'org/repo#issue-1',
|
|
156
|
+
fieldPath: 'spec.title',
|
|
157
|
+
localValue: 'Local Title',
|
|
158
|
+
externalValue: 'External Title'
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
assert.ok(result, 'detectExternalConflict must return a result');
|
|
162
|
+
assert.ok(result.conflict, 'result must include a conflict resource when values differ');
|
|
163
|
+
assert.equal(result.conflict.status.phase, 'Open',
|
|
164
|
+
'detected conflict must start as Open');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('detectExternalConflict returns null conflict when values match', async () => {
|
|
168
|
+
const controller = makeController();
|
|
169
|
+
const result = await controller.detectExternalConflict({
|
|
170
|
+
resourceRef: 'org/repo#issue-2',
|
|
171
|
+
fieldPath: 'spec.title',
|
|
172
|
+
localValue: 'Same Value',
|
|
173
|
+
externalValue: 'Same Value'
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
assert.ok(result, 'detectExternalConflict must return a result');
|
|
177
|
+
assert.equal(result.conflict, null,
|
|
178
|
+
'conflict must be null when values are equal');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Test 7: Full flow — webhook → normalize → sync → detect conflict → resolve
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
test('api-controller exposes processExternalWebhook method', () => {
|
|
186
|
+
const controller = makeController();
|
|
187
|
+
assert.equal(typeof controller.processExternalWebhook, 'function',
|
|
188
|
+
'controller must expose processExternalWebhook');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('processExternalWebhook processes delivery and emits to subscribers', async () => {
|
|
192
|
+
const controller = makeController();
|
|
193
|
+
const eventsReceived = [];
|
|
194
|
+
|
|
195
|
+
const result = await controller.processExternalWebhook({
|
|
196
|
+
deliveryId: 'delivery-001',
|
|
197
|
+
eventType: 'pull_request',
|
|
198
|
+
payload: { action: 'opened', repo: 'org/repo' },
|
|
199
|
+
rawBody: '{"action":"opened","repo":"org/repo"}',
|
|
200
|
+
providerType: 'github',
|
|
201
|
+
secret: 'my-secret'
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
assert.ok(result, 'processExternalWebhook must return a result');
|
|
205
|
+
assert.ok(!result.error, `must not error: ${result.message}`);
|
|
206
|
+
assert.ok('duplicate' in result || 'deliveryId' in result || 'queued' in result,
|
|
207
|
+
'result must have delivery tracking fields');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// Test 8: api-controller.syncExternalBinding creates sync controller and runs sync
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
test('syncExternalBinding returns sync state for the binding', async () => {
|
|
215
|
+
const controller = makeController();
|
|
216
|
+
|
|
217
|
+
// Sync a resource
|
|
218
|
+
await controller.syncExternalBinding('binding-status-test', {
|
|
219
|
+
kind: 'Repository',
|
|
220
|
+
localName: 'status-repo',
|
|
221
|
+
namespace: 'default',
|
|
222
|
+
spec: { organizationRef: 'default' },
|
|
223
|
+
externalEnvelope: {
|
|
224
|
+
nativeId: 'id-status-1',
|
|
225
|
+
url: 'https://github.com/org/status-repo',
|
|
226
|
+
etag: '"v1"',
|
|
227
|
+
providerRef: 'github'
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
assert.ok(true, 'syncExternalBinding must complete without error');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// Test 9: api-controller.resolveExternalConflict calls conflict controller
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
test('api-controller exposes resolveExternalConflict method', () => {
|
|
239
|
+
const controller = makeController();
|
|
240
|
+
assert.equal(typeof controller.resolveExternalConflict, 'function',
|
|
241
|
+
'controller must expose resolveExternalConflict');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('resolveExternalConflict resolves an existing Open conflict', async () => {
|
|
245
|
+
const controller = makeController();
|
|
246
|
+
|
|
247
|
+
// First detect a conflict
|
|
248
|
+
const detectResult = await controller.detectExternalConflict({
|
|
249
|
+
resourceRef: 'org/repo#3',
|
|
250
|
+
fieldPath: 'spec.body',
|
|
251
|
+
localValue: 'Old',
|
|
252
|
+
externalValue: 'New'
|
|
253
|
+
});
|
|
254
|
+
assert.ok(detectResult.conflict, 'must detect conflict first');
|
|
255
|
+
|
|
256
|
+
// Now resolve it using the prefer-external strategy
|
|
257
|
+
const resolveResult = await controller.resolveExternalConflict({
|
|
258
|
+
conflictName: detectResult.conflict.metadata.name,
|
|
259
|
+
strategy: 'prefer-external',
|
|
260
|
+
resources: { ExternalSyncConflict: [detectResult.conflict] }
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
assert.ok(resolveResult, 'resolveExternalConflict must return a result');
|
|
264
|
+
assert.ok(!resolveResult.error, `must not error: ${resolveResult.message}`);
|
|
265
|
+
assert.equal(resolveResult.conflict.status.phase, 'Resolved',
|
|
266
|
+
'conflict must be Resolved after resolution');
|
|
267
|
+
assert.equal(resolveResult.resolution.chosenValue, 'New',
|
|
268
|
+
'prefer-external must choose the external value');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
// Test 10: api-controller.approveExternalWriteIntent calls write controller
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
test('approveExternalWriteIntent approves a PendingApproval intent', async () => {
|
|
276
|
+
const controller = makeController();
|
|
277
|
+
|
|
278
|
+
// Create a pending approval intent
|
|
279
|
+
const createResult = await controller.createExternalWriteIntent({
|
|
280
|
+
interfaceKey: 'gitForge',
|
|
281
|
+
operation: 'createPR',
|
|
282
|
+
payload: { title: 'My PR' },
|
|
283
|
+
resourceRef: 'org/repo#pr-1',
|
|
284
|
+
requiresApproval: true
|
|
285
|
+
});
|
|
286
|
+
assert.ok(createResult.intent, 'must create intent');
|
|
287
|
+
assert.equal(createResult.intent.status.phase, 'PendingApproval', 'must start as PendingApproval');
|
|
288
|
+
|
|
289
|
+
// Now approve it
|
|
290
|
+
const approveResult = await controller.approveExternalWriteIntent({
|
|
291
|
+
intentName: createResult.intent.metadata.name,
|
|
292
|
+
approvedBy: 'admin-user',
|
|
293
|
+
resources: { ExternalWriteIntent: [createResult.intent] }
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
assert.ok(approveResult, 'approveExternalWriteIntent must return a result');
|
|
297
|
+
assert.ok(!approveResult.error, `must not error: ${approveResult.message}`);
|
|
298
|
+
assert.equal(approveResult.intent.status.phase, 'ReadyToSend',
|
|
299
|
+
'approved intent must transition to ReadyToSend');
|
|
300
|
+
assert.equal(approveResult.intent.status.approvedBy, 'admin-user',
|
|
301
|
+
'approver must be recorded');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
// Test 11: api-controller.cancelExternalWriteIntent cancels an intent
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
test('api-controller exposes cancelExternalWriteIntent method', () => {
|
|
309
|
+
const controller = makeController();
|
|
310
|
+
assert.equal(typeof controller.cancelExternalWriteIntent, 'function',
|
|
311
|
+
'controller must expose cancelExternalWriteIntent');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('cancelExternalWriteIntent rejects a PendingApproval intent', async () => {
|
|
315
|
+
const controller = makeController();
|
|
316
|
+
|
|
317
|
+
const createResult = await controller.createExternalWriteIntent({
|
|
318
|
+
interfaceKey: 'cicd',
|
|
319
|
+
operation: 'triggerBuild',
|
|
320
|
+
payload: { branch: 'main' },
|
|
321
|
+
resourceRef: 'org/repo#build-1',
|
|
322
|
+
requiresApproval: true
|
|
323
|
+
});
|
|
324
|
+
assert.ok(createResult.intent, 'must create intent');
|
|
325
|
+
|
|
326
|
+
const cancelResult = await controller.cancelExternalWriteIntent({
|
|
327
|
+
intentName: createResult.intent.metadata.name,
|
|
328
|
+
cancelledBy: 'user-1',
|
|
329
|
+
resources: { ExternalWriteIntent: [createResult.intent] }
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
assert.ok(cancelResult, 'cancelExternalWriteIntent must return a result');
|
|
333
|
+
assert.ok(!cancelResult.error, `must not error: ${cancelResult.message}`);
|
|
334
|
+
assert.equal(cancelResult.intent.status.phase, 'Rejected',
|
|
335
|
+
'cancelled intent must be Rejected');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
339
|
+
// Test 12: api-controller.getExternalSyncStatus returns binding sync state
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
|
|
342
|
+
test('api-controller exposes getExternalSyncStatus method', () => {
|
|
343
|
+
const controller = makeController();
|
|
344
|
+
assert.equal(typeof controller.getExternalSyncStatus, 'function',
|
|
345
|
+
'controller must expose getExternalSyncStatus');
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test('getExternalSyncStatus returns null watermark for unknown binding', async () => {
|
|
349
|
+
const controller = makeController();
|
|
350
|
+
const result = await controller.getExternalSyncStatus('no-such-binding');
|
|
351
|
+
|
|
352
|
+
assert.ok(result, 'getExternalSyncStatus must return a result');
|
|
353
|
+
assert.equal(result.watermark, null,
|
|
354
|
+
'watermark must be null for unseen binding');
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
// Test 13: processWebhookAndSync — webhook delivery → event normalization → sync controller upserts resource
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
test('syncExternalBinding normalizes the provided envelope into a K8s-style resource', async () => {
|
|
362
|
+
const controller = makeController();
|
|
363
|
+
const result = await controller.syncExternalBinding('github-binding-norm', {
|
|
364
|
+
kind: 'PullRequest',
|
|
365
|
+
localName: 'pr-101',
|
|
366
|
+
namespace: 'krate-org-acme',
|
|
367
|
+
spec: { organizationRef: 'acme', title: 'Fix bug' },
|
|
368
|
+
externalEnvelope: {
|
|
369
|
+
nativeId: 'pr-github-101',
|
|
370
|
+
url: 'https://github.com/acme/repo/pull/101',
|
|
371
|
+
etag: '"pr-etag-1"',
|
|
372
|
+
providerRef: 'github'
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
assert.ok(result.resource, 'result must include resource');
|
|
377
|
+
assert.equal(result.resource.kind, 'PullRequest', 'resource kind must match input');
|
|
378
|
+
assert.equal(result.resource.metadata.name, 'pr-101', 'resource name must match localName');
|
|
379
|
+
assert.equal(result.resource.metadata.namespace, 'krate-org-acme', 'namespace must be preserved');
|
|
380
|
+
assert.equal(result.resource.status.external.nativeId, 'pr-github-101', 'nativeId must be set');
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
// Test 14: syncController called with persistFn persists watermark and resource
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
|
|
387
|
+
test('syncExternalBinding with watermark option advances the watermark', async () => {
|
|
388
|
+
const applied = [];
|
|
389
|
+
const controller = createKrateApiController({
|
|
390
|
+
resourceGateway: makeGateway({
|
|
391
|
+
async apply(resource) {
|
|
392
|
+
applied.push(JSON.parse(JSON.stringify(resource)));
|
|
393
|
+
return { operation: 'apply', resource };
|
|
394
|
+
}
|
|
395
|
+
})
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const watermarkTs = '2025-03-15T09:30:00.000Z';
|
|
399
|
+
await controller.syncExternalBinding('wm-binding-adv', {
|
|
400
|
+
kind: 'Repository',
|
|
401
|
+
localName: 'adv-repo',
|
|
402
|
+
namespace: 'default',
|
|
403
|
+
spec: {},
|
|
404
|
+
externalEnvelope: { nativeId: 'id-wm', url: 'https://x.com', etag: '"wm"', providerRef: 'github' },
|
|
405
|
+
watermark: watermarkTs
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
await new Promise((r) => setImmediate(r));
|
|
409
|
+
|
|
410
|
+
// At least the upserted resource must have been applied
|
|
411
|
+
assert.ok(applied.length >= 1, 'at least one resource must be applied');
|
|
412
|
+
|
|
413
|
+
// Look for the watermark in the applied resources payload
|
|
414
|
+
const applied2 = await controller.getExternalSyncStatus('wm-binding-adv');
|
|
415
|
+
assert.equal(applied2.watermark, watermarkTs,
|
|
416
|
+
'getExternalSyncStatus must reflect the advanced watermark');
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// ---------------------------------------------------------------------------
|
|
420
|
+
// Test 15: Full end-to-end flow: webhook → normalize → sync → detect conflict → resolve
|
|
421
|
+
// ---------------------------------------------------------------------------
|
|
422
|
+
|
|
423
|
+
test('end-to-end: processExternalWebhook → syncExternalBinding → detectExternalConflict → resolveExternalConflict', async () => {
|
|
424
|
+
const controller = makeController();
|
|
425
|
+
|
|
426
|
+
// Step 1: Process an inbound webhook
|
|
427
|
+
const webhookResult = await controller.processExternalWebhook({
|
|
428
|
+
deliveryId: 'e2e-delivery-001',
|
|
429
|
+
eventType: 'issues',
|
|
430
|
+
payload: { action: 'edited', issue: { id: 5001, title: 'New Title' } },
|
|
431
|
+
rawBody: '{"action":"edited","issue":{"id":5001,"title":"New Title"}}',
|
|
432
|
+
providerType: 'github',
|
|
433
|
+
secret: 'e2e-secret'
|
|
434
|
+
});
|
|
435
|
+
assert.ok(webhookResult, 'processExternalWebhook must succeed');
|
|
436
|
+
|
|
437
|
+
// Step 2: Sync the resource from the external provider
|
|
438
|
+
const syncResult = await controller.syncExternalBinding('e2e-github-binding', {
|
|
439
|
+
kind: 'Issue',
|
|
440
|
+
localName: 'issue-5001',
|
|
441
|
+
namespace: 'default',
|
|
442
|
+
spec: { title: 'New Title', organizationRef: 'default' },
|
|
443
|
+
externalEnvelope: {
|
|
444
|
+
nativeId: 'gh-issue-5001',
|
|
445
|
+
url: 'https://github.com/org/repo/issues/5001',
|
|
446
|
+
etag: '"etag-v2"',
|
|
447
|
+
providerRef: 'github'
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
assert.ok(syncResult.resource, 'syncExternalBinding must return a resource');
|
|
451
|
+
|
|
452
|
+
// Step 3: Detect a conflict between local and external state
|
|
453
|
+
const conflictResult = await controller.detectExternalConflict({
|
|
454
|
+
resourceRef: 'org/repo#issue-5001',
|
|
455
|
+
fieldPath: 'spec.title',
|
|
456
|
+
localValue: 'Old Title',
|
|
457
|
+
externalValue: 'New Title'
|
|
458
|
+
});
|
|
459
|
+
assert.ok(conflictResult.conflict, 'conflict must be detected when titles differ');
|
|
460
|
+
|
|
461
|
+
// Step 4: Resolve the conflict
|
|
462
|
+
const resolveResult = await controller.resolveExternalConflict({
|
|
463
|
+
conflictName: conflictResult.conflict.metadata.name,
|
|
464
|
+
strategy: 'prefer-external',
|
|
465
|
+
resources: { ExternalSyncConflict: [conflictResult.conflict] }
|
|
466
|
+
});
|
|
467
|
+
assert.ok(!resolveResult.error, `conflict resolution must succeed: ${resolveResult.message}`);
|
|
468
|
+
assert.equal(resolveResult.conflict.status.phase, 'Resolved',
|
|
469
|
+
'conflict must be Resolved after end-to-end flow');
|
|
470
|
+
});
|