@a5c-ai/kradle 5.0.1-staging.3abdf9534c25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +31 -0
- package/README.md +187 -0
- package/bin/kradle-demo.mjs +23 -0
- package/bin/kradle-server.mjs +14 -0
- package/dist/kradle-controller-ui.json +3482 -0
- package/dist/kradle-lifecycle.json +201 -0
- package/dist/kradle-runtime-snapshot.json +3125 -0
- package/dist/kradle-summary.json +724 -0
- package/docs/README.md +61 -0
- package/docs/agents/README.md +83 -0
- package/docs/agents/acceptance-test-matrix.md +193 -0
- package/docs/agents/agent-mux-adapter-contract.md +167 -0
- package/docs/agents/agent-mux-source-map.md +310 -0
- package/docs/agents/agent-run-memory-import-spec.md +256 -0
- package/docs/agents/agent-stack-management-spec.md +421 -0
- package/docs/agents/api-contract-spec.md +309 -0
- package/docs/agents/artifacts-writeback-spec.md +145 -0
- package/docs/agents/chart-packaging-spec.md +128 -0
- package/docs/agents/ci-orchestration-spec.md +140 -0
- package/docs/agents/context-assembly-spec.md +219 -0
- package/docs/agents/controller-reconciliation-spec.md +255 -0
- package/docs/agents/crd-schema-spec.md +315 -0
- package/docs/agents/decision-log-open-questions.md +169 -0
- package/docs/agents/developer-implementation-checklist.md +329 -0
- package/docs/agents/dispatching-design.md +262 -0
- package/docs/agents/gaps-agent-mux-to-kradle-crds.md +298 -0
- package/docs/agents/glossary.md +66 -0
- package/docs/agents/implementation-blueprint.md +324 -0
- package/docs/agents/implementation-rollout-slices.md +251 -0
- package/docs/agents/memory-context-integration-spec.md +194 -0
- package/docs/agents/memory-ontology-schema-spec.md +253 -0
- package/docs/agents/memory-operations-runbook.md +121 -0
- package/docs/agents/mvp-vertical-slice-spec.md +146 -0
- package/docs/agents/observability-audit-spec.md +265 -0
- package/docs/agents/operator-runbook.md +174 -0
- package/docs/agents/org-memory-api-payload-examples.md +333 -0
- package/docs/agents/org-memory-controller-sequence-spec.md +181 -0
- package/docs/agents/org-memory-e2e-fixture-plan.md +161 -0
- package/docs/agents/org-memory-ui-implementation-map.md +114 -0
- package/docs/agents/org-memory-vertical-slice-spec.md +168 -0
- package/docs/agents/org-resource-model-delta-spec.md +111 -0
- package/docs/agents/org-route-resource-model-spec.md +183 -0
- package/docs/agents/org-scoping-namespace-spec.md +114 -0
- package/docs/agents/rbac-secrets-management-spec.md +406 -0
- package/docs/agents/repository-page-integration-spec.md +255 -0
- package/docs/agents/resource-contract-examples.md +808 -0
- package/docs/agents/resource-relationship-map.md +190 -0
- package/docs/agents/security-threat-model.md +188 -0
- package/docs/agents/shared-memory-company-brain-spec.md +358 -0
- package/docs/agents/storage-migration-spec.md +168 -0
- package/docs/agents/subagent-orchestration-spec.md +152 -0
- package/docs/agents/system-overview.md +88 -0
- package/docs/agents/tools-mcp-skills-spec.md +189 -0
- package/docs/agents/traceability-matrix.md +79 -0
- package/docs/agents/ui-flow-spec.md +211 -0
- package/docs/agents/ui-ux-system-spec.md +426 -0
- package/docs/agents/workspace-lifecycle-spec.md +166 -0
- package/docs/architecture-spec.md +78 -0
- package/docs/architecture-v2.md +2759 -0
- package/docs/components/control-plane.md +78 -0
- package/docs/components/data-plane.md +69 -0
- package/docs/components/hooks-events.md +67 -0
- package/docs/components/identity-rbac-policy.md +73 -0
- package/docs/components/kubevela-oam.md +70 -0
- package/docs/components/operations-publishing.md +81 -0
- package/docs/components/runners-ci.md +66 -0
- package/docs/components/web-ui.md +94 -0
- package/docs/crd-behaviors-and-relationships.md +3926 -0
- package/docs/external/README.md +47 -0
- package/docs/external/bidirectional-sync-design.md +134 -0
- package/docs/external/cicd-interface.md +64 -0
- package/docs/external/external-backend-controllers.md +170 -0
- package/docs/external/external-backend-crds.md +234 -0
- package/docs/external/external-backend-ui-spec.md +151 -0
- package/docs/external/external-backend-ux-flows.md +115 -0
- package/docs/external/external-object-mapping.md +125 -0
- package/docs/external/git-forge-interface.md +68 -0
- package/docs/external/github-integration-design.md +151 -0
- package/docs/external/issue-tracking-interface.md +66 -0
- package/docs/external/provider-capability-manifests.md +204 -0
- package/docs/external/provider-catalog.md +139 -0
- package/docs/external/provider-rollout-testing.md +78 -0
- package/docs/external/research-results.md +48 -0
- package/docs/external/security-auth-permissions.md +81 -0
- package/docs/external/sync-state-machines.md +108 -0
- package/docs/external/unified-external-backend-model.md +107 -0
- package/docs/external/user-facing-changes.md +67 -0
- package/docs/gaps.md +161 -0
- package/docs/install.md +94 -0
- package/docs/integration-and-design-decisions.md +1530 -0
- package/docs/kradle-design.md +334 -0
- package/docs/local-minikube.md +55 -0
- package/docs/ontology/README.md +32 -0
- package/docs/ontology/bounded-contexts.md +29 -0
- package/docs/ontology/events-and-hooks.md +32 -0
- package/docs/ontology/oam-kubevela.md +32 -0
- package/docs/ontology/operations-and-release.md +25 -0
- package/docs/ontology/personas-and-actors.md +32 -0
- package/docs/ontology/policies-and-invariants.md +33 -0
- package/docs/ontology/problem-space.md +30 -0
- package/docs/ontology/resource-contracts.md +40 -0
- package/docs/ontology/resource-taxonomy.md +42 -0
- package/docs/ontology/runners-and-ci.md +29 -0
- package/docs/ontology/solution-space.md +24 -0
- package/docs/ontology/storage-and-data-boundaries.md +29 -0
- package/docs/ontology/validation-matrix.md +24 -0
- package/docs/ontology/web-ui-excellent-flows.md +32 -0
- package/docs/ontology/workflows.md +39 -0
- package/docs/ontology/world.md +35 -0
- package/docs/openapi.yaml +1291 -0
- package/docs/product-requirements.md +62 -0
- package/docs/requirements-v2.md +235 -0
- package/docs/roadmap-mvp.md +87 -0
- package/docs/sdk-api-reference.md +1108 -0
- package/docs/system-requirements.md +90 -0
- package/docs/system-spec-v2.md +1230 -0
- package/docs/tests/README.md +53 -0
- package/docs/tests/agent-qa-plan.md +63 -0
- package/docs/tests/browser-ui-tests.md +62 -0
- package/docs/tests/ci-quality-gates.md +48 -0
- package/docs/tests/coverage-model.md +64 -0
- package/docs/tests/e2e-scenario-tests.md +53 -0
- package/docs/tests/fixtures-test-data.md +63 -0
- package/docs/tests/observability-reliability-tests.md +54 -0
- package/docs/tests/product-test-matrix.md +145 -0
- package/docs/tests/qa-adoption-roadmap.md +130 -0
- package/docs/tests/qa-automation-plan.md +101 -0
- package/docs/tests/security-compliance-tests.md +57 -0
- package/docs/tests/test-framework-tools.md +88 -0
- package/docs/tests/test-suite-layout.md +121 -0
- package/docs/tests/unit-integration-tests.md +48 -0
- package/docs/todo-kyverno +714 -0
- package/docs/todos.md +4 -0
- package/docs/user-stories.md +78 -0
- package/docs/web-console-spec.md +533 -0
- package/examples/minikube-demo.yaml +190 -0
- package/examples/oam-application.yaml +23 -0
- package/examples/policy-kyverno-pr-title.yaml +18 -0
- package/package.json +66 -0
- package/scripts/build.mjs +29 -0
- package/scripts/setup-minikube.mjs +65 -0
- package/scripts/smoke.mjs +37 -0
- package/scripts/validate-doc-coverage.mjs +152 -0
- package/scripts/validate-package.mjs +95 -0
- package/scripts/validate-ui.mjs +305 -0
- package/src/agent-adapter-controller.js +169 -0
- package/src/agent-approval-controller.js +170 -0
- package/src/agent-context-bundles.js +242 -0
- package/src/agent-dispatch-controller.js +549 -0
- package/src/agent-gateway-config-controller.js +147 -0
- package/src/agent-identity-migration.js +115 -0
- package/src/agent-memory-controller.js +357 -0
- package/src/agent-memory-import.js +327 -0
- package/src/agent-memory-query.js +292 -0
- package/src/agent-memory-repository-source-controller.js +255 -0
- package/src/agent-mux-client.js +589 -0
- package/src/agent-permission-review.js +250 -0
- package/src/agent-persona-controller.js +135 -0
- package/src/agent-project-controller.js +117 -0
- package/src/agent-prompt-composition.js +55 -0
- package/src/agent-provider-config-controller.js +151 -0
- package/src/agent-secret-config-grant-controller.js +282 -0
- package/src/agent-session-transcript-controller.js +189 -0
- package/src/agent-stack-controller.js +421 -0
- package/src/agent-subagent-controller.js +160 -0
- package/src/agent-transport-binding-controller.js +121 -0
- package/src/agent-trigger-controller.js +387 -0
- package/src/agent-workspace-controller.js +702 -0
- package/src/agent-writeback-controller.js +302 -0
- package/src/api-controller.js +621 -0
- package/src/argocd-gitops.js +43 -0
- package/src/artifact-registry-controller.js +542 -0
- package/src/assistant-runtime.js +284 -0
- package/src/async-controller.js +207 -0
- package/src/audit-controller.js +191 -0
- package/src/auth.js +310 -0
- package/src/component-catalog.js +41 -0
- package/src/control-plane.js +136 -0
- package/src/controller-client.js +112 -0
- package/src/controller-ui.js +620 -0
- package/src/data-plane.js +179 -0
- package/src/event-bus.js +397 -0
- package/src/external/conflict-controller.js +225 -0
- package/src/external/github/auth.js +96 -0
- package/src/external/github/cicd.js +180 -0
- package/src/external/github/git-forge.js +240 -0
- package/src/external/github/index.js +144 -0
- package/src/external/github/issue-tracking.js +163 -0
- package/src/external/provider-adapter.js +161 -0
- package/src/external/provider-resource-factory.js +221 -0
- package/src/external/sync-controller.js +235 -0
- package/src/external/webhook-controller.js +144 -0
- package/src/external/write-controller.js +283 -0
- package/src/gitea-backend.js +131 -0
- package/src/gitea-service.js +173 -0
- package/src/handoff.js +98 -0
- package/src/health-probes.js +134 -0
- package/src/hooks-events.js +63 -0
- package/src/hooks-lifecycle.js +117 -0
- package/src/http-server.js +409 -0
- package/src/identity-policy.js +86 -0
- package/src/index.js +71 -0
- package/src/jitsi-agent-bridge.js +141 -0
- package/src/jitsi-meeting-controller.js +291 -0
- package/src/jitsi-sync-controller.js +198 -0
- package/src/kradle-inference-service-controller.js +246 -0
- package/src/kubernetes-controller-async.js +531 -0
- package/src/kubernetes-controller.js +904 -0
- package/src/kubernetes-resource-gateway.js +48 -0
- package/src/model-route-controller.js +364 -0
- package/src/notification-controller.js +178 -0
- package/src/operations.js +112 -0
- package/src/org-scoping.js +5 -0
- package/src/resource-model.js +282 -0
- package/src/runner-controller.js +272 -0
- package/src/runners-ci.js +48 -0
- package/src/runtime.js +196 -0
- package/src/snapshot-cache.js +157 -0
- package/src/virtual-model-controller.js +538 -0
- package/src/virtual-model-hook-bridge.js +200 -0
- package/src/web-ui.js +40 -0
- package/tests/agent-adapter-controller.test.js +361 -0
- package/tests/agent-approval-controller.test.js +173 -0
- package/tests/agent-context-bundles.test.js +278 -0
- package/tests/agent-dispatch-controller.test.js +679 -0
- package/tests/agent-gateway-config-controller.test.js +386 -0
- package/tests/agent-identity-migration.test.js +87 -0
- package/tests/agent-memory-controller.test.js +461 -0
- package/tests/agent-memory-import-snapshot.test.js +477 -0
- package/tests/agent-memory-query.test.js +404 -0
- package/tests/agent-memory-repository-source.test.js +514 -0
- package/tests/agent-mux-client.test.js +389 -0
- package/tests/agent-mux-integration.test.js +971 -0
- package/tests/agent-permission-review-v2.test.js +317 -0
- package/tests/agent-permission-review.test.js +209 -0
- package/tests/agent-persona-controller.test.js +127 -0
- package/tests/agent-project-controller.test.js +302 -0
- package/tests/agent-prompt-composition.test.js +76 -0
- package/tests/agent-provider-config-controller.test.js +376 -0
- package/tests/agent-resources.test.js +303 -0
- package/tests/agent-secret-config-grant.test.js +231 -0
- package/tests/agent-session-transcript-controller.test.js +499 -0
- package/tests/agent-stack-controller.test.js +283 -0
- package/tests/agent-subagent-controller.test.js +201 -0
- package/tests/agent-transport-binding-controller.test.js +294 -0
- package/tests/agent-trigger-controller.test.js +271 -0
- package/tests/agent-trigger-routes.test.js +190 -0
- package/tests/agent-trigger-sources.test.js +245 -0
- package/tests/agent-workspace-controller.test.js +181 -0
- package/tests/agent-writeback.test.js +292 -0
- package/tests/approval-persistence.test.js +171 -0
- package/tests/artifact-registry.test.js +511 -0
- package/tests/assistant-runtime.test.js +506 -0
- package/tests/async-controller.test.js +252 -0
- package/tests/audit-controller.test.js +227 -0
- package/tests/codespace-controller.test.js +318 -0
- package/tests/controller-client.test.js +133 -0
- package/tests/deployment.test.js +527 -0
- package/tests/e2e/lifecycle.test.js +120 -0
- package/tests/event-bus-integration.test.js +355 -0
- package/tests/external-github-forge.test.js +560 -0
- package/tests/external-github-issues-cicd.test.js +520 -0
- package/tests/external-integration.test.js +470 -0
- package/tests/external-persistence.test.js +415 -0
- package/tests/external-provider-adapter.test.js +365 -0
- package/tests/external-resource-model.test.js +223 -0
- package/tests/external-webhook-sync.test.js +287 -0
- package/tests/external-write-conflict.test.js +353 -0
- package/tests/gitea-service.test.js +253 -0
- package/tests/health-check-real.test.js +165 -0
- package/tests/health-probes.test.js +90 -0
- package/tests/hooks-lifecycle.test.js +364 -0
- package/tests/integration/full-flow.test.js +266 -0
- package/tests/jitsi-agent-bridge.test.js +119 -0
- package/tests/jitsi-helm-integration.test.js +77 -0
- package/tests/jitsi-meeting-controller.test.js +170 -0
- package/tests/jitsi-resource-model.test.js +73 -0
- package/tests/jitsi-sync-controller.test.js +112 -0
- package/tests/kradle-inference-service.test.js +689 -0
- package/tests/kradle.test.js +779 -0
- package/tests/memory-search-wiring.test.js +270 -0
- package/tests/model-route-controller.test.js +733 -0
- package/tests/notification-controller.test.js +196 -0
- package/tests/notification-integration.test.js +179 -0
- package/tests/org-scoping.test.js +687 -0
- package/tests/runner-controller.test.js +327 -0
- package/tests/runner-integration.test.js +231 -0
- package/tests/session-cookie-hmac.test.js +151 -0
- package/tests/snapshot-performance.test.js +315 -0
- package/tests/sse-events.test.js +107 -0
- package/tests/virtual-model-controller.test.js +877 -0
- package/tests/virtual-model-hook-bridge.test.js +384 -0
- package/tests/webhook-trigger.test.js +198 -0
- package/tests/workspace-volumes.test.js +312 -0
- package/tests/writeback-persistence.test.js +207 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { createAgentMuxClient, createJitsiAgentBridge, createResource } from '../src/index.js';
|
|
4
|
+
|
|
5
|
+
function stack(spec = {}) {
|
|
6
|
+
return createResource('AgentStack', { name: 'standup-bot', namespace: 'kradle-org-default' }, {
|
|
7
|
+
organizationRef: 'default',
|
|
8
|
+
baseAgent: 'claude-code',
|
|
9
|
+
adapter: 'claude-code',
|
|
10
|
+
runtimeIdentity: { serviceAccountRef: 'kradle' },
|
|
11
|
+
...spec,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
test('Jitsi agent bridge gates capability and prepares meeting context', async () => {
|
|
16
|
+
const emitted = [];
|
|
17
|
+
const bridge = createJitsiAgentBridge({
|
|
18
|
+
meetingController: {
|
|
19
|
+
async getMeeting(ref) {
|
|
20
|
+
assert.equal(ref, 'daily');
|
|
21
|
+
return {
|
|
22
|
+
metadata: { name: 'daily' },
|
|
23
|
+
spec: { roomId: 'daily-default', ttlMinutes: 30 },
|
|
24
|
+
status: { phase: 'Active', roomUrl: 'https://meet.example/daily-default' },
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
generateParticipantJwt(roomId, participant, ttlMinutes) {
|
|
28
|
+
assert.equal(roomId, 'daily-default');
|
|
29
|
+
assert.equal(participant.id, 'dispatch-1');
|
|
30
|
+
assert.equal(ttlMinutes, 30);
|
|
31
|
+
return 'kradle-jitsi.jwt.sig';
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
eventBus: { emit: (event) => emitted.push(event) },
|
|
35
|
+
now: () => new Date('2026-05-30T12:00:00Z'),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
assert.equal(bridge.hasMeetingCapability(stack()), false);
|
|
39
|
+
assert.equal(bridge.hasMeetingCapability(stack({ jitsiCapability: true })), true);
|
|
40
|
+
|
|
41
|
+
const run = { metadata: { name: 'dispatch-1' }, spec: {}, status: {} };
|
|
42
|
+
const context = await bridge.prepareMeetingContext(run, 'daily', stack({
|
|
43
|
+
jitsiCapability: true,
|
|
44
|
+
jitsiConfig: { participantName: 'Standup Bot', role: 'observer', capabilities: { chat: 'readwrite', audio: 'listen' } },
|
|
45
|
+
}));
|
|
46
|
+
assert.equal(context.roomUrl, 'https://meet.example/daily-default');
|
|
47
|
+
assert.equal(context.jwt, 'kradle-jitsi.jwt.sig');
|
|
48
|
+
assert.equal(context.role, 'observer');
|
|
49
|
+
assert.equal(run.spec.meetingRef, 'daily');
|
|
50
|
+
assert.equal(run.spec.meetingContext.roomId, 'daily-default');
|
|
51
|
+
assert.equal(run.spec.meetingContext.jwt, undefined);
|
|
52
|
+
assert.deepEqual(run.spec.meetingContext.tokenRef, { runtimeOnly: true });
|
|
53
|
+
|
|
54
|
+
await bridge.onAgentJoined('dispatch-1', 'daily');
|
|
55
|
+
await bridge.onAgentLeft('dispatch-1', 'daily', 'completed');
|
|
56
|
+
assert.deepEqual(emitted.map((event) => event.type), ['agent-joined-meeting', 'agent-left-meeting', 'participant-left']);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('Jitsi agent bridge can resolve meetings from resources and keeps generated JWT runtime-only', async () => {
|
|
60
|
+
const bridge = createJitsiAgentBridge({ now: () => new Date('2026-05-30T12:00:00Z') });
|
|
61
|
+
const run = { metadata: { name: 'dispatch-1' }, spec: {}, status: {} };
|
|
62
|
+
const context = await bridge.prepareMeetingContext(run, 'daily', stack({
|
|
63
|
+
jitsiCapability: true,
|
|
64
|
+
jitsiConfig: { role: 'participant', capabilities: { chat: 'readwrite', audio: 'listen' } },
|
|
65
|
+
}), {
|
|
66
|
+
resources: {
|
|
67
|
+
JitsiMeeting: [
|
|
68
|
+
createResource('JitsiMeeting', { name: 'daily', namespace: 'kradle-org-default' }, {
|
|
69
|
+
organizationRef: 'default',
|
|
70
|
+
providerRef: 'jitsi-prod',
|
|
71
|
+
roomId: 'daily-default',
|
|
72
|
+
ttlMinutes: 30,
|
|
73
|
+
}, {
|
|
74
|
+
phase: 'Active',
|
|
75
|
+
roomUrl: 'https://meet.example/daily-default',
|
|
76
|
+
}),
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
assert.match(context.jwt, /^kradle-jitsi\./);
|
|
82
|
+
assert.equal(run.spec.meetingContext.jwt, undefined);
|
|
83
|
+
assert.equal(run.spec.meetingContext.role, 'participant');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('Jitsi agent bridge builds sidecar specs and Agent Mux injects them only for meeting runs', () => {
|
|
87
|
+
const bridge = createJitsiAgentBridge({
|
|
88
|
+
sidecarImage: 'ghcr.io/a5c-ai/jitsi-agent-sidecar:test',
|
|
89
|
+
});
|
|
90
|
+
const sidecar = bridge.buildSidecarSpec('https://meet.example/daily-default', 'kradle-jitsi.jwt.sig', 'Standup Bot', {
|
|
91
|
+
roomId: 'daily-default',
|
|
92
|
+
role: 'observer',
|
|
93
|
+
capabilities: { audio: 'listen', chat: 'readwrite' },
|
|
94
|
+
});
|
|
95
|
+
assert.equal(sidecar.name, 'jitsi-agent-sidecar');
|
|
96
|
+
assert.equal(sidecar.image, 'ghcr.io/a5c-ai/jitsi-agent-sidecar:test');
|
|
97
|
+
assert.equal(sidecar.env.find((entry) => entry.name === 'JITSI_ROOM_URL').value, 'https://meet.example/daily-default');
|
|
98
|
+
assert.equal(sidecar.env.find((entry) => entry.name === 'JITSI_CHAT_MODE').value, 'readwrite');
|
|
99
|
+
|
|
100
|
+
const client = createAgentMuxClient();
|
|
101
|
+
const plainJob = client.createAgentJob({ adapter: 'claude-code', org: 'default' }).jobManifest;
|
|
102
|
+
assert.equal(plainJob.spec.template.spec.containers.length, 1);
|
|
103
|
+
|
|
104
|
+
const meetingJob = client.createAgentJob({
|
|
105
|
+
adapter: 'claude-code',
|
|
106
|
+
org: 'default',
|
|
107
|
+
meetingContext: {
|
|
108
|
+
roomUrl: 'https://meet.example/daily-default',
|
|
109
|
+
jwt: 'kradle-jitsi.jwt.sig',
|
|
110
|
+
roomId: 'daily-default',
|
|
111
|
+
participantName: 'Standup Bot',
|
|
112
|
+
role: 'observer',
|
|
113
|
+
capabilities: { audio: 'listen', chat: 'readwrite' },
|
|
114
|
+
},
|
|
115
|
+
}).jobManifest;
|
|
116
|
+
assert.equal(meetingJob.spec.template.spec.containers.length, 2);
|
|
117
|
+
assert.ok(meetingJob.spec.template.spec.containers[0].env.some((entry) => entry.name === 'JITSI_MEETING_ACTIVE' && entry.value === 'true'));
|
|
118
|
+
assert.ok(meetingJob.spec.template.spec.volumes.some((volume) => volume.name === 'agent-socket'));
|
|
119
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import test from 'node:test';
|
|
4
|
+
|
|
5
|
+
const chart = readFileSync(new URL('../../charts/Chart.yaml', import.meta.url), 'utf8');
|
|
6
|
+
const values = readFileSync(new URL('../../charts/values.yaml', import.meta.url), 'utf8');
|
|
7
|
+
const networkPolicy = readFileSync(new URL('../../charts/templates/networkpolicy.yaml', import.meta.url), 'utf8');
|
|
8
|
+
const secrets = readFileSync(new URL('../../charts/templates/jitsi-secrets.yaml', import.meta.url), 'utf8');
|
|
9
|
+
const jitsiCrds = readFileSync(new URL('../../charts/crds/jitsi-resources.yaml', import.meta.url), 'utf8');
|
|
10
|
+
const agentCrds = readFileSync(new URL('../../charts/crds/agent-resources.yaml', import.meta.url), 'utf8');
|
|
11
|
+
const aggregatedCrds = readFileSync(new URL('../../charts/crds/aggregated-resources.yaml', import.meta.url), 'utf8');
|
|
12
|
+
const externalCrds = readFileSync(new URL('../../charts/crds/external-resources.yaml', import.meta.url), 'utf8');
|
|
13
|
+
|
|
14
|
+
test('Kradle chart declares the Jitsi Meet subchart dependency behind jitsi.install', () => {
|
|
15
|
+
assert.match(chart, /name:\s+jitsi-meet/);
|
|
16
|
+
assert.match(chart, /repository:\s+https:\/\/jitsi-contrib\.github\.io\/jitsi-helm\//);
|
|
17
|
+
assert.match(chart, /condition:\s+jitsi\.install/);
|
|
18
|
+
assert.match(chart, /alias:\s+jitsi-subchart/);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('values expose internal and external Jitsi deployment settings', () => {
|
|
22
|
+
for (const term of [
|
|
23
|
+
'jitsi:',
|
|
24
|
+
'install: false',
|
|
25
|
+
'external:',
|
|
26
|
+
'publicURL: https://meet.kradle.local',
|
|
27
|
+
'jwtAppSecret:',
|
|
28
|
+
'web:',
|
|
29
|
+
'prosody:',
|
|
30
|
+
'type: jwt',
|
|
31
|
+
'existingSecretName:',
|
|
32
|
+
'jicofo:',
|
|
33
|
+
'jvb:',
|
|
34
|
+
'useNodeIP: true',
|
|
35
|
+
'UDPPort: 10000',
|
|
36
|
+
'udpPort: 10000',
|
|
37
|
+
'jibri:',
|
|
38
|
+
'recordingStorageClass:',
|
|
39
|
+
'jitsi-subchart:',
|
|
40
|
+
]) {
|
|
41
|
+
assert.ok(values.includes(term), `values.yaml should include ${term}`);
|
|
42
|
+
}
|
|
43
|
+
assert.match(values, /jitsi-subchart:[\s\S]*prosody:\n\s+<<: \*jitsiProsody[\s\S]*secretEnvs:\n\s+JWT_APP_SECRET: ""[\s\S]*jwt:\n\s+secret: ""\n\s+existingSecretName: ""/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('Jitsi CRDs are installed from a dedicated chart CRD file', () => {
|
|
47
|
+
const expectedKinds = ['JitsiMeetProvider', 'JitsiMeetingTemplate', 'JitsiMeeting', 'JitsiRecording'];
|
|
48
|
+
for (const kind of expectedKinds) {
|
|
49
|
+
assert.match(jitsiCrds, new RegExp(`kind:\\s+${kind}`));
|
|
50
|
+
}
|
|
51
|
+
assert.match(jitsiCrds, /required:\n\s+- organizationRef/);
|
|
52
|
+
assert.match(jitsiCrds, /name:\s+jitsimeetproviders\.kradle\.a5c\.ai/);
|
|
53
|
+
assert.match(jitsiCrds, /name:\s+jitsimeetings\.kradle\.a5c\.ai/);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('Jitsi CRDs are not duplicated in non-Jitsi chart CRD bundles', () => {
|
|
57
|
+
for (const crdBundle of [agentCrds, aggregatedCrds, externalCrds]) {
|
|
58
|
+
assert.doesNotMatch(crdBundle, /name:\s+jitsi(?:meetproviders|meetingtemplates|meetings|recordings)\.kradle\.a5c\.ai/);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('chart templates manage Jitsi secrets without committing literal credentials', () => {
|
|
63
|
+
assert.match(secrets, /jitsi-jwt/);
|
|
64
|
+
assert.match(secrets, /appSecret:\s+\{\{ \$jwtAppSecret \| quote \}\}/);
|
|
65
|
+
assert.match(secrets, /jitsi-webhook/);
|
|
66
|
+
assert.match(secrets, /webhookExistingSecret/);
|
|
67
|
+
assert.match(secrets, /webhookSecret:\s+\{\{ \.Values\.jitsi\.kradle\.webhookSecret \| quote \}\}/);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('network policy opens JVB UDP media traffic only when Jitsi is installed', () => {
|
|
71
|
+
assert.match(networkPolicy, /if \.Values\.jitsi\.install/);
|
|
72
|
+
assert.match(networkPolicy, /jitsi-media/);
|
|
73
|
+
assert.match(networkPolicy, /component/);
|
|
74
|
+
assert.match(networkPolicy, /jvb/);
|
|
75
|
+
assert.match(networkPolicy, /protocol:\s+UDP/);
|
|
76
|
+
assert.match(networkPolicy, /port:\s+\{\{ \.Values\.jitsi\.jvb\.service\.udpPort \| default 10000 \}\}/);
|
|
77
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { createJitsiMeetingController, createResource } from '../src/index.js';
|
|
4
|
+
|
|
5
|
+
function meeting(overrides = {}) {
|
|
6
|
+
return createResource('JitsiMeeting', { name: 'daily', namespace: 'kradle-org-default' }, {
|
|
7
|
+
organizationRef: 'default',
|
|
8
|
+
providerRef: 'jitsi-prod',
|
|
9
|
+
roomId: 'daily-default',
|
|
10
|
+
displayName: 'Daily',
|
|
11
|
+
ttlMinutes: 30,
|
|
12
|
+
...overrides.spec,
|
|
13
|
+
}, {
|
|
14
|
+
phase: 'Scheduled',
|
|
15
|
+
participants: { current: [], total: 0, peak: 0 },
|
|
16
|
+
recording: { active: false, recordingId: null },
|
|
17
|
+
...overrides.status,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test('Jitsi meeting controller validates required meeting resources and JWT claims', () => {
|
|
22
|
+
const controller = createJitsiMeetingController({ jwtSecret: 'test-secret', now: () => new Date('2026-05-30T12:00:00Z') });
|
|
23
|
+
const valid = meeting();
|
|
24
|
+
|
|
25
|
+
assert.equal(controller.validate(valid), valid);
|
|
26
|
+
assert.throws(() => controller.validate(meeting({ spec: { roomId: '' } })), /JitsiMeeting spec.roomId is required/);
|
|
27
|
+
|
|
28
|
+
const jwt = controller.generateParticipantJwt('daily-default', {
|
|
29
|
+
id: 'agent-run-1',
|
|
30
|
+
name: 'Standup Bot',
|
|
31
|
+
type: 'agent',
|
|
32
|
+
role: 'observer',
|
|
33
|
+
}, 15);
|
|
34
|
+
const [header, encoded, signature] = jwt.split('.');
|
|
35
|
+
assert.ok(header);
|
|
36
|
+
assert.ok(signature);
|
|
37
|
+
const claims = JSON.parse(Buffer.from(encoded, 'base64url').toString('utf8'));
|
|
38
|
+
assert.equal(claims.aud, 'jitsi');
|
|
39
|
+
assert.equal(claims.iss, 'kradle');
|
|
40
|
+
assert.equal(claims.room, 'daily-default');
|
|
41
|
+
assert.equal(claims.context.user.id, 'agent-run-1');
|
|
42
|
+
assert.equal(claims.context.user.name, 'Standup Bot');
|
|
43
|
+
assert.equal(claims.context.user.type, 'agent');
|
|
44
|
+
assert.equal(claims.context.user.role, 'observer');
|
|
45
|
+
assert.equal(claims.exp, Math.floor(new Date('2026-05-30T12:15:00Z').getTime() / 1000));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('Jitsi meeting controller delegates room lifecycle, recording, stats, and emits events', async () => {
|
|
49
|
+
const persisted = [];
|
|
50
|
+
const emitted = [];
|
|
51
|
+
const providerCalls = [];
|
|
52
|
+
const resources = [meeting({ status: { phase: 'Active', roomUrl: 'https://meet.example/daily-default' } })];
|
|
53
|
+
const providers = [
|
|
54
|
+
createResource('JitsiMeetProvider', { name: 'jitsi-prod', namespace: 'kradle-org-default' }, {
|
|
55
|
+
organizationRef: 'default',
|
|
56
|
+
endpoint: 'https://meet.example',
|
|
57
|
+
authMode: 'jwt',
|
|
58
|
+
defaultRoomTTL: 90,
|
|
59
|
+
defaultRoomConfig: { lobby: true },
|
|
60
|
+
}),
|
|
61
|
+
];
|
|
62
|
+
const templates = [
|
|
63
|
+
createResource('JitsiMeetingTemplate', { name: 'standup', namespace: 'kradle-org-default' }, {
|
|
64
|
+
organizationRef: 'default',
|
|
65
|
+
providerRef: 'jitsi-prod',
|
|
66
|
+
displayName: 'Standup',
|
|
67
|
+
ttlMinutes: 45,
|
|
68
|
+
roomConfig: { startWithAudioMuted: true },
|
|
69
|
+
}),
|
|
70
|
+
];
|
|
71
|
+
const recordings = [];
|
|
72
|
+
const controller = createJitsiMeetingController({
|
|
73
|
+
jwtSecret: 'test-secret',
|
|
74
|
+
providerClient: {
|
|
75
|
+
async createRoom(spec) {
|
|
76
|
+
providerCalls.push(['createRoom', spec.roomId, spec.ttlMinutes, spec.roomConfig]);
|
|
77
|
+
return { roomUrl: `https://meet.example/${spec.roomId}` };
|
|
78
|
+
},
|
|
79
|
+
async endRoom(roomId) { providerCalls.push(['endRoom', roomId]); return { ended: true }; },
|
|
80
|
+
async getRoom(roomId) { providerCalls.push(['getRoom', roomId]); return { phase: 'Active', participantCount: 2 }; },
|
|
81
|
+
async getStats(roomId) { providerCalls.push(['getStats', roomId]); return { participantCount: 2, active: true }; },
|
|
82
|
+
async startRecording(roomId) { providerCalls.push(['startRecording', roomId]); return { recordingId: 'rec-1' }; },
|
|
83
|
+
async stopRecording(roomId) { providerCalls.push(['stopRecording', roomId]); return { recordingId: 'rec-1', duration: 120 }; },
|
|
84
|
+
},
|
|
85
|
+
resourceGateway: {
|
|
86
|
+
async list(kind) { assert.equal(kind, 'JitsiMeeting'); return { items: resources }; },
|
|
87
|
+
async apply(resource) {
|
|
88
|
+
persisted.push(resource);
|
|
89
|
+
if (resource.kind === 'JitsiRecording') recordings.push(resource);
|
|
90
|
+
return { resource };
|
|
91
|
+
},
|
|
92
|
+
async get(kind, name) {
|
|
93
|
+
if (kind === 'JitsiMeetProvider') return providers.find((resource) => resource.metadata.name === name);
|
|
94
|
+
if (kind === 'JitsiMeetingTemplate') return templates.find((resource) => resource.metadata.name === name);
|
|
95
|
+
if (kind === 'JitsiRecording') return recordings.find((resource) => resource.metadata.name === name);
|
|
96
|
+
assert.equal(kind, 'JitsiMeeting');
|
|
97
|
+
return resources.find((resource) => resource.metadata.name === name);
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
eventBus: { emit(event) { emitted.push(event); } },
|
|
101
|
+
now: () => new Date('2026-05-30T12:00:00Z'),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const created = await controller.createRoom({ organizationRef: 'default', providerRef: 'jitsi-prod', templateRef: 'standup', roomId: 'daily-default', displayName: 'Daily', roomConfig: { startWithVideoMuted: true } });
|
|
105
|
+
assert.equal(created.status.phase, 'Active');
|
|
106
|
+
assert.equal(created.status.roomUrl, 'https://meet.example/daily-default');
|
|
107
|
+
assert.equal(created.spec.ttlMinutes, 45);
|
|
108
|
+
assert.deepEqual(created.spec.roomConfig, { lobby: true, startWithAudioMuted: true, startWithVideoMuted: true });
|
|
109
|
+
assert.equal(emitted.at(-1).type, 'meeting-created');
|
|
110
|
+
|
|
111
|
+
const active = await controller.listActiveMeetings('default');
|
|
112
|
+
assert.equal(active.length, 1);
|
|
113
|
+
|
|
114
|
+
const reconciled = await controller.reconcile(resources[0]);
|
|
115
|
+
assert.equal(reconciled.status.participants.total, 2);
|
|
116
|
+
|
|
117
|
+
assert.deepEqual(await controller.getMeetingStats('daily-default'), { participantCount: 2, active: true });
|
|
118
|
+
assert.equal((await controller.startRecording('daily')).status.recording.recordingId, 'rec-1');
|
|
119
|
+
assert.equal(recordings.at(-1).status.phase, 'Recording');
|
|
120
|
+
assert.equal(emitted.at(-1).type, 'recording-started');
|
|
121
|
+
assert.equal((await controller.stopRecording('daily')).status.recording.active, false);
|
|
122
|
+
assert.equal(recordings.at(-1).status.phase, 'Completed');
|
|
123
|
+
assert.equal(recordings.at(-1).status.duration, 120);
|
|
124
|
+
assert.equal((await controller.endRoom('daily-default')).status.phase, 'Ended');
|
|
125
|
+
assert.deepEqual(providerCalls.map(([name]) => name), ['createRoom', 'getRoom', 'getStats', 'startRecording', 'stopRecording', 'endRoom']);
|
|
126
|
+
assert.ok(persisted.length >= 4);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('Jitsi meeting controller auto-dispatches template agents when autoJoin is enabled', async () => {
|
|
130
|
+
const calls = [];
|
|
131
|
+
const activeMeeting = meeting({
|
|
132
|
+
spec: {
|
|
133
|
+
templateRef: 'standup-template',
|
|
134
|
+
},
|
|
135
|
+
status: { phase: 'Active', roomUrl: 'https://meet.example/daily-default' },
|
|
136
|
+
});
|
|
137
|
+
const controller = createJitsiMeetingController({
|
|
138
|
+
resourceGateway: {
|
|
139
|
+
async list() { return { items: [activeMeeting] }; },
|
|
140
|
+
async get() { return activeMeeting; },
|
|
141
|
+
},
|
|
142
|
+
dispatchController: {
|
|
143
|
+
async createManualDispatch(input) {
|
|
144
|
+
calls.push(input);
|
|
145
|
+
return { error: false, run: { metadata: { name: `run-${calls.length}` } } };
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const results = await controller.dispatchAutoJoinAgents('daily', {
|
|
151
|
+
resources: {
|
|
152
|
+
JitsiMeetingTemplate: [
|
|
153
|
+
createResource('JitsiMeetingTemplate', { name: 'standup-template', namespace: 'kradle-org-default' }, {
|
|
154
|
+
organizationRef: 'default',
|
|
155
|
+
providerRef: 'jitsi-prod',
|
|
156
|
+
displayName: 'Standup',
|
|
157
|
+
agentConfig: { autoJoin: true },
|
|
158
|
+
participants: { autoInvite: [{ type: 'agentStack', ref: 'standup-bot' }] },
|
|
159
|
+
}),
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
repository: 'repo',
|
|
163
|
+
ref: 'main',
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
assert.equal(results.length, 1);
|
|
167
|
+
assert.equal(calls[0].agentStack, 'standup-bot');
|
|
168
|
+
assert.equal(calls[0].meetingRef, 'daily');
|
|
169
|
+
assert.equal(calls[0].taskKind, 'meeting');
|
|
170
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { createResource, listResourceDefinitions, resourceSchemaForKind, storageClassForKind, validateResource } from '../src/index.js';
|
|
4
|
+
|
|
5
|
+
const JITSI_KINDS = {
|
|
6
|
+
JitsiMeetProvider: {
|
|
7
|
+
storage: 'etcd',
|
|
8
|
+
context: 'external-backends',
|
|
9
|
+
plural: 'jitsimeetproviders',
|
|
10
|
+
requiredSpec: ['organizationRef', 'endpoint', 'authMode'],
|
|
11
|
+
validSpec: { organizationRef: 'default', endpoint: 'https://meet.kradle.local', authMode: 'jwt' },
|
|
12
|
+
},
|
|
13
|
+
JitsiMeetingTemplate: {
|
|
14
|
+
storage: 'etcd',
|
|
15
|
+
context: 'agents',
|
|
16
|
+
plural: 'jitsimeetingtemplates',
|
|
17
|
+
requiredSpec: ['organizationRef', 'providerRef', 'displayName'],
|
|
18
|
+
validSpec: { organizationRef: 'default', providerRef: 'jitsi-prod', displayName: 'Daily Standup' },
|
|
19
|
+
},
|
|
20
|
+
JitsiMeeting: {
|
|
21
|
+
storage: 'postgres',
|
|
22
|
+
context: 'agents',
|
|
23
|
+
plural: 'jitsimeetings',
|
|
24
|
+
requiredSpec: ['organizationRef', 'providerRef', 'roomId'],
|
|
25
|
+
validSpec: { organizationRef: 'default', providerRef: 'jitsi-prod', roomId: 'standup-20260530-default' },
|
|
26
|
+
},
|
|
27
|
+
JitsiRecording: {
|
|
28
|
+
storage: 'postgres',
|
|
29
|
+
context: 'agents',
|
|
30
|
+
plural: 'jitsirecordings',
|
|
31
|
+
requiredSpec: ['organizationRef', 'meetingRef', 'providerRef'],
|
|
32
|
+
validSpec: { organizationRef: 'default', meetingRef: 'standup-2026-05-30', providerRef: 'jitsi-prod' },
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
test('Jitsi resource kinds are registered with #624 frozen storage contracts', () => {
|
|
37
|
+
const definitions = listResourceDefinitions();
|
|
38
|
+
for (const [kind, expected] of Object.entries(JITSI_KINDS)) {
|
|
39
|
+
const definition = definitions.find((entry) => entry.kind === kind);
|
|
40
|
+
assert.ok(definition, `${kind} must be present in listResourceDefinitions`);
|
|
41
|
+
assert.equal(definition.storage, expected.storage);
|
|
42
|
+
assert.equal(definition.context, expected.context);
|
|
43
|
+
assert.equal(definition.plural, expected.plural);
|
|
44
|
+
assert.deepEqual(definition.requiredSpec, expected.requiredSpec);
|
|
45
|
+
assert.equal(storageClassForKind(kind), expected.storage);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('Jitsi resource schemas expose documented required spec fields', () => {
|
|
50
|
+
for (const [kind, expected] of Object.entries(JITSI_KINDS)) {
|
|
51
|
+
const schema = resourceSchemaForKind(kind);
|
|
52
|
+
assert.equal(schema.kind, kind);
|
|
53
|
+
assert.equal(schema.plural, expected.plural);
|
|
54
|
+
assert.equal(schema.storage, expected.storage);
|
|
55
|
+
assert.deepEqual(schema.required.spec, expected.requiredSpec);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('Jitsi resources validate required org-scoped specs', () => {
|
|
60
|
+
for (const [kind, expected] of Object.entries(JITSI_KINDS)) {
|
|
61
|
+
const resource = createResource(kind, { name: kind.toLowerCase(), namespace: 'kradle-org-default' }, expected.validSpec);
|
|
62
|
+
assert.equal(resource.kind, kind);
|
|
63
|
+
assert.equal(validateResource(resource), resource);
|
|
64
|
+
|
|
65
|
+
for (const field of expected.requiredSpec) {
|
|
66
|
+
const invalid = createResource(kind, { name: `${kind.toLowerCase()}-missing-${field}` }, {
|
|
67
|
+
...expected.validSpec,
|
|
68
|
+
[field]: '',
|
|
69
|
+
});
|
|
70
|
+
assert.throws(() => validateResource(invalid), new RegExp(`${kind} spec.${field} is required`));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { createJitsiSyncController } from '../src/index.js';
|
|
4
|
+
|
|
5
|
+
test('Jitsi sync controller normalizes room, participant, and recording events', async () => {
|
|
6
|
+
const persisted = [];
|
|
7
|
+
const emitted = [];
|
|
8
|
+
const controller = createJitsiSyncController({
|
|
9
|
+
persistFn: async (resource) => persisted.push(resource),
|
|
10
|
+
eventBus: { emit: (event) => emitted.push(event) },
|
|
11
|
+
now: () => new Date('2026-05-30T12:00:00Z'),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const normalized = controller.normalizeEvent({
|
|
15
|
+
type: 'participant_joined',
|
|
16
|
+
room: 'daily-default',
|
|
17
|
+
participant: { id: 'alice', name: 'Alice', type: 'user' },
|
|
18
|
+
providerRef: 'jitsi-prod',
|
|
19
|
+
timestamp: '2026-05-30T12:01:00Z',
|
|
20
|
+
});
|
|
21
|
+
assert.equal(normalized.eventType, 'participant-joined');
|
|
22
|
+
assert.equal(normalized.roomId, 'daily-default');
|
|
23
|
+
assert.equal(normalized.participant.id, 'alice');
|
|
24
|
+
|
|
25
|
+
const active = await controller.syncRoom('daily-default', {
|
|
26
|
+
eventType: 'room-created',
|
|
27
|
+
organizationRef: 'default',
|
|
28
|
+
providerRef: 'jitsi-prod',
|
|
29
|
+
roomUrl: 'https://meet.example/daily-default',
|
|
30
|
+
displayName: 'Daily',
|
|
31
|
+
});
|
|
32
|
+
assert.equal(active.kind, 'JitsiMeeting');
|
|
33
|
+
assert.equal(active.status.phase, 'Active');
|
|
34
|
+
assert.equal(emitted.at(-1).type, 'meeting-created');
|
|
35
|
+
|
|
36
|
+
await controller.syncParticipant('daily-default', {
|
|
37
|
+
eventType: 'participant-joined',
|
|
38
|
+
participant: { id: 'alice', name: 'Alice', type: 'user', audioMuted: false },
|
|
39
|
+
organizationRef: 'default',
|
|
40
|
+
});
|
|
41
|
+
const joinedAgain = await controller.syncParticipant('daily-default', {
|
|
42
|
+
eventType: 'participant-joined',
|
|
43
|
+
participant: { id: 'alice', name: 'Alice Updated', type: 'user' },
|
|
44
|
+
organizationRef: 'default',
|
|
45
|
+
});
|
|
46
|
+
assert.equal(joinedAgain.status.participants.current.length, 1, 'participant join must be idempotent by id');
|
|
47
|
+
assert.equal(joinedAgain.status.participants.total, 1);
|
|
48
|
+
assert.equal(joinedAgain.status.participants.peak, 1);
|
|
49
|
+
assert.equal(emitted.at(-1).type, 'participant-joined');
|
|
50
|
+
|
|
51
|
+
const left = await controller.syncParticipant('daily-default', {
|
|
52
|
+
eventType: 'participant-left',
|
|
53
|
+
participant: { id: 'alice', type: 'user' },
|
|
54
|
+
organizationRef: 'default',
|
|
55
|
+
});
|
|
56
|
+
assert.equal(left.status.participants.current.length, 0);
|
|
57
|
+
assert.equal(left.status.participants.total, 1);
|
|
58
|
+
assert.equal(left.status.participants.peak, 1);
|
|
59
|
+
assert.equal(emitted.at(-1).type, 'participant-left');
|
|
60
|
+
|
|
61
|
+
const recording = await controller.syncRecording('daily-default', {
|
|
62
|
+
eventType: 'recording-started',
|
|
63
|
+
recordingId: 'rec-1',
|
|
64
|
+
organizationRef: 'default',
|
|
65
|
+
providerRef: 'jitsi-prod',
|
|
66
|
+
meetingRef: 'daily-default',
|
|
67
|
+
});
|
|
68
|
+
assert.equal(recording.kind, 'JitsiRecording');
|
|
69
|
+
assert.equal(recording.status.phase, 'Recording');
|
|
70
|
+
assert.equal(emitted.at(-1).type, 'recording-started');
|
|
71
|
+
|
|
72
|
+
const stopped = await controller.syncRecording('daily-default', {
|
|
73
|
+
eventType: 'recording-stopped',
|
|
74
|
+
recordingId: 'rec-1',
|
|
75
|
+
duration: 120,
|
|
76
|
+
organizationRef: 'default',
|
|
77
|
+
});
|
|
78
|
+
assert.equal(stopped.status.phase, 'Completed');
|
|
79
|
+
assert.equal(stopped.status.duration, 120);
|
|
80
|
+
|
|
81
|
+
const ended = await controller.syncRoom('daily-default', { eventType: 'room-destroyed', organizationRef: 'default' });
|
|
82
|
+
assert.equal(ended.status.phase, 'Ended');
|
|
83
|
+
const duplicate = await controller.syncParticipant('daily-default', {
|
|
84
|
+
eventType: 'participant-joined',
|
|
85
|
+
deliveryId: 'delivery-1',
|
|
86
|
+
participant: { id: 'bob', name: 'Bob', type: 'user' },
|
|
87
|
+
organizationRef: 'default',
|
|
88
|
+
});
|
|
89
|
+
const duplicateAgain = await controller.syncParticipant('daily-default', {
|
|
90
|
+
eventType: 'participant-joined',
|
|
91
|
+
deliveryId: 'delivery-1',
|
|
92
|
+
participant: { id: 'charlie', name: 'Charlie', type: 'user' },
|
|
93
|
+
organizationRef: 'default',
|
|
94
|
+
});
|
|
95
|
+
assert.deepEqual(duplicateAgain.status.participants.current, duplicate.status.participants.current);
|
|
96
|
+
assert.ok(persisted.some((resource) => resource.kind === 'JitsiMeeting'));
|
|
97
|
+
assert.ok(persisted.some((resource) => resource.kind === 'JitsiRecording'));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('Jitsi sync controller maintains monotonic org-scoped provider watermarks', async () => {
|
|
101
|
+
const persisted = [];
|
|
102
|
+
const controller = createJitsiSyncController({ persistFn: (resource) => persisted.push(resource) });
|
|
103
|
+
|
|
104
|
+
await controller.updateWatermark('jitsi-prod', '2026-05-30T12:00:00Z', { organizationRef: 'default' });
|
|
105
|
+
await controller.updateWatermark('jitsi-prod', '2026-05-30T11:00:00Z', { organizationRef: 'default' });
|
|
106
|
+
await controller.updateWatermark('jitsi-prod', '2026-05-30T12:05:00Z', { organizationRef: 'default' });
|
|
107
|
+
|
|
108
|
+
assert.equal(controller.getWatermark('jitsi-prod'), '2026-05-30T12:05:00Z');
|
|
109
|
+
assert.equal(persisted.filter((resource) => resource.kind === 'ExternalSyncState').length, 2);
|
|
110
|
+
assert.equal(persisted.at(-1).metadata.namespace, 'kradle-org-default');
|
|
111
|
+
assert.equal(persisted.at(-1).spec.organizationRef, 'default');
|
|
112
|
+
});
|