@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,252 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import {
|
|
4
|
+
createEventBatcher,
|
|
5
|
+
createRetryPolicy,
|
|
6
|
+
createDeliveryQueue,
|
|
7
|
+
createCheckpointer,
|
|
8
|
+
} from '../src/async-controller.js';
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// createEventBatcher
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
test('createEventBatcher returns push, flush, and stop methods', () => {
|
|
15
|
+
const batcher = createEventBatcher(() => {});
|
|
16
|
+
assert.ok(typeof batcher.push === 'function', 'has push');
|
|
17
|
+
assert.ok(typeof batcher.flush === 'function', 'has flush');
|
|
18
|
+
assert.ok(typeof batcher.stop === 'function', 'has stop');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('createEventBatcher: manual flush delivers accumulated events', async () => {
|
|
22
|
+
const flushed = [];
|
|
23
|
+
const batcher = createEventBatcher((events) => { flushed.push(...events); }, { flushIntervalMs: 60000 });
|
|
24
|
+
batcher.push('a');
|
|
25
|
+
batcher.push('b');
|
|
26
|
+
batcher.push('c');
|
|
27
|
+
assert.equal(flushed.length, 0, 'not flushed yet');
|
|
28
|
+
await batcher.flush();
|
|
29
|
+
assert.deepEqual(flushed, ['a', 'b', 'c']);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('createEventBatcher: auto-flush when maxBatchSize is reached', async () => {
|
|
33
|
+
const flushed = [];
|
|
34
|
+
const batcher = createEventBatcher((events) => { flushed.push(...events); }, { maxBatchSize: 3, flushIntervalMs: 60000 });
|
|
35
|
+
batcher.push(1);
|
|
36
|
+
batcher.push(2);
|
|
37
|
+
batcher.push(3); // triggers auto-flush synchronously
|
|
38
|
+
// Allow microtasks to settle
|
|
39
|
+
await new Promise((r) => setImmediate(r));
|
|
40
|
+
assert.equal(flushed.length, 3, 'batch of 3 was flushed');
|
|
41
|
+
assert.deepEqual(flushed, [1, 2, 3]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('createEventBatcher: flush clears the batch (subsequent flush sends nothing)', async () => {
|
|
45
|
+
const calls = [];
|
|
46
|
+
const batcher = createEventBatcher((events) => calls.push(events), { flushIntervalMs: 60000 });
|
|
47
|
+
batcher.push('x');
|
|
48
|
+
await batcher.flush();
|
|
49
|
+
await batcher.flush();
|
|
50
|
+
assert.equal(calls.length, 1, 'only one flush call had data');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('createEventBatcher: stop discards pending events', async () => {
|
|
54
|
+
const flushed = [];
|
|
55
|
+
const batcher = createEventBatcher((events) => flushed.push(...events), { flushIntervalMs: 60000 });
|
|
56
|
+
batcher.push('pending');
|
|
57
|
+
batcher.stop();
|
|
58
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
59
|
+
assert.equal(flushed.length, 0, 'stopped batcher should not flush');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('createEventBatcher: handler is called with array of events', async () => {
|
|
63
|
+
let received = null;
|
|
64
|
+
const batcher = createEventBatcher((events) => { received = events; }, { flushIntervalMs: 60000 });
|
|
65
|
+
batcher.push({ id: 1 });
|
|
66
|
+
await batcher.flush();
|
|
67
|
+
assert.ok(Array.isArray(received), 'handler receives an array');
|
|
68
|
+
assert.equal(received.length, 1);
|
|
69
|
+
assert.deepEqual(received[0], { id: 1 });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// createRetryPolicy
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
test('createRetryPolicy returns shouldRetry and getDelay methods', () => {
|
|
77
|
+
const policy = createRetryPolicy();
|
|
78
|
+
assert.ok(typeof policy.shouldRetry === 'function', 'has shouldRetry');
|
|
79
|
+
assert.ok(typeof policy.getDelay === 'function', 'has getDelay');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('createRetryPolicy: shouldRetry returns true while under maxRetries', () => {
|
|
83
|
+
const policy = createRetryPolicy({ maxRetries: 3 });
|
|
84
|
+
assert.equal(policy.shouldRetry(0, new Error()), true);
|
|
85
|
+
assert.equal(policy.shouldRetry(1, new Error()), true);
|
|
86
|
+
assert.equal(policy.shouldRetry(2, new Error()), true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('createRetryPolicy: shouldRetry returns false when maxRetries is reached', () => {
|
|
90
|
+
const policy = createRetryPolicy({ maxRetries: 3 });
|
|
91
|
+
assert.equal(policy.shouldRetry(3, new Error()), false);
|
|
92
|
+
assert.equal(policy.shouldRetry(4, new Error()), false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('createRetryPolicy: getDelay implements exponential backoff', () => {
|
|
96
|
+
const policy = createRetryPolicy({ baseDelayMs: 100, maxDelayMs: 10000, jitter: false });
|
|
97
|
+
assert.equal(policy.getDelay(0), 100);
|
|
98
|
+
assert.equal(policy.getDelay(1), 200);
|
|
99
|
+
assert.equal(policy.getDelay(2), 400);
|
|
100
|
+
assert.equal(policy.getDelay(3), 800);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('createRetryPolicy: getDelay respects maxDelayMs cap', () => {
|
|
104
|
+
const policy = createRetryPolicy({ baseDelayMs: 1000, maxDelayMs: 3000, jitter: false });
|
|
105
|
+
assert.ok(policy.getDelay(5) <= 3000, 'delay should be capped at maxDelayMs');
|
|
106
|
+
assert.equal(policy.getDelay(10), 3000);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('createRetryPolicy: getDelay with jitter returns non-negative value within cap', () => {
|
|
110
|
+
const policy = createRetryPolicy({ baseDelayMs: 100, maxDelayMs: 1000, jitter: true });
|
|
111
|
+
for (let i = 0; i < 20; i++) {
|
|
112
|
+
const delay = policy.getDelay(2);
|
|
113
|
+
assert.ok(delay >= 0, 'jitter delay must be >= 0');
|
|
114
|
+
assert.ok(delay <= 1000, 'jitter delay must be <= maxDelayMs');
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// createDeliveryQueue
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
test('createDeliveryQueue returns enqueue, drain, size, and stop methods', () => {
|
|
123
|
+
const q = createDeliveryQueue(async () => {});
|
|
124
|
+
assert.ok(typeof q.enqueue === 'function', 'has enqueue');
|
|
125
|
+
assert.ok(typeof q.drain === 'function', 'has drain');
|
|
126
|
+
assert.ok(typeof q.size === 'function', 'has size');
|
|
127
|
+
assert.ok(typeof q.stop === 'function', 'has stop');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('createDeliveryQueue: processes enqueued items', async () => {
|
|
131
|
+
const processed = [];
|
|
132
|
+
const q = createDeliveryQueue(async (item) => processed.push(item));
|
|
133
|
+
q.enqueue('alpha');
|
|
134
|
+
q.enqueue('beta');
|
|
135
|
+
await q.drain();
|
|
136
|
+
assert.deepEqual(processed, ['alpha', 'beta']);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('createDeliveryQueue: drain resolves when queue is empty before enqueue', async () => {
|
|
140
|
+
const q = createDeliveryQueue(async () => {});
|
|
141
|
+
await q.drain(); // should resolve immediately
|
|
142
|
+
assert.ok(true, 'drain resolved without hanging');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('createDeliveryQueue: concurrency limit is respected', async () => {
|
|
146
|
+
let concurrent = 0;
|
|
147
|
+
let maxConcurrent = 0;
|
|
148
|
+
const q = createDeliveryQueue(async (_item) => {
|
|
149
|
+
concurrent++;
|
|
150
|
+
if (concurrent > maxConcurrent) maxConcurrent = concurrent;
|
|
151
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
152
|
+
concurrent--;
|
|
153
|
+
}, { concurrency: 2 });
|
|
154
|
+
|
|
155
|
+
for (let i = 0; i < 6; i++) q.enqueue(i);
|
|
156
|
+
await q.drain();
|
|
157
|
+
assert.ok(maxConcurrent <= 2, `max concurrent was ${maxConcurrent}, expected <= 2`);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('createDeliveryQueue: retries item on processor failure using retryPolicy', async () => {
|
|
161
|
+
let callCount = 0;
|
|
162
|
+
const retryPolicy = createRetryPolicy({ maxRetries: 2, baseDelayMs: 1, jitter: false });
|
|
163
|
+
const q = createDeliveryQueue(async (_item) => {
|
|
164
|
+
callCount++;
|
|
165
|
+
if (callCount < 3) throw new Error('transient');
|
|
166
|
+
}, { retryPolicy });
|
|
167
|
+
|
|
168
|
+
q.enqueue('item');
|
|
169
|
+
await q.drain();
|
|
170
|
+
assert.equal(callCount, 3, 'processor should have been called 3 times (2 retries)');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('createDeliveryQueue: stop empties the queue', async () => {
|
|
174
|
+
const processed = [];
|
|
175
|
+
let resolveBlock;
|
|
176
|
+
const block = new Promise((r) => { resolveBlock = r; });
|
|
177
|
+
const q = createDeliveryQueue(async (item) => {
|
|
178
|
+
await block;
|
|
179
|
+
processed.push(item);
|
|
180
|
+
}, { concurrency: 1 });
|
|
181
|
+
|
|
182
|
+
q.enqueue('first');
|
|
183
|
+
q.enqueue('second');
|
|
184
|
+
q.enqueue('third');
|
|
185
|
+
q.stop();
|
|
186
|
+
resolveBlock();
|
|
187
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
188
|
+
// After stop, queue is cleared so 'second' and 'third' should not be processed
|
|
189
|
+
assert.ok(processed.length < 3, 'stopped queue should not process remaining items');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// createCheckpointer
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
test('createCheckpointer returns save, load, clear, and listKeys methods', () => {
|
|
197
|
+
const cp = createCheckpointer();
|
|
198
|
+
assert.ok(typeof cp.save === 'function', 'has save');
|
|
199
|
+
assert.ok(typeof cp.load === 'function', 'has load');
|
|
200
|
+
assert.ok(typeof cp.clear === 'function', 'has clear');
|
|
201
|
+
assert.ok(typeof cp.listKeys === 'function', 'has listKeys');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('createCheckpointer: save and load a value', () => {
|
|
205
|
+
const cp = createCheckpointer();
|
|
206
|
+
cp.save('cursor', 42);
|
|
207
|
+
assert.equal(cp.load('cursor'), 42);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('createCheckpointer: load returns undefined for unknown key', () => {
|
|
211
|
+
const cp = createCheckpointer();
|
|
212
|
+
assert.equal(cp.load('missing'), undefined);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('createCheckpointer: clear removes a key', () => {
|
|
216
|
+
const cp = createCheckpointer();
|
|
217
|
+
cp.save('key', 'value');
|
|
218
|
+
cp.clear('key');
|
|
219
|
+
assert.equal(cp.load('key'), undefined);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('createCheckpointer: listKeys returns all saved keys', () => {
|
|
223
|
+
const cp = createCheckpointer();
|
|
224
|
+
cp.save('a', 1);
|
|
225
|
+
cp.save('b', 2);
|
|
226
|
+
cp.save('c', 3);
|
|
227
|
+
const keys = cp.listKeys();
|
|
228
|
+
assert.deepEqual(keys.sort(), ['a', 'b', 'c']);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('createCheckpointer: listKeys excludes cleared keys', () => {
|
|
232
|
+
const cp = createCheckpointer();
|
|
233
|
+
cp.save('keep', 1);
|
|
234
|
+
cp.save('remove', 2);
|
|
235
|
+
cp.clear('remove');
|
|
236
|
+
assert.deepEqual(cp.listKeys(), ['keep']);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('createCheckpointer: save overwrites existing value', () => {
|
|
240
|
+
const cp = createCheckpointer();
|
|
241
|
+
cp.save('seq', 1);
|
|
242
|
+
cp.save('seq', 99);
|
|
243
|
+
assert.equal(cp.load('seq'), 99);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('createCheckpointer: accepts external Map storage', () => {
|
|
247
|
+
const storage = new Map([['existing', 'data']]);
|
|
248
|
+
const cp = createCheckpointer(storage);
|
|
249
|
+
assert.equal(cp.load('existing'), 'data');
|
|
250
|
+
cp.save('new', 'value');
|
|
251
|
+
assert.equal(storage.get('new'), 'value');
|
|
252
|
+
});
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { createAuditController, createEventPoller } from '../src/audit-controller.js';
|
|
4
|
+
|
|
5
|
+
// ─── createAuditController shape ─────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
test('createAuditController returns controller with log, query, getStream, getMetrics methods', () => {
|
|
8
|
+
const controller = createAuditController();
|
|
9
|
+
assert.ok(typeof controller.log === 'function', 'has log method');
|
|
10
|
+
assert.ok(typeof controller.query === 'function', 'has query method');
|
|
11
|
+
assert.ok(typeof controller.getStream === 'function', 'has getStream method');
|
|
12
|
+
assert.ok(typeof controller.getMetrics === 'function', 'has getMetrics method');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// ─── log ─────────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
test('log records an audit event with org, actor, action, resource, timestamp', () => {
|
|
18
|
+
const controller = createAuditController();
|
|
19
|
+
const event = controller.log({
|
|
20
|
+
org: 'acme',
|
|
21
|
+
actor: 'alice',
|
|
22
|
+
action: 'repository.create',
|
|
23
|
+
resource: { kind: 'Repository', name: 'my-repo' },
|
|
24
|
+
});
|
|
25
|
+
assert.equal(event.org, 'acme');
|
|
26
|
+
assert.equal(event.actor, 'alice');
|
|
27
|
+
assert.equal(event.action, 'repository.create');
|
|
28
|
+
assert.deepEqual(event.resource, { kind: 'Repository', name: 'my-repo' });
|
|
29
|
+
assert.ok(typeof event.timestamp === 'string', 'timestamp is a string');
|
|
30
|
+
assert.ok(new Date(event.timestamp).getTime() > 0, 'timestamp is a valid ISO date');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('log generates sequential event IDs', () => {
|
|
34
|
+
const controller = createAuditController();
|
|
35
|
+
const e1 = controller.log({ org: 'acme', actor: 'alice', action: 'repo.create', resource: {} });
|
|
36
|
+
const e2 = controller.log({ org: 'acme', actor: 'alice', action: 'repo.delete', resource: {} });
|
|
37
|
+
const e3 = controller.log({ org: 'acme', actor: 'bob', action: 'user.create', resource: {} });
|
|
38
|
+
assert.ok(typeof e1.id === 'number', 'id is numeric');
|
|
39
|
+
assert.ok(e2.id > e1.id, 'e2.id > e1.id');
|
|
40
|
+
assert.ok(e3.id > e2.id, 'e3.id > e2.id');
|
|
41
|
+
assert.equal(e2.id - e1.id, 1, 'IDs are strictly sequential');
|
|
42
|
+
assert.equal(e3.id - e2.id, 1, 'IDs are strictly sequential');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('log rejects missing org', () => {
|
|
46
|
+
const controller = createAuditController();
|
|
47
|
+
assert.throws(
|
|
48
|
+
() => controller.log({ actor: 'alice', action: 'repo.create', resource: {} }),
|
|
49
|
+
/org/i,
|
|
50
|
+
'throws when org is missing'
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('log rejects missing action', () => {
|
|
55
|
+
const controller = createAuditController();
|
|
56
|
+
assert.throws(
|
|
57
|
+
() => controller.log({ org: 'acme', actor: 'alice', resource: {} }),
|
|
58
|
+
/action/i,
|
|
59
|
+
'throws when action is missing'
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ─── query ────────────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
test('query returns events filtered by org', () => {
|
|
66
|
+
const controller = createAuditController();
|
|
67
|
+
controller.log({ org: 'acme', actor: 'alice', action: 'repo.create', resource: {} });
|
|
68
|
+
controller.log({ org: 'globex', actor: 'bob', action: 'repo.create', resource: {} });
|
|
69
|
+
controller.log({ org: 'acme', actor: 'carol', action: 'user.invite', resource: {} });
|
|
70
|
+
|
|
71
|
+
const result = controller.query({ org: 'acme' });
|
|
72
|
+
assert.equal(result.events.length, 2, 'returns only acme events');
|
|
73
|
+
assert.ok(result.events.every(e => e.org === 'acme'), 'all events belong to acme');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('query returns events filtered by action', () => {
|
|
77
|
+
const controller = createAuditController();
|
|
78
|
+
controller.log({ org: 'acme', actor: 'alice', action: 'repo.create', resource: {} });
|
|
79
|
+
controller.log({ org: 'acme', actor: 'bob', action: 'user.invite', resource: {} });
|
|
80
|
+
controller.log({ org: 'acme', actor: 'carol', action: 'repo.create', resource: {} });
|
|
81
|
+
|
|
82
|
+
const result = controller.query({ org: 'acme', action: 'repo.create' });
|
|
83
|
+
assert.equal(result.events.length, 2, 'returns only repo.create events');
|
|
84
|
+
assert.ok(result.events.every(e => e.action === 'repo.create'), 'all events have action repo.create');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('query returns events filtered by time range (since/until)', () => {
|
|
88
|
+
const controller = createAuditController();
|
|
89
|
+
|
|
90
|
+
const t0 = new Date('2026-01-01T00:00:00Z');
|
|
91
|
+
const t1 = new Date('2026-01-02T00:00:00Z');
|
|
92
|
+
const t2 = new Date('2026-01-03T00:00:00Z');
|
|
93
|
+
const t3 = new Date('2026-01-04T00:00:00Z');
|
|
94
|
+
|
|
95
|
+
controller.log({ org: 'acme', actor: 'alice', action: 'a', resource: {}, timestamp: t0.toISOString() });
|
|
96
|
+
controller.log({ org: 'acme', actor: 'alice', action: 'b', resource: {}, timestamp: t1.toISOString() });
|
|
97
|
+
controller.log({ org: 'acme', actor: 'alice', action: 'c', resource: {}, timestamp: t2.toISOString() });
|
|
98
|
+
controller.log({ org: 'acme', actor: 'alice', action: 'd', resource: {}, timestamp: t3.toISOString() });
|
|
99
|
+
|
|
100
|
+
const result = controller.query({ org: 'acme', since: t1.toISOString(), until: t2.toISOString() });
|
|
101
|
+
assert.equal(result.events.length, 2, 'returns events within [since, until] inclusive');
|
|
102
|
+
assert.ok(result.events.some(e => e.action === 'b'), 'includes event at since boundary');
|
|
103
|
+
assert.ok(result.events.some(e => e.action === 'c'), 'includes event at until boundary');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('query supports pagination (limit, offset)', () => {
|
|
107
|
+
const controller = createAuditController();
|
|
108
|
+
for (let i = 0; i < 10; i++) {
|
|
109
|
+
controller.log({ org: 'acme', actor: 'alice', action: `action-${i}`, resource: {} });
|
|
110
|
+
}
|
|
111
|
+
const page1 = controller.query({ org: 'acme', limit: 3, offset: 0 });
|
|
112
|
+
const page2 = controller.query({ org: 'acme', limit: 3, offset: 3 });
|
|
113
|
+
|
|
114
|
+
assert.equal(page1.events.length, 3, 'page1 has 3 events');
|
|
115
|
+
assert.equal(page2.events.length, 3, 'page2 has 3 events');
|
|
116
|
+
// IDs should be different between pages
|
|
117
|
+
const page1Ids = new Set(page1.events.map(e => e.id));
|
|
118
|
+
const page2Ids = new Set(page2.events.map(e => e.id));
|
|
119
|
+
for (const id of page2Ids) assert.ok(!page1Ids.has(id), 'page2 events are distinct from page1');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('query returns events in reverse chronological order', () => {
|
|
123
|
+
const controller = createAuditController();
|
|
124
|
+
controller.log({ org: 'acme', actor: 'alice', action: 'a', resource: {}, timestamp: '2026-01-01T00:00:00Z' });
|
|
125
|
+
controller.log({ org: 'acme', actor: 'alice', action: 'b', resource: {}, timestamp: '2026-01-02T00:00:00Z' });
|
|
126
|
+
controller.log({ org: 'acme', actor: 'alice', action: 'c', resource: {}, timestamp: '2026-01-03T00:00:00Z' });
|
|
127
|
+
|
|
128
|
+
const result = controller.query({ org: 'acme' });
|
|
129
|
+
const timestamps = result.events.map(e => new Date(e.timestamp).getTime());
|
|
130
|
+
for (let i = 1; i < timestamps.length; i++) {
|
|
131
|
+
assert.ok(timestamps[i - 1] >= timestamps[i], `event[${i - 1}] should be >= event[${i}] (desc order)`);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// ─── getStream ────────────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
test('getStream returns events after a sequence number (for replay)', () => {
|
|
138
|
+
const controller = createAuditController();
|
|
139
|
+
const e1 = controller.log({ org: 'acme', actor: 'alice', action: 'a', resource: {} });
|
|
140
|
+
const e2 = controller.log({ org: 'acme', actor: 'alice', action: 'b', resource: {} });
|
|
141
|
+
const e3 = controller.log({ org: 'acme', actor: 'alice', action: 'c', resource: {} });
|
|
142
|
+
|
|
143
|
+
const stream = controller.getStream({ org: 'acme', afterSeq: e1.id });
|
|
144
|
+
assert.ok(Array.isArray(stream.events), 'returns array of events');
|
|
145
|
+
assert.equal(stream.events.length, 2, 'returns 2 events after seq e1');
|
|
146
|
+
assert.ok(stream.events.every(e => e.id > e1.id), 'all returned events have id > afterSeq');
|
|
147
|
+
assert.ok(stream.events.some(e => e.id === e2.id), 'includes e2');
|
|
148
|
+
assert.ok(stream.events.some(e => e.id === e3.id), 'includes e3');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('getStream returns empty array when no new events', () => {
|
|
152
|
+
const controller = createAuditController();
|
|
153
|
+
const e1 = controller.log({ org: 'acme', actor: 'alice', action: 'a', resource: {} });
|
|
154
|
+
|
|
155
|
+
const stream = controller.getStream({ org: 'acme', afterSeq: e1.id });
|
|
156
|
+
assert.equal(stream.events.length, 0, 'returns empty array when no new events');
|
|
157
|
+
assert.ok(Array.isArray(stream.events), 'events is still an array');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ─── createEventPoller ────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
test('createEventPoller returns poller with poll method and backoff state', () => {
|
|
163
|
+
const controller = createAuditController();
|
|
164
|
+
const poller = createEventPoller({ controller, org: 'acme' });
|
|
165
|
+
assert.ok(typeof poller.poll === 'function', 'has poll method');
|
|
166
|
+
assert.ok(typeof poller.getBackoff === 'function', 'has getBackoff method');
|
|
167
|
+
assert.ok(typeof poller.reset === 'function', 'has reset method');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('poll increases backoff interval when no new events (1s, 2s, 4s... max 30s)', () => {
|
|
171
|
+
const controller = createAuditController();
|
|
172
|
+
const poller = createEventPoller({ controller, org: 'acme', initialBackoff: 1000, maxBackoff: 30000 });
|
|
173
|
+
|
|
174
|
+
// No events exist — each poll should grow backoff
|
|
175
|
+
assert.equal(poller.getBackoff(), 1000, 'initial backoff is 1s');
|
|
176
|
+
poller.poll();
|
|
177
|
+
assert.equal(poller.getBackoff(), 2000, 'after 1 empty poll: 2s');
|
|
178
|
+
poller.poll();
|
|
179
|
+
assert.equal(poller.getBackoff(), 4000, 'after 2 empty polls: 4s');
|
|
180
|
+
poller.poll();
|
|
181
|
+
assert.equal(poller.getBackoff(), 8000, 'after 3 empty polls: 8s');
|
|
182
|
+
poller.poll();
|
|
183
|
+
assert.equal(poller.getBackoff(), 16000, 'after 4 empty polls: 16s');
|
|
184
|
+
poller.poll();
|
|
185
|
+
assert.equal(poller.getBackoff(), 30000, 'capped at maxBackoff 30s');
|
|
186
|
+
poller.poll();
|
|
187
|
+
assert.equal(poller.getBackoff(), 30000, 'stays at maxBackoff');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('poll resets backoff when new events arrive', () => {
|
|
191
|
+
const controller = createAuditController();
|
|
192
|
+
const poller = createEventPoller({ controller, org: 'acme', initialBackoff: 1000, maxBackoff: 30000 });
|
|
193
|
+
|
|
194
|
+
// First poll: no events — backoff grows
|
|
195
|
+
poller.poll();
|
|
196
|
+
assert.equal(poller.getBackoff(), 2000, 'backoff grew to 2s');
|
|
197
|
+
|
|
198
|
+
// Add a new event
|
|
199
|
+
controller.log({ org: 'acme', actor: 'alice', action: 'repo.create', resource: {} });
|
|
200
|
+
|
|
201
|
+
// Poll again — should see the event and reset backoff
|
|
202
|
+
const result = poller.poll();
|
|
203
|
+
assert.ok(result.events.length > 0, 'poll returns new events');
|
|
204
|
+
assert.equal(poller.getBackoff(), 1000, 'backoff resets to initial after receiving events');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ─── getMetrics ───────────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
test('getMetrics returns aggregate counts (events by action, by org, by hour)', () => {
|
|
210
|
+
const controller = createAuditController();
|
|
211
|
+
controller.log({ org: 'acme', actor: 'alice', action: 'repo.create', resource: {}, timestamp: '2026-05-13T10:00:00Z' });
|
|
212
|
+
controller.log({ org: 'acme', actor: 'alice', action: 'repo.create', resource: {}, timestamp: '2026-05-13T10:30:00Z' });
|
|
213
|
+
controller.log({ org: 'acme', actor: 'bob', action: 'user.invite', resource: {}, timestamp: '2026-05-13T11:00:00Z' });
|
|
214
|
+
controller.log({ org: 'globex', actor: 'carol', action: 'repo.create', resource: {}, timestamp: '2026-05-13T10:15:00Z' });
|
|
215
|
+
|
|
216
|
+
const metrics = controller.getMetrics({ org: 'acme' });
|
|
217
|
+
|
|
218
|
+
assert.ok(typeof metrics.byAction === 'object', 'has byAction');
|
|
219
|
+
assert.ok(typeof metrics.byOrg === 'object', 'has byOrg');
|
|
220
|
+
assert.ok(typeof metrics.byHour === 'object', 'has byHour');
|
|
221
|
+
assert.equal(metrics.byAction['repo.create'], 2, 'acme has 2 repo.create events');
|
|
222
|
+
assert.equal(metrics.byAction['user.invite'], 1, 'acme has 1 user.invite event');
|
|
223
|
+
assert.equal(metrics.byOrg['acme'], 3, 'acme org total is 3');
|
|
224
|
+
// byHour keys are ISO hour strings (e.g. "2026-05-13T10")
|
|
225
|
+
assert.ok(metrics.byHour['2026-05-13T10'] >= 2, 'at least 2 events in 2026-05-13T10 hour');
|
|
226
|
+
assert.ok(metrics.byHour['2026-05-13T11'] >= 1, 'at least 1 event in 2026-05-13T11 hour');
|
|
227
|
+
});
|