@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,527 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { createControllerUiModel, createKradleApiController, createKradleHttpServer } from '../src/index.js';
|
|
6
|
+
|
|
7
|
+
function read(path) {
|
|
8
|
+
return readFileSync(path, 'utf8');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function workflowJobBlock(workflow, jobName) {
|
|
12
|
+
const match = workflow.match(new RegExp(`\r?\n ${jobName}:\r?\n([\\s\\S]*?)(?=\r?\n [a-zA-Z0-9_-]+:\r?\n|$)`));
|
|
13
|
+
assert.ok(match, `workflow has ${jobName} job`);
|
|
14
|
+
return match[1];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function fixtureKubernetesController() {
|
|
18
|
+
return {
|
|
19
|
+
async snapshot() {
|
|
20
|
+
return {
|
|
21
|
+
source: 'kubernetes',
|
|
22
|
+
namespace: 'kradle-org-default',
|
|
23
|
+
generatedAt: 'test-time',
|
|
24
|
+
correlationId: 'test-correlation',
|
|
25
|
+
kubectl: { available: true, context: 'kind-kradle', clientVersion: 'v1.test', errors: [] },
|
|
26
|
+
apiService: { metadata: { name: 'v1alpha1.kradle.a5c.ai' } },
|
|
27
|
+
crds: [{ metadata: { name: 'repositories.kradle.a5c.ai' } }],
|
|
28
|
+
storage: { etcd: 'etcd', postgres: 'postgres', repositories: 'rwx', objects: 'object' },
|
|
29
|
+
commands: [],
|
|
30
|
+
permissions: [],
|
|
31
|
+
events: [],
|
|
32
|
+
resources: {
|
|
33
|
+
Organization: [{ apiVersion: 'kradle.a5c.ai/v1alpha1', kind: 'Organization', metadata: { name: 'default', namespace: 'kradle-system' }, spec: { slug: 'default', namespaceName: 'kradle-org-default', displayName: 'Default org' } }],
|
|
34
|
+
User: [{ apiVersion: 'kradle.a5c.ai/v1alpha1', kind: 'User', metadata: { name: 'alice', namespace: 'kradle-org-default' }, spec: { organizationRef: 'default', displayName: 'Alice', email: 'alice@example.com', teams: ['maintainers'] }, status: { phase: 'Active' } }],
|
|
35
|
+
Team: [{ apiVersion: 'kradle.a5c.ai/v1alpha1', kind: 'Team', metadata: { name: 'maintainers', namespace: 'kradle-org-default' }, spec: { organizationRef: 'default', displayName: 'Maintainers', members: ['alice'] }, status: { phase: 'Active' } }],
|
|
36
|
+
Invite: [],
|
|
37
|
+
IdentityMapping: [{ apiVersion: 'kradle.a5c.ai/v1alpha1', kind: 'IdentityMapping', metadata: { name: 'sso-alice', namespace: 'kradle-org-default' }, spec: { organizationRef: 'default', user: 'alice', provider: 'sso', subject: 'alice', repositoryIdentity: { username: 'alice' } }, status: { phase: 'Synced' } }],
|
|
38
|
+
AuthProvider: [{ apiVersion: 'kradle.a5c.ai/v1alpha1', kind: 'AuthProvider', metadata: { name: 'sso', namespace: 'kradle-org-default' }, spec: { organizationRef: 'default', type: 'oidc', label: 'Company SSO', enabled: true }, status: { phase: 'Configured' } }],
|
|
39
|
+
Repository: [{ apiVersion: 'kradle.a5c.ai/v1alpha1', kind: 'Repository', metadata: { name: 'app', namespace: 'kradle-org-default' }, spec: { organizationRef: 'default', visibility: 'internal', defaultBranch: 'main' }, status: { phase: 'Ready' } }],
|
|
40
|
+
PolicyProfile: [{ apiVersion: 'kradle.a5c.ai/v1alpha1', kind: 'PolicyProfile', metadata: { name: 'default-profile', namespace: 'kradle-org-default' }, spec: { organizationRef: 'default', displayName: 'Default policy posture', mode: 'audit' }, status: { phase: 'Synced', lastViolationCount: 1 } }],
|
|
41
|
+
PolicyTemplate: [{ apiVersion: 'kradle.a5c.ai/v1alpha1', kind: 'PolicyTemplate', metadata: { name: 'require-pr-description', namespace: 'kradle-org-default' }, spec: { organizationRef: 'default', displayName: 'Require PR description', targetKinds: ['PullRequest'], kyverno: { kind: 'ValidatingPolicy' } }, status: { phase: 'Ready' } }],
|
|
42
|
+
PolicyBinding: [{ apiVersion: 'kradle.a5c.ai/v1alpha1', kind: 'PolicyBinding', metadata: { name: 'require-pr-description-audit', namespace: 'kradle-org-default' }, spec: { organizationRef: 'default', templateRef: 'require-pr-description', mode: 'audit' }, status: { phase: 'Applied', lastViolationCount: 1 } }],
|
|
43
|
+
PolicyExceptionRequest: [{ apiVersion: 'kradle.a5c.ai/v1alpha1', kind: 'PolicyExceptionRequest', metadata: { name: 'pr-1-exception', namespace: 'kradle-org-default' }, spec: { organizationRef: 'default', policyRef: { name: 'require-pr-description' }, justification: 'temporary rollout', expiresAt: '2026-06-01T00:00:00Z' }, status: { phase: 'Requested' } }],
|
|
44
|
+
PullRequest: [],
|
|
45
|
+
Pipeline: [],
|
|
46
|
+
RunnerPool: [],
|
|
47
|
+
WebhookSubscription: []
|
|
48
|
+
},
|
|
49
|
+
kyverno: {
|
|
50
|
+
mode: 'byo',
|
|
51
|
+
namespace: 'kyverno',
|
|
52
|
+
policyNamespace: 'kradle-org-default',
|
|
53
|
+
detected: true,
|
|
54
|
+
controllers: [{ name: 'kyverno-admission-controller', ready: true, readyReplicas: 1, replicas: 1 }],
|
|
55
|
+
permissions: [{ kind: 'PolicyReport', verbs: { list: true, watch: true } }],
|
|
56
|
+
resources: { KyvernoValidatingPolicy: [{ metadata: { name: 'require-pr-description' } }], PolicyReport: [{ metadata: { name: 'app-policy', namespace: 'kradle-org-default' }, results: [{ policy: 'require-pr-description', rule: 'description', result: 'fail', message: 'description required', resources: [{ kind: 'PullRequest', name: 'pr-1' }] }] }] },
|
|
57
|
+
reports: { policyReports: [{ metadata: { name: 'app-policy', namespace: 'kradle-org-default' } }], clusterPolicyReports: [], results: [{ policy: 'require-pr-description', rule: 'description', result: 'fail', message: 'description required' }], violations: [{ policy: 'require-pr-description', rule: 'description', result: 'fail', message: 'description required' }] },
|
|
58
|
+
degraded: []
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
async applyResource(resource) {
|
|
63
|
+
return { operation: 'apply', resource };
|
|
64
|
+
},
|
|
65
|
+
async deleteResource(kind, name) {
|
|
66
|
+
return { operation: 'delete', resource: { kind, metadata: { name } } };
|
|
67
|
+
},
|
|
68
|
+
async createRepository(input) {
|
|
69
|
+
return { operation: 'apply', resource: { kind: 'Repository', metadata: { name: input.name } } };
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
test('controller deployment assets build and publish the runnable controller', () => {
|
|
75
|
+
for (const file of ['Dockerfile', '.dockerignore', '.github/workflows/publish.yml']) assert.equal(existsSync(file), true, `${file} exists`);
|
|
76
|
+
|
|
77
|
+
const dockerfile = read('Dockerfile');
|
|
78
|
+
assert.match(dockerfile, /FROM node:\d+-alpine AS (deps|build)/);
|
|
79
|
+
assert.match(dockerfile, /npm (ci|install)/);
|
|
80
|
+
assert.match(dockerfile, /HEALTHCHECK/);
|
|
81
|
+
assert.match(dockerfile, /kradle-server\.mjs/);
|
|
82
|
+
for (const runtimePath of ['/app/bin', '/app/src', '/app/dist']) assert.ok(dockerfile.includes(runtimePath), `Dockerfile copies ${runtimePath}`);
|
|
83
|
+
|
|
84
|
+
const dockerignore = read('.dockerignore');
|
|
85
|
+
for (const ignored of ['.a5c', 'node_modules', '**/.next', 'dist']) assert.ok(dockerignore.includes(ignored), `.dockerignore excludes ${ignored}`);
|
|
86
|
+
|
|
87
|
+
const chartDeployment = read('../charts/templates/deployments.yaml');
|
|
88
|
+
const chartRbac = read('../charts/templates/rbac.yaml');
|
|
89
|
+
const chartIngress = read('../charts/templates/ingress.yaml');
|
|
90
|
+
const chartValues = read('../charts/values.yaml');
|
|
91
|
+
const authSecret = read('../charts/templates/auth-secret.yaml');
|
|
92
|
+
assert.ok(chartValues.includes('auth:'), 'chart values include auth configuration');
|
|
93
|
+
assert.ok(chartValues.includes('github:') && chartValues.includes('sso:'), 'chart values expose GitHub and SSO configuration');
|
|
94
|
+
assert.ok(chartValues.includes('assistant:') && chartValues.includes('anthropic-api-key'), 'chart values expose assistant secret references');
|
|
95
|
+
assert.ok(chartValues.includes('agentMux:') && chartValues.includes('gatewayUrl'), 'chart values expose Agent Mux endpoint configuration');
|
|
96
|
+
assert.ok(chartValues.includes('token:') && chartValues.includes('existingSecret: ""'), 'chart values expose Gitea token secret reference');
|
|
97
|
+
assert.ok(chartValues.includes('mode: auto') && chartValues.includes('policyReporter:'), 'chart values expose Kyverno auto-discovery modes and policy reporter settings');
|
|
98
|
+
assert.ok(chartRbac.includes('"*"') && chartRbac.includes('policies.kyverno.io') && chartRbac.includes('policyreports'), 'RBAC covers all Kradle resources via wildcard and Kyverno read/write surfaces');
|
|
99
|
+
assert.ok(chartDeployment.includes('KRADLE_KYVERNO_MODE') && chartDeployment.includes('KRADLE_KYVERNO_POLICY_NAMESPACE') && chartDeployment.includes('KRADLE_KYVERNO_DISCOVER_EXISTING'), 'deployments receive Kyverno discovery env');
|
|
100
|
+
assert.ok(read('../charts/templates/argocd-kyverno-application.yaml').includes('kradle.a5c.ai/policy-engine: kyverno'), 'managed Kyverno Argo CD application template is present');
|
|
101
|
+
assert.ok(authSecret.includes('github-client-id') && authSecret.includes('sso-client-secret'), 'chart renders auth secret keys');
|
|
102
|
+
assert.ok(chartIngress.includes('kind: Ingress') && chartIngress.includes('ingressClassName') && chartIngress.includes('app.kubernetes.io/component: web'), 'chart renders web ingress');
|
|
103
|
+
assert.ok(chartDeployment.includes('imagePullSecrets') && chartDeployment.includes('global.imagePullSecrets'), 'workloads can use registry pull secrets');
|
|
104
|
+
assert.ok(chartDeployment.includes('KRADLE_AUTH_GITHUB_ENABLED') && chartDeployment.includes('KRADLE_AUTH_SSO_ENABLED') && chartDeployment.includes('KRADLE_AUTH_DELEGATED_EMAIL_HEADER'), 'workloads receive auth provider configuration');
|
|
105
|
+
assert.ok(chartDeployment.includes('readinessProbe:') && chartDeployment.includes('path: /login'), 'web deployment has an HTTP readiness probe');
|
|
106
|
+
assert.ok(chartDeployment.includes('KRADLE_AUTH_DELEGATED_LOCAL_DEVELOPMENT') && chartDeployment.includes('KRADLE_AUTH_DELEGATED_LOCAL_GROUPS'), 'workloads can opt into local delegated development login');
|
|
107
|
+
assert.ok(chartDeployment.includes('ANTHROPIC_API_KEY') && chartDeployment.includes('KRADLE_ASSISTANT_API_KEY'), 'web workload can receive assistant API key references');
|
|
108
|
+
assert.ok(chartDeployment.includes('KRADLE_GITEA_TOKEN') && chartDeployment.includes('AGENT_MUX_URL') && chartDeployment.includes('AGENT_GATEWAY_URL'), 'workloads can receive Gitea token and Agent Mux endpoint configuration');
|
|
109
|
+
assert.ok(chartRbac.includes('core.oam.dev') && chartRbac.includes('applications') && chartRbac.includes('create'), 'delivery resources can be composed through Kradle');
|
|
110
|
+
assert.ok(chartValues.includes('localDevelopment:') && chartValues.includes('enabled: false'), 'local delegated development login is off by default');
|
|
111
|
+
for (const token of ['command: ["node", "bin/kradle-server.mjs"]', '--port=3080', 'app.kubernetes.io/component: controllers']) {
|
|
112
|
+
assert.ok(chartDeployment.includes(token), `chart deployment includes ${token}`);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('web proxy protects UI pages and authenticated APIs behind login', async () => {
|
|
117
|
+
let NextRequest, proxy, config;
|
|
118
|
+
try {
|
|
119
|
+
({ NextRequest } = await import('next/server.js'));
|
|
120
|
+
({ proxy, config } = await import(`../../web/proxy.js?test=${Date.now()}`));
|
|
121
|
+
} catch { return; }
|
|
122
|
+
|
|
123
|
+
assert.ok(config.matcher.some((entry) => entry.includes('_next/static')));
|
|
124
|
+
for (const path of ['/', '/orgs/default/repositories?tab=code', '/orgs/default/repositories/demo/code', '/orgs/default/people', '/logout', '/api/controller']) {
|
|
125
|
+
const response = proxy(new NextRequest(`http://localhost:3000${path}`));
|
|
126
|
+
assert.equal(response.status, 307, `${path} redirects to login without session`);
|
|
127
|
+
assert.equal(new URL(response.headers.get('location')).pathname, '/login');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const nextResponse = proxy(new NextRequest('http://localhost:3000/orgs/default/repositories?tab=code'));
|
|
131
|
+
assert.equal(new URL(nextResponse.headers.get('location')).searchParams.get('next'), '/orgs/default/repositories?tab=code');
|
|
132
|
+
assert.equal(proxy(new NextRequest('http://localhost:3000/login')).headers.get('x-middleware-next'), '1');
|
|
133
|
+
assert.equal(proxy(new NextRequest('http://localhost:3000/api/auth/delegated')).headers.get('x-middleware-next'), '1');
|
|
134
|
+
assert.equal(proxy(new NextRequest('http://localhost:3000/orgs/default/repositories', { headers: { cookie: 'kradle_session=dev' } })).headers.get('x-middleware-next'), '1');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('login page stays minimal and does not expose the authenticated console shell', () => {
|
|
138
|
+
const layout = read('../web/app/layout.jsx');
|
|
139
|
+
const managePages = read('../web/app/pages/manage-pages.jsx');
|
|
140
|
+
const loginStart = managePages.indexOf('export function LoginPage');
|
|
141
|
+
assert.notEqual(loginStart, -1, 'LoginPage is defined in manage-pages module');
|
|
142
|
+
assert.ok(!layout.includes('<AppShell>{children}</AppShell>'), 'root layout does not wrap public routes in AppShell');
|
|
143
|
+
const loginSource = managePages.slice(loginStart, managePages.indexOf('export ', loginStart + 1));
|
|
144
|
+
assert.ok(loginSource.includes('loginMain') || loginSource.includes('login'), 'login page uses standalone login layout');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('auth chart uses existing secrets without rendering empty provider keys', () => {
|
|
148
|
+
const mixed = execFileSync('helm', [
|
|
149
|
+
'template', 'kradle', '../charts', '-n', 'kradle-system',
|
|
150
|
+
'--set', 'argocd.enabled=false',
|
|
151
|
+
'--set', 'auth.github.clientId=github-client',
|
|
152
|
+
'--set', 'auth.github.clientSecret=github-secret',
|
|
153
|
+
'--set', 'auth.sso.enabled=true',
|
|
154
|
+
'--set', 'auth.sso.existingSecret=shared-auth'
|
|
155
|
+
], { encoding: 'utf8' });
|
|
156
|
+
assert.match(mixed, /name: kradle-kradle-auth/);
|
|
157
|
+
assert.match(mixed, /github-client-id: "github-client"/);
|
|
158
|
+
assert.doesNotMatch(mixed, /sso-client-id:/);
|
|
159
|
+
assert.match(mixed, /name: "shared-auth"[\s\S]*?key: sso-client-id/);
|
|
160
|
+
|
|
161
|
+
const externalOnly = execFileSync('helm', [
|
|
162
|
+
'template', 'kradle', '../charts', '-n', 'kradle-system',
|
|
163
|
+
'--set', 'argocd.enabled=false',
|
|
164
|
+
'--set', 'auth.github.existingSecret=shared-auth',
|
|
165
|
+
'--set', 'auth.sso.enabled=true',
|
|
166
|
+
'--set', 'auth.sso.existingSecret=shared-auth'
|
|
167
|
+
], { encoding: 'utf8' });
|
|
168
|
+
assert.doesNotMatch(externalOnly, /name: kradle-kradle-auth/);
|
|
169
|
+
assert.match(externalOnly, /name: "shared-auth"[\s\S]*?key: github-client-id/);
|
|
170
|
+
assert.match(externalOnly, /name: "shared-auth"[\s\S]*?key: sso-client-secret/);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('staging service env vars are opt-in and render secret references without plaintext values', () => {
|
|
174
|
+
const defaultRender = execFileSync('helm', [
|
|
175
|
+
'template', 'kradle', '../charts', '-n', 'kradle-system',
|
|
176
|
+
'--set', 'argocd.enabled=false'
|
|
177
|
+
], { encoding: 'utf8' });
|
|
178
|
+
assert.doesNotMatch(defaultRender, /name: ANTHROPIC_API_KEY/);
|
|
179
|
+
assert.doesNotMatch(defaultRender, /name: KRADLE_ASSISTANT_API_KEY/);
|
|
180
|
+
assert.doesNotMatch(defaultRender, /name: KRADLE_GITEA_TOKEN/);
|
|
181
|
+
assert.doesNotMatch(defaultRender, /name: AGENT_MUX_URL/);
|
|
182
|
+
assert.doesNotMatch(defaultRender, /name: AGENT_GATEWAY_URL/);
|
|
183
|
+
|
|
184
|
+
const configured = execFileSync('helm', [
|
|
185
|
+
'template', 'kradle', '../charts', '-n', 'kradle-system',
|
|
186
|
+
'--set', 'argocd.enabled=false',
|
|
187
|
+
'--set', 'gitea.httpUrl=http://gitea-http.kradle-system.svc.cluster.local:3000/kradle',
|
|
188
|
+
'--set', 'gitea.token.existingSecret=kradle-gitea-token',
|
|
189
|
+
'--set', 'gitea.token.key=token',
|
|
190
|
+
'--set', 'agentMux.url=http://agent-mux.kradle-system.svc.cluster.local:8080',
|
|
191
|
+
'--set', 'agentMux.gatewayUrl=http://agent-gateway.kradle-system.svc.cluster.local:8080',
|
|
192
|
+
'--set', 'assistant.anthropic.existingSecret=kradle-assistant',
|
|
193
|
+
'--set', 'assistant.anthropic.key=anthropic-api-key',
|
|
194
|
+
'--set', 'assistant.kradleAssistant.existingSecret=kradle-assistant',
|
|
195
|
+
'--set', 'assistant.kradleAssistant.key=kradle-assistant-api-key'
|
|
196
|
+
], { encoding: 'utf8' });
|
|
197
|
+
|
|
198
|
+
assert.match(configured, /name: KRADLE_GITEA_HTTP_URL\s+value: "http:\/\/gitea-http\.kradle-system\.svc\.cluster\.local:3000\/kradle"/);
|
|
199
|
+
assert.match(configured, /name: KRADLE_GITEA_TOKEN[\s\S]*?secretKeyRef:[\s\S]*?name: "kradle-gitea-token"[\s\S]*?key: "token"[\s\S]*?optional: true/);
|
|
200
|
+
assert.match(configured, /name: ANTHROPIC_API_KEY[\s\S]*?secretKeyRef:[\s\S]*?name: "kradle-assistant"[\s\S]*?key: "anthropic-api-key"[\s\S]*?optional: true/);
|
|
201
|
+
assert.match(configured, /name: KRADLE_ASSISTANT_API_KEY[\s\S]*?secretKeyRef:[\s\S]*?name: "kradle-assistant"[\s\S]*?key: "kradle-assistant-api-key"[\s\S]*?optional: true/);
|
|
202
|
+
assert.match(configured, /name: AGENT_MUX_URL\s+value: "http:\/\/agent-mux\.kradle-system\.svc\.cluster\.local:8080"/);
|
|
203
|
+
assert.match(configured, /name: AGENT_GATEWAY_URL\s+value: "http:\/\/agent-gateway\.kradle-system\.svc\.cluster\.local:8080"/);
|
|
204
|
+
assert.doesNotMatch(configured, /sk-ant-|ghp_|github_pat_|glpat-|AKIA[0-9A-Z]{16}/);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('NATS event transport renders inline and secret-backed broker URLs', () => {
|
|
208
|
+
const inline = execFileSync('helm', [
|
|
209
|
+
'template', 'kradle', '../charts', '-n', 'kradle-system',
|
|
210
|
+
'--set', 'argocd.enabled=false',
|
|
211
|
+
'--set', 'externalDependencies.nats.eventTransport.enabled=true',
|
|
212
|
+
'--set', 'externalDependencies.nats.url=nats://nats.kradle-system.svc.cluster.local:4222',
|
|
213
|
+
'--set', 'externalDependencies.nats.eventTransport.subject=kradle.staging.events',
|
|
214
|
+
'--set', 'externalDependencies.nats.eventTransport.stream=KRADLE_STAGING_EVENTS',
|
|
215
|
+
'--set', 'externalDependencies.nats.eventTransport.requireBroker=true'
|
|
216
|
+
], { encoding: 'utf8' });
|
|
217
|
+
|
|
218
|
+
assert.match(inline, /name: KRADLE_EVENT_TRANSPORT\s+value: "nats"/);
|
|
219
|
+
assert.match(inline, /name: KRADLE_EVENT_NATS_URL\s+value: "nats:\/\/nats\.kradle-system\.svc\.cluster\.local:4222"/);
|
|
220
|
+
assert.match(inline, /name: KRADLE_EVENT_NATS_SUBJECT\s+value: "kradle\.staging\.events"/);
|
|
221
|
+
assert.match(inline, /name: KRADLE_EVENT_NATS_STREAM\s+value: "KRADLE_STAGING_EVENTS"/);
|
|
222
|
+
assert.match(inline, /name: KRADLE_EVENT_REQUIRE_BROKER\s+value: "true"/);
|
|
223
|
+
|
|
224
|
+
const secretBacked = execFileSync('helm', [
|
|
225
|
+
'template', 'kradle', '../charts', '-n', 'kradle-system',
|
|
226
|
+
'--set', 'argocd.enabled=false',
|
|
227
|
+
'--set', 'externalDependencies.nats.eventTransport.enabled=true',
|
|
228
|
+
'--set', 'externalDependencies.nats.existingSecret=kradle-nats',
|
|
229
|
+
'--set', 'externalDependencies.nats.key=connection-url'
|
|
230
|
+
], { encoding: 'utf8' });
|
|
231
|
+
|
|
232
|
+
assert.match(secretBacked, /name: KRADLE_EVENT_NATS_URL[\s\S]*?secretKeyRef:[\s\S]*?name: "kradle-nats"[\s\S]*?key: "connection-url"[\s\S]*?optional: true/);
|
|
233
|
+
assert.doesNotMatch(secretBacked, /nats:\/\/[^"\s]+/);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('demo NATS renders JetStream broker and wires event transport by default', () => {
|
|
237
|
+
const rendered = execFileSync('helm', [
|
|
238
|
+
'template', 'kradle', '../charts', '-n', 'kradle-system',
|
|
239
|
+
'--set', 'argocd.enabled=false',
|
|
240
|
+
'--set', 'demo.enabled=true',
|
|
241
|
+
'--set', 'demo.nats.mode=local-dev-nats'
|
|
242
|
+
], { encoding: 'utf8' });
|
|
243
|
+
|
|
244
|
+
assert.match(rendered, /name: kradle-kradle-nats/);
|
|
245
|
+
assert.match(rendered, /args: \["--jetstream", "--store_dir=\/data\/jetstream"\]/);
|
|
246
|
+
assert.match(rendered, /name: KRADLE_EVENT_TRANSPORT\s+value: "nats"/);
|
|
247
|
+
assert.match(rendered, /name: KRADLE_EVENT_NATS_URL\s+value: "nats:\/\/kradle-kradle-nats\.kradle-system\.svc\.cluster\.local:4222"/);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('controller healthz returns deep dependency and event transport probe details', async () => {
|
|
251
|
+
const server = createKradleHttpServer({
|
|
252
|
+
controller: fixtureKubernetesController(),
|
|
253
|
+
healthProbeOptions: {
|
|
254
|
+
env: {
|
|
255
|
+
KRADLE_GITEA_HTTP_URL: 'https://gitea.internal',
|
|
256
|
+
AGENT_MUX_URL: 'https://mux.internal',
|
|
257
|
+
KRADLE_CONTROLLER_URL: 'https://controller.internal',
|
|
258
|
+
ANTHROPIC_API_KEY: 'sk-ant-api03-test-health-key',
|
|
259
|
+
KRADLE_KUBECTL: 'kubectl-test',
|
|
260
|
+
},
|
|
261
|
+
fetchImpl: async () => ({ ok: true, status: 200 }),
|
|
262
|
+
execFileImpl: async () => ({ stdout: 'Kubernetes control plane is running', stderr: '' }),
|
|
263
|
+
eventBus: {
|
|
264
|
+
status: () => ({ transport: 'nats-jetstream', status: 'ok', durable: true }),
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
await new Promise((resolve) => server.listen(0, resolve));
|
|
269
|
+
try {
|
|
270
|
+
const response = await fetch(`http://127.0.0.1:${server.address().port}/healthz`);
|
|
271
|
+
const body = await response.json();
|
|
272
|
+
assert.equal(response.status, 200);
|
|
273
|
+
assert.equal(body.ok, true);
|
|
274
|
+
assert.equal(body.status, 'ok');
|
|
275
|
+
assert.equal(body.health.gitea, 'ok');
|
|
276
|
+
assert.equal(body.health.agentMux, 'ok');
|
|
277
|
+
assert.equal(body.health.controller, 'ok');
|
|
278
|
+
assert.equal(body.health.eventTransport, 'ok');
|
|
279
|
+
assert.equal(body.health.details.eventTransport.transport, 'nats-jetstream');
|
|
280
|
+
assert.doesNotMatch(JSON.stringify(body), /sk-ant-api03-test-health-key/);
|
|
281
|
+
} finally {
|
|
282
|
+
await new Promise((resolve) => server.close(resolve));
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
test('web UI and controller API expose live Kubernetes deployment and publishing metadata', async () => {
|
|
288
|
+
const controller = fixtureKubernetesController();
|
|
289
|
+
const model = createControllerUiModel(await controller.snapshot());
|
|
290
|
+
assert.equal(model.controller.mode, 'kradle-workspace');
|
|
291
|
+
assert.ok(model.controller.endpoints.some((endpoint) => endpoint.path === '/api/controller?org=:org'));
|
|
292
|
+
assert.ok(model.controller.endpoints.some((endpoint) => endpoint.path === '/api/orgs/:org/resources'));
|
|
293
|
+
assert.equal(model.controller.architecture.apiController.role, 'kradle-api-controller');
|
|
294
|
+
assert.equal(model.controller.architecture.resourceGateway.role, 'kradle-resource-gateway');
|
|
295
|
+
assert.equal(model.controller.architecture.resourceClient.role, 'kradle-resource-client');
|
|
296
|
+
assert.equal(model.controller.architecture.repositoryService.role, 'repository-service');
|
|
297
|
+
assert.deepEqual(model.controller.architecture.apiController.delegatesTo, ['kradle-resource-gateway', 'repository-service']);
|
|
298
|
+
assert.ok(model.resources.some((resource) => resource.kind === 'Repository' && resource.count > 0));
|
|
299
|
+
assert.ok(model.resources.some((resource) => resource.kind === 'KradleProject'));
|
|
300
|
+
assert.ok(model.views.dashboard.issueSync?.gitea?.repo, 'dashboard exposes issue sync backend plan');
|
|
301
|
+
assert.equal(model.policyEngine.health, 'ready');
|
|
302
|
+
assert.equal(model.policyEngine.violations.length, 1);
|
|
303
|
+
assert.ok(model.resources.some((resource) => resource.kind === 'PolicyBinding' && resource.count > 0));
|
|
304
|
+
assert.ok(model.resources.find((resource) => resource.kind === 'Repository').action.list.includes('Open Repository records'));
|
|
305
|
+
assert.match(model.operations.image, /kradle-controller/);
|
|
306
|
+
assert.equal(model.operations.chart, 'charts/kradle');
|
|
307
|
+
for (const gate of ['npm run check', 'docker build', 'helm package charts/kradle', 'npm pack --json']) assert.ok(model.operations.releaseGates.includes(gate), `release gate ${gate}`);
|
|
308
|
+
assert.ok(model.validation.every((item) => typeof item.evidence === 'string' && item.evidence.length > 0));
|
|
309
|
+
|
|
310
|
+
const server = createKradleHttpServer({ controller });
|
|
311
|
+
await new Promise((resolve) => server.listen(0, resolve));
|
|
312
|
+
try {
|
|
313
|
+
const response = await fetch(`http://127.0.0.1:${server.address().port}/api/controller`);
|
|
314
|
+
assert.equal(response.status, 200);
|
|
315
|
+
const body = await response.json();
|
|
316
|
+
assert.deepEqual(body.operations.releaseGates, model.operations.releaseGates);
|
|
317
|
+
assert.equal(body.metrics.resources, model.metrics.resources);
|
|
318
|
+
} finally {
|
|
319
|
+
await new Promise((resolve) => server.close(resolve));
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test('web UI is wired to the Kubernetes controller API instead of a static local snapshot', () => {
|
|
324
|
+
const page = read('../web/app/page.jsx');
|
|
325
|
+
const orgPage = read('../web/app/orgs/[org]/page.jsx');
|
|
326
|
+
const shellModules = ['../web/app/lib/kradle-ui.jsx', '../web/app/lib/page-frame.jsx', '../web/app/pages/agent-pages.jsx', '../web/app/pages/agent-helpers.jsx', '../web/app/pages/repo-pages.jsx', '../web/app/pages/manage-pages.jsx', '../web/app/pages/settings-pages.jsx', '../web/app/pages/external-pages.jsx'];
|
|
327
|
+
const shell = shellModules.map((m) => { try { return readFileSync(m, 'utf8'); } catch { return ''; } }).join('\n');
|
|
328
|
+
const actions = read('../web/app/components/resource-actions.jsx');
|
|
329
|
+
const client = read('src/controller-client.js');
|
|
330
|
+
const apiController = read('src/api-controller.js');
|
|
331
|
+
const gateway = read('src/kubernetes-resource-gateway.js');
|
|
332
|
+
const kubernetes = read('src/kubernetes-controller.js');
|
|
333
|
+
const server = read('src/http-server.js');
|
|
334
|
+
const webControllerRoute = read('../web/app/api/controller/route.js');
|
|
335
|
+
assert.ok(page.includes("redirect('/orgs/'"));
|
|
336
|
+
assert.ok(orgPage.includes('DashboardPage'));
|
|
337
|
+
assert.ok(read('../web/app/orgs/page.jsx').includes('Choose an organization'));
|
|
338
|
+
assert.ok(client.includes('KRADLE_CONTROLLER_URL'));
|
|
339
|
+
assert.ok(client.includes('KRADLE_CONTROLLER_REQUEST_TIMEOUT_MS'));
|
|
340
|
+
assert.ok(client.includes('AbortSignal.timeout'));
|
|
341
|
+
assert.ok(client.includes('if (!useCache) return revalidateFn();'));
|
|
342
|
+
assert.ok(client.includes('staleWhileRevalidate(organization, revalidateFn, swrOptions)'));
|
|
343
|
+
assert.ok(client.includes('getControllerSnapshotAsync'));
|
|
344
|
+
assert.ok(client.includes('fallbackSnapshot'));
|
|
345
|
+
assert.ok(!client.includes('createKubernetesResourceGateway'));
|
|
346
|
+
assert.ok(!client.includes('createKradleApiController'));
|
|
347
|
+
assert.ok(apiController.includes('resourceGateway'));
|
|
348
|
+
assert.ok(apiController.includes('withArchitecture'));
|
|
349
|
+
assert.ok(apiController.includes('kubernetes-resource-gateway'));
|
|
350
|
+
assert.ok(!apiController.includes('createKubernetesController'));
|
|
351
|
+
assert.ok(gateway.includes('createKubernetesResourceClient'));
|
|
352
|
+
assert.ok(kubernetes.includes('createKubernetesController(options = {})')); // resource-client alias
|
|
353
|
+
assert.ok(kubernetes.includes('/var/run/secrets/kubernetes.io/serviceaccount'), 'Kubernetes client can use in-cluster service-account credentials');
|
|
354
|
+
assert.ok(gateway.includes('repositoryManifest'));
|
|
355
|
+
assert.ok(shell.includes('/api/controller'));
|
|
356
|
+
assert.ok(webControllerRoute.includes('KRADLE_CONTROLLER_URL'));
|
|
357
|
+
assert.ok(webControllerRoute.includes('hydrateOrgResourceSummaries'), 'web API route hydrates empty controller summaries from org-scoped resources');
|
|
358
|
+
assert.ok(shell.includes('ArchitectureMap'));
|
|
359
|
+
assert.ok(shell.includes('Repository home'));
|
|
360
|
+
assert.ok(shell.includes('IssueWorkspace'));
|
|
361
|
+
assert.ok(shell.includes('IssueViewSwitcher'));
|
|
362
|
+
assert.ok(shell.includes('issuesForScope'));
|
|
363
|
+
assert.ok(shell.includes('issueRepositoryRefs'));
|
|
364
|
+
assert.ok(shell.includes('issueProjectRefs'));
|
|
365
|
+
assert.ok(shell.includes('DeploymentCenter'));
|
|
366
|
+
assert.ok(shell.includes('DeploymentManager'));
|
|
367
|
+
assert.ok(shell.includes('Kradle deployment center'));
|
|
368
|
+
assert.ok(shell.includes('PlanCard'));
|
|
369
|
+
assert.ok(shell.includes('Advanced resource details'));
|
|
370
|
+
assert.ok(shell.includes('Releases'));
|
|
371
|
+
assert.ok(shell.includes('managed resources'));
|
|
372
|
+
assert.ok(shell.includes('Advanced resource records'));
|
|
373
|
+
assert.ok(shell.includes('Deployments'));
|
|
374
|
+
assert.ok(actions.includes('Create deployment'));
|
|
375
|
+
assert.ok(actions.includes('Prepare deployment'));
|
|
376
|
+
assert.ok(shell.includes('environments'));
|
|
377
|
+
assert.ok(shell.includes('Repository code browser'));
|
|
378
|
+
assert.ok(shell.includes('Manage access'));
|
|
379
|
+
assert.ok(shell.includes('PeopleAdmin'));
|
|
380
|
+
assert.ok(shell.includes('getSignedInUser'));
|
|
381
|
+
assert.ok(shell.includes('topbarAccount'));
|
|
382
|
+
assert.ok(shell.includes('Signed-in user'));
|
|
383
|
+
assert.ok(shell.includes('Sign out'));
|
|
384
|
+
assert.ok(shell.includes('Invite people'));
|
|
385
|
+
assert.ok(shell.includes('identity links'));
|
|
386
|
+
assert.ok(shell.includes('repository permissions'));
|
|
387
|
+
assert.ok(shell.includes('Access readiness'));
|
|
388
|
+
assert.match(actions, /Mark accepted/i);
|
|
389
|
+
assert.ok(actions.includes('Revoke invite'));
|
|
390
|
+
assert.ok(actions.includes('Disable user'));
|
|
391
|
+
assert.ok(actions.includes('Restore user'));
|
|
392
|
+
assert.ok(actions.includes('Revoke grant'));
|
|
393
|
+
assert.ok(actions.includes('SSH keys'));
|
|
394
|
+
assert.ok(actions.includes('Save SSH key'));
|
|
395
|
+
assert.ok(actions.includes('Revoke SSH key'));
|
|
396
|
+
assert.equal(existsSync('../web/app/orgs/[org]/people/page.jsx'), true);
|
|
397
|
+
assert.equal(existsSync('../web/app/login/page.jsx'), true);
|
|
398
|
+
assert.equal(existsSync('../web/app/logout/page.jsx'), true);
|
|
399
|
+
assert.equal(existsSync('../web/app/orgs/[org]/runs/page.jsx'), true);
|
|
400
|
+
assert.equal(existsSync('../web/app/runs/page.jsx'), false);
|
|
401
|
+
assert.equal(existsSync('../web/app/pipelines/page.jsx'), false);
|
|
402
|
+
assert.equal(existsSync('../web/app/orgs/[org]/repositories/[repo]/runs/page.jsx'), true);
|
|
403
|
+
assert.equal(existsSync('../web/app/orgs/[org]/repositories/[repo]/issues/[issue]/page.jsx'), true);
|
|
404
|
+
assert.equal(existsSync('../web/app/orgs/[org]/agents/projects/[projectId]/issues/page.jsx'), true);
|
|
405
|
+
assert.equal(existsSync('../web/app/orgs/[org]/agents/projects/[projectId]/issues/[issue]/page.jsx'), true);
|
|
406
|
+
assert.equal(existsSync('../web/app/repositories/[repo]/runs/page.jsx'), false);
|
|
407
|
+
assert.equal(existsSync('../web/app/repositories/[repo]/pipelines/page.jsx'), false);
|
|
408
|
+
assert.equal(existsSync('../web/proxy.js'), true);
|
|
409
|
+
assert.equal(existsSync('../web/app/api/auth/[provider]/route.js'), true);
|
|
410
|
+
assert.equal(existsSync('../web/app/api/auth/callback/[provider]/route.js'), true);
|
|
411
|
+
assert.equal(existsSync('../web/app/api/auth/delegated/route.js'), true);
|
|
412
|
+
assert.ok(shell.includes('RepositoryCodePage'));
|
|
413
|
+
assert.ok(shell.includes('RepositoryPullRequestsPage'));
|
|
414
|
+
assert.ok(shell.includes('RepositorySettingsPage'));
|
|
415
|
+
assert.ok(shell.includes('PullRequestReviewPanel'));
|
|
416
|
+
assert.ok(shell.includes('RunCenter'));
|
|
417
|
+
assert.ok(shell.includes('Workspace runs'));
|
|
418
|
+
assert.ok(shell.includes('Run event stream'));
|
|
419
|
+
assert.ok(shell.includes('/runs'));
|
|
420
|
+
assert.ok(!shell.includes("['/pipelines', 'Runs']"));
|
|
421
|
+
assert.ok(!shell.includes('PipelinesPage'), 'legacy pipelines UI component is not exported');
|
|
422
|
+
assert.ok(!shell.includes('RepositoryPipelinesPage'), 'legacy repository pipelines UI component is not exported');
|
|
423
|
+
assert.ok(!shell.includes('function PipelineDebugger'), 'runs page does not use the old debugger-first panel');
|
|
424
|
+
assert.ok(shell.includes('Automation inspector'));
|
|
425
|
+
assert.ok(shell.includes('Readiness checklist'));
|
|
426
|
+
assert.ok(kubernetes.includes('spawnSync'));
|
|
427
|
+
assert.ok(kubernetes.includes('kubectl'));
|
|
428
|
+
assert.ok(!client.includes('createKradleUiDemoRuntime'));
|
|
429
|
+
assert.ok(!page.includes('createKradleMvpDemo'));
|
|
430
|
+
assert.ok(!page.includes('createKradleLifecycleSnapshot'));
|
|
431
|
+
assert.ok(apiController.includes('const repoPath = `/orgs/${encodeURIComponent(org)}/repositories/${encodeURIComponent(name)}`'));
|
|
432
|
+
assert.ok(apiController.includes('runs: `${repoPath}/runs`'));
|
|
433
|
+
assert.ok(server.includes('/api/controller'));
|
|
434
|
+
assert.ok(server.includes('orgResourceCollectionMatch'));
|
|
435
|
+
assert.ok(!server.includes("url.pathname === '/api/repositories'"));
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test('API controller delegates through resource gateway instead of owning Kubernetes scope', async () => {
|
|
439
|
+
const calls = [];
|
|
440
|
+
const resourceGateway = {
|
|
441
|
+
namespace: 'kradle-org-default',
|
|
442
|
+
resourceDefinitions: [],
|
|
443
|
+
async snapshot() { calls.push('snapshot'); return { source: 'kubernetes', namespace: 'kradle-org-default', resources: {}, kubectl: { available: true, errors: [] }, commands: [], events: [], permissions: [], storage: {} }; },
|
|
444
|
+
async list(kind) { calls.push(`list:${kind}`); return { kind, items: [] }; },
|
|
445
|
+
async get(kind, name) { calls.push(`get:${kind}/${name}`); return { kind, metadata: { name } }; },
|
|
446
|
+
async apply(resource) { calls.push(`apply:${resource.kind}`); return { operation: 'apply', resource }; },
|
|
447
|
+
async delete(kind, name) { calls.push(`delete:${kind}/${name}`); return { operation: 'delete', kind, name }; },
|
|
448
|
+
async createRepository(input) { calls.push(`createRepository:${input.name}`); return { operation: 'apply', resource: { kind: 'Repository', metadata: { name: input.name } } }; },
|
|
449
|
+
async createOrganization(input) { calls.push(`createOrganization:${input.name}`); return { operation: 'create-organization', organization: { metadata: { name: input.name } } }; },
|
|
450
|
+
watch(resourcePath) { calls.push(`watch:${resourcePath}`); return { child: { kill() {} } }; }
|
|
451
|
+
};
|
|
452
|
+
const controller = createKradleApiController({ resourceGateway });
|
|
453
|
+
await controller.snapshot();
|
|
454
|
+
await controller.listResource('Repository');
|
|
455
|
+
await controller.getResource('Repository', 'app');
|
|
456
|
+
await controller.applyResource({ kind: 'Repository', metadata: { name: 'app' } });
|
|
457
|
+
await controller.deleteResource('Repository', 'app');
|
|
458
|
+
await controller.createRepository({ name: 'next-app' });
|
|
459
|
+
await controller.createOrganization({ name: 'product' });
|
|
460
|
+
controller.watchResource('orgs/default/repositories');
|
|
461
|
+
assert.deepEqual(calls, ['snapshot', 'list:Repository', 'get:Repository/app', 'apply:Repository', 'delete:Repository/app', 'createRepository:next-app', 'createOrganization:product', 'watch:orgs/default/repositories']);
|
|
462
|
+
assert.equal(controller.resourceGateway, resourceGateway);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test('GitHub workflow publishes deployable image and chart artifacts with safe gates', () => {
|
|
466
|
+
const workflow = read('.github/workflows/publish.yml');
|
|
467
|
+
const validate = workflowJobBlock(workflow, 'validate');
|
|
468
|
+
assert.ok(workflow.includes('branches: [develop, staging, main]'));
|
|
469
|
+
assert.ok(workflow.includes('concurrency:') && workflow.includes('group: publish-${{ github.ref_name }}'));
|
|
470
|
+
assert.ok(validate.includes('npm run check'));
|
|
471
|
+
assert.ok(validate.includes('npm pack --json'));
|
|
472
|
+
assert.ok(validate.includes('name: npm-package'));
|
|
473
|
+
assert.ok(validate.includes('name: dist-artifacts'));
|
|
474
|
+
assert.ok(validate.includes('name: ui-standalone'));
|
|
475
|
+
assert.ok(validate.includes('Generate release checksums'));
|
|
476
|
+
assert.ok(validate.includes('sha256sum'));
|
|
477
|
+
assert.ok(validate.includes('name: release-checksums'));
|
|
478
|
+
assert.ok(read('../charts/templates/gitea.yaml').includes('gitea-backend'));
|
|
479
|
+
const argocdTemplate = read('../charts/templates/argocd-application.yaml');
|
|
480
|
+
const values = read('../charts/values.yaml');
|
|
481
|
+
assert.ok(argocdTemplate.includes('kind: Application'));
|
|
482
|
+
assert.ok(argocdTemplate.includes('.Values.argocd.syncPolicy.prune'));
|
|
483
|
+
assert.ok(argocdTemplate.includes('.Values.argocd.syncPolicy.selfHeal'));
|
|
484
|
+
assert.ok(!values.includes('`n'));
|
|
485
|
+
assert.match(values, /^ syncPolicy:\r?\n\s+automated: true\r?\n\s+prune: true\r?\n\s+selfHeal: true/m);
|
|
486
|
+
|
|
487
|
+
const image = workflowJobBlock(workflow, 'publish-image');
|
|
488
|
+
assert.ok(image.includes('needs: validate'));
|
|
489
|
+
assert.ok(image.includes("push: ${{ github.event_name != 'pull_request' }}"));
|
|
490
|
+
assert.ok(image.includes('docker/build-push-action'));
|
|
491
|
+
assert.ok(image.includes('ghcr.io/${{ github.repository }}/kradle-controller'));
|
|
492
|
+
|
|
493
|
+
const chart = workflowJobBlock(workflow, 'publish-chart');
|
|
494
|
+
assert.ok(chart.includes('needs: validate'));
|
|
495
|
+
assert.ok(chart.includes('helm package charts/kradle'));
|
|
496
|
+
assert.ok(chart.includes('SHA256SUMS'));
|
|
497
|
+
assert.ok(chart.includes('sha256sum dist/charts/*.tgz'));
|
|
498
|
+
assert.ok(chart.includes("if: startsWith(github.ref, 'refs/tags/v')"));
|
|
499
|
+
assert.ok(chart.includes('helm push dist/charts/*.tgz'));
|
|
500
|
+
|
|
501
|
+
const deploy = workflowJobBlock(workflow, 'deploy-kradle');
|
|
502
|
+
assert.ok(deploy.includes('Deploy Kradle To AKS'));
|
|
503
|
+
assert.ok(deploy.includes("github.ref == 'refs/heads/develop'"));
|
|
504
|
+
assert.ok(deploy.includes("github.ref == 'refs/heads/staging'"));
|
|
505
|
+
assert.ok(deploy.includes("github.ref == 'refs/heads/main'"));
|
|
506
|
+
assert.ok(deploy.includes('environment:') && deploy.includes('kradle-production') && deploy.includes('https://kradle.a5c.ai'));
|
|
507
|
+
assert.ok(deploy.includes('AZURE_ACR_NAME') && deploy.includes('KUBE_CONFIG'));
|
|
508
|
+
assert.ok(deploy.includes('KRADLE_GITHUB_CLIENT_ID') && deploy.includes('KRADLE_GITHUB_CLIENT_SECRET'));
|
|
509
|
+
assert.ok(deploy.includes('kradle-develop.a5c.ai') && deploy.includes('kradle-staging.a5c.ai') && deploy.includes('kradle.a5c.ai'));
|
|
510
|
+
assert.ok(deploy.includes('docker build -f Dockerfile') && deploy.includes('docker push'));
|
|
511
|
+
assert.ok(deploy.includes('create secret docker-registry acr-pull'));
|
|
512
|
+
assert.ok(deploy.includes('helm upgrade --install'));
|
|
513
|
+
assert.ok(deploy.includes('--values /tmp/kradle-deploy-values.yaml'));
|
|
514
|
+
assert.ok(deploy.includes('--wait'));
|
|
515
|
+
assert.ok(deploy.includes('rollout status deployment/"${HELM_RELEASE}-kradle-web"'));
|
|
516
|
+
assert.doesNotMatch(workflow, /publish-npm:/);
|
|
517
|
+
assert.doesNotMatch(workflow, /npm publish/);
|
|
518
|
+
assert.doesNotMatch(workflow, /PUBLISH_NPM/);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
|