@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,253 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { createGiteaService } from '../src/gitea-service.js';
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Helpers — minimal fetch mocks
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
function makeFetch(responses) {
|
|
10
|
+
// responses: Map<string, { status, body, isText? }>
|
|
11
|
+
return async (url) => {
|
|
12
|
+
const match = [...responses.entries()].find(([pattern]) => url.includes(pattern));
|
|
13
|
+
if (!match) {
|
|
14
|
+
return {
|
|
15
|
+
ok: false,
|
|
16
|
+
status: 404,
|
|
17
|
+
json: async () => null,
|
|
18
|
+
text: async () => '',
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const [, { status = 200, body, isText = false }] = match;
|
|
22
|
+
const ok = status >= 200 && status < 300;
|
|
23
|
+
return {
|
|
24
|
+
ok,
|
|
25
|
+
status,
|
|
26
|
+
json: async () => (isText ? body : body),
|
|
27
|
+
text: async () => (typeof body === 'string' ? body : JSON.stringify(body)),
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// 1. No URL → null
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
test('createGiteaService returns null when no URL is configured', () => {
|
|
37
|
+
const originalEnv = process.env.KRATE_GITEA_HTTP_URL;
|
|
38
|
+
delete process.env.KRATE_GITEA_HTTP_URL;
|
|
39
|
+
try {
|
|
40
|
+
const service = createGiteaService({});
|
|
41
|
+
assert.equal(service, null, 'should return null when giteaUrl is absent');
|
|
42
|
+
} finally {
|
|
43
|
+
if (originalEnv !== undefined) process.env.KRATE_GITEA_HTTP_URL = originalEnv;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// 2. URL provided → service object
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
test('createGiteaService returns a service object when a URL is provided', () => {
|
|
52
|
+
const service = createGiteaService({ giteaUrl: 'http://localhost:3000', fetchImpl: makeFetch(new Map()) });
|
|
53
|
+
assert.ok(service !== null, 'should return a service');
|
|
54
|
+
assert.equal(service.available, true, 'available should be true');
|
|
55
|
+
assert.ok(typeof service.listTree === 'function', 'has listTree');
|
|
56
|
+
assert.ok(typeof service.getBlob === 'function', 'has getBlob');
|
|
57
|
+
assert.ok(typeof service.listBranches === 'function', 'has listBranches');
|
|
58
|
+
assert.ok(typeof service.getFileContent === 'function', 'has getFileContent');
|
|
59
|
+
assert.ok(typeof service.createRepository === 'function', 'has createRepository');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// 3. listTree — calls correct Gitea endpoint
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
test('listTree calls the Gitea contents endpoint and maps type:dir to tree', async () => {
|
|
67
|
+
const captured = [];
|
|
68
|
+
const fetchImpl = async (url, opts) => {
|
|
69
|
+
captured.push(url);
|
|
70
|
+
return {
|
|
71
|
+
ok: true,
|
|
72
|
+
status: 200,
|
|
73
|
+
json: async () => [
|
|
74
|
+
{ name: 'src', path: 'src', type: 'dir', size: 0, sha: 'abc' },
|
|
75
|
+
{ name: 'README.md', path: 'README.md', type: 'file', size: 512, sha: 'def' },
|
|
76
|
+
],
|
|
77
|
+
text: async () => '',
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const service = createGiteaService({ giteaUrl: 'http://localhost:3000', fetchImpl });
|
|
82
|
+
const entries = await service.listTree('myorg', 'myrepo', 'main', '');
|
|
83
|
+
|
|
84
|
+
assert.ok(captured[0].includes('/repos/myorg/myrepo/contents/'), 'called contents endpoint');
|
|
85
|
+
assert.ok(captured[0].includes('ref=main'), 'passed ref param');
|
|
86
|
+
|
|
87
|
+
assert.equal(entries.length, 2, 'should return 2 entries');
|
|
88
|
+
const dir = entries.find((e) => e.name === 'src');
|
|
89
|
+
assert.equal(dir.type, 'tree', 'type:dir should map to tree');
|
|
90
|
+
const file = entries.find((e) => e.name === 'README.md');
|
|
91
|
+
assert.equal(file.type, 'blob', 'type:file should map to blob');
|
|
92
|
+
assert.equal(file.size, 512, 'size is preserved');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// 4. listTree — returns null for 404
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
test('listTree returns null when Gitea responds with 404', async () => {
|
|
100
|
+
const fetchImpl = async () => ({ ok: false, status: 404, json: async () => null, text: async () => '' });
|
|
101
|
+
const service = createGiteaService({ giteaUrl: 'http://localhost:3000', fetchImpl });
|
|
102
|
+
const result = await service.listTree('org', 'missing-repo', 'main', '');
|
|
103
|
+
assert.equal(result, null, 'should return null for 404');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// 5. getBlob — calls raw endpoint and returns text
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
test('getBlob calls the Gitea raw endpoint and returns file text', async () => {
|
|
111
|
+
const capturedUrls = [];
|
|
112
|
+
const expectedContent = 'console.log("hello");\n';
|
|
113
|
+
const fetchImpl = async (url) => {
|
|
114
|
+
capturedUrls.push(url);
|
|
115
|
+
return {
|
|
116
|
+
ok: true,
|
|
117
|
+
status: 200,
|
|
118
|
+
json: async () => null,
|
|
119
|
+
text: async () => expectedContent,
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const service = createGiteaService({ giteaUrl: 'http://localhost:3000', fetchImpl });
|
|
124
|
+
const content = await service.getBlob('myorg', 'myrepo', 'main', 'src/index.js');
|
|
125
|
+
|
|
126
|
+
assert.ok(capturedUrls[0].includes('/repos/myorg/myrepo/raw/'), 'called raw endpoint');
|
|
127
|
+
assert.ok(capturedUrls[0].includes('ref=main'), 'passed ref param');
|
|
128
|
+
assert.equal(content, expectedContent, 'returned raw file text');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// 6. getBlob — returns null for 404
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
test('getBlob returns null when file is not found in Gitea', async () => {
|
|
136
|
+
const fetchImpl = async () => ({ ok: false, status: 404, json: async () => null, text: async () => '' });
|
|
137
|
+
const service = createGiteaService({ giteaUrl: 'http://localhost:3000', fetchImpl });
|
|
138
|
+
const result = await service.getBlob('org', 'repo', 'main', 'nonexistent.js');
|
|
139
|
+
assert.equal(result, null, 'should return null for missing file');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// 7. listBranches — maps branch response correctly
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
test('listBranches returns array with name, sha, protected fields', async () => {
|
|
147
|
+
const fetchImpl = async () => ({
|
|
148
|
+
ok: true,
|
|
149
|
+
status: 200,
|
|
150
|
+
json: async () => [
|
|
151
|
+
{ name: 'main', commit: { id: 'sha-main' }, protected: true },
|
|
152
|
+
{ name: 'dev', commit: { id: 'sha-dev' }, protected: false },
|
|
153
|
+
],
|
|
154
|
+
text: async () => '',
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const service = createGiteaService({ giteaUrl: 'http://localhost:3000', fetchImpl });
|
|
158
|
+
const branches = await service.listBranches('myorg', 'myrepo');
|
|
159
|
+
|
|
160
|
+
assert.equal(branches.length, 2, 'two branches returned');
|
|
161
|
+
assert.equal(branches[0].name, 'main');
|
|
162
|
+
assert.equal(branches[0].sha, 'sha-main');
|
|
163
|
+
assert.equal(branches[0].protected, true);
|
|
164
|
+
assert.equal(branches[1].name, 'dev');
|
|
165
|
+
assert.equal(branches[1].protected, false);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// 8. getFileContent — decodes base64 content
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
test('getFileContent decodes base64 content from Gitea response', async () => {
|
|
173
|
+
const originalText = 'export default function hello() {}\n';
|
|
174
|
+
const b64 = Buffer.from(originalText).toString('base64');
|
|
175
|
+
|
|
176
|
+
const fetchImpl = async () => ({
|
|
177
|
+
ok: true,
|
|
178
|
+
status: 200,
|
|
179
|
+
json: async () => ({
|
|
180
|
+
path: 'src/hello.js',
|
|
181
|
+
size: originalText.length,
|
|
182
|
+
sha: 'abc123',
|
|
183
|
+
content: b64,
|
|
184
|
+
last_commit_sha: 'commitsha',
|
|
185
|
+
}),
|
|
186
|
+
text: async () => '',
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const service = createGiteaService({ giteaUrl: 'http://localhost:3000', fetchImpl });
|
|
190
|
+
const file = await service.getFileContent('myorg', 'myrepo', 'main', 'src/hello.js');
|
|
191
|
+
|
|
192
|
+
assert.equal(file.content, originalText, 'content decoded from base64');
|
|
193
|
+
assert.equal(file.path, 'src/hello.js', 'path preserved');
|
|
194
|
+
assert.equal(file.sha, 'abc123', 'sha preserved');
|
|
195
|
+
assert.equal(file.lastCommit, 'commitsha', 'lastCommit mapped');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// 9. Error handling — non-404 errors propagate
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
test('listTree throws when Gitea returns a non-404 error status', async () => {
|
|
203
|
+
const fetchImpl = async () => ({ ok: false, status: 500, json: async () => null, text: async () => '' });
|
|
204
|
+
const service = createGiteaService({ giteaUrl: 'http://localhost:3000', fetchImpl });
|
|
205
|
+
|
|
206
|
+
await assert.rejects(
|
|
207
|
+
() => service.listTree('org', 'repo', 'main', ''),
|
|
208
|
+
/500/,
|
|
209
|
+
'should throw on 500 error'
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// 10. createGiteaService picks up KRATE_GITEA_HTTP_URL from env
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
test('createGiteaService reads KRATE_GITEA_HTTP_URL from environment', () => {
|
|
218
|
+
const original = process.env.KRATE_GITEA_HTTP_URL;
|
|
219
|
+
process.env.KRATE_GITEA_HTTP_URL = 'http://gitea-from-env:3000';
|
|
220
|
+
try {
|
|
221
|
+
const fetchImpl = makeFetch(new Map());
|
|
222
|
+
const service = createGiteaService({ fetchImpl });
|
|
223
|
+
assert.ok(service !== null, 'should return a service when env var is set');
|
|
224
|
+
assert.equal(service.baseUrl, 'http://gitea-from-env:3000', 'baseUrl should match env var');
|
|
225
|
+
} finally {
|
|
226
|
+
if (original !== undefined) process.env.KRATE_GITEA_HTTP_URL = original;
|
|
227
|
+
else delete process.env.KRATE_GITEA_HTTP_URL;
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
// 11. listTree — subdirectory path is URL-encoded correctly
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
test('listTree encodes subdirectory path in the API URL', async () => {
|
|
236
|
+
const capturedUrls = [];
|
|
237
|
+
const fetchImpl = async (url) => {
|
|
238
|
+
capturedUrls.push(url);
|
|
239
|
+
return {
|
|
240
|
+
ok: true,
|
|
241
|
+
status: 200,
|
|
242
|
+
json: async () => [{ name: 'App.jsx', path: 'src/components/App.jsx', type: 'file', size: 100, sha: 'x' }],
|
|
243
|
+
text: async () => '',
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const service = createGiteaService({ giteaUrl: 'http://localhost:3000', fetchImpl });
|
|
248
|
+
await service.listTree('org', 'repo', 'main', 'src/components');
|
|
249
|
+
|
|
250
|
+
// The path 'src/components' should appear in the URL (slashes may or may not be encoded)
|
|
251
|
+
assert.ok(capturedUrls[0].includes('src'), 'URL contains path segment src');
|
|
252
|
+
assert.ok(capturedUrls[0].includes('components'), 'URL contains path segment components');
|
|
253
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { createResource } from '../src/index.js';
|
|
4
|
+
import { createAgentAdapterController } from '../src/agent-adapter-controller.js';
|
|
5
|
+
import { createAgentStackController } from '../src/agent-stack-controller.js';
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Slice C4 — Real health checks for adapters + MCP
|
|
9
|
+
//
|
|
10
|
+
// healthCheck() must perform a real HTTP fetch when spec.healthEndpoint is set.
|
|
11
|
+
// These tests use a mock fetch via dependency injection.
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
function makeAdapter(name, overrides = {}) {
|
|
15
|
+
return createResource('AgentAdapter', { name, namespace: 'krate-org-default' }, {
|
|
16
|
+
organizationRef: 'default',
|
|
17
|
+
adapterType: 'subprocess',
|
|
18
|
+
transport: 'http',
|
|
19
|
+
capabilities: ['tool-use'],
|
|
20
|
+
...overrides
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function makeMcpServer(name, overrides = {}) {
|
|
25
|
+
return createResource('AgentMcpServer', { name, namespace: 'krate-org-default' }, {
|
|
26
|
+
endpoint: 'http://localhost:9090/mcp',
|
|
27
|
+
...overrides
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// C4.1: healthCheck with endpoint returns healthy when fetch succeeds
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
test('healthCheck with endpoint returns healthy when fetch succeeds', async () => {
|
|
36
|
+
const mockFetch = async (url, options) => {
|
|
37
|
+
return { ok: true, status: 200 };
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const controller = createAgentAdapterController({ fetch: mockFetch });
|
|
41
|
+
const adapter = makeAdapter('healthy-adapter', { healthEndpoint: 'http://localhost:9090/health' });
|
|
42
|
+
|
|
43
|
+
const result = await controller.healthCheck(adapter);
|
|
44
|
+
|
|
45
|
+
assert.ok(result, 'healthCheck must return a result');
|
|
46
|
+
assert.equal(result.status, 'healthy', 'status must be "healthy" when fetch succeeds');
|
|
47
|
+
assert.equal(result.adapterName, adapter.metadata.name, 'result must carry the adapter name');
|
|
48
|
+
assert.ok(typeof result.latencyMs === 'number', 'result must include latencyMs');
|
|
49
|
+
assert.ok(result.latencyMs >= 0, 'latencyMs must be non-negative');
|
|
50
|
+
assert.ok(!result.error, 'result must not have an error when healthy');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// C4.2: healthCheck with endpoint returns unhealthy when fetch fails (non-ok)
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
test('healthCheck with endpoint returns unhealthy when fetch returns non-ok status', async () => {
|
|
58
|
+
const mockFetch = async (url, options) => {
|
|
59
|
+
return { ok: false, status: 503 };
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const controller = createAgentAdapterController({ fetch: mockFetch });
|
|
63
|
+
const adapter = makeAdapter('unhealthy-adapter', { healthEndpoint: 'http://localhost:9090/health' });
|
|
64
|
+
|
|
65
|
+
const result = await controller.healthCheck(adapter);
|
|
66
|
+
|
|
67
|
+
assert.ok(result, 'healthCheck must return a result');
|
|
68
|
+
assert.equal(result.status, 'unhealthy', 'status must be "unhealthy" when fetch returns non-ok');
|
|
69
|
+
assert.equal(result.adapterName, adapter.metadata.name, 'result must carry the adapter name');
|
|
70
|
+
assert.ok(typeof result.latencyMs === 'number', 'result must include latencyMs');
|
|
71
|
+
assert.ok(result.error, 'result must include an error message when unhealthy');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// C4.3: healthCheck with endpoint returns unhealthy on network error / timeout
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
test('healthCheck with endpoint returns unhealthy when fetch throws (timeout/network)', async () => {
|
|
79
|
+
const mockFetch = async (url, options) => {
|
|
80
|
+
throw new Error('AbortError: The operation was aborted');
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const controller = createAgentAdapterController({ fetch: mockFetch });
|
|
84
|
+
const adapter = makeAdapter('timeout-adapter', { healthEndpoint: 'http://localhost:9090/health' });
|
|
85
|
+
|
|
86
|
+
const result = await controller.healthCheck(adapter);
|
|
87
|
+
|
|
88
|
+
assert.ok(result, 'healthCheck must return a result');
|
|
89
|
+
assert.equal(result.status, 'unhealthy', 'status must be "unhealthy" when fetch throws');
|
|
90
|
+
assert.equal(result.adapterName, adapter.metadata.name, 'result must carry the adapter name');
|
|
91
|
+
assert.ok(typeof result.latencyMs === 'number', 'result must include latencyMs');
|
|
92
|
+
assert.ok(result.error, 'result must include an error message when fetch throws');
|
|
93
|
+
assert.ok(result.error.includes('AbortError') || result.error.includes('aborted') || result.error.length > 0, 'error message must be non-empty');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// C4.4: healthCheck without endpoint returns unknown (existing behaviour preserved)
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
test('healthCheck without endpoint returns unknown with reason no-endpoint', async () => {
|
|
101
|
+
const controller = createAgentAdapterController();
|
|
102
|
+
const adapter = makeAdapter('no-endpoint-adapter');
|
|
103
|
+
// no healthEndpoint in spec
|
|
104
|
+
|
|
105
|
+
const result = await controller.healthCheck(adapter);
|
|
106
|
+
|
|
107
|
+
assert.ok(result, 'healthCheck must return a result');
|
|
108
|
+
assert.equal(result.status, 'unknown', 'status must be "unknown" when no endpoint configured');
|
|
109
|
+
assert.equal(result.reason, 'no-endpoint', 'reason must be "no-endpoint"');
|
|
110
|
+
assert.equal(result.adapterName, adapter.metadata.name, 'result must carry the adapter name');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// C4.5: MCP server health check — healthy when endpoint fetch succeeds
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
test('AgentStackController MCP health check returns healthy when server endpoint responds', async () => {
|
|
118
|
+
const mockFetch = async (url, options) => {
|
|
119
|
+
return { ok: true, status: 200 };
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const controller = createAgentStackController({ fetch: mockFetch });
|
|
123
|
+
|
|
124
|
+
const mcpServer = makeMcpServer('test-mcp', { endpoint: 'http://localhost:9090/mcp' });
|
|
125
|
+
const result = await controller.checkMcpHealth(mcpServer);
|
|
126
|
+
|
|
127
|
+
assert.ok(result, 'checkMcpHealth must return a result');
|
|
128
|
+
assert.equal(result.status, 'healthy', 'status must be "healthy" when fetch succeeds');
|
|
129
|
+
assert.ok(typeof result.latencyMs === 'number', 'result must include latencyMs');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// C4.6: MCP server health check — unhealthy when fetch fails
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
test('AgentStackController MCP health check returns unhealthy when server fetch fails', async () => {
|
|
137
|
+
const mockFetch = async (url, options) => {
|
|
138
|
+
throw new Error('Connection refused');
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const controller = createAgentStackController({ fetch: mockFetch });
|
|
142
|
+
|
|
143
|
+
const mcpServer = makeMcpServer('unreachable-mcp', { endpoint: 'http://localhost:9999/mcp' });
|
|
144
|
+
const result = await controller.checkMcpHealth(mcpServer);
|
|
145
|
+
|
|
146
|
+
assert.ok(result, 'checkMcpHealth must return a result');
|
|
147
|
+
assert.equal(result.status, 'unhealthy', 'status must be "unhealthy" when fetch throws');
|
|
148
|
+
assert.ok(result.error, 'result must include an error message');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// C4.7: MCP server health check — unknown when no endpoint configured
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
test('AgentStackController MCP health check returns unknown when no endpoint configured', async () => {
|
|
156
|
+
const controller = createAgentStackController();
|
|
157
|
+
|
|
158
|
+
const mcpServer = makeMcpServer('no-endpoint-mcp', { endpoint: undefined });
|
|
159
|
+
delete mcpServer.spec.endpoint;
|
|
160
|
+
const result = await controller.checkMcpHealth(mcpServer);
|
|
161
|
+
|
|
162
|
+
assert.ok(result, 'checkMcpHealth must return a result');
|
|
163
|
+
assert.equal(result.status, 'unknown', 'status must be "unknown" when no endpoint configured');
|
|
164
|
+
assert.equal(result.reason, 'no-endpoint', 'reason must be "no-endpoint"');
|
|
165
|
+
});
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Integration Tests — End-to-end data flow tests using mock kubectl gateway
|
|
3
|
+
*
|
|
4
|
+
* Tests the full data flow through the Krate API controller using a mock
|
|
5
|
+
* resource gateway that stores resources in a Map instead of kubectl.
|
|
6
|
+
*/
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
import test from 'node:test';
|
|
9
|
+
import {
|
|
10
|
+
createKrateApiController,
|
|
11
|
+
createResource,
|
|
12
|
+
createAuditController,
|
|
13
|
+
createAgentSecretGrantController,
|
|
14
|
+
createAgentMemoryController,
|
|
15
|
+
listGrantsForAgent,
|
|
16
|
+
} from '../../src/index.js';
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Mock resource gateway — stores resources in a Map instead of kubectl
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
function createMockGateway() {
|
|
23
|
+
const store = new Map();
|
|
24
|
+
return {
|
|
25
|
+
namespace: 'krate-org-test',
|
|
26
|
+
async apply(resource) {
|
|
27
|
+
store.set(`${resource.kind}/${resource.metadata.name}`, resource);
|
|
28
|
+
return { operation: 'apply', resource };
|
|
29
|
+
},
|
|
30
|
+
async list(kind) {
|
|
31
|
+
return { items: [...store.values()].filter((r) => r.kind === kind) };
|
|
32
|
+
},
|
|
33
|
+
async get(kind, name) {
|
|
34
|
+
return { resource: store.get(`${kind}/${name}`) || null };
|
|
35
|
+
},
|
|
36
|
+
async delete(kind, name) {
|
|
37
|
+
store.delete(`${kind}/${name}`);
|
|
38
|
+
return { operation: 'delete' };
|
|
39
|
+
},
|
|
40
|
+
async snapshot() {
|
|
41
|
+
return { namespace: 'krate-org-test', resources: {} };
|
|
42
|
+
},
|
|
43
|
+
watch() {
|
|
44
|
+
return { close: () => {} };
|
|
45
|
+
},
|
|
46
|
+
resourceDefinitions: [],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Test 1: Org/user/repo flow
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
test('full flow: create org → user → repository → list shows repo', async () => {
|
|
55
|
+
const gw = createMockGateway();
|
|
56
|
+
const controller = createKrateApiController({ resourceGateway: gw });
|
|
57
|
+
|
|
58
|
+
// Create org
|
|
59
|
+
const org = createResource('Organization', { name: 'test-org', namespace: 'krate-system' }, { displayName: 'Test Org', namespaceName: 'krate-org-test-org' });
|
|
60
|
+
await controller.applyResource(org);
|
|
61
|
+
|
|
62
|
+
// Create user
|
|
63
|
+
const user = createResource('User', { name: 'alice', namespace: 'krate-org-test-org' }, { organizationRef: 'test-org', displayName: 'Alice', email: 'alice@example.com' });
|
|
64
|
+
await controller.applyResource(user);
|
|
65
|
+
|
|
66
|
+
// Create repository
|
|
67
|
+
const repo = createResource('Repository', { name: 'my-repo', namespace: 'krate-org-test-org' }, { organizationRef: 'test-org', visibility: 'internal' });
|
|
68
|
+
await controller.applyResource(repo);
|
|
69
|
+
|
|
70
|
+
// List repositories — should include the new repo
|
|
71
|
+
const result = await controller.listResource('Repository');
|
|
72
|
+
assert.ok(Array.isArray(result.items), 'result.items must be an array');
|
|
73
|
+
const found = result.items.find((r) => r.metadata?.name === 'my-repo');
|
|
74
|
+
assert.ok(found, 'my-repo should appear in listResource(Repository)');
|
|
75
|
+
assert.equal(found.spec?.organizationRef, 'test-org');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Test 2: Agent stack + trigger rule flow
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
test('full flow: create agent stack → create trigger rule → both appear in list', async () => {
|
|
83
|
+
const gw = createMockGateway();
|
|
84
|
+
const controller = createKrateApiController({ resourceGateway: gw });
|
|
85
|
+
|
|
86
|
+
// Create agent stack
|
|
87
|
+
const stack = createResource(
|
|
88
|
+
'AgentStack',
|
|
89
|
+
{ name: 'review-bot', namespace: 'krate-org-myorg' },
|
|
90
|
+
{ organizationRef: 'myorg', baseAgent: 'claude-code', adapter: 'default', runtimeIdentity: 'workspace' }
|
|
91
|
+
);
|
|
92
|
+
await controller.applyResource(stack);
|
|
93
|
+
|
|
94
|
+
// Create trigger rule
|
|
95
|
+
const rule = createResource(
|
|
96
|
+
'AgentTriggerRule',
|
|
97
|
+
{ name: 'pr-trigger', namespace: 'krate-org-myorg' },
|
|
98
|
+
{ organizationRef: 'myorg', sources: [{ type: 'pull_request', events: ['opened'] }], agentStack: 'review-bot', taskKind: 'code-review' }
|
|
99
|
+
);
|
|
100
|
+
await controller.applyResource(rule);
|
|
101
|
+
|
|
102
|
+
// Verify both appear
|
|
103
|
+
const stackList = await controller.listResource('AgentStack');
|
|
104
|
+
assert.ok(stackList.items.some((s) => s.metadata?.name === 'review-bot'), 'review-bot stack should appear');
|
|
105
|
+
|
|
106
|
+
const ruleList = await controller.listResource('AgentTriggerRule');
|
|
107
|
+
assert.ok(ruleList.items.some((r) => r.metadata?.name === 'pr-trigger'), 'pr-trigger rule should appear');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Test 3: Workspace flow
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
test('full flow: create workspace → verify it appears in list', async () => {
|
|
115
|
+
const gw = createMockGateway();
|
|
116
|
+
const controller = createKrateApiController({ resourceGateway: gw });
|
|
117
|
+
|
|
118
|
+
// Create workspace
|
|
119
|
+
const workspace = createResource(
|
|
120
|
+
'KrateWorkspace',
|
|
121
|
+
{ name: 'dev-workspace', namespace: 'krate-org-myorg' },
|
|
122
|
+
{ organizationRef: 'myorg', repository: 'my-repo', volumeSpec: { storageClass: 'standard', size: '10Gi' } }
|
|
123
|
+
);
|
|
124
|
+
await controller.applyResource(workspace);
|
|
125
|
+
|
|
126
|
+
// List workspaces — should include the new one
|
|
127
|
+
const result = await controller.listResource('KrateWorkspace');
|
|
128
|
+
assert.ok(Array.isArray(result.items));
|
|
129
|
+
const found = result.items.find((r) => r.metadata?.name === 'dev-workspace');
|
|
130
|
+
assert.ok(found, 'dev-workspace should appear in listResource(KrateWorkspace)');
|
|
131
|
+
assert.equal(found.spec?.repository, 'my-repo');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Test 4: Secret grant flow
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
test('full flow: create secret grant → listGrantsForAgent returns it', async () => {
|
|
139
|
+
const gw = createMockGateway();
|
|
140
|
+
const ctrl = createAgentSecretGrantController();
|
|
141
|
+
|
|
142
|
+
// Create a secret grant
|
|
143
|
+
const result = ctrl.createSecretGrant({
|
|
144
|
+
name: 'db-pass-grant',
|
|
145
|
+
orgRef: 'myorg',
|
|
146
|
+
secretName: 'db-password',
|
|
147
|
+
grantedTo: 'review-bot',
|
|
148
|
+
permissions: ['read'],
|
|
149
|
+
namespace: 'krate-org-myorg',
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
assert.ok(result.grant, 'createSecretGrant must return a grant');
|
|
153
|
+
assert.equal(result.grant.kind, 'AgentSecretGrant');
|
|
154
|
+
assert.equal(result.grant.metadata.name, 'db-pass-grant');
|
|
155
|
+
assert.equal(result.grant.spec.grantedTo, 'review-bot');
|
|
156
|
+
|
|
157
|
+
// Apply the grant resource via mock gateway
|
|
158
|
+
await gw.apply(result.grant);
|
|
159
|
+
|
|
160
|
+
// List grants and filter for this agent
|
|
161
|
+
const list = await gw.list('AgentSecretGrant');
|
|
162
|
+
const agentGrants = listGrantsForAgent(list.items, 'review-bot');
|
|
163
|
+
assert.ok(agentGrants.length >= 1, 'listGrantsForAgent should return at least 1 grant for review-bot');
|
|
164
|
+
assert.equal(agentGrants[0].metadata.name, 'db-pass-grant');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Test 5: External flow — syncExternalBinding
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
test('full flow: syncExternalBinding creates resource with external envelope', async () => {
|
|
172
|
+
const gw = createMockGateway();
|
|
173
|
+
const controller = createKrateApiController({ resourceGateway: gw });
|
|
174
|
+
|
|
175
|
+
const result = await controller.syncExternalBinding('github-binding', {
|
|
176
|
+
kind: 'Repository',
|
|
177
|
+
localName: 'synced-repo',
|
|
178
|
+
namespace: 'default',
|
|
179
|
+
spec: { organizationRef: 'default', visibility: 'private' },
|
|
180
|
+
externalEnvelope: {
|
|
181
|
+
nativeId: 'gh-repo-42',
|
|
182
|
+
url: 'https://github.com/org/synced-repo',
|
|
183
|
+
etag: '"abc123"',
|
|
184
|
+
providerRef: 'github-provider',
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
assert.ok(result, 'syncExternalBinding must return a result');
|
|
189
|
+
assert.ok(result.resource, 'result must include the upserted resource');
|
|
190
|
+
assert.equal(
|
|
191
|
+
result.resource.status?.external?.nativeId || result.resource?.status?.external?.nativeId,
|
|
192
|
+
'gh-repo-42',
|
|
193
|
+
'upserted resource must have correct nativeId'
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
// Test 6: Memory flow — create repository source → verify in list
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
test('full flow: create memory repository → apply → appears in list', async () => {
|
|
202
|
+
const gw = createMockGateway();
|
|
203
|
+
const controller = createKrateApiController({ resourceGateway: gw });
|
|
204
|
+
|
|
205
|
+
// Create a memory repository resource
|
|
206
|
+
const memRepo = createResource(
|
|
207
|
+
'AgentMemoryRepository',
|
|
208
|
+
{ name: 'org-brain', namespace: 'krate-org-myorg' },
|
|
209
|
+
{ organizationRef: 'myorg', repositoryRef: 'memory-repo', defaultBranch: 'main', layoutProfile: 'standard' }
|
|
210
|
+
);
|
|
211
|
+
await controller.applyResource(memRepo);
|
|
212
|
+
|
|
213
|
+
// Create a memory source
|
|
214
|
+
const memSource = createResource(
|
|
215
|
+
'AgentMemorySource',
|
|
216
|
+
{ name: 'codebase-source', namespace: 'krate-org-myorg' },
|
|
217
|
+
{
|
|
218
|
+
organizationRef: 'myorg',
|
|
219
|
+
repositoryRef: 'org-brain',
|
|
220
|
+
appliesTo: { stacks: ['review-bot'] },
|
|
221
|
+
include: ['decisions/', 'patterns/'],
|
|
222
|
+
}
|
|
223
|
+
);
|
|
224
|
+
await controller.applyResource(memSource);
|
|
225
|
+
|
|
226
|
+
// Verify both appear in their lists
|
|
227
|
+
const repoList = await controller.listResource('AgentMemoryRepository');
|
|
228
|
+
assert.ok(repoList.items.some((r) => r.metadata?.name === 'org-brain'), 'org-brain memory repo should appear');
|
|
229
|
+
|
|
230
|
+
const sourceList = await controller.listResource('AgentMemorySource');
|
|
231
|
+
assert.ok(sourceList.items.some((s) => s.metadata?.name === 'codebase-source'), 'codebase-source should appear');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// Test 7: Audit flow — apply resource → audit log contains event
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
test('full flow: apply resource with onAuditEvent → audit log contains the event', async () => {
|
|
239
|
+
const gw = createMockGateway();
|
|
240
|
+
const audit = createAuditController();
|
|
241
|
+
|
|
242
|
+
const controller = createKrateApiController({
|
|
243
|
+
resourceGateway: gw,
|
|
244
|
+
onAuditEvent: (evt) => audit.log({
|
|
245
|
+
org: evt.org || 'audited',
|
|
246
|
+
actor: 'system',
|
|
247
|
+
action: evt.operation || 'apply',
|
|
248
|
+
resource: { kind: evt.kind, name: evt.name },
|
|
249
|
+
timestamp: evt.timestamp,
|
|
250
|
+
}),
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Apply a resource
|
|
254
|
+
const repo = createResource(
|
|
255
|
+
'Repository',
|
|
256
|
+
{ name: 'audited-repo', namespace: 'krate-org-audited' },
|
|
257
|
+
{ organizationRef: 'audited', visibility: 'private' }
|
|
258
|
+
);
|
|
259
|
+
await controller.applyResource(repo);
|
|
260
|
+
|
|
261
|
+
// Verify audit log contains the event
|
|
262
|
+
const { events, total } = audit.query({ org: 'audited' });
|
|
263
|
+
assert.ok(total >= 1, 'audit log must contain at least 1 event after apply');
|
|
264
|
+
assert.equal(events[0].action, 'apply', 'audit event action must be "apply"');
|
|
265
|
+
assert.equal(events[0].org, 'audited', 'audit event org must match');
|
|
266
|
+
});
|