@a5c-ai/krate 5.0.1-staging.00fa5317c
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 +3205 -0
- package/dist/krate-lifecycle.json +201 -0
- package/dist/krate-runtime-snapshot.json +3125 -0
- package/dist/krate-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-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/openapi.yaml +1275 -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 +278 -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 +381 -0
- package/src/agent-workspace-controller.js +702 -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 +72 -0
- package/src/controller-ui.js +617 -0
- package/src/data-plane.js +179 -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 +131 -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 +57 -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/notification-controller.js +178 -0
- package/src/operations.js +112 -0
- package/src/org-scoping.js +5 -0
- package/src/resource-model.js +221 -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/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/codespace-controller.test.js +318 -0
- package/tests/deployment.test.js +407 -0
- package/tests/e2e/lifecycle.test.js +117 -0
- package/tests/event-bus-integration.test.js +190 -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 +756 -0
- package/tests/memory-search-wiring.test.js +270 -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 +247 -0
- package/tests/sse-events.test.js +107 -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,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C5: Session cookie HMAC signing tests
|
|
3
|
+
* Tests for HMAC-SHA256 signing of session cookies when KRATE_SESSION_SECRET is set.
|
|
4
|
+
*/
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import test from 'node:test';
|
|
7
|
+
import { createSessionCookie, parseSessionCookie } from '../src/auth.js';
|
|
8
|
+
|
|
9
|
+
function makeConfig(cookieName = 'krate_session') {
|
|
10
|
+
return { session: { cookieName } };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const TEST_SECRET = 'super-secret-hmac-key-for-testing-1234567890';
|
|
14
|
+
|
|
15
|
+
const testProfile = {
|
|
16
|
+
provider: 'github',
|
|
17
|
+
subject: 'user-42',
|
|
18
|
+
username: 'alice',
|
|
19
|
+
email: 'alice@example.com'
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// --- createSessionCookie with HMAC signing ---
|
|
23
|
+
|
|
24
|
+
test('createSessionCookie includes HMAC signature in cookie value when secret is set', () => {
|
|
25
|
+
const config = makeConfig();
|
|
26
|
+
const cookie = createSessionCookie(config, testProfile, { secret: TEST_SECRET });
|
|
27
|
+
|
|
28
|
+
// Cookie format: name=payload.signature; Path=/; ...
|
|
29
|
+
const cookieValue = cookie.split(';')[0].split('=').slice(1).join('=');
|
|
30
|
+
assert.ok(cookieValue.includes('.'), 'Signed cookie value should contain a period separating payload from signature');
|
|
31
|
+
|
|
32
|
+
const parts = cookieValue.split('.');
|
|
33
|
+
assert.equal(parts.length, 2, 'Should have exactly payload and signature parts');
|
|
34
|
+
assert.ok(parts[0].length > 0, 'Payload should be non-empty');
|
|
35
|
+
assert.ok(parts[1].length > 0, 'Signature should be non-empty');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('createSessionCookie without secret produces unsigned cookie (no period separator)', () => {
|
|
39
|
+
const config = makeConfig();
|
|
40
|
+
const cookie = createSessionCookie(config, testProfile);
|
|
41
|
+
|
|
42
|
+
const cookieValue = cookie.split(';')[0].split('=').slice(1).join('=');
|
|
43
|
+
// Unsigned: base64url encoded JSON, no dot separator
|
|
44
|
+
assert.ok(!cookieValue.includes('.'), 'Unsigned cookie should not have period separator');
|
|
45
|
+
|
|
46
|
+
// Payload should be valid base64url JSON
|
|
47
|
+
const parsed = JSON.parse(Buffer.from(cookieValue, 'base64url').toString('utf8'));
|
|
48
|
+
assert.equal(parsed.provider, testProfile.provider);
|
|
49
|
+
assert.equal(parsed.subject, testProfile.subject);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('createSessionCookie without secret but with empty string secret is unsigned', () => {
|
|
53
|
+
const config = makeConfig();
|
|
54
|
+
const cookie = createSessionCookie(config, testProfile, { secret: '' });
|
|
55
|
+
|
|
56
|
+
const cookieValue = cookie.split(';')[0].split('=').slice(1).join('=');
|
|
57
|
+
assert.ok(!cookieValue.includes('.'), 'Empty secret should produce unsigned cookie');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// --- parseSessionCookie with HMAC verification ---
|
|
61
|
+
|
|
62
|
+
test('parseSessionCookie verifies HMAC signature and returns session when valid', () => {
|
|
63
|
+
const config = makeConfig();
|
|
64
|
+
const cookie = createSessionCookie(config, testProfile, { secret: TEST_SECRET });
|
|
65
|
+
const cookieValue = cookie.split(';')[0].split('=').slice(1).join('=');
|
|
66
|
+
|
|
67
|
+
const session = parseSessionCookie(config, cookieValue, { secret: TEST_SECRET });
|
|
68
|
+
|
|
69
|
+
assert.ok(session, 'Should parse and verify correctly');
|
|
70
|
+
assert.equal(session.provider, testProfile.provider);
|
|
71
|
+
assert.equal(session.subject, testProfile.subject);
|
|
72
|
+
assert.equal(session.user, testProfile.username);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('parseSessionCookie rejects tampered cookie payload', () => {
|
|
76
|
+
const config = makeConfig();
|
|
77
|
+
const cookie = createSessionCookie(config, testProfile, { secret: TEST_SECRET });
|
|
78
|
+
const cookieValue = cookie.split(';')[0].split('=').slice(1).join('=');
|
|
79
|
+
|
|
80
|
+
// Tamper with the payload by replacing it with different data
|
|
81
|
+
const [, signature] = cookieValue.split('.');
|
|
82
|
+
const tamperedPayload = Buffer.from(JSON.stringify({ provider: 'github', subject: 'hacker', user: 'hacker' })).toString('base64url');
|
|
83
|
+
const tamperedCookieValue = `${tamperedPayload}.${signature}`;
|
|
84
|
+
|
|
85
|
+
const session = parseSessionCookie(config, tamperedCookieValue, { secret: TEST_SECRET });
|
|
86
|
+
|
|
87
|
+
assert.equal(session, null, 'Should reject tampered cookie');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('parseSessionCookie rejects cookie with wrong signature', () => {
|
|
91
|
+
const config = makeConfig();
|
|
92
|
+
const cookie = createSessionCookie(config, testProfile, { secret: TEST_SECRET });
|
|
93
|
+
const cookieValue = cookie.split(';')[0].split('=').slice(1).join('=');
|
|
94
|
+
|
|
95
|
+
// Replace signature with garbage
|
|
96
|
+
const [payload] = cookieValue.split('.');
|
|
97
|
+
const tamperedCookieValue = `${payload}.invalidsignatureXXXXXX`;
|
|
98
|
+
|
|
99
|
+
const session = parseSessionCookie(config, tamperedCookieValue, { secret: TEST_SECRET });
|
|
100
|
+
|
|
101
|
+
assert.equal(session, null, 'Should reject cookie with wrong signature');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('parseSessionCookie accepts unsigned cookie when no secret provided (backward compat)', () => {
|
|
105
|
+
const config = makeConfig();
|
|
106
|
+
// Create unsigned cookie (no secret)
|
|
107
|
+
const cookie = createSessionCookie(config, testProfile);
|
|
108
|
+
const cookieValue = cookie.split(';')[0].split('=').slice(1).join('=');
|
|
109
|
+
|
|
110
|
+
const session = parseSessionCookie(config, cookieValue);
|
|
111
|
+
|
|
112
|
+
assert.ok(session, 'Should parse unsigned cookie when no secret provided');
|
|
113
|
+
assert.equal(session.provider, testProfile.provider);
|
|
114
|
+
assert.equal(session.subject, testProfile.subject);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('parseSessionCookie rejects signed cookie when no verification secret is given', () => {
|
|
118
|
+
const config = makeConfig();
|
|
119
|
+
// Signed cookie but parsed without secret
|
|
120
|
+
const cookie = createSessionCookie(config, testProfile, { secret: TEST_SECRET });
|
|
121
|
+
const cookieValue = cookie.split(';')[0].split('=').slice(1).join('=');
|
|
122
|
+
|
|
123
|
+
// Parse without secret — signed payload contains a dot, so base64url decode will fail or return null
|
|
124
|
+
const session = parseSessionCookie(config, cookieValue);
|
|
125
|
+
|
|
126
|
+
assert.equal(session, null, 'Should reject signed cookie when no secret is provided for verification');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('parseSessionCookie with different secret rejects the cookie', () => {
|
|
130
|
+
const config = makeConfig();
|
|
131
|
+
const cookie = createSessionCookie(config, testProfile, { secret: TEST_SECRET });
|
|
132
|
+
const cookieValue = cookie.split(';')[0].split('=').slice(1).join('=');
|
|
133
|
+
|
|
134
|
+
const session = parseSessionCookie(config, cookieValue, { secret: 'wrong-secret-completely-different' });
|
|
135
|
+
|
|
136
|
+
assert.equal(session, null, 'Should reject cookie signed with a different secret');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('createSessionCookie with same input and same secret produces same signature (deterministic)', () => {
|
|
140
|
+
const config = makeConfig();
|
|
141
|
+
const cookie1 = createSessionCookie(config, testProfile, { secret: TEST_SECRET });
|
|
142
|
+
const cookie2 = createSessionCookie(config, testProfile, { secret: TEST_SECRET });
|
|
143
|
+
|
|
144
|
+
const value1 = cookie1.split(';')[0].split('=').slice(1).join('=');
|
|
145
|
+
const value2 = cookie2.split(';')[0].split('=').slice(1).join('=');
|
|
146
|
+
|
|
147
|
+
const sig1 = value1.split('.')[1];
|
|
148
|
+
const sig2 = value2.split('.')[1];
|
|
149
|
+
|
|
150
|
+
assert.equal(sig1, sig2, 'Same input+secret should produce same HMAC signature');
|
|
151
|
+
});
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for async kubectl helpers, partial snapshot, and stale-while-revalidate cache.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import test from 'node:test';
|
|
7
|
+
import { runKubectlAsync, getPartialSnapshot, getControllerSnapshotAsync } from '../src/kubernetes-controller-async.js';
|
|
8
|
+
import {
|
|
9
|
+
getSnapshotCache,
|
|
10
|
+
setSnapshotCache,
|
|
11
|
+
clearSnapshotCache,
|
|
12
|
+
getOrgCache,
|
|
13
|
+
setOrgCache,
|
|
14
|
+
clearOrgCache,
|
|
15
|
+
cachedOrgs,
|
|
16
|
+
isCacheFresh,
|
|
17
|
+
staleWhileRevalidate,
|
|
18
|
+
CACHE_TTL_MS
|
|
19
|
+
} from '../src/snapshot-cache.js';
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// runKubectlAsync
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
test('runKubectlAsync resolves with ok=true for a succeeding command', async () => {
|
|
26
|
+
const result = await runKubectlAsync(['version', '--client=true', '-o', 'json'], {
|
|
27
|
+
kubectl: process.env.KRATE_KUBECTL || 'kubectl',
|
|
28
|
+
timeoutMs: 5000,
|
|
29
|
+
allowFailure: true
|
|
30
|
+
});
|
|
31
|
+
// We can't guarantee kubectl is installed in CI, so we just check the shape.
|
|
32
|
+
assert.ok(typeof result.ok === 'boolean', 'result.ok is boolean');
|
|
33
|
+
assert.ok(typeof result.stdout === 'string', 'result.stdout is string');
|
|
34
|
+
assert.ok(typeof result.stderr === 'string', 'result.stderr is string');
|
|
35
|
+
assert.ok(typeof result.command === 'string', 'result.command is string');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('runKubectlAsync with allowFailure=true resolves even on non-zero exit', async () => {
|
|
39
|
+
// Use a bogus sub-command that kubectl will reject
|
|
40
|
+
const result = await runKubectlAsync(['get', 'nonexistent-resource-kind-xyz-abc', '--request-timeout=1s'], {
|
|
41
|
+
kubectl: process.env.KRATE_KUBECTL || 'kubectl',
|
|
42
|
+
timeoutMs: 5000,
|
|
43
|
+
allowFailure: true
|
|
44
|
+
});
|
|
45
|
+
assert.ok(typeof result.ok === 'boolean', 'shape preserved on failure');
|
|
46
|
+
assert.ok(typeof result.stderr === 'string', 'stderr present');
|
|
47
|
+
assert.ok(typeof result.command === 'string', 'command present');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('runKubectlAsync result has same shape keys as runKubectl sync version', async () => {
|
|
51
|
+
const result = await runKubectlAsync(['version', '--client=true', '-o', 'json'], {
|
|
52
|
+
kubectl: process.env.KRATE_KUBECTL || 'kubectl',
|
|
53
|
+
timeoutMs: 5000,
|
|
54
|
+
allowFailure: true
|
|
55
|
+
});
|
|
56
|
+
const expectedKeys = ['ok', 'status', 'signal', 'stdout', 'stderr', 'error', 'command'];
|
|
57
|
+
for (const key of expectedKeys) {
|
|
58
|
+
assert.ok(key in result, `result has key "${key}"`);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('runKubectlAsync times out and resolves with ok=false when allowFailure=true', async () => {
|
|
63
|
+
// Use an extremely short timeout to force a timeout path
|
|
64
|
+
const result = await runKubectlAsync(['version', '--client=true', '-o', 'json'], {
|
|
65
|
+
kubectl: process.env.KRATE_KUBECTL || 'kubectl',
|
|
66
|
+
timeoutMs: 1, // 1 ms — will always time out
|
|
67
|
+
allowFailure: true
|
|
68
|
+
});
|
|
69
|
+
assert.equal(result.ok, false, 'timed out result has ok=false');
|
|
70
|
+
assert.ok(result.error, 'timed out result has error message');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('runKubectlAsync rejects on timeout when allowFailure is not set', async () => {
|
|
74
|
+
await assert.rejects(
|
|
75
|
+
() => runKubectlAsync(['version', '--client=true', '-o', 'json'], {
|
|
76
|
+
kubectl: process.env.KRATE_KUBECTL || 'kubectl',
|
|
77
|
+
timeoutMs: 1
|
|
78
|
+
}),
|
|
79
|
+
(err) => {
|
|
80
|
+
assert.ok(err instanceof Error, 'throws Error');
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// getPartialSnapshot
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
test('getPartialSnapshot returns empty resources object for empty kinds array', async () => {
|
|
91
|
+
const result = await getPartialSnapshot([], {
|
|
92
|
+
kubectl: process.env.KRATE_KUBECTL || 'kubectl',
|
|
93
|
+
timeoutMs: 100,
|
|
94
|
+
allowFailure: true
|
|
95
|
+
});
|
|
96
|
+
assert.deepEqual(result.resources, {}, 'no resources when kinds is empty');
|
|
97
|
+
assert.equal(result.mode, 'partial', 'mode is partial');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('getPartialSnapshot only queries the requested resource kinds', async () => {
|
|
101
|
+
// We stub out the kubectl to track which resource kinds are queried.
|
|
102
|
+
// Since we can't easily hook into the internal calls, we verify the returned
|
|
103
|
+
// resources object only has keys for the requested kinds.
|
|
104
|
+
const kinds = ['AgentStack', 'AgentSession'];
|
|
105
|
+
const result = await getPartialSnapshot(kinds, {
|
|
106
|
+
kubectl: process.env.KRATE_KUBECTL || 'kubectl',
|
|
107
|
+
timeoutMs: 500,
|
|
108
|
+
allowFailure: true
|
|
109
|
+
});
|
|
110
|
+
const keys = Object.keys(result.resources);
|
|
111
|
+
// All returned keys must be among the requested kinds
|
|
112
|
+
for (const key of keys) {
|
|
113
|
+
assert.ok(kinds.includes(key), `unexpected key in partial snapshot: ${key}`);
|
|
114
|
+
}
|
|
115
|
+
// All requested kinds must be present as keys
|
|
116
|
+
for (const kind of kinds) {
|
|
117
|
+
assert.ok(kind in result.resources, `missing kind in partial snapshot: ${kind}`);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('getPartialSnapshot resources values are arrays', async () => {
|
|
122
|
+
const result = await getPartialSnapshot(['Organization'], {
|
|
123
|
+
kubectl: process.env.KRATE_KUBECTL || 'kubectl',
|
|
124
|
+
timeoutMs: 500,
|
|
125
|
+
allowFailure: true
|
|
126
|
+
});
|
|
127
|
+
for (const value of Object.values(result.resources)) {
|
|
128
|
+
assert.ok(Array.isArray(value), 'each resource value is an array');
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('getPartialSnapshot ignores unknown kinds gracefully', async () => {
|
|
133
|
+
const result = await getPartialSnapshot(['AgentStack', 'NotARealKind'], {
|
|
134
|
+
kubectl: process.env.KRATE_KUBECTL || 'kubectl',
|
|
135
|
+
timeoutMs: 500,
|
|
136
|
+
allowFailure: true
|
|
137
|
+
});
|
|
138
|
+
// NotARealKind should be absent; AgentStack should be present
|
|
139
|
+
assert.ok('AgentStack' in result.resources, 'valid kind present');
|
|
140
|
+
assert.ok(!('NotARealKind' in result.resources), 'unknown kind absent');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Per-org cache
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
test('setOrgCache and getOrgCache store and retrieve data per org', () => {
|
|
148
|
+
clearSnapshotCache();
|
|
149
|
+
const data1 = { resources: { Organization: [{ metadata: { name: 'org-a' } }] } };
|
|
150
|
+
const data2 = { resources: { Organization: [{ metadata: { name: 'org-b' } }] } };
|
|
151
|
+
setOrgCache(data1, 'org-a');
|
|
152
|
+
setOrgCache(data2, 'org-b');
|
|
153
|
+
assert.deepEqual(getOrgCache('org-a').data, data1, 'org-a cached independently');
|
|
154
|
+
assert.deepEqual(getOrgCache('org-b').data, data2, 'org-b cached independently');
|
|
155
|
+
clearSnapshotCache();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('cachedOrgs returns all orgs with active cache entries', () => {
|
|
159
|
+
clearSnapshotCache();
|
|
160
|
+
setOrgCache({ x: 1 }, 'org-x');
|
|
161
|
+
setOrgCache({ y: 2 }, 'org-y');
|
|
162
|
+
const orgs = cachedOrgs();
|
|
163
|
+
assert.ok(orgs.includes('org-x'), 'org-x in cachedOrgs');
|
|
164
|
+
assert.ok(orgs.includes('org-y'), 'org-y in cachedOrgs');
|
|
165
|
+
clearSnapshotCache();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('clearOrgCache removes only the target org', () => {
|
|
169
|
+
clearSnapshotCache();
|
|
170
|
+
setOrgCache({ a: 1 }, 'alpha');
|
|
171
|
+
setOrgCache({ b: 2 }, 'beta');
|
|
172
|
+
clearOrgCache('alpha');
|
|
173
|
+
assert.equal(getOrgCache('alpha'), null, 'alpha cleared');
|
|
174
|
+
assert.ok(getOrgCache('beta') !== null, 'beta still present');
|
|
175
|
+
clearSnapshotCache();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('isCacheFresh returns false when cache is empty', () => {
|
|
179
|
+
clearSnapshotCache();
|
|
180
|
+
assert.equal(isCacheFresh('org-empty'), false, 'empty cache is not fresh');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('isCacheFresh returns true immediately after setOrgCache', () => {
|
|
184
|
+
clearSnapshotCache();
|
|
185
|
+
setOrgCache({ z: 3 }, 'freshorg');
|
|
186
|
+
assert.equal(isCacheFresh('freshorg'), true, 'fresh entry is fresh');
|
|
187
|
+
clearSnapshotCache();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// stale-while-revalidate
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
test('staleWhileRevalidate returns fresh data when cache is empty', async () => {
|
|
195
|
+
clearSnapshotCache();
|
|
196
|
+
const fresh = { resources: { AgentStack: [] } };
|
|
197
|
+
const result = await staleWhileRevalidate('swr-empty', async () => fresh);
|
|
198
|
+
assert.deepEqual(result, fresh, 'returns result of revalidateFn');
|
|
199
|
+
clearSnapshotCache();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('staleWhileRevalidate returns cached data immediately when fresh', async () => {
|
|
203
|
+
clearSnapshotCache();
|
|
204
|
+
const cached = { resources: { AgentStack: [{ metadata: { name: 'existing' } }] } };
|
|
205
|
+
setOrgCache(cached, 'swr-fresh');
|
|
206
|
+
let revalidateCalled = false;
|
|
207
|
+
const result = await staleWhileRevalidate('swr-fresh', async () => {
|
|
208
|
+
revalidateCalled = true;
|
|
209
|
+
return { resources: { AgentStack: [] } };
|
|
210
|
+
}, { ttlMs: CACHE_TTL_MS });
|
|
211
|
+
assert.deepEqual(result, cached, 'returned cached data');
|
|
212
|
+
assert.equal(revalidateCalled, false, 'revalidate not called when cache is fresh');
|
|
213
|
+
clearSnapshotCache();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('staleWhileRevalidate triggers background refresh when entry is stale', async () => {
|
|
217
|
+
clearSnapshotCache();
|
|
218
|
+
const staleData = { resources: { AgentStack: [{ metadata: { name: 'stale' } }] } };
|
|
219
|
+
setOrgCache(staleData, 'swr-stale');
|
|
220
|
+
// Manually mark the entry as expired (set timestamp far in the past)
|
|
221
|
+
const key = 'swr-stale';
|
|
222
|
+
// Access orgCacheMap indirectly by using a very short ttlMs
|
|
223
|
+
let revalidateCalled = false;
|
|
224
|
+
let resolveRevalidate;
|
|
225
|
+
const revalidatePromise = new Promise((resolve) => { resolveRevalidate = resolve; });
|
|
226
|
+
const result = await staleWhileRevalidate('swr-stale', async () => {
|
|
227
|
+
revalidateCalled = true;
|
|
228
|
+
resolveRevalidate();
|
|
229
|
+
return { resources: { AgentStack: [] } };
|
|
230
|
+
}, { ttlMs: 0, staleMs: CACHE_TTL_MS * 10 });
|
|
231
|
+
|
|
232
|
+
// Should return stale data immediately
|
|
233
|
+
assert.deepEqual(result, staleData, 'returned stale data immediately');
|
|
234
|
+
// Wait a tick for background refresh to fire
|
|
235
|
+
await new Promise((r) => setImmediate(r));
|
|
236
|
+
clearSnapshotCache();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('staleWhileRevalidate caches the result of revalidateFn', async () => {
|
|
240
|
+
clearSnapshotCache();
|
|
241
|
+
const fresh = { resources: { User: [{ metadata: { name: 'u1' } }] } };
|
|
242
|
+
await staleWhileRevalidate('swr-cache-write', async () => fresh);
|
|
243
|
+
const entry = getOrgCache('swr-cache-write');
|
|
244
|
+
assert.ok(entry !== null, 'cache written after revalidate');
|
|
245
|
+
assert.deepEqual(entry.data, fresh, 'correct data written to cache');
|
|
246
|
+
clearSnapshotCache();
|
|
247
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { createEventBus } from '../src/event-bus.js';
|
|
4
|
+
|
|
5
|
+
// ─── createEventBus shape ─────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
test('createEventBus returns bus with subscribe, unsubscribe, emit', () => {
|
|
8
|
+
const bus = createEventBus();
|
|
9
|
+
assert.ok(typeof bus.subscribe === 'function', 'has subscribe method');
|
|
10
|
+
assert.ok(typeof bus.unsubscribe === 'function', 'has unsubscribe method');
|
|
11
|
+
assert.ok(typeof bus.emit === 'function', 'has emit method');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// ─── subscribe ────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
test('subscribe adds a listener that receives emitted events', () => {
|
|
17
|
+
const bus = createEventBus();
|
|
18
|
+
const received = [];
|
|
19
|
+
bus.subscribe((event) => received.push(event));
|
|
20
|
+
bus.emit({ type: 'test', value: 42 });
|
|
21
|
+
assert.equal(received.length, 1, 'listener received one event');
|
|
22
|
+
assert.deepEqual(received[0], { type: 'test', value: 42 });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// ─── unsubscribe ──────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
test('unsubscribe removes a listener', () => {
|
|
28
|
+
const bus = createEventBus();
|
|
29
|
+
const received = [];
|
|
30
|
+
const listener = (event) => received.push(event);
|
|
31
|
+
bus.subscribe(listener);
|
|
32
|
+
bus.emit({ type: 'first' });
|
|
33
|
+
bus.unsubscribe(listener);
|
|
34
|
+
bus.emit({ type: 'second' });
|
|
35
|
+
assert.equal(received.length, 1, 'listener only received event before unsubscribe');
|
|
36
|
+
assert.equal(received[0].type, 'first');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// ─── emit ─────────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
test('emit sends events to all subscribers', () => {
|
|
42
|
+
const bus = createEventBus();
|
|
43
|
+
const receivedA = [];
|
|
44
|
+
const receivedB = [];
|
|
45
|
+
bus.subscribe((e) => receivedA.push(e));
|
|
46
|
+
bus.subscribe((e) => receivedB.push(e));
|
|
47
|
+
bus.emit({ type: 'broadcast' });
|
|
48
|
+
assert.equal(receivedA.length, 1, 'subscriber A received the event');
|
|
49
|
+
assert.equal(receivedB.length, 1, 'subscriber B received the event');
|
|
50
|
+
assert.equal(receivedA[0].type, 'broadcast');
|
|
51
|
+
assert.equal(receivedB[0].type, 'broadcast');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('emit with no subscribers does not throw', () => {
|
|
55
|
+
const bus = createEventBus();
|
|
56
|
+
assert.doesNotThrow(() => bus.emit({ type: 'lonely' }), 'emit with no subscribers is safe');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ─── emitResourceChange ───────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
test('emitResourceChange sends event with kind, name, operation, timestamp', () => {
|
|
62
|
+
const bus = createEventBus();
|
|
63
|
+
const received = [];
|
|
64
|
+
bus.subscribe((event) => received.push(event));
|
|
65
|
+
bus.emitResourceChange('Repository', 'my-repo', 'apply');
|
|
66
|
+
assert.equal(received.length, 1, 'listener received one event');
|
|
67
|
+
const event = received[0];
|
|
68
|
+
assert.equal(event.type, 'resource-change', 'type is resource-change');
|
|
69
|
+
assert.equal(event.kind, 'Repository', 'kind is correct');
|
|
70
|
+
assert.equal(event.name, 'my-repo', 'name is correct');
|
|
71
|
+
assert.equal(event.operation, 'apply', 'operation is correct');
|
|
72
|
+
assert.ok(typeof event.timestamp === 'string', 'timestamp is a string');
|
|
73
|
+
assert.ok(new Date(event.timestamp).getTime() > 0, 'timestamp is a valid ISO date');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ─── multiple subscribers ─────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
test('multiple subscribers all receive the same event', () => {
|
|
79
|
+
const bus = createEventBus();
|
|
80
|
+
const listeners = Array.from({ length: 5 }, () => []);
|
|
81
|
+
listeners.forEach((arr) => bus.subscribe((e) => arr.push(e)));
|
|
82
|
+
bus.emit({ type: 'multi', id: 99 });
|
|
83
|
+
for (const arr of listeners) {
|
|
84
|
+
assert.equal(arr.length, 1, 'each listener received exactly one event');
|
|
85
|
+
assert.equal(arr[0].id, 99, 'each listener got the same event id');
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ─── unsubscribed listener stops receiving ────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
test('unsubscribed listener stops receiving events', () => {
|
|
92
|
+
const bus = createEventBus();
|
|
93
|
+
const eventsA = [];
|
|
94
|
+
const eventsB = [];
|
|
95
|
+
const listenerA = (e) => eventsA.push(e);
|
|
96
|
+
const listenerB = (e) => eventsB.push(e);
|
|
97
|
+
bus.subscribe(listenerA);
|
|
98
|
+
bus.subscribe(listenerB);
|
|
99
|
+
bus.emit({ type: 'first' });
|
|
100
|
+
bus.unsubscribe(listenerA);
|
|
101
|
+
bus.emit({ type: 'second' });
|
|
102
|
+
bus.emit({ type: 'third' });
|
|
103
|
+
assert.equal(eventsA.length, 1, 'unsubscribed listener only got the first event');
|
|
104
|
+
assert.equal(eventsB.length, 3, 'still-subscribed listener got all three events');
|
|
105
|
+
assert.equal(eventsA[0].type, 'first');
|
|
106
|
+
assert.equal(eventsB[2].type, 'third');
|
|
107
|
+
});
|