@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,312 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { createAgentWorkspaceController, AGENT_WORKSPACE_CONTROLLER_BOUNDARY, createResource } from '../src/index.js';
|
|
4
|
+
|
|
5
|
+
function makeWorkspace(name, opts = {}) {
|
|
6
|
+
const ws = createResource('KrateWorkspace', { name, namespace: opts.namespace || 'default' }, {
|
|
7
|
+
organizationRef: opts.organizationRef || 'default',
|
|
8
|
+
repository: opts.repository || 'https://github.com/acme/app.git',
|
|
9
|
+
volumeSpec: opts.volumeSpec || { storageClassName: 'standard', capacity: '10Gi', accessModes: ['ReadWriteOnce'] },
|
|
10
|
+
branch: opts.branch || 'main',
|
|
11
|
+
pvcName: opts.pvcName || `krate-ws-${name}`,
|
|
12
|
+
});
|
|
13
|
+
ws.status = {
|
|
14
|
+
phase: opts.phase || 'Ready',
|
|
15
|
+
volumeStatus: opts.volumeStatus || 'Bound',
|
|
16
|
+
createdAt: new Date().toISOString(),
|
|
17
|
+
...(opts.status || {}),
|
|
18
|
+
};
|
|
19
|
+
return ws;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// --- createWorkspace ---
|
|
23
|
+
|
|
24
|
+
test('createWorkspace generates valid PVC manifest with correct labels and capacity', () => {
|
|
25
|
+
const ctrl = createAgentWorkspaceController();
|
|
26
|
+
const result = ctrl.createWorkspace({
|
|
27
|
+
name: 'my-ws',
|
|
28
|
+
organizationRef: 'acme',
|
|
29
|
+
repository: 'https://github.com/acme/app.git',
|
|
30
|
+
volumeSpec: { capacity: '20Gi' },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
assert.equal(result.error, false);
|
|
34
|
+
assert.ok(result.workspace);
|
|
35
|
+
assert.ok(result.pvcManifest);
|
|
36
|
+
assert.equal(result.pvcManifest.kind, 'PersistentVolumeClaim');
|
|
37
|
+
assert.equal(result.pvcManifest.metadata.name, 'krate-ws-my-ws');
|
|
38
|
+
assert.equal(result.pvcManifest.metadata.labels['krate.a5c.ai/workspace'], 'my-ws');
|
|
39
|
+
assert.equal(result.pvcManifest.metadata.labels['krate.a5c.ai/org'], 'acme');
|
|
40
|
+
assert.equal(result.pvcManifest.spec.resources.requests.storage, '20Gi');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('createWorkspace uses custom storageClassName when provided', () => {
|
|
44
|
+
const ctrl = createAgentWorkspaceController();
|
|
45
|
+
const result = ctrl.createWorkspace({
|
|
46
|
+
name: 'custom-sc',
|
|
47
|
+
organizationRef: 'acme',
|
|
48
|
+
repository: 'https://github.com/acme/app.git',
|
|
49
|
+
volumeSpec: { storageClassName: 'fast-ssd' },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
assert.equal(result.error, false);
|
|
53
|
+
assert.equal(result.pvcManifest.spec.storageClassName, 'fast-ssd');
|
|
54
|
+
assert.equal(result.workspace.spec.volumeSpec.storageClassName, 'fast-ssd');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('createWorkspace defaults to 10Gi capacity', () => {
|
|
58
|
+
const ctrl = createAgentWorkspaceController();
|
|
59
|
+
const result = ctrl.createWorkspace({
|
|
60
|
+
name: 'default-cap',
|
|
61
|
+
organizationRef: 'acme',
|
|
62
|
+
repository: 'https://github.com/acme/app.git',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
assert.equal(result.error, false);
|
|
66
|
+
assert.equal(result.pvcManifest.spec.resources.requests.storage, '10Gi');
|
|
67
|
+
assert.equal(result.workspace.spec.volumeSpec.capacity, '10Gi');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('createWorkspace validates required fields (org, repository)', () => {
|
|
71
|
+
const ctrl = createAgentWorkspaceController();
|
|
72
|
+
|
|
73
|
+
const noOrg = ctrl.createWorkspace({ name: 'x', repository: 'repo' });
|
|
74
|
+
assert.equal(noOrg.error, true);
|
|
75
|
+
assert.equal(noOrg.reason, 'missing-org');
|
|
76
|
+
|
|
77
|
+
const noRepo = ctrl.createWorkspace({ name: 'x', organizationRef: 'acme' });
|
|
78
|
+
assert.equal(noRepo.error, true);
|
|
79
|
+
assert.equal(noRepo.reason, 'missing-repository');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('createWorkspace rejects null resource (missing both org and repo)', () => {
|
|
83
|
+
const ctrl = createAgentWorkspaceController();
|
|
84
|
+
const result = ctrl.createWorkspace({});
|
|
85
|
+
assert.equal(result.error, true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// --- deleteWorkspace ---
|
|
89
|
+
|
|
90
|
+
test('deleteWorkspace generates PVC delete manifest', () => {
|
|
91
|
+
const ctrl = createAgentWorkspaceController();
|
|
92
|
+
const ws = makeWorkspace('del-ws', { phase: 'Ready' });
|
|
93
|
+
|
|
94
|
+
const result = ctrl.deleteWorkspace({ name: 'del-ws', resources: { KrateWorkspace: [ws] } });
|
|
95
|
+
|
|
96
|
+
assert.equal(result.error, false);
|
|
97
|
+
assert.equal(result.workspace.status.phase, 'Terminating');
|
|
98
|
+
assert.ok(result.pvcDeleteManifest);
|
|
99
|
+
assert.equal(result.pvcDeleteManifest.metadata.name, 'krate-ws-del-ws');
|
|
100
|
+
assert.equal(result.pvcDeleteManifest.action, 'delete');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// --- getWorkspaceStatus ---
|
|
104
|
+
|
|
105
|
+
test('getWorkspaceStatus returns Pending for new workspace', () => {
|
|
106
|
+
const ctrl = createAgentWorkspaceController();
|
|
107
|
+
const ws = makeWorkspace('new-ws', { phase: 'Pending', volumeStatus: 'Pending' });
|
|
108
|
+
|
|
109
|
+
const result = ctrl.getWorkspaceStatus({ name: 'new-ws', resources: { KrateWorkspace: [ws] } });
|
|
110
|
+
|
|
111
|
+
assert.equal(result.error, false);
|
|
112
|
+
assert.equal(result.volumeStatus, 'Pending');
|
|
113
|
+
assert.equal(result.phase, 'Pending');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// --- initializeWorkspace ---
|
|
117
|
+
|
|
118
|
+
test('initializeWorkspace returns git clone command for https repo', () => {
|
|
119
|
+
const ctrl = createAgentWorkspaceController();
|
|
120
|
+
const ws = makeWorkspace('https-ws', { repository: 'https://github.com/acme/app.git' });
|
|
121
|
+
|
|
122
|
+
const result = ctrl.initializeWorkspace({ workspace: ws });
|
|
123
|
+
|
|
124
|
+
assert.equal(result.error, false);
|
|
125
|
+
assert.equal(result.commandSpec.command, 'git');
|
|
126
|
+
assert.deepEqual(result.commandSpec.args, ['clone', 'https://github.com/acme/app.git', '/workspace']);
|
|
127
|
+
assert.equal(Object.keys(result.commandSpec.env).length, 0, 'No special env for https');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('initializeWorkspace returns git clone command for ssh repo', () => {
|
|
131
|
+
const ctrl = createAgentWorkspaceController();
|
|
132
|
+
const ws = makeWorkspace('ssh-ws', { repository: 'git@github.com:acme/app.git' });
|
|
133
|
+
|
|
134
|
+
const result = ctrl.initializeWorkspace({ workspace: ws });
|
|
135
|
+
|
|
136
|
+
assert.equal(result.error, false);
|
|
137
|
+
assert.equal(result.commandSpec.command, 'git');
|
|
138
|
+
assert.deepEqual(result.commandSpec.args, ['clone', 'git@github.com:acme/app.git', '/workspace']);
|
|
139
|
+
assert.ok(result.commandSpec.env.GIT_SSH_COMMAND, 'Should set GIT_SSH_COMMAND for SSH repos');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// --- checkoutBranch ---
|
|
143
|
+
|
|
144
|
+
test('checkoutBranch returns git checkout command', () => {
|
|
145
|
+
const ctrl = createAgentWorkspaceController();
|
|
146
|
+
const ws = makeWorkspace('co-ws');
|
|
147
|
+
|
|
148
|
+
const result = ctrl.checkoutBranch({ workspace: ws, branch: 'feature-42' });
|
|
149
|
+
|
|
150
|
+
assert.equal(result.error, false);
|
|
151
|
+
assert.equal(result.commandSpec.command, 'git');
|
|
152
|
+
assert.deepEqual(result.commandSpec.args, ['checkout', 'feature-42']);
|
|
153
|
+
assert.equal(result.commandSpec.cwd, '/workspace');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// --- syncWorkspace ---
|
|
157
|
+
|
|
158
|
+
test('syncWorkspace returns git fetch + reset commands', () => {
|
|
159
|
+
const ctrl = createAgentWorkspaceController();
|
|
160
|
+
const ws = makeWorkspace('sync-ws', { branch: 'develop' });
|
|
161
|
+
|
|
162
|
+
const result = ctrl.syncWorkspace({ workspace: ws });
|
|
163
|
+
|
|
164
|
+
assert.equal(result.error, false);
|
|
165
|
+
assert.equal(result.commandSpecs.length, 2);
|
|
166
|
+
assert.deepEqual(result.commandSpecs[0].args, ['fetch', 'origin']);
|
|
167
|
+
assert.deepEqual(result.commandSpecs[1].args, ['reset', '--hard', 'origin/develop']);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// --- getMountSpec ---
|
|
171
|
+
|
|
172
|
+
test('getMountSpec returns valid K8s volume and volumeMount', () => {
|
|
173
|
+
const ctrl = createAgentWorkspaceController();
|
|
174
|
+
const ws = makeWorkspace('mount-ws', { pvcName: 'krate-ws-mount-ws' });
|
|
175
|
+
|
|
176
|
+
const result = ctrl.getMountSpec({ workspace: ws });
|
|
177
|
+
|
|
178
|
+
assert.equal(result.error, false);
|
|
179
|
+
assert.ok(result.volume);
|
|
180
|
+
assert.ok(result.volumeMount);
|
|
181
|
+
assert.equal(result.volume.name, 'workspace');
|
|
182
|
+
assert.equal(result.volumeMount.name, 'workspace');
|
|
183
|
+
assert.equal(result.volumeMount.mountPath, '/workspace');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('getMountSpec uses correct PVC claim name', () => {
|
|
187
|
+
const ctrl = createAgentWorkspaceController();
|
|
188
|
+
const ws = makeWorkspace('pvc-test', { pvcName: 'krate-ws-pvc-test' });
|
|
189
|
+
|
|
190
|
+
const result = ctrl.getMountSpec({ workspace: ws });
|
|
191
|
+
|
|
192
|
+
assert.equal(result.error, false);
|
|
193
|
+
assert.equal(result.volume.persistentVolumeClaim.claimName, 'krate-ws-pvc-test');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// --- findReusableWorkspace ---
|
|
197
|
+
|
|
198
|
+
test('findReusableWorkspace returns matching Ready workspace', () => {
|
|
199
|
+
const ctrl = createAgentWorkspaceController();
|
|
200
|
+
const ws1 = makeWorkspace('reuse-ws', {
|
|
201
|
+
organizationRef: 'acme',
|
|
202
|
+
repository: 'https://github.com/acme/app.git',
|
|
203
|
+
branch: 'main',
|
|
204
|
+
phase: 'Ready',
|
|
205
|
+
});
|
|
206
|
+
const ws2 = makeWorkspace('other-ws', {
|
|
207
|
+
organizationRef: 'acme',
|
|
208
|
+
repository: 'https://github.com/acme/other.git',
|
|
209
|
+
branch: 'main',
|
|
210
|
+
phase: 'Ready',
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const result = ctrl.findReusableWorkspace({
|
|
214
|
+
organizationRef: 'acme',
|
|
215
|
+
repository: 'https://github.com/acme/app.git',
|
|
216
|
+
branch: 'main',
|
|
217
|
+
resources: { KrateWorkspace: [ws1, ws2] },
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
assert.ok(result);
|
|
221
|
+
assert.equal(result.metadata.name, 'reuse-ws');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('findReusableWorkspace returns null when no match', () => {
|
|
225
|
+
const ctrl = createAgentWorkspaceController();
|
|
226
|
+
const ws = makeWorkspace('busy-ws', {
|
|
227
|
+
organizationRef: 'acme',
|
|
228
|
+
repository: 'https://github.com/acme/app.git',
|
|
229
|
+
branch: 'main',
|
|
230
|
+
phase: 'InUse',
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const result = ctrl.findReusableWorkspace({
|
|
234
|
+
organizationRef: 'acme',
|
|
235
|
+
repository: 'https://github.com/acme/app.git',
|
|
236
|
+
branch: 'main',
|
|
237
|
+
resources: { KrateWorkspace: [ws] },
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
assert.equal(result, null);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// --- claimWorkspace ---
|
|
244
|
+
|
|
245
|
+
test('claimWorkspace transitions to InUse with runRef', () => {
|
|
246
|
+
const ctrl = createAgentWorkspaceController();
|
|
247
|
+
const ws = makeWorkspace('claim-ws', { phase: 'Ready' });
|
|
248
|
+
|
|
249
|
+
const result = ctrl.claimWorkspace({
|
|
250
|
+
name: 'claim-ws',
|
|
251
|
+
runRef: 'run-123',
|
|
252
|
+
resources: { KrateWorkspace: [ws] },
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
assert.equal(result.error, false);
|
|
256
|
+
assert.equal(result.workspace.status.phase, 'InUse');
|
|
257
|
+
assert.equal(result.workspace.status.runRef, 'run-123');
|
|
258
|
+
assert.ok(result.workspace.status.claimedAt);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('claimWorkspace rejects already-InUse workspace', () => {
|
|
262
|
+
const ctrl = createAgentWorkspaceController();
|
|
263
|
+
const ws = makeWorkspace('busy-ws', { phase: 'InUse', status: { runRef: 'run-existing' } });
|
|
264
|
+
|
|
265
|
+
const result = ctrl.claimWorkspace({
|
|
266
|
+
name: 'busy-ws',
|
|
267
|
+
runRef: 'run-new',
|
|
268
|
+
resources: { KrateWorkspace: [ws] },
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
assert.equal(result.error, true);
|
|
272
|
+
assert.equal(result.reason, 'already-in-use');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// --- releaseWorkspace ---
|
|
276
|
+
|
|
277
|
+
test('releaseWorkspace transitions to Ready', () => {
|
|
278
|
+
const ctrl = createAgentWorkspaceController();
|
|
279
|
+
const ws = makeWorkspace('release-ws', { phase: 'InUse', status: { runRef: 'run-done' } });
|
|
280
|
+
|
|
281
|
+
const result = ctrl.releaseWorkspace({
|
|
282
|
+
name: 'release-ws',
|
|
283
|
+
resources: { KrateWorkspace: [ws] },
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
assert.equal(result.error, false);
|
|
287
|
+
assert.equal(result.workspace.status.phase, 'Ready');
|
|
288
|
+
assert.equal(result.workspace.status.runRef, undefined);
|
|
289
|
+
assert.ok(result.workspace.status.releasedAt);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('releaseWorkspace rejects non-InUse workspace', () => {
|
|
293
|
+
const ctrl = createAgentWorkspaceController();
|
|
294
|
+
const ws = makeWorkspace('ready-ws', { phase: 'Ready' });
|
|
295
|
+
|
|
296
|
+
const result = ctrl.releaseWorkspace({
|
|
297
|
+
name: 'ready-ws',
|
|
298
|
+
resources: { KrateWorkspace: [ws] },
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
assert.equal(result.error, true);
|
|
302
|
+
assert.equal(result.reason, 'not-in-use');
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// --- BOUNDARY export ---
|
|
306
|
+
|
|
307
|
+
test('BOUNDARY exported', () => {
|
|
308
|
+
assert.ok(AGENT_WORKSPACE_CONTROLLER_BOUNDARY);
|
|
309
|
+
assert.equal(AGENT_WORKSPACE_CONTROLLER_BOUNDARY.role, 'agent-workspace-controller');
|
|
310
|
+
assert.ok(AGENT_WORKSPACE_CONTROLLER_BOUNDARY.owns.includes('PVC manifest generation'));
|
|
311
|
+
assert.ok(AGENT_WORKSPACE_CONTROLLER_BOUNDARY.owns.includes('workspace reuse'));
|
|
312
|
+
});
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* B2: Write-back persistence + execution tests
|
|
3
|
+
* Tests for persistWriteIntent (saves AgentWriteIntent via applyResource),
|
|
4
|
+
* executeWriteIntent (calls gateway for branch push, validates status checks for PR merge).
|
|
5
|
+
*/
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import test from 'node:test';
|
|
8
|
+
import { createAgentWritebackController } from '../src/index.js';
|
|
9
|
+
|
|
10
|
+
function makePushIntent(name, runRef, branch = 'feature/x', approvalStatus = 'pending') {
|
|
11
|
+
return {
|
|
12
|
+
kind: 'AgentWriteIntent',
|
|
13
|
+
metadata: { name, namespace: 'krate-org-default', labels: {}, annotations: {} },
|
|
14
|
+
spec: {
|
|
15
|
+
organizationRef: 'default',
|
|
16
|
+
runRef,
|
|
17
|
+
branch,
|
|
18
|
+
targetRepo: 'org/my-repo',
|
|
19
|
+
writeType: 'branch-push',
|
|
20
|
+
requestedBy: 'agent-stack-1',
|
|
21
|
+
requestedAt: new Date().toISOString()
|
|
22
|
+
},
|
|
23
|
+
status: { approvalStatus, phase: approvalStatus === 'approved' ? 'Approved' : 'Pending', createdAt: new Date().toISOString() }
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function makeMergeIntent(name, runRef, prRef, approvalStatus = 'pending', statusChecks = []) {
|
|
28
|
+
return {
|
|
29
|
+
kind: 'AgentWriteIntent',
|
|
30
|
+
metadata: { name, namespace: 'krate-org-default', labels: {}, annotations: {} },
|
|
31
|
+
spec: {
|
|
32
|
+
organizationRef: 'default',
|
|
33
|
+
runRef,
|
|
34
|
+
prRef,
|
|
35
|
+
writeType: 'pr-merge',
|
|
36
|
+
statusChecks,
|
|
37
|
+
requestedBy: 'agent-stack-1',
|
|
38
|
+
requestedAt: new Date().toISOString()
|
|
39
|
+
},
|
|
40
|
+
status: { approvalStatus, phase: approvalStatus === 'approved' ? 'Approved' : 'Pending', createdAt: new Date().toISOString() }
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// --- persistWriteIntent ---
|
|
45
|
+
|
|
46
|
+
test('persistWriteIntent calls applyResource with the intent and returns success', async () => {
|
|
47
|
+
const controller = createAgentWritebackController();
|
|
48
|
+
const intent = makePushIntent('push-persist-1', 'run-persist-1', 'feature/new-thing', 'pending');
|
|
49
|
+
|
|
50
|
+
const applied = [];
|
|
51
|
+
const mockApplyResource = async (resource) => {
|
|
52
|
+
applied.push(resource);
|
|
53
|
+
return { ok: true, resource };
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const result = await controller.persistWriteIntent({ intent, applyResource: mockApplyResource });
|
|
57
|
+
|
|
58
|
+
assert.equal(result.error, false, 'Should succeed');
|
|
59
|
+
assert.equal(applied.length, 1, 'applyResource should be called exactly once');
|
|
60
|
+
assert.equal(applied[0].kind, 'AgentWriteIntent', 'Applied resource should be AgentWriteIntent');
|
|
61
|
+
assert.equal(applied[0].metadata.name, 'push-persist-1');
|
|
62
|
+
assert.equal(applied[0].spec.runRef, 'run-persist-1');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('persistWriteIntent returns error when applyResource throws', async () => {
|
|
66
|
+
const controller = createAgentWritebackController();
|
|
67
|
+
const intent = makePushIntent('push-persist-2', 'run-persist-2', 'main', 'pending');
|
|
68
|
+
|
|
69
|
+
const mockApplyResource = async () => {
|
|
70
|
+
throw new Error('Storage backend unreachable');
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const result = await controller.persistWriteIntent({ intent, applyResource: mockApplyResource });
|
|
74
|
+
|
|
75
|
+
assert.equal(result.error, true, 'Should return error when applyResource throws');
|
|
76
|
+
assert.equal(result.reason, 'persist-failed');
|
|
77
|
+
assert.ok(result.message, 'Should include error message');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('persistWriteIntent returns error when intent is missing', async () => {
|
|
81
|
+
const controller = createAgentWritebackController();
|
|
82
|
+
|
|
83
|
+
const mockApplyResource = async () => ({ ok: true });
|
|
84
|
+
const result = await controller.persistWriteIntent({ intent: null, applyResource: mockApplyResource });
|
|
85
|
+
|
|
86
|
+
assert.equal(result.error, true, 'Should fail when intent is null');
|
|
87
|
+
assert.equal(result.reason, 'missing-intent');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('persistWriteIntent returns error when applyResource is not provided', async () => {
|
|
91
|
+
const controller = createAgentWritebackController();
|
|
92
|
+
const intent = makePushIntent('push-persist-3', 'run-persist-3', 'develop', 'pending');
|
|
93
|
+
|
|
94
|
+
const result = await controller.persistWriteIntent({ intent, applyResource: null });
|
|
95
|
+
|
|
96
|
+
assert.equal(result.error, true, 'Should fail without applyResource');
|
|
97
|
+
assert.equal(result.reason, 'missing-apply-resource');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('persistWriteIntent passes applyResource result back in response', async () => {
|
|
101
|
+
const controller = createAgentWritebackController();
|
|
102
|
+
const intent = makePushIntent('push-persist-4', 'run-persist-4', 'hotfix/urgent', 'approved');
|
|
103
|
+
|
|
104
|
+
const mockApplyResult = { ok: true, uid: 'some-uid-abc' };
|
|
105
|
+
const mockApplyResource = async () => mockApplyResult;
|
|
106
|
+
|
|
107
|
+
const result = await controller.persistWriteIntent({ intent, applyResource: mockApplyResource });
|
|
108
|
+
|
|
109
|
+
assert.equal(result.error, false, 'Should succeed');
|
|
110
|
+
assert.deepEqual(result.applyResult, mockApplyResult, 'Should include applyResource result');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// --- executeWriteIntent ---
|
|
114
|
+
|
|
115
|
+
test('executeWriteIntent calls gateway for approved branch-push intent', async () => {
|
|
116
|
+
const controller = createAgentWritebackController();
|
|
117
|
+
const intent = makePushIntent('push-exec-1', 'run-exec-1', 'feature/ready', 'approved');
|
|
118
|
+
|
|
119
|
+
const gatewayCalls = [];
|
|
120
|
+
const mockGateway = {
|
|
121
|
+
async pushBranch({ branch, targetRepo, runRef }) {
|
|
122
|
+
gatewayCalls.push({ branch, targetRepo, runRef });
|
|
123
|
+
return { ok: true, sha: 'abc123def456' };
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const result = await controller.executeWriteIntent({ intent, gateway: mockGateway });
|
|
128
|
+
|
|
129
|
+
assert.equal(result.error, false, 'Should succeed');
|
|
130
|
+
assert.equal(gatewayCalls.length, 1, 'Gateway pushBranch should be called once');
|
|
131
|
+
assert.equal(gatewayCalls[0].branch, 'feature/ready');
|
|
132
|
+
assert.equal(gatewayCalls[0].targetRepo, 'org/my-repo');
|
|
133
|
+
assert.equal(gatewayCalls[0].runRef, 'run-exec-1');
|
|
134
|
+
assert.ok(result.executionResult, 'Should return execution result');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('executeWriteIntent blocks execution when intent approval is still pending', async () => {
|
|
138
|
+
const controller = createAgentWritebackController();
|
|
139
|
+
const intent = makePushIntent('push-exec-2', 'run-exec-2', 'feature/unreviewed', 'pending');
|
|
140
|
+
|
|
141
|
+
const mockGateway = {
|
|
142
|
+
async pushBranch() { return { ok: true }; }
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const result = await controller.executeWriteIntent({ intent, gateway: mockGateway });
|
|
146
|
+
|
|
147
|
+
assert.equal(result.error, true, 'Should block when not approved');
|
|
148
|
+
assert.equal(result.reason, 'not-approved');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('executeWriteIntent validates status checks are passing for pr-merge intent', async () => {
|
|
152
|
+
const controller = createAgentWritebackController();
|
|
153
|
+
const intent = makeMergeIntent('merge-exec-1', 'run-exec-3', 'pr-55', 'approved', [
|
|
154
|
+
{ name: 'ci/build', state: 'success' },
|
|
155
|
+
{ name: 'ci/tests', state: 'success' }
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
const gatewayCalls = [];
|
|
159
|
+
const mockGateway = {
|
|
160
|
+
async mergePr({ prRef, runRef }) {
|
|
161
|
+
gatewayCalls.push({ prRef, runRef });
|
|
162
|
+
return { ok: true, mergeCommit: 'deadbeef' };
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const result = await controller.executeWriteIntent({ intent, gateway: mockGateway });
|
|
167
|
+
|
|
168
|
+
assert.equal(result.error, false, 'Should succeed when checks pass and intent is approved');
|
|
169
|
+
assert.equal(gatewayCalls.length, 1, 'Gateway mergePr should be called once');
|
|
170
|
+
assert.equal(gatewayCalls[0].prRef, 'pr-55');
|
|
171
|
+
assert.ok(result.executionResult, 'Should return execution result');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('executeWriteIntent rejects pr-merge when status checks are failing', async () => {
|
|
175
|
+
const controller = createAgentWritebackController();
|
|
176
|
+
const intent = makeMergeIntent('merge-exec-2', 'run-exec-4', 'pr-66', 'approved', [
|
|
177
|
+
{ name: 'ci/build', state: 'success' },
|
|
178
|
+
{ name: 'security/scan', state: 'failure' }
|
|
179
|
+
]);
|
|
180
|
+
|
|
181
|
+
const mockGateway = {
|
|
182
|
+
async mergePr() { return { ok: true }; }
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const result = await controller.executeWriteIntent({ intent, gateway: mockGateway });
|
|
186
|
+
|
|
187
|
+
assert.equal(result.error, true, 'Should block when checks are failing');
|
|
188
|
+
assert.equal(result.reason, 'status-checks-failing');
|
|
189
|
+
assert.ok(result.message.includes('security/scan'), 'Message should name the failing check');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('executeWriteIntent returns error when gateway throws', async () => {
|
|
193
|
+
const controller = createAgentWritebackController();
|
|
194
|
+
const intent = makePushIntent('push-exec-3', 'run-exec-5', 'feature/crash', 'approved');
|
|
195
|
+
|
|
196
|
+
const mockGateway = {
|
|
197
|
+
async pushBranch() {
|
|
198
|
+
throw new Error('Git remote unreachable');
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const result = await controller.executeWriteIntent({ intent, gateway: mockGateway });
|
|
203
|
+
|
|
204
|
+
assert.equal(result.error, true, 'Should return error when gateway throws');
|
|
205
|
+
assert.equal(result.reason, 'execution-failed');
|
|
206
|
+
assert.ok(result.message, 'Should include error message');
|
|
207
|
+
});
|