@a5c-ai/krate 5.0.1-staging.660d2b90f → 5.0.1-staging.69cb593ea
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 -29
- package/bin/krate-demo.mjs +0 -0
- package/bin/krate-server.mjs +0 -0
- package/dist/krate-controller-ui.json +808 -10
- package/dist/krate-lifecycle.json +1 -1
- package/dist/krate-runtime-snapshot.json +223 -53
- package/dist/krate-summary.json +40 -3
- package/docs/agents/gaps-agent-mux-to-krate-crds.md +298 -0
- package/docs/architecture-v2.md +431 -0
- package/docs/openapi.yaml +1275 -0
- package/docs/requirements-v2.md +238 -0
- package/docs/sdk-api-reference.md +782 -0
- package/docs/system-spec-v2.md +352 -0
- package/docs/todos.md +4 -0
- package/docs/web-console-spec.md +433 -0
- package/package.json +1 -1
- package/scripts/validate-ui.mjs +305 -207
- package/src/agent-adapter-controller.js +169 -0
- package/src/agent-approval-controller.js +47 -0
- package/src/agent-dispatch-controller.js +130 -7
- 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 +1 -1
- package/src/agent-permission-review.js +102 -14
- 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 +52 -1
- package/src/agent-subagent-controller.js +160 -0
- package/src/agent-transport-binding-controller.js +121 -0
- package/src/agent-trigger-controller.js +273 -0
- package/src/agent-workspace-controller.js +702 -0
- package/src/agent-writeback-controller.js +302 -0
- package/src/api-controller.js +338 -3
- package/src/async-controller.js +207 -0
- package/src/audit-controller.js +191 -0
- package/src/auth.js +48 -6
- package/src/controller-client.js +112 -38
- package/src/controller-ui.js +96 -16
- package/src/data-plane.js +3 -2
- 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 +36 -0
- package/src/gitea-service.js +173 -0
- package/src/http-server.js +226 -0
- package/src/index.js +27 -0
- package/src/kubernetes-controller-async.js +531 -0
- package/src/kubernetes-controller.js +156 -84
- package/src/notification-controller.js +178 -0
- package/src/org-scoping.js +5 -0
- package/src/resource-model.js +26 -8
- package/src/runner-controller.js +272 -0
- package/src/snapshot-cache.js +157 -0
- package/tests/agent-adapter-controller.test.js +361 -0
- package/tests/agent-dispatch-controller.test.js +139 -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-permission-review-v2.test.js +317 -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 +35 -19
- package/tests/agent-secret-config-grant.test.js +231 -0
- package/tests/agent-session-transcript-controller.test.js +499 -0
- package/tests/agent-subagent-controller.test.js +201 -0
- package/tests/agent-transport-binding-controller.test.js +294 -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/controller-client.test.js +133 -0
- package/tests/deployment.test.js +43 -29
- package/tests/e2e/lifecycle.test.js +5 -2
- 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 +58 -6
- 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 +315 -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
package/tests/krate.test.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import test from 'node:test';
|
|
3
|
-
import { ControlPlane, GiteaGitService, GiteaRepositoryStore, RunnerScheduler, WebhookBus, createAdmissionPolicy, createAuthProviderConfig, buildAuthorizationRedirect, exchangeOAuthCodeForProfile, profileFromDelegatedHeaders, registerLoginProfile, createSessionCookie, parseSessionCookie, createInviteResource, createTeamResource, identityBackendSyncPlan, mapLoginProfileToKrateIdentity, createKrateApiController, createControllerUiModel, createKrateComponentCatalog, createKrateHandoffSummary, createKrateLifecycleSnapshot, createKrateMvpDemo, createPolicyRolloutModel, createResource, createKrateKubernetesReconciler, identityAccessReconciliationPlan, createKubernetesResourceClient, createKubernetesResourceGateway, createGiteaBackend, createGiteaRepositoryHosting, createKrateGitOpsPlan, listResourceDefinitions, mapOidcIdentity, resourceSchemaForKind, resourceToYaml, runSmokeAssertions } from '../src/index.js';
|
|
3
|
+
import { ControlPlane, GiteaGitService, GiteaRepositoryStore, RunnerScheduler, WebhookBus, createAdmissionPolicy, createAuthProviderConfig, buildAuthorizationRedirect, exchangeOAuthCodeForProfile, profileFromDelegatedHeaders, registerLoginProfile, createSessionCookie, parseSessionCookie, createInviteResource, createTeamResource, identityBackendSyncPlan, mapLoginProfileToKrateIdentity, createKrateApiController, createControllerUiModel, createKrateComponentCatalog, createKrateHandoffSummary, createKrateLifecycleSnapshot, createKrateMvpDemo, createPolicyRolloutModel, createResource, createKrateKubernetesReconciler, identityAccessReconciliationPlan, createKubernetesResourceClient, createKubernetesResourceGateway, createGiteaBackend, createGiteaRepositoryHosting, giteaIssueSyncPlan, githubProjectIssueSyncPlan, issueProjectRefs, issueRepositoryRefs, orgMemoryRepositoryName, createKrateGitOpsPlan, listResourceDefinitions, mapOidcIdentity, resourceSchemaForKind, resourceToYaml, runSmokeAssertions } from '../src/index.js';
|
|
4
4
|
|
|
5
5
|
const platform = mapOidcIdentity({ subject: 'platform', email: 'platform@example.com', groups: ['krate:platform-engineers'] });
|
|
6
6
|
const developer = mapOidcIdentity({ subject: 'dev', email: 'dev@example.com', groups: ['krate:developers'] });
|
|
@@ -61,12 +61,15 @@ test('Gitea repository hosting plan includes Argo CD deploy key and webhook inte
|
|
|
61
61
|
assert.match(hosting.httpUrl, /platform-config\.git$/);
|
|
62
62
|
assert.ok(hosting.integrationPlan.operations.some((step) => step.action === 'addDeployKey' && step.title === 'krate-argocd'));
|
|
63
63
|
assert.ok(hosting.integrationPlan.operations.some((step) => step.action === 'createWebhook'));
|
|
64
|
+
assert.equal(hosting.forgeRecords.issues, 'Gitea /repos/krate/_krate-system_/issues');
|
|
65
|
+
assert.equal(hosting.issueSync.repo, '_krate-system_');
|
|
66
|
+
assert.ok(hosting.issueSync.actions.some((step) => step.action === 'ensureOrgMemoryRepository'));
|
|
64
67
|
});
|
|
65
68
|
test('resource catalog exposes all ontology kinds and storage boundaries', () => {
|
|
66
69
|
const definitions = listResourceDefinitions();
|
|
67
|
-
assert.equal(definitions.length,
|
|
68
|
-
assert.deepEqual(definitions.filter((definition) => definition.storage === 'etcd').map((definition) => definition.kind), ['Organization', 'OrgNamespaceBinding', 'User', 'Team', 'Invite', 'IdentityMapping', 'AuthProvider', 'Repository', 'SSHKey', 'RepositoryPermission', 'WebhookSubscription', 'RefPolicy', 'BranchProtection', 'PolicyProfile', 'PolicyTemplate', 'PolicyBinding', 'PolicyExceptionRequest', 'View', 'Selector', 'RunnerPool', 'AgentStack', 'AgentSubagent', 'AgentToolProfile', 'AgentMcpServer', 'AgentSkill', 'AgentTriggerRule', 'AgentContextLabel', '
|
|
69
|
-
assert.deepEqual(definitions.filter((definition) => definition.storage === 'postgres').map((definition) => definition.kind), ['PullRequest', 'Issue', 'Review', 'Pipeline', 'Job', 'WebhookDelivery', 'AgentDispatchRun', 'AgentDispatchAttempt', 'AgentSession', 'AgentContextBundle', '
|
|
70
|
+
assert.equal(definitions.length, 75);
|
|
71
|
+
assert.deepEqual(definitions.filter((definition) => definition.storage === 'etcd').map((definition) => definition.kind), ['Organization', 'OrgNamespaceBinding', 'User', 'Team', 'Invite', 'IdentityMapping', 'AuthProvider', 'Repository', 'SSHKey', 'RepositoryPermission', 'WebhookSubscription', 'RefPolicy', 'BranchProtection', 'PolicyProfile', 'PolicyTemplate', 'PolicyBinding', 'PolicyExceptionRequest', 'View', 'Selector', 'RunnerPool', 'AgentStack', 'AgentSubagent', 'AgentToolProfile', 'AgentMcpServer', 'AgentSkill', 'AgentTriggerRule', 'AgentContextLabel', 'KrateWorkspacePolicy', 'AgentServiceAccount', 'AgentRoleBinding', 'AgentSecretGrant', 'AgentConfigGrant', 'KrateWorkspace', 'AgentAdapter', 'AgentTransportBinding', 'AgentProviderConfig', 'KrateProject', 'AgentGatewayConfig', 'AgentMemoryRepository', 'AgentMemorySource', 'AgentMemoryOntology', 'AgentMemoryAssociation', 'ExternalBackendProvider', 'ExternalBackendBinding', 'ExternalBackendSyncPolicy', 'ExternalProviderCapabilityManifest']);
|
|
72
|
+
assert.deepEqual(definitions.filter((definition) => definition.storage === 'postgres').map((definition) => definition.kind), ['PullRequest', 'Issue', 'Review', 'Pipeline', 'Job', 'WebhookDelivery', 'AgentDispatchRun', 'AgentDispatchAttempt', 'AgentSession', 'AgentContextBundle', 'KrateArtifact', 'AgentApproval', 'AgentTriggerExecution', 'AgentCapabilityRequirement', 'WorkItemSessionLink', 'WorkItemWorkspaceLink', 'AgentSessionTranscript', 'AgentSessionAttachment', 'KrateWorkspaceRuntime', 'AgentMemorySnapshot', 'AgentMemoryQuery', 'AgentMemoryUpdate', 'AgentRunMemoryImport', 'ExternalWebhookDelivery', 'ExternalSyncEvent', 'ExternalSyncState', 'ExternalWriteIntent', 'ExternalSyncConflict', 'ExternalObjectLink']);
|
|
70
73
|
assert.equal(resourceSchemaForKind('Organization').plural, 'organizations');
|
|
71
74
|
assert.equal(resourceSchemaForKind('User').plural, 'users');
|
|
72
75
|
assert.equal(resourceSchemaForKind('Team').plural, 'teams');
|
|
@@ -75,13 +78,39 @@ test('resource catalog exposes all ontology kinds and storage boundaries', () =>
|
|
|
75
78
|
assert.equal(resourceSchemaForKind('AuthProvider').plural, 'authproviders');
|
|
76
79
|
assert.equal(resourceSchemaForKind('SSHKey').plural, 'sshkeys');
|
|
77
80
|
assert.equal(resourceSchemaForKind('RepositoryPermission').plural, 'repositorypermissions');
|
|
81
|
+
assert.equal(resourceSchemaForKind('KrateProject').plural, 'krateprojects');
|
|
78
82
|
const schema = resourceSchemaForKind('PullRequest');
|
|
79
83
|
assert.deepEqual(schema.required.spec, ['organizationRef', 'repository', 'title']);
|
|
80
84
|
assert.match(resourceToYaml(createResource('Repository', { name: 'yaml-demo' }, { organizationRef: 'default', visibility: 'private' })), /kind: Repository/);
|
|
81
85
|
assert.match(resourceToYaml(createResource('SSHKey', { name: 'alice-key' }, { organizationRef: 'default', scope: 'user', key: 'ssh-ed25519 AAAA' })), /kind: SSHKey/);
|
|
86
|
+
assert.match(resourceToYaml(createResource('KrateProject', { name: 'triage' }, { organizationRef: 'default', displayName: 'Triage', repositories: ['app'] })), /kind: KrateProject/);
|
|
82
87
|
assert.match(resourceToYaml({ apiVersion: 'core.oam.dev/v1beta1', kind: 'Application', spec: { components: [{ name: 'web', properties: { image: 'krate/mvp-model:0.1.0' }, traits: [{ type: 'scaler', properties: { replicas: 1 } }] }] } }), /components:\n - name: web\n properties:\n image: krate\/mvp-model:0.1.0\n traits:\n - type: scaler/);
|
|
83
88
|
});
|
|
84
89
|
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
test('issue project and repository associations are normalized for scoped views', () => {
|
|
93
|
+
const issue = createResource('Issue', { name: 'bug-1', annotations: { 'krate.a5c.ai/repositories': 'app, docs', 'krate.a5c.ai/projects': 'planning' } }, {
|
|
94
|
+
organizationRef: 'default',
|
|
95
|
+
title: 'Bug',
|
|
96
|
+
repositoryRefs: [{ name: 'api' }, { repository: 'ui' }],
|
|
97
|
+
repositories: ['worker'],
|
|
98
|
+
projectRef: { name: 'release' },
|
|
99
|
+
projects: ['roadmap']
|
|
100
|
+
});
|
|
101
|
+
assert.deepEqual(issueRepositoryRefs(issue).sort(), ['api', 'app', 'docs', 'ui', 'worker']);
|
|
102
|
+
assert.deepEqual(issueProjectRefs(issue).sort(), ['planning', 'release', 'roadmap']);
|
|
103
|
+
|
|
104
|
+
const gitea = giteaIssueSyncPlan({ org: 'default', project: 'release', issue, repositories: issueRepositoryRefs(issue) });
|
|
105
|
+
assert.equal(orgMemoryRepositoryName('default'), '_default_');
|
|
106
|
+
assert.equal(gitea.repo, '_default_');
|
|
107
|
+
assert.deepEqual(gitea.metadataKeys, ['krate.a5c.ai/project', 'krate.a5c.ai/repositories']);
|
|
108
|
+
assert.ok(gitea.actions.some((step) => step.action === 'writeIssueRepositoryMetadata'));
|
|
109
|
+
|
|
110
|
+
const github = githubProjectIssueSyncPlan({ org: 'default', project: 'release', issue, repositories: issueRepositoryRefs(issue) });
|
|
111
|
+
assert.equal(github.project, 'release');
|
|
112
|
+
assert.ok(github.actions.includes('syncProjectItem'));
|
|
113
|
+
});
|
|
85
114
|
|
|
86
115
|
|
|
87
116
|
test('Krate auth resources map sign-in, teams, invites, and repository identities', () => {
|
|
@@ -212,7 +241,7 @@ test('OAuth callback route completes fake SSO exchange and registers Krate ident
|
|
|
212
241
|
const response = await handler(new Request('https://krate.example.test/api/auth/callback/sso?code=fake-code&state=state'), { params: { provider: 'sso' } });
|
|
213
242
|
|
|
214
243
|
assert.equal(response.status, 302);
|
|
215
|
-
assert.equal(response.headers.get('location'), '/people');
|
|
244
|
+
assert.equal(response.headers.get('location'), '/orgs/default/people');
|
|
216
245
|
assert.equal(response.headers.get('x-krate-user'), 'route-alice');
|
|
217
246
|
assert.match(response.headers.get('set-cookie'), /krate_session=/);
|
|
218
247
|
assert.deepEqual(fetchCalls.map((call) => call.url), ['https://idp.example.test/token', 'https://idp.example.test/userinfo']);
|
|
@@ -290,7 +319,7 @@ test('Delegated identity route redirects localhost fallback even when Kubernetes
|
|
|
290
319
|
const response = await GET(new Request('http://localhost:3000/api/auth/delegated?user=Route%20Alice&email=route@example.test&groups=krate:developers'));
|
|
291
320
|
|
|
292
321
|
assert.equal(response.status, 302);
|
|
293
|
-
assert.equal(response.headers.get('location'), '/people');
|
|
322
|
+
assert.equal(response.headers.get('location'), '/orgs/default/people');
|
|
294
323
|
assert.equal(response.headers.get('x-krate-user'), 'route-alice');
|
|
295
324
|
assert.match(response.headers.get('set-cookie'), /krate_session=/);
|
|
296
325
|
} finally {
|
|
@@ -300,6 +329,29 @@ test('Delegated identity route redirects localhost fallback even when Kubernetes
|
|
|
300
329
|
}
|
|
301
330
|
});
|
|
302
331
|
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
test('controller UI model keeps namespace-scoped resources visible for their org', () => {
|
|
335
|
+
const model = createControllerUiModel({
|
|
336
|
+
source: 'kubernetes',
|
|
337
|
+
namespace: 'krate-system',
|
|
338
|
+
generatedAt: 'test-time',
|
|
339
|
+
kubectl: { available: true, context: 'kind-krate', errors: [] },
|
|
340
|
+
crds: [],
|
|
341
|
+
resources: {
|
|
342
|
+
Organization: [{ apiVersion: 'krate.a5c.ai/v1alpha1', kind: 'Organization', metadata: { name: 'default', namespace: 'krate-system' }, spec: { slug: 'default', namespaceName: 'krate-org-default' } }],
|
|
343
|
+
RunnerPool: [{ apiVersion: 'krate.a5c.ai/v1alpha1', kind: 'RunnerPool', metadata: { name: 'default', namespace: 'krate-org-default' }, spec: { image: 'ubuntu:24.04' } }]
|
|
344
|
+
},
|
|
345
|
+
events: [],
|
|
346
|
+
permissions: [],
|
|
347
|
+
storage: {},
|
|
348
|
+
commands: []
|
|
349
|
+
}, { organization: 'default' });
|
|
350
|
+
|
|
351
|
+
assert.equal(model.metrics.runnerPools, 1);
|
|
352
|
+
assert.deepEqual(model.resources.find((resource) => resource.kind === 'RunnerPool').names, ['default']);
|
|
353
|
+
});
|
|
354
|
+
|
|
303
355
|
test('Krate delivery resources surface through controller UI model', () => {
|
|
304
356
|
const model = createControllerUiModel({
|
|
305
357
|
source: 'kubernetes',
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for C3: Wire memory search to the real query engine
|
|
3
|
+
*
|
|
4
|
+
* Verifies that:
|
|
5
|
+
* - queryAgentMemory delegates to queryMemory from agent-memory-query.js
|
|
6
|
+
* - searchGraph delegates to queryGraph from agent-memory-query.js
|
|
7
|
+
* - searchGrep delegates to queryGrep from agent-memory-query.js
|
|
8
|
+
*
|
|
9
|
+
* These tests verify wiring — they exercise real data flows, not just interface shape.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import test from 'node:test';
|
|
14
|
+
import { createAgentMemoryController } from '../src/agent-memory-controller.js';
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Fixtures
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
function makeRecords() {
|
|
21
|
+
return [
|
|
22
|
+
{
|
|
23
|
+
id: 'service/auth-api',
|
|
24
|
+
nodeKind: 'Service',
|
|
25
|
+
attributes: { name: 'auth-api', language: 'typescript', team: 'platform' },
|
|
26
|
+
edges: [
|
|
27
|
+
{ target: 'service/user-db', kind: 'depends-on' },
|
|
28
|
+
{ target: 'team/platform', kind: 'owned-by' },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'service/user-db',
|
|
33
|
+
nodeKind: 'Service',
|
|
34
|
+
attributes: { name: 'user-db', language: 'go', team: 'data' },
|
|
35
|
+
edges: [{ target: 'infra/postgres-cluster', kind: 'depends-on' }],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'team/platform',
|
|
39
|
+
nodeKind: 'Team',
|
|
40
|
+
attributes: { name: 'platform', lead: 'alice' },
|
|
41
|
+
edges: [],
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function makeDocuments() {
|
|
47
|
+
return [
|
|
48
|
+
{
|
|
49
|
+
path: 'docs/architecture.md',
|
|
50
|
+
content: [
|
|
51
|
+
'The auth-api service handles authentication.',
|
|
52
|
+
'It uses JWT tokens for session management.',
|
|
53
|
+
'The service connects to user-db for persistence.',
|
|
54
|
+
].join('\n'),
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
path: 'src/auth/handler.ts',
|
|
58
|
+
content: [
|
|
59
|
+
'export function handleAuth(req) {',
|
|
60
|
+
' const token = req.headers.authorization;',
|
|
61
|
+
' return validateToken(token);',
|
|
62
|
+
'}',
|
|
63
|
+
].join('\n'),
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// queryAgentMemory — delegating to queryMemory from agent-memory-query.js
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
test('queryAgentMemory with mode graph-only calls queryGraph and returns results', () => {
|
|
73
|
+
const controller = createAgentMemoryController();
|
|
74
|
+
const records = makeRecords();
|
|
75
|
+
|
|
76
|
+
const result = controller.queryAgentMemory({
|
|
77
|
+
query: 'auth-api',
|
|
78
|
+
mode: 'graph-only',
|
|
79
|
+
records,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
assert.ok(result !== null && typeof result === 'object', 'Should return a result object');
|
|
83
|
+
assert.ok(result.graph !== null, 'graph results should be present in graph-only mode');
|
|
84
|
+
assert.equal(result.grep, null, 'grep results should be null in graph-only mode');
|
|
85
|
+
assert.ok(typeof result.graph.totalMatches === 'number', 'graph.totalMatches should be a number');
|
|
86
|
+
assert.ok(result.graph.totalMatches >= 1, 'Should find auth-api in records');
|
|
87
|
+
|
|
88
|
+
// Verify stats are present (from query engine)
|
|
89
|
+
assert.ok('stats' in result, 'result should have stats');
|
|
90
|
+
assert.equal(result.stats.mode, 'graph-only', 'stats.mode should reflect requested mode');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('queryAgentMemory with mode grep-only calls queryGrep and returns results', () => {
|
|
94
|
+
const controller = createAgentMemoryController();
|
|
95
|
+
const documents = makeDocuments();
|
|
96
|
+
|
|
97
|
+
const result = controller.queryAgentMemory({
|
|
98
|
+
query: 'auth-api',
|
|
99
|
+
mode: 'grep-only',
|
|
100
|
+
documents,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
assert.ok(result !== null && typeof result === 'object', 'Should return a result object');
|
|
104
|
+
assert.equal(result.graph, null, 'graph results should be null in grep-only mode');
|
|
105
|
+
assert.ok(result.grep !== null, 'grep results should be present in grep-only mode');
|
|
106
|
+
assert.ok(typeof result.grep.totalMatches === 'number', 'grep.totalMatches should be a number');
|
|
107
|
+
assert.ok(result.grep.totalMatches >= 1, 'Should find auth-api in documents');
|
|
108
|
+
|
|
109
|
+
// Verify highlighted output (real queryGrep from query engine produces ** markers)
|
|
110
|
+
const highlighted = result.grep.excerpts.some(e => e.highlighted && e.highlighted.includes('**'));
|
|
111
|
+
assert.ok(highlighted, 'Excerpts should have highlighted markers from the real query engine');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('queryAgentMemory with mode graph-and-grep calls queryMemory and returns both', () => {
|
|
115
|
+
const controller = createAgentMemoryController();
|
|
116
|
+
const records = makeRecords();
|
|
117
|
+
const documents = makeDocuments();
|
|
118
|
+
|
|
119
|
+
const result = controller.queryAgentMemory({
|
|
120
|
+
query: 'auth-api',
|
|
121
|
+
mode: 'graph-and-grep',
|
|
122
|
+
records,
|
|
123
|
+
documents,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
assert.ok(result.graph !== null, 'graph results should be present');
|
|
127
|
+
assert.ok(result.grep !== null, 'grep results should be present');
|
|
128
|
+
assert.ok(typeof result.stats.totalMatches === 'number', 'stats.totalMatches should be a number');
|
|
129
|
+
assert.equal(result.stats.totalMatches, result.stats.graphCount + result.stats.grepCount,
|
|
130
|
+
'totalMatches should equal graphCount + grepCount');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('queryAgentMemory returns empty results for no matches', () => {
|
|
134
|
+
const controller = createAgentMemoryController();
|
|
135
|
+
const records = makeRecords();
|
|
136
|
+
const documents = makeDocuments();
|
|
137
|
+
|
|
138
|
+
const result = controller.queryAgentMemory({
|
|
139
|
+
query: 'zzz-no-match-xyz',
|
|
140
|
+
mode: 'graph-and-grep',
|
|
141
|
+
records,
|
|
142
|
+
documents,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
assert.equal(result.graph.totalMatches, 0, 'graph should have 0 matches');
|
|
146
|
+
assert.equal(result.grep.totalMatches, 0, 'grep should have 0 matches');
|
|
147
|
+
assert.equal(result.stats.totalMatches, 0, 'stats.totalMatches should be 0');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('queryAgentMemory rejects missing query text', () => {
|
|
151
|
+
const controller = createAgentMemoryController();
|
|
152
|
+
|
|
153
|
+
assert.throws(
|
|
154
|
+
() => controller.queryAgentMemory({ mode: 'graph-only', records: [] }),
|
|
155
|
+
/query text is required/,
|
|
156
|
+
'Should throw when query is missing'
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('queryAgentMemory rejects empty query text', () => {
|
|
161
|
+
const controller = createAgentMemoryController();
|
|
162
|
+
|
|
163
|
+
assert.throws(
|
|
164
|
+
() => controller.queryAgentMemory({ query: '', mode: 'graph-only', records: [] }),
|
|
165
|
+
/non-empty string/,
|
|
166
|
+
'Should throw when query is empty string'
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// searchGraph — delegates to queryGraph from agent-memory-query.js
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
test('searchGraph delegates to queryGraph from agent-memory-query.js', () => {
|
|
175
|
+
const controller = createAgentMemoryController();
|
|
176
|
+
const records = makeRecords();
|
|
177
|
+
|
|
178
|
+
// The real queryGraph produces sorted results by score (descending)
|
|
179
|
+
// and uses `depth` semantics — verify via a real query
|
|
180
|
+
const result = controller.searchGraph({
|
|
181
|
+
records,
|
|
182
|
+
kinds: [],
|
|
183
|
+
query: 'auth-api',
|
|
184
|
+
edgeDepth: 1,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
assert.ok(result.totalMatches >= 1, 'Should find auth-api');
|
|
188
|
+
assert.ok(result.matches.length >= 1, 'matches array should be populated');
|
|
189
|
+
|
|
190
|
+
// The real queryGraph sorts by score descending — verify first result is highest scored
|
|
191
|
+
const scores = result.matches.map(m => m.score);
|
|
192
|
+
for (let i = 1; i < scores.length; i++) {
|
|
193
|
+
assert.ok(scores[i] <= scores[i - 1], 'Results should be sorted descending by score (real queryGraph behavior)');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Verify highlighted or edge following via depth — auth-api should have edges to user-db
|
|
197
|
+
const authMatch = result.matches.find(m => m.record.id === 'service/auth-api');
|
|
198
|
+
assert.ok(authMatch, 'Should find auth-api record');
|
|
199
|
+
const edgeTargets = authMatch.edges.map(e => e.target);
|
|
200
|
+
assert.ok(edgeTargets.includes('service/user-db'), 'Should follow edge to user-db at depth 1');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('searchGraph uses flat edges array when delegating to queryGraph', () => {
|
|
204
|
+
const controller = createAgentMemoryController();
|
|
205
|
+
const records = [
|
|
206
|
+
{ id: 'node/a', nodeKind: 'Service', attributes: { name: 'node-a' }, edges: [] },
|
|
207
|
+
{ id: 'node/b', nodeKind: 'Service', attributes: { name: 'node-b' }, edges: [] },
|
|
208
|
+
];
|
|
209
|
+
const flatEdges = [{ source: 'node/a', target: 'node/b', kind: 'calls' }];
|
|
210
|
+
|
|
211
|
+
const result = controller.searchGraph({
|
|
212
|
+
records,
|
|
213
|
+
edges: flatEdges,
|
|
214
|
+
query: 'node-a',
|
|
215
|
+
edgeDepth: 1,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const matchA = result.matches.find(m => m.record.id === 'node/a');
|
|
219
|
+
assert.ok(matchA, 'Should match node/a');
|
|
220
|
+
const edgeTargets = matchA.edges.map(e => e.target);
|
|
221
|
+
assert.ok(edgeTargets.includes('node/b'), 'Flat edge should be followed via queryGraph');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// searchGrep — delegates to queryGrep from agent-memory-query.js
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
test('searchGrep delegates to queryGrep from agent-memory-query.js', () => {
|
|
229
|
+
const controller = createAgentMemoryController();
|
|
230
|
+
const documents = makeDocuments();
|
|
231
|
+
|
|
232
|
+
// The real queryGrep produces `highlighted` field with ** markers — verify this
|
|
233
|
+
const result = controller.searchGrep({
|
|
234
|
+
documents,
|
|
235
|
+
pattern: 'auth-api',
|
|
236
|
+
maxMatches: 25,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
assert.ok(result.totalMatches >= 1, 'Should find auth-api in documents');
|
|
240
|
+
assert.ok(result.excerpts.length >= 1, 'excerpts should be populated');
|
|
241
|
+
|
|
242
|
+
// The real queryGrep produces `highlighted` with ** markers
|
|
243
|
+
// This is the key differentiator vs the stub — the stub did not produce highlighted
|
|
244
|
+
const withHighlight = result.excerpts.filter(e => e.highlighted && e.highlighted.includes('**auth-api**'));
|
|
245
|
+
assert.ok(withHighlight.length >= 1, 'At least one excerpt should have **auth-api** in highlighted (real queryGrep)');
|
|
246
|
+
|
|
247
|
+
// Also verify contextStart and contextEnd (fields only present in real queryGrep)
|
|
248
|
+
const hasContextBounds = result.excerpts.every(e =>
|
|
249
|
+
typeof e.contextStart === 'number' && typeof e.contextEnd === 'number'
|
|
250
|
+
);
|
|
251
|
+
assert.ok(hasContextBounds, 'All excerpts should have contextStart and contextEnd from real queryGrep');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test('searchGrep path filtering works via queryGrep delegation', () => {
|
|
255
|
+
const controller = createAgentMemoryController();
|
|
256
|
+
const documents = makeDocuments();
|
|
257
|
+
|
|
258
|
+
const result = controller.searchGrep({
|
|
259
|
+
documents,
|
|
260
|
+
paths: ['docs/*'],
|
|
261
|
+
pattern: 'auth',
|
|
262
|
+
maxMatches: 25,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
assert.ok(result.totalMatches >= 1, 'Should find auth in docs/');
|
|
266
|
+
assert.ok(
|
|
267
|
+
result.excerpts.every(e => e.path.startsWith('docs/')),
|
|
268
|
+
'All excerpts should be from docs/ via queryGrep path filter'
|
|
269
|
+
);
|
|
270
|
+
});
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { createNotificationController } from '../src/notification-controller.js';
|
|
4
|
+
|
|
5
|
+
// Helper to create a fresh controller for each test
|
|
6
|
+
function makeController() {
|
|
7
|
+
return createNotificationController();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
test('createNotification with AgentDispatchRun completed → type run-complete, severity info', () => {
|
|
11
|
+
const ctrl = makeController();
|
|
12
|
+
const notif = ctrl.createNotification({ type: 'AgentDispatchRun', status: 'completed', name: 'my-run', org: 'test-org' });
|
|
13
|
+
assert.equal(notif.type, 'run-complete');
|
|
14
|
+
assert.equal(notif.severity, 'info');
|
|
15
|
+
assert.match(notif.title, /completed/i);
|
|
16
|
+
assert.equal(notif.org, 'test-org');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('createNotification with AgentDispatchRun failed → type run-complete, severity error', () => {
|
|
20
|
+
const ctrl = makeController();
|
|
21
|
+
const notif = ctrl.createNotification({ type: 'AgentDispatchRun', status: 'failed', name: 'my-run', org: 'test-org' });
|
|
22
|
+
assert.equal(notif.type, 'run-complete');
|
|
23
|
+
assert.equal(notif.severity, 'error');
|
|
24
|
+
assert.match(notif.title, /failed/i);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('createNotification with AgentApproval pending → type approval-needed, severity warning', () => {
|
|
28
|
+
const ctrl = makeController();
|
|
29
|
+
const notif = ctrl.createNotification({ type: 'AgentApproval', status: 'pending', action: 'tool-use', org: 'test-org' });
|
|
30
|
+
assert.equal(notif.type, 'approval-needed');
|
|
31
|
+
assert.equal(notif.severity, 'warning');
|
|
32
|
+
assert.match(notif.title, /approval/i);
|
|
33
|
+
assert.match(notif.title, /tool-use/i);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('createNotification with ExternalSyncConflict → type conflict-detected, severity warning', () => {
|
|
37
|
+
const ctrl = makeController();
|
|
38
|
+
const notif = ctrl.createNotification({ type: 'ExternalSyncConflict', resourceRef: 'my-repo', org: 'test-org' });
|
|
39
|
+
assert.equal(notif.type, 'conflict-detected');
|
|
40
|
+
assert.equal(notif.severity, 'warning');
|
|
41
|
+
assert.match(notif.title, /conflict/i);
|
|
42
|
+
assert.match(notif.title, /my-repo/i);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('createNotification with KrateWorkspace claimed → type workspace-ready, severity info', () => {
|
|
46
|
+
const ctrl = makeController();
|
|
47
|
+
const notif = ctrl.createNotification({ type: 'KrateWorkspace', claimed: true, name: 'ws-1', claimedBy: 'run-abc', org: 'test-org' });
|
|
48
|
+
assert.equal(notif.type, 'workspace-ready');
|
|
49
|
+
assert.equal(notif.severity, 'info');
|
|
50
|
+
assert.match(notif.title, /workspace/i);
|
|
51
|
+
assert.match(notif.title, /ws-1/i);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('createNotification returns notification with id, createdAt, read: false', () => {
|
|
55
|
+
const ctrl = makeController();
|
|
56
|
+
const notif = ctrl.createNotification({ type: 'AgentDispatchRun', status: 'completed', name: 'run-x', org: 'test-org' });
|
|
57
|
+
assert.equal(typeof notif.id, 'string');
|
|
58
|
+
assert.ok(notif.id.length > 0);
|
|
59
|
+
assert.equal(typeof notif.createdAt, 'string');
|
|
60
|
+
assert.ok(notif.createdAt.length > 0);
|
|
61
|
+
assert.equal(notif.read, false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('createNotification default event → type system, severity info', () => {
|
|
65
|
+
const ctrl = makeController();
|
|
66
|
+
const notif = ctrl.createNotification({ type: 'UnknownEvent', org: 'test-org' });
|
|
67
|
+
assert.equal(notif.type, 'system');
|
|
68
|
+
assert.equal(notif.severity, 'info');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('listNotifications returns all notifications for org', () => {
|
|
72
|
+
const ctrl = makeController();
|
|
73
|
+
ctrl.createNotification({ type: 'AgentDispatchRun', status: 'completed', name: 'run-1', org: 'org-a' });
|
|
74
|
+
ctrl.createNotification({ type: 'AgentDispatchRun', status: 'completed', name: 'run-2', org: 'org-a' });
|
|
75
|
+
ctrl.createNotification({ type: 'AgentDispatchRun', status: 'completed', name: 'run-3', org: 'org-b' }); // different org
|
|
76
|
+
|
|
77
|
+
const results = ctrl.listNotifications('org-a');
|
|
78
|
+
assert.equal(results.length, 2);
|
|
79
|
+
assert.ok(results.every((n) => n.org === 'org-a'));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('listNotifications with { unreadOnly: true } filters out read notifications', () => {
|
|
83
|
+
const ctrl = makeController();
|
|
84
|
+
const n1 = ctrl.createNotification({ type: 'AgentDispatchRun', status: 'completed', name: 'run-1', org: 'org-a' });
|
|
85
|
+
ctrl.createNotification({ type: 'AgentDispatchRun', status: 'completed', name: 'run-2', org: 'org-a' });
|
|
86
|
+
ctrl.markAsRead(n1.id);
|
|
87
|
+
|
|
88
|
+
const unreadOnly = ctrl.listNotifications('org-a', { unreadOnly: true });
|
|
89
|
+
assert.equal(unreadOnly.length, 1);
|
|
90
|
+
assert.equal(unreadOnly[0].read, false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('listNotifications with { limit: 2 } caps results to 2', () => {
|
|
94
|
+
const ctrl = makeController();
|
|
95
|
+
ctrl.createNotification({ type: 'AgentDispatchRun', status: 'completed', name: 'run-1', org: 'org-a' });
|
|
96
|
+
ctrl.createNotification({ type: 'AgentDispatchRun', status: 'completed', name: 'run-2', org: 'org-a' });
|
|
97
|
+
ctrl.createNotification({ type: 'AgentDispatchRun', status: 'completed', name: 'run-3', org: 'org-a' });
|
|
98
|
+
|
|
99
|
+
const limited = ctrl.listNotifications('org-a', { limit: 2 });
|
|
100
|
+
assert.equal(limited.length, 2);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('listNotifications returns notifications sorted by createdAt descending', () => {
|
|
104
|
+
const ctrl = makeController();
|
|
105
|
+
const n1 = ctrl.createNotification({ type: 'AgentDispatchRun', status: 'completed', name: 'run-1', org: 'org-a' });
|
|
106
|
+
const n2 = ctrl.createNotification({ type: 'AgentDispatchRun', status: 'completed', name: 'run-2', org: 'org-a' });
|
|
107
|
+
|
|
108
|
+
// Sort order: they come back by createdAt descending (no guaranteed diff if same ms, just verify all present)
|
|
109
|
+
const results = ctrl.listNotifications('org-a');
|
|
110
|
+
assert.equal(results.length, 2);
|
|
111
|
+
// Both IDs present
|
|
112
|
+
const ids = results.map((n) => n.id);
|
|
113
|
+
assert.ok(ids.includes(n1.id));
|
|
114
|
+
assert.ok(ids.includes(n2.id));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('markAsRead sets the target notification read to true', () => {
|
|
118
|
+
const ctrl = makeController();
|
|
119
|
+
const notif = ctrl.createNotification({ type: 'AgentDispatchRun', status: 'completed', name: 'run-1', org: 'org-a' });
|
|
120
|
+
assert.equal(notif.read, false);
|
|
121
|
+
|
|
122
|
+
const result = ctrl.markAsRead(notif.id);
|
|
123
|
+
assert.equal(result, true);
|
|
124
|
+
|
|
125
|
+
const results = ctrl.listNotifications('org-a');
|
|
126
|
+
const found = results.find((n) => n.id === notif.id);
|
|
127
|
+
assert.equal(found.read, true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('markAsRead returns false for unknown id', () => {
|
|
131
|
+
const ctrl = makeController();
|
|
132
|
+
const result = ctrl.markAsRead('nonexistent-id');
|
|
133
|
+
assert.equal(result, false);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('markAllAsRead sets all org notifications as read and returns count', () => {
|
|
137
|
+
const ctrl = makeController();
|
|
138
|
+
ctrl.createNotification({ type: 'AgentDispatchRun', status: 'completed', name: 'run-1', org: 'org-a' });
|
|
139
|
+
ctrl.createNotification({ type: 'AgentDispatchRun', status: 'completed', name: 'run-2', org: 'org-a' });
|
|
140
|
+
ctrl.createNotification({ type: 'AgentDispatchRun', status: 'completed', name: 'run-3', org: 'org-a' });
|
|
141
|
+
|
|
142
|
+
const count = ctrl.markAllAsRead('org-a');
|
|
143
|
+
assert.equal(count, 3);
|
|
144
|
+
|
|
145
|
+
const unread = ctrl.listNotifications('org-a', { unreadOnly: true });
|
|
146
|
+
assert.equal(unread.length, 0);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('getUnreadCount returns correct count after some are read', () => {
|
|
150
|
+
const ctrl = makeController();
|
|
151
|
+
const n1 = ctrl.createNotification({ type: 'AgentDispatchRun', status: 'completed', name: 'run-1', org: 'org-a' });
|
|
152
|
+
ctrl.createNotification({ type: 'AgentDispatchRun', status: 'completed', name: 'run-2', org: 'org-a' });
|
|
153
|
+
ctrl.createNotification({ type: 'AgentDispatchRun', status: 'completed', name: 'run-3', org: 'org-a' });
|
|
154
|
+
|
|
155
|
+
assert.equal(ctrl.getUnreadCount('org-a'), 3);
|
|
156
|
+
ctrl.markAsRead(n1.id);
|
|
157
|
+
assert.equal(ctrl.getUnreadCount('org-a'), 2);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('getPreferences returns default prefs for new userId', () => {
|
|
161
|
+
const ctrl = makeController();
|
|
162
|
+
const prefs = ctrl.getPreferences('user-xyz');
|
|
163
|
+
assert.equal(prefs.runs, true);
|
|
164
|
+
assert.equal(prefs.approvals, true);
|
|
165
|
+
assert.equal(prefs.conflicts, true);
|
|
166
|
+
assert.equal(prefs.workspaces, true);
|
|
167
|
+
assert.equal(prefs.sound, false);
|
|
168
|
+
assert.equal(prefs.desktop, false);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('updatePreferences merges and persists new prefs', () => {
|
|
172
|
+
const ctrl = makeController();
|
|
173
|
+
const updated = ctrl.updatePreferences('user-abc', { sound: true, runs: false });
|
|
174
|
+
assert.equal(updated.sound, true);
|
|
175
|
+
assert.equal(updated.runs, false);
|
|
176
|
+
assert.equal(updated.approvals, true); // default preserved
|
|
177
|
+
|
|
178
|
+
// Verify persisted by reading again
|
|
179
|
+
const prefs = ctrl.getPreferences('user-abc');
|
|
180
|
+
assert.equal(prefs.sound, true);
|
|
181
|
+
assert.equal(prefs.runs, false);
|
|
182
|
+
assert.equal(prefs.approvals, true);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('notifications from different orgs do not mix', () => {
|
|
186
|
+
const ctrl = makeController();
|
|
187
|
+
ctrl.createNotification({ type: 'AgentDispatchRun', status: 'completed', name: 'run-1', org: 'org-a' });
|
|
188
|
+
ctrl.createNotification({ type: 'AgentDispatchRun', status: 'completed', name: 'run-2', org: 'org-b' });
|
|
189
|
+
|
|
190
|
+
const orgA = ctrl.listNotifications('org-a');
|
|
191
|
+
const orgB = ctrl.listNotifications('org-b');
|
|
192
|
+
assert.equal(orgA.length, 1);
|
|
193
|
+
assert.equal(orgB.length, 1);
|
|
194
|
+
assert.equal(ctrl.getUnreadCount('org-a'), 1);
|
|
195
|
+
assert.equal(ctrl.getUnreadCount('org-b'), 1);
|
|
196
|
+
});
|