@a5c-ai/krate 5.0.1-staging.3a341c33c
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 +2455 -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/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/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 +207 -0
- package/src/agent-approval-controller.js +123 -0
- package/src/agent-context-bundles.js +242 -0
- package/src/agent-dispatch-controller.js +86 -0
- package/src/agent-memory-controller.js +374 -0
- package/src/agent-mux-client.js +280 -0
- package/src/agent-permission-review.js +162 -0
- package/src/agent-stack-controller.js +296 -0
- package/src/agent-trigger-controller.js +108 -0
- package/src/agent-workspace-controller.js +208 -0
- package/src/api-controller.js +248 -0
- package/src/argocd-gitops.js +43 -0
- package/src/auth.js +265 -0
- package/src/component-catalog.js +41 -0
- package/src/control-plane.js +136 -0
- package/src/controller-client.js +38 -0
- package/src/controller-ui.js +551 -0
- package/src/data-plane.js +178 -0
- package/src/gitea-backend.js +95 -0
- package/src/handoff.js +98 -0
- package/src/hooks-events.js +63 -0
- package/src/http-server.js +151 -0
- package/src/identity-policy.js +86 -0
- package/src/index.js +32 -0
- package/src/kubernetes-controller.js +812 -0
- package/src/kubernetes-resource-gateway.js +48 -0
- package/src/operations.js +112 -0
- package/src/resource-model.js +211 -0
- package/src/runners-ci.js +48 -0
- package/src/runtime.js +196 -0
- package/src/web-ui.js +40 -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 +176 -0
- package/tests/agent-memory-controller.test.js +308 -0
- package/tests/agent-mux-client.test.js +204 -0
- package/tests/agent-permission-review.test.js +209 -0
- package/tests/agent-resources.test.js +228 -0
- package/tests/agent-stack-controller.test.js +221 -0
- package/tests/agent-trigger-controller.test.js +211 -0
- package/tests/agent-workspace-controller.test.js +215 -0
- package/tests/deployment.test.js +393 -0
- package/tests/e2e/lifecycle.test.js +117 -0
- package/tests/krate.test.js +727 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { createAgentMemoryController } from '../src/agent-memory-controller.js';
|
|
4
|
+
|
|
5
|
+
function makeRecords() {
|
|
6
|
+
return [
|
|
7
|
+
{
|
|
8
|
+
id: 'service/auth-api',
|
|
9
|
+
nodeKind: 'Service',
|
|
10
|
+
attributes: { name: 'auth-api', language: 'typescript', team: 'platform' },
|
|
11
|
+
edges: [
|
|
12
|
+
{ target: 'service/user-db', kind: 'depends-on' },
|
|
13
|
+
{ target: 'team/platform', kind: 'owned-by' },
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: 'service/user-db',
|
|
18
|
+
nodeKind: 'Service',
|
|
19
|
+
attributes: { name: 'user-db', language: 'go', team: 'data' },
|
|
20
|
+
edges: [
|
|
21
|
+
{ target: 'infra/postgres-cluster', kind: 'depends-on' },
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'team/platform',
|
|
26
|
+
nodeKind: 'Team',
|
|
27
|
+
attributes: { name: 'platform', lead: 'alice' },
|
|
28
|
+
edges: [],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'infra/postgres-cluster',
|
|
32
|
+
nodeKind: 'Infrastructure',
|
|
33
|
+
attributes: { name: 'postgres-cluster', provider: 'aws' },
|
|
34
|
+
edges: [],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'runbook/deploy-auth',
|
|
38
|
+
nodeKind: 'Runbook',
|
|
39
|
+
attributes: { name: 'deploy-auth', service: 'auth-api' },
|
|
40
|
+
edges: [
|
|
41
|
+
{ target: 'service/auth-api', kind: 'references' },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function makeDocuments() {
|
|
48
|
+
return [
|
|
49
|
+
{ path: 'docs/architecture.md', content: 'The auth-api service handles authentication.\nIt uses JWT tokens for session management.\nThe service connects to user-db for persistence.' },
|
|
50
|
+
{ path: 'docs/runbooks/deploy.md', content: 'Step 1: Run the deploy script.\nStep 2: Verify health checks.\nStep 3: Monitor error rates.' },
|
|
51
|
+
{ path: 'src/auth/handler.ts', content: 'export function handleAuth(req) {\n const token = req.headers.authorization;\n return validateToken(token);\n}' },
|
|
52
|
+
{ path: 'configs/prod.yaml', content: 'database:\n host: prod-db.internal\n port: 5432\n password: [redacted]' },
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
test('createMemorySnapshot creates valid resource with digests', () => {
|
|
57
|
+
const controller = createAgentMemoryController();
|
|
58
|
+
const records = makeRecords();
|
|
59
|
+
const documents = makeDocuments();
|
|
60
|
+
|
|
61
|
+
const snapshot = controller.createMemorySnapshot({
|
|
62
|
+
memoryRepository: 'company-brain',
|
|
63
|
+
requestedRef: 'main',
|
|
64
|
+
resolvedCommit: 'abc123def456',
|
|
65
|
+
queryManifest: { include: ['Service', 'Team'] },
|
|
66
|
+
selectedRecords: records,
|
|
67
|
+
selectedDocuments: documents,
|
|
68
|
+
ontologyDigest: 'onto-digest-1',
|
|
69
|
+
namespace: 'krate-org-default',
|
|
70
|
+
organizationRef: 'acme',
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
assert.equal(snapshot.kind, 'AgentMemorySnapshot');
|
|
74
|
+
assert.ok(snapshot.metadata.name.startsWith('memsnapshot-'));
|
|
75
|
+
assert.equal(snapshot.spec.memoryRepository, 'company-brain');
|
|
76
|
+
assert.equal(snapshot.spec.requestedRef, 'main');
|
|
77
|
+
assert.equal(snapshot.spec.resolvedCommit, 'abc123def456');
|
|
78
|
+
assert.ok(snapshot.spec.queryManifestDigest, 'Should have queryManifestDigest');
|
|
79
|
+
assert.ok(snapshot.spec.selectedRecordsDigest, 'Should have selectedRecordsDigest');
|
|
80
|
+
assert.ok(snapshot.spec.selectedDocumentsDigest, 'Should have selectedDocumentsDigest');
|
|
81
|
+
assert.equal(snapshot.spec.ontologyDigest, 'onto-digest-1');
|
|
82
|
+
assert.equal(snapshot.spec.recordCount, records.length);
|
|
83
|
+
assert.equal(snapshot.spec.documentCount, documents.length);
|
|
84
|
+
assert.equal(snapshot.status.phase, 'Pinned');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('searchGraph filters by node kind', () => {
|
|
88
|
+
const controller = createAgentMemoryController();
|
|
89
|
+
const records = makeRecords();
|
|
90
|
+
|
|
91
|
+
const result = controller.searchGraph({ records, kinds: ['Service'], query: '' });
|
|
92
|
+
|
|
93
|
+
assert.equal(result.totalMatches, 2, 'Should match exactly 2 Service records');
|
|
94
|
+
assert.ok(result.matches.every(m => m.record.nodeKind === 'Service'));
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('searchGraph matches query text', () => {
|
|
98
|
+
const controller = createAgentMemoryController();
|
|
99
|
+
const records = makeRecords();
|
|
100
|
+
|
|
101
|
+
const result = controller.searchGraph({ records, kinds: [], query: 'auth' });
|
|
102
|
+
|
|
103
|
+
assert.ok(result.totalMatches >= 1, 'Should match at least auth-api');
|
|
104
|
+
const authMatch = result.matches.find(m => m.record.id === 'service/auth-api');
|
|
105
|
+
assert.ok(authMatch, 'Should include auth-api in matches');
|
|
106
|
+
assert.equal(authMatch.score, 2, 'ID match should score 2');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('searchGraph respects edgeDepth', () => {
|
|
110
|
+
const controller = createAgentMemoryController();
|
|
111
|
+
const records = makeRecords();
|
|
112
|
+
|
|
113
|
+
// edgeDepth=1: from auth-api should reach user-db and team/platform but NOT postgres-cluster
|
|
114
|
+
const depth1 = controller.searchGraph({ records, kinds: ['Service'], query: 'auth-api', edgeDepth: 1 });
|
|
115
|
+
const authMatch1 = depth1.matches.find(m => m.record.id === 'service/auth-api');
|
|
116
|
+
assert.ok(authMatch1, 'Should find auth-api');
|
|
117
|
+
const depth1Targets = authMatch1.edges.map(e => e.target);
|
|
118
|
+
assert.ok(depth1Targets.includes('service/user-db'), 'Depth 1 should include user-db');
|
|
119
|
+
assert.ok(!depth1Targets.includes('infra/postgres-cluster'), 'Depth 1 should NOT include postgres-cluster');
|
|
120
|
+
|
|
121
|
+
// edgeDepth=2: should also reach postgres-cluster
|
|
122
|
+
const depth2 = controller.searchGraph({ records, kinds: ['Service'], query: 'auth-api', edgeDepth: 2 });
|
|
123
|
+
const authMatch2 = depth2.matches.find(m => m.record.id === 'service/auth-api');
|
|
124
|
+
const depth2Targets = authMatch2.edges.map(e => e.target);
|
|
125
|
+
assert.ok(depth2Targets.includes('infra/postgres-cluster'), 'Depth 2 should include postgres-cluster');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('searchGrep finds pattern matches', () => {
|
|
129
|
+
const controller = createAgentMemoryController();
|
|
130
|
+
const documents = makeDocuments();
|
|
131
|
+
|
|
132
|
+
const result = controller.searchGrep({ documents, pattern: 'auth', maxMatches: 25 });
|
|
133
|
+
|
|
134
|
+
assert.ok(result.totalMatches >= 1, 'Should find auth pattern');
|
|
135
|
+
assert.ok(result.excerpts.some(e => e.path === 'docs/architecture.md'), 'Should match in architecture.md');
|
|
136
|
+
assert.ok(result.excerpts.every(e => e.lineNumber > 0), 'Every excerpt should have a lineNumber');
|
|
137
|
+
assert.ok(result.excerpts.every(e => typeof e.context === 'string'), 'Every excerpt should have context');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('searchGrep caps at maxMatches', () => {
|
|
141
|
+
const controller = createAgentMemoryController();
|
|
142
|
+
const documents = makeDocuments();
|
|
143
|
+
|
|
144
|
+
const result = controller.searchGrep({ documents, pattern: 'the', maxMatches: 2 });
|
|
145
|
+
|
|
146
|
+
assert.ok(result.totalMatches <= 2, 'Should not exceed maxMatches');
|
|
147
|
+
assert.ok(result.excerpts.length <= 2, 'Excerpts length should not exceed maxMatches');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('searchGrep filters by path patterns', () => {
|
|
151
|
+
const controller = createAgentMemoryController();
|
|
152
|
+
const documents = makeDocuments();
|
|
153
|
+
|
|
154
|
+
const result = controller.searchGrep({ documents, paths: ['docs/*'], pattern: 'deploy' });
|
|
155
|
+
|
|
156
|
+
assert.ok(result.totalMatches >= 1, 'Should find deploy in docs');
|
|
157
|
+
assert.ok(result.excerpts.every(e => e.path.startsWith('docs/')), 'All matches should be in docs/');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('resolveTimeTravel mode=current returns latest commit', () => {
|
|
161
|
+
const controller = createAgentMemoryController();
|
|
162
|
+
const commits = [
|
|
163
|
+
{ sha: 'latest-sha', timestamp: '2026-05-10T10:00:00Z' },
|
|
164
|
+
{ sha: 'older-sha', timestamp: '2026-05-09T10:00:00Z' },
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
const result = controller.resolveTimeTravel({ mode: 'current', commits });
|
|
168
|
+
|
|
169
|
+
assert.equal(result.resolvedCommit, 'latest-sha');
|
|
170
|
+
assert.equal(result.mode, 'current');
|
|
171
|
+
assert.ok(result.resolvedAt);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('resolveTimeTravel mode=ref-at-time finds correct commit', () => {
|
|
175
|
+
const controller = createAgentMemoryController();
|
|
176
|
+
const commits = [
|
|
177
|
+
{ sha: 'c3', timestamp: '2026-05-10T10:00:00Z' },
|
|
178
|
+
{ sha: 'c2', timestamp: '2026-05-08T10:00:00Z' },
|
|
179
|
+
{ sha: 'c1', timestamp: '2026-05-05T10:00:00Z' },
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
const result = controller.resolveTimeTravel({
|
|
183
|
+
mode: 'ref-at-time',
|
|
184
|
+
requestedTime: '2026-05-09T00:00:00Z',
|
|
185
|
+
commits,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
assert.equal(result.resolvedCommit, 'c2', 'Should resolve to c2 (latest before requested time)');
|
|
189
|
+
assert.equal(result.mode, 'ref-at-time');
|
|
190
|
+
assert.ok(result.staleBy !== null, 'staleBy should be set');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('scanForRedaction catches API keys', () => {
|
|
194
|
+
const controller = createAgentMemoryController();
|
|
195
|
+
const content = 'API_KEY = sk-test1234567890abcdefghij\nSome normal text\nSECRET_KEY=my-super-secret-value';
|
|
196
|
+
|
|
197
|
+
const result = controller.scanForRedaction(content);
|
|
198
|
+
|
|
199
|
+
assert.equal(result.clean, false, 'Content should not be clean');
|
|
200
|
+
assert.ok(result.redactionCount > 0, 'Should have redactions');
|
|
201
|
+
assert.ok(result.redactionsByKind['secret-key'] > 0, 'Should catch secret-key pattern');
|
|
202
|
+
assert.ok(result.redactedContent.includes('[REDACTED:'), 'Redacted content should have markers');
|
|
203
|
+
assert.ok(!result.redactedContent.includes('my-super-secret-value'), 'Secret should be redacted');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('scanForRedaction catches provider tokens', () => {
|
|
207
|
+
const controller = createAgentMemoryController();
|
|
208
|
+
const content = 'token: ghp_abcdefghijklmnopqrstuvwxyz1234567890\nslack: xoxb-123-456-abcdefghij';
|
|
209
|
+
|
|
210
|
+
const result = controller.scanForRedaction(content);
|
|
211
|
+
|
|
212
|
+
assert.equal(result.clean, false);
|
|
213
|
+
assert.ok(result.redactionsByKind['provider-token'] > 0, 'Should catch provider-token pattern');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('validateOntology catches missing required fields', () => {
|
|
217
|
+
const controller = createAgentMemoryController();
|
|
218
|
+
const records = [
|
|
219
|
+
{ id: 'svc/a', nodeKind: 'Service', attributes: { name: 'a' }, edges: [] },
|
|
220
|
+
{ id: 'svc/b', nodeKind: 'Service', attributes: { name: 'b', language: 'go' }, edges: [] },
|
|
221
|
+
];
|
|
222
|
+
const ontology = {
|
|
223
|
+
requiredFields: { Service: ['name', 'language', 'team'] },
|
|
224
|
+
allowedEdgeKinds: ['depends-on', 'owned-by'],
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const result = controller.validateOntology({ records, ontology });
|
|
228
|
+
|
|
229
|
+
assert.equal(result.valid, false, 'Should be invalid due to missing fields');
|
|
230
|
+
assert.ok(result.errors.length >= 2, 'Should have errors for missing language and team on svc/a, and team on svc/b');
|
|
231
|
+
const missingLang = result.errors.find(e => e.record === 'svc/a' && e.field === 'language');
|
|
232
|
+
assert.ok(missingLang, 'Should report missing language on svc/a');
|
|
233
|
+
const missingTeam = result.errors.find(e => e.record === 'svc/a' && e.field === 'team');
|
|
234
|
+
assert.ok(missingTeam, 'Should report missing team on svc/a');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('validateOntology passes valid records', () => {
|
|
238
|
+
const controller = createAgentMemoryController();
|
|
239
|
+
const records = [
|
|
240
|
+
{ id: 'svc/a', nodeKind: 'Service', attributes: { name: 'a', language: 'ts' }, edges: [{ target: 'svc/b', kind: 'depends-on' }] },
|
|
241
|
+
{ id: 'svc/b', nodeKind: 'Service', attributes: { name: 'b', language: 'go' }, edges: [] },
|
|
242
|
+
];
|
|
243
|
+
const ontology = {
|
|
244
|
+
requiredFields: { Service: ['name', 'language'] },
|
|
245
|
+
allowedEdgeKinds: ['depends-on', 'owned-by'],
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const result = controller.validateOntology({ records, ontology });
|
|
249
|
+
|
|
250
|
+
assert.equal(result.valid, true, 'Should be valid');
|
|
251
|
+
assert.equal(result.errors.length, 0);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test('createImport sets phase=Pending', () => {
|
|
255
|
+
const controller = createAgentMemoryController();
|
|
256
|
+
|
|
257
|
+
const importResource = controller.createImport({
|
|
258
|
+
organizationRef: 'acme',
|
|
259
|
+
memoryRepository: 'company-brain',
|
|
260
|
+
source: 'babysitter-run/run-123',
|
|
261
|
+
include: { kinds: ['decision', 'learning'] },
|
|
262
|
+
validationPolicy: 'strict',
|
|
263
|
+
namespace: 'krate-org-default',
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
assert.equal(importResource.kind, 'AgentRunMemoryImport');
|
|
267
|
+
assert.ok(importResource.metadata.name.startsWith('memimport-'));
|
|
268
|
+
assert.equal(importResource.status.phase, 'Pending');
|
|
269
|
+
assert.equal(importResource.spec.memoryRepository, 'company-brain');
|
|
270
|
+
assert.equal(importResource.spec.source, 'babysitter-run/run-123');
|
|
271
|
+
assert.equal(importResource.spec.validationPolicy, 'strict');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('processImport advances phases', () => {
|
|
275
|
+
const controller = createAgentMemoryController();
|
|
276
|
+
|
|
277
|
+
let importResource = controller.createImport({
|
|
278
|
+
organizationRef: 'acme',
|
|
279
|
+
memoryRepository: 'company-brain',
|
|
280
|
+
source: 'run-1',
|
|
281
|
+
include: { kinds: ['all'] },
|
|
282
|
+
namespace: 'krate-org-default',
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
assert.equal(importResource.status.phase, 'Pending');
|
|
286
|
+
|
|
287
|
+
// Pending -> Collecting
|
|
288
|
+
importResource = controller.processImport({ importResource, content: 'some content' });
|
|
289
|
+
assert.equal(importResource.status.phase, 'Collecting');
|
|
290
|
+
|
|
291
|
+
// Collecting -> Redacting (triggers scanForRedaction)
|
|
292
|
+
importResource = controller.processImport({ importResource, content: 'API_KEY=secret123' });
|
|
293
|
+
assert.equal(importResource.status.phase, 'Redacting');
|
|
294
|
+
assert.ok(importResource.status.redactionScan, 'Should have redaction scan result');
|
|
295
|
+
assert.equal(importResource.status.redactionScan.clean, false, 'Should detect secret');
|
|
296
|
+
|
|
297
|
+
// Redacting -> Normalizing
|
|
298
|
+
importResource = controller.processImport({ importResource, content: 'normalized' });
|
|
299
|
+
assert.equal(importResource.status.phase, 'Normalizing');
|
|
300
|
+
|
|
301
|
+
// Normalizing -> Validating
|
|
302
|
+
importResource = controller.processImport({ importResource, content: 'validated' });
|
|
303
|
+
assert.equal(importResource.status.phase, 'Validating');
|
|
304
|
+
|
|
305
|
+
// Validating -> AwaitingReview
|
|
306
|
+
importResource = controller.processImport({ importResource, content: '' });
|
|
307
|
+
assert.equal(importResource.status.phase, 'AwaitingReview');
|
|
308
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import { createAgentMuxClient, parseSseLines, AGENT_MUX_CLIENT_BOUNDARY } from '../src/agent-mux-client.js';
|
|
4
|
+
|
|
5
|
+
describe('AGENT_MUX_CLIENT_BOUNDARY', () => {
|
|
6
|
+
it('declares the expected role and scope', () => {
|
|
7
|
+
assert.equal(AGENT_MUX_CLIENT_BOUNDARY.role, 'agent-mux-client');
|
|
8
|
+
assert.ok(AGENT_MUX_CLIENT_BOUNDARY.scope.includes('HTTP/SSE'));
|
|
9
|
+
assert.ok(AGENT_MUX_CLIENT_BOUNDARY.owns.includes('gateway HTTP calls'));
|
|
10
|
+
assert.ok(AGENT_MUX_CLIENT_BOUNDARY.delegatesTo.includes('resource-model'));
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('createAgentMuxClient — isAvailable', () => {
|
|
15
|
+
it('returns false when enabled is false', () => {
|
|
16
|
+
const client = createAgentMuxClient({ gateway: 'http://localhost:8080', enabled: false });
|
|
17
|
+
assert.equal(client.isAvailable(), false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('returns false when gateway is empty', () => {
|
|
21
|
+
const client = createAgentMuxClient({ gateway: '', enabled: true });
|
|
22
|
+
assert.equal(client.isAvailable(), false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('returns false when no options are provided', () => {
|
|
26
|
+
const client = createAgentMuxClient();
|
|
27
|
+
assert.equal(client.isAvailable(), false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('returns true when enabled is true and gateway is set', () => {
|
|
31
|
+
const client = createAgentMuxClient({ gateway: 'http://localhost:8080', enabled: true });
|
|
32
|
+
assert.equal(client.isAvailable(), true);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('createAgentMuxClient — queryCapabilities', () => {
|
|
37
|
+
it('returns null when client is unavailable', async () => {
|
|
38
|
+
const client = createAgentMuxClient({ enabled: false });
|
|
39
|
+
const result = await client.queryCapabilities('claude-code');
|
|
40
|
+
assert.equal(result, null);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('createAgentMuxClient — launchSession', () => {
|
|
45
|
+
it('returns null when client is unavailable', async () => {
|
|
46
|
+
const client = createAgentMuxClient({ enabled: false });
|
|
47
|
+
const result = await client.launchSession({
|
|
48
|
+
stack: { baseAgent: 'claude-code' },
|
|
49
|
+
contextBundle: {},
|
|
50
|
+
permissionSnapshot: {},
|
|
51
|
+
workspace: {}
|
|
52
|
+
});
|
|
53
|
+
assert.equal(result, null);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('parseSseLines', () => {
|
|
58
|
+
it('parses valid SSE data lines', () => {
|
|
59
|
+
const text = [
|
|
60
|
+
'data: {"role":"assistant","content":"hello"}',
|
|
61
|
+
'',
|
|
62
|
+
'data: {"role":"user","content":"world"}',
|
|
63
|
+
'',
|
|
64
|
+
].join('\n');
|
|
65
|
+
const events = parseSseLines(text);
|
|
66
|
+
assert.equal(events.length, 2);
|
|
67
|
+
assert.equal(events[0].role, 'assistant');
|
|
68
|
+
assert.equal(events[0].content, 'hello');
|
|
69
|
+
assert.equal(events[1].role, 'user');
|
|
70
|
+
assert.equal(events[1].content, 'world');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('handles multiple data lines in a single block', () => {
|
|
74
|
+
const text = [
|
|
75
|
+
'data: {"seq":1}',
|
|
76
|
+
'data: {"seq":2}',
|
|
77
|
+
'',
|
|
78
|
+
].join('\n');
|
|
79
|
+
const events = parseSseLines(text);
|
|
80
|
+
assert.equal(events.length, 2);
|
|
81
|
+
assert.equal(events[0].seq, 1);
|
|
82
|
+
assert.equal(events[1].seq, 2);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('skips malformed JSON gracefully', () => {
|
|
86
|
+
const text = [
|
|
87
|
+
'data: {"valid":true}',
|
|
88
|
+
'data: not-json',
|
|
89
|
+
'data: {"also":"valid"}',
|
|
90
|
+
'',
|
|
91
|
+
].join('\n');
|
|
92
|
+
const events = parseSseLines(text);
|
|
93
|
+
assert.equal(events.length, 2);
|
|
94
|
+
assert.equal(events[0].valid, true);
|
|
95
|
+
assert.equal(events[1].also, 'valid');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('ignores non-data SSE lines', () => {
|
|
99
|
+
const text = [
|
|
100
|
+
'event: message',
|
|
101
|
+
'id: 42',
|
|
102
|
+
'data: {"role":"assistant","content":"test"}',
|
|
103
|
+
'retry: 3000',
|
|
104
|
+
'',
|
|
105
|
+
].join('\n');
|
|
106
|
+
const events = parseSseLines(text);
|
|
107
|
+
assert.equal(events.length, 1);
|
|
108
|
+
assert.equal(events[0].role, 'assistant');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('returns empty array for empty text', () => {
|
|
112
|
+
assert.deepEqual(parseSseLines(''), []);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('returns empty array for text with no data lines', () => {
|
|
116
|
+
assert.deepEqual(parseSseLines('event: ping\nid: 1\n\n'), []);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('createAgentMuxClient — reconcileTranscript', () => {
|
|
121
|
+
it('creates a valid AgentSessionTranscript resource from events', () => {
|
|
122
|
+
const client = createAgentMuxClient({ gateway: 'http://localhost:8080', enabled: true });
|
|
123
|
+
const events = [
|
|
124
|
+
{ role: 'user', content: 'Fix the bug', timestamp: '2026-01-01T00:00:00Z' },
|
|
125
|
+
{ role: 'assistant', content: 'Looking at the code...', timestamp: '2026-01-01T00:00:01Z', usage: { inputTokens: 100, outputTokens: 50 } },
|
|
126
|
+
{ role: 'assistant', content: 'Fixed it.', timestamp: '2026-01-01T00:00:02Z', usage: { inputTokens: 200, outputTokens: 80 } },
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
const transcript = client.reconcileTranscript('sess-123', events, { namespace: 'krate-org-acme', organizationRef: 'acme' });
|
|
130
|
+
|
|
131
|
+
assert.equal(transcript.kind, 'AgentSessionTranscript');
|
|
132
|
+
assert.equal(transcript.apiVersion, 'krate.a5c.ai/v1alpha1');
|
|
133
|
+
assert.equal(transcript.metadata.name, 'transcript-sess-123');
|
|
134
|
+
assert.equal(transcript.metadata.namespace, 'krate-org-acme');
|
|
135
|
+
assert.equal(transcript.spec.organizationRef, 'acme');
|
|
136
|
+
assert.equal(transcript.spec.sessionRef, 'sess-123');
|
|
137
|
+
assert.equal(transcript.spec.messages.length, 3);
|
|
138
|
+
assert.equal(transcript.spec.messages[0].role, 'user');
|
|
139
|
+
assert.equal(transcript.spec.messages[0].content, 'Fix the bug');
|
|
140
|
+
assert.equal(transcript.spec.messages[1].role, 'assistant');
|
|
141
|
+
assert.equal(transcript.spec.cost.inputTokens, 300);
|
|
142
|
+
assert.equal(transcript.spec.cost.outputTokens, 130);
|
|
143
|
+
assert.equal(transcript.spec.cost.totalTokens, 430);
|
|
144
|
+
assert.equal(transcript.status.phase, 'Reconciled');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('handles empty events array', () => {
|
|
148
|
+
const client = createAgentMuxClient({ gateway: 'http://localhost:8080', enabled: true });
|
|
149
|
+
const transcript = client.reconcileTranscript('sess-empty', [], { namespace: 'default', organizationRef: 'default' });
|
|
150
|
+
|
|
151
|
+
assert.equal(transcript.kind, 'AgentSessionTranscript');
|
|
152
|
+
assert.equal(transcript.spec.sessionRef, 'sess-empty');
|
|
153
|
+
assert.equal(transcript.spec.messages.length, 0);
|
|
154
|
+
assert.equal(transcript.spec.cost.inputTokens, 0);
|
|
155
|
+
assert.equal(transcript.spec.cost.outputTokens, 0);
|
|
156
|
+
assert.equal(transcript.spec.cost.totalTokens, 0);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('handles events with toolUse and toolResult', () => {
|
|
160
|
+
const client = createAgentMuxClient({ gateway: 'http://localhost:8080', enabled: true });
|
|
161
|
+
const events = [
|
|
162
|
+
{ role: 'assistant', content: 'Using tool...', toolUse: { name: 'read_file', input: { path: '/tmp/f' } }, timestamp: '2026-01-01T00:00:00Z' },
|
|
163
|
+
{ role: 'tool', content: 'file contents', toolResult: { output: 'ok' }, timestamp: '2026-01-01T00:00:01Z' },
|
|
164
|
+
];
|
|
165
|
+
const transcript = client.reconcileTranscript('sess-tools', events, { namespace: 'default', organizationRef: 'default' });
|
|
166
|
+
|
|
167
|
+
assert.equal(transcript.spec.messages.length, 2);
|
|
168
|
+
assert.deepEqual(transcript.spec.messages[0].toolUse, { name: 'read_file', input: { path: '/tmp/f' } });
|
|
169
|
+
assert.deepEqual(transcript.spec.messages[1].toolResult, { output: 'ok' });
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('handles events with non-string content', () => {
|
|
173
|
+
const client = createAgentMuxClient({ gateway: 'http://localhost:8080', enabled: true });
|
|
174
|
+
const events = [
|
|
175
|
+
{ role: 'assistant', content: { type: 'structured', data: [1, 2, 3] }, timestamp: '2026-01-01T00:00:00Z' },
|
|
176
|
+
];
|
|
177
|
+
const transcript = client.reconcileTranscript('sess-obj', events, { namespace: 'default', organizationRef: 'default' });
|
|
178
|
+
|
|
179
|
+
assert.equal(typeof transcript.spec.messages[0].content, 'string');
|
|
180
|
+
assert.ok(transcript.spec.messages[0].content.includes('"type":"structured"'));
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('skips null/non-object events gracefully', () => {
|
|
184
|
+
const client = createAgentMuxClient({ gateway: 'http://localhost:8080', enabled: true });
|
|
185
|
+
const events = [
|
|
186
|
+
null,
|
|
187
|
+
'not-an-object',
|
|
188
|
+
42,
|
|
189
|
+
{ role: 'user', content: 'valid', timestamp: '2026-01-01T00:00:00Z' },
|
|
190
|
+
];
|
|
191
|
+
const transcript = client.reconcileTranscript('sess-mixed', events, { namespace: 'default', organizationRef: 'default' });
|
|
192
|
+
|
|
193
|
+
assert.equal(transcript.spec.messages.length, 1);
|
|
194
|
+
assert.equal(transcript.spec.messages[0].role, 'user');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('uses default namespace and organizationRef when not specified', () => {
|
|
198
|
+
const client = createAgentMuxClient({ gateway: 'http://localhost:8080', enabled: true });
|
|
199
|
+
const transcript = client.reconcileTranscript('sess-defaults', []);
|
|
200
|
+
|
|
201
|
+
assert.equal(transcript.metadata.namespace, 'default');
|
|
202
|
+
assert.equal(transcript.spec.organizationRef, 'default');
|
|
203
|
+
});
|
|
204
|
+
});
|