@a5c-ai/krate 5.0.1-staging.00fa5317c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +31 -0
- package/README.md +183 -0
- package/bin/krate-demo.mjs +23 -0
- package/bin/krate-server.mjs +14 -0
- package/dist/krate-controller-ui.json +3205 -0
- package/dist/krate-lifecycle.json +201 -0
- package/dist/krate-runtime-snapshot.json +3125 -0
- package/dist/krate-summary.json +724 -0
- package/docs/README.md +61 -0
- package/docs/agents/README.md +83 -0
- package/docs/agents/acceptance-test-matrix.md +193 -0
- package/docs/agents/agent-mux-adapter-contract.md +167 -0
- package/docs/agents/agent-mux-source-map.md +310 -0
- package/docs/agents/agent-run-memory-import-spec.md +256 -0
- package/docs/agents/agent-stack-management-spec.md +421 -0
- package/docs/agents/api-contract-spec.md +309 -0
- package/docs/agents/artifacts-writeback-spec.md +145 -0
- package/docs/agents/chart-packaging-spec.md +128 -0
- package/docs/agents/ci-orchestration-spec.md +140 -0
- package/docs/agents/context-assembly-spec.md +219 -0
- package/docs/agents/controller-reconciliation-spec.md +255 -0
- package/docs/agents/crd-schema-spec.md +315 -0
- package/docs/agents/decision-log-open-questions.md +169 -0
- package/docs/agents/developer-implementation-checklist.md +329 -0
- package/docs/agents/dispatching-design.md +262 -0
- package/docs/agents/gaps-agent-mux-to-krate-crds.md +298 -0
- package/docs/agents/glossary.md +66 -0
- package/docs/agents/implementation-blueprint.md +324 -0
- package/docs/agents/implementation-rollout-slices.md +251 -0
- package/docs/agents/memory-context-integration-spec.md +194 -0
- package/docs/agents/memory-ontology-schema-spec.md +253 -0
- package/docs/agents/memory-operations-runbook.md +121 -0
- package/docs/agents/mvp-vertical-slice-spec.md +146 -0
- package/docs/agents/observability-audit-spec.md +265 -0
- package/docs/agents/operator-runbook.md +174 -0
- package/docs/agents/org-memory-api-payload-examples.md +333 -0
- package/docs/agents/org-memory-controller-sequence-spec.md +181 -0
- package/docs/agents/org-memory-e2e-fixture-plan.md +161 -0
- package/docs/agents/org-memory-ui-implementation-map.md +114 -0
- package/docs/agents/org-memory-vertical-slice-spec.md +168 -0
- package/docs/agents/org-resource-model-delta-spec.md +111 -0
- package/docs/agents/org-route-resource-model-spec.md +183 -0
- package/docs/agents/org-scoping-namespace-spec.md +114 -0
- package/docs/agents/rbac-secrets-management-spec.md +406 -0
- package/docs/agents/repository-page-integration-spec.md +255 -0
- package/docs/agents/resource-contract-examples.md +808 -0
- package/docs/agents/resource-relationship-map.md +190 -0
- package/docs/agents/security-threat-model.md +188 -0
- package/docs/agents/shared-memory-company-brain-spec.md +358 -0
- package/docs/agents/storage-migration-spec.md +168 -0
- package/docs/agents/subagent-orchestration-spec.md +152 -0
- package/docs/agents/system-overview.md +88 -0
- package/docs/agents/tools-mcp-skills-spec.md +189 -0
- package/docs/agents/traceability-matrix.md +79 -0
- package/docs/agents/ui-flow-spec.md +211 -0
- package/docs/agents/ui-ux-system-spec.md +426 -0
- package/docs/agents/workspace-lifecycle-spec.md +166 -0
- package/docs/architecture-spec.md +78 -0
- package/docs/components/control-plane.md +78 -0
- package/docs/components/data-plane.md +69 -0
- package/docs/components/hooks-events.md +67 -0
- package/docs/components/identity-rbac-policy.md +73 -0
- package/docs/components/kubevela-oam.md +70 -0
- package/docs/components/operations-publishing.md +81 -0
- package/docs/components/runners-ci.md +66 -0
- package/docs/components/web-ui.md +94 -0
- package/docs/external/README.md +47 -0
- package/docs/external/bidirectional-sync-design.md +134 -0
- package/docs/external/cicd-interface.md +64 -0
- package/docs/external/external-backend-controllers.md +170 -0
- package/docs/external/external-backend-crds.md +234 -0
- package/docs/external/external-backend-ui-spec.md +151 -0
- package/docs/external/external-backend-ux-flows.md +115 -0
- package/docs/external/external-object-mapping.md +125 -0
- package/docs/external/git-forge-interface.md +68 -0
- package/docs/external/github-integration-design.md +151 -0
- package/docs/external/issue-tracking-interface.md +66 -0
- package/docs/external/provider-capability-manifests.md +204 -0
- package/docs/external/provider-catalog.md +139 -0
- package/docs/external/provider-rollout-testing.md +78 -0
- package/docs/external/research-results.md +48 -0
- package/docs/external/security-auth-permissions.md +81 -0
- package/docs/external/sync-state-machines.md +108 -0
- package/docs/external/unified-external-backend-model.md +107 -0
- package/docs/external/user-facing-changes.md +67 -0
- package/docs/gaps.md +161 -0
- package/docs/install.md +94 -0
- package/docs/krate-design.md +334 -0
- package/docs/local-minikube.md +55 -0
- package/docs/ontology/README.md +32 -0
- package/docs/ontology/bounded-contexts.md +29 -0
- package/docs/ontology/events-and-hooks.md +32 -0
- package/docs/ontology/oam-kubevela.md +32 -0
- package/docs/ontology/operations-and-release.md +25 -0
- package/docs/ontology/personas-and-actors.md +32 -0
- package/docs/ontology/policies-and-invariants.md +33 -0
- package/docs/ontology/problem-space.md +30 -0
- package/docs/ontology/resource-contracts.md +40 -0
- package/docs/ontology/resource-taxonomy.md +42 -0
- package/docs/ontology/runners-and-ci.md +29 -0
- package/docs/ontology/solution-space.md +24 -0
- package/docs/ontology/storage-and-data-boundaries.md +29 -0
- package/docs/ontology/validation-matrix.md +24 -0
- package/docs/ontology/web-ui-excellent-flows.md +32 -0
- package/docs/ontology/workflows.md +39 -0
- package/docs/ontology/world.md +35 -0
- package/docs/openapi.yaml +1275 -0
- package/docs/product-requirements.md +62 -0
- package/docs/roadmap-mvp.md +87 -0
- package/docs/system-requirements.md +90 -0
- package/docs/tests/README.md +53 -0
- package/docs/tests/agent-qa-plan.md +63 -0
- package/docs/tests/browser-ui-tests.md +62 -0
- package/docs/tests/ci-quality-gates.md +48 -0
- package/docs/tests/coverage-model.md +64 -0
- package/docs/tests/e2e-scenario-tests.md +53 -0
- package/docs/tests/fixtures-test-data.md +63 -0
- package/docs/tests/observability-reliability-tests.md +54 -0
- package/docs/tests/product-test-matrix.md +145 -0
- package/docs/tests/qa-adoption-roadmap.md +130 -0
- package/docs/tests/qa-automation-plan.md +101 -0
- package/docs/tests/security-compliance-tests.md +57 -0
- package/docs/tests/test-framework-tools.md +88 -0
- package/docs/tests/test-suite-layout.md +121 -0
- package/docs/tests/unit-integration-tests.md +48 -0
- package/docs/todo-kyverno +714 -0
- package/docs/todos.md +4 -0
- package/docs/user-stories.md +78 -0
- package/examples/minikube-demo.yaml +190 -0
- package/examples/oam-application.yaml +23 -0
- package/examples/policy-kyverno-pr-title.yaml +18 -0
- package/package.json +63 -0
- package/scripts/build.mjs +29 -0
- package/scripts/setup-minikube.mjs +65 -0
- package/scripts/smoke.mjs +37 -0
- package/scripts/validate-doc-coverage.mjs +152 -0
- package/scripts/validate-package.mjs +93 -0
- package/scripts/validate-ui.mjs +278 -0
- package/src/agent-adapter-controller.js +169 -0
- package/src/agent-approval-controller.js +170 -0
- package/src/agent-context-bundles.js +242 -0
- package/src/agent-dispatch-controller.js +209 -0
- package/src/agent-gateway-config-controller.js +147 -0
- package/src/agent-memory-controller.js +357 -0
- package/src/agent-memory-import.js +327 -0
- package/src/agent-memory-query.js +292 -0
- package/src/agent-memory-repository-source-controller.js +255 -0
- package/src/agent-mux-client.js +280 -0
- package/src/agent-permission-review.js +250 -0
- package/src/agent-project-controller.js +117 -0
- package/src/agent-provider-config-controller.js +150 -0
- package/src/agent-secret-config-grant-controller.js +282 -0
- package/src/agent-session-transcript-controller.js +189 -0
- package/src/agent-stack-controller.js +347 -0
- package/src/agent-subagent-controller.js +160 -0
- package/src/agent-transport-binding-controller.js +121 -0
- package/src/agent-trigger-controller.js +381 -0
- package/src/agent-workspace-controller.js +702 -0
- package/src/agent-writeback-controller.js +302 -0
- package/src/api-controller.js +541 -0
- package/src/argocd-gitops.js +43 -0
- package/src/async-controller.js +207 -0
- package/src/audit-controller.js +191 -0
- package/src/auth.js +307 -0
- package/src/component-catalog.js +41 -0
- package/src/control-plane.js +136 -0
- package/src/controller-client.js +72 -0
- package/src/controller-ui.js +617 -0
- package/src/data-plane.js +179 -0
- package/src/event-bus.js +61 -0
- package/src/external/conflict-controller.js +225 -0
- package/src/external/github/auth.js +96 -0
- package/src/external/github/cicd.js +180 -0
- package/src/external/github/git-forge.js +240 -0
- package/src/external/github/index.js +144 -0
- package/src/external/github/issue-tracking.js +163 -0
- package/src/external/provider-adapter.js +161 -0
- package/src/external/provider-resource-factory.js +161 -0
- package/src/external/sync-controller.js +235 -0
- package/src/external/webhook-controller.js +144 -0
- package/src/external/write-controller.js +283 -0
- package/src/gitea-backend.js +131 -0
- package/src/gitea-service.js +173 -0
- package/src/handoff.js +98 -0
- package/src/hooks-events.js +63 -0
- package/src/http-server.js +377 -0
- package/src/identity-policy.js +86 -0
- package/src/index.js +57 -0
- package/src/kubernetes-controller-async.js +511 -0
- package/src/kubernetes-controller.js +878 -0
- package/src/kubernetes-resource-gateway.js +48 -0
- package/src/notification-controller.js +178 -0
- package/src/operations.js +112 -0
- package/src/org-scoping.js +5 -0
- package/src/resource-model.js +221 -0
- package/src/runner-controller.js +272 -0
- package/src/runners-ci.js +48 -0
- package/src/runtime.js +196 -0
- package/src/snapshot-cache.js +157 -0
- package/src/web-ui.js +40 -0
- package/tests/agent-adapter-controller.test.js +361 -0
- package/tests/agent-approval-controller.test.js +173 -0
- package/tests/agent-context-bundles.test.js +278 -0
- package/tests/agent-dispatch-controller.test.js +315 -0
- package/tests/agent-gateway-config-controller.test.js +386 -0
- package/tests/agent-memory-controller.test.js +308 -0
- package/tests/agent-memory-import-snapshot.test.js +477 -0
- package/tests/agent-memory-query.test.js +404 -0
- package/tests/agent-memory-repository-source.test.js +514 -0
- package/tests/agent-mux-client.test.js +204 -0
- package/tests/agent-permission-review-v2.test.js +317 -0
- package/tests/agent-permission-review.test.js +209 -0
- package/tests/agent-project-controller.test.js +302 -0
- package/tests/agent-provider-config-controller.test.js +376 -0
- package/tests/agent-resources.test.js +228 -0
- package/tests/agent-secret-config-grant.test.js +231 -0
- package/tests/agent-session-transcript-controller.test.js +499 -0
- package/tests/agent-stack-controller.test.js +221 -0
- package/tests/agent-subagent-controller.test.js +201 -0
- package/tests/agent-transport-binding-controller.test.js +294 -0
- package/tests/agent-trigger-controller.test.js +211 -0
- package/tests/agent-trigger-routes.test.js +190 -0
- package/tests/agent-trigger-sources.test.js +245 -0
- package/tests/agent-workspace-controller.test.js +181 -0
- package/tests/agent-writeback.test.js +292 -0
- package/tests/approval-persistence.test.js +171 -0
- package/tests/async-controller.test.js +252 -0
- package/tests/audit-controller.test.js +227 -0
- package/tests/codespace-controller.test.js +318 -0
- package/tests/deployment.test.js +407 -0
- package/tests/e2e/lifecycle.test.js +117 -0
- package/tests/event-bus-integration.test.js +190 -0
- package/tests/external-github-forge.test.js +560 -0
- package/tests/external-github-issues-cicd.test.js +520 -0
- package/tests/external-integration.test.js +470 -0
- package/tests/external-persistence.test.js +340 -0
- package/tests/external-provider-adapter.test.js +365 -0
- package/tests/external-resource-model.test.js +215 -0
- package/tests/external-webhook-sync.test.js +287 -0
- package/tests/external-write-conflict.test.js +353 -0
- package/tests/gitea-service.test.js +253 -0
- package/tests/health-check-real.test.js +165 -0
- package/tests/integration/full-flow.test.js +266 -0
- package/tests/krate.test.js +756 -0
- package/tests/memory-search-wiring.test.js +270 -0
- package/tests/notification-controller.test.js +196 -0
- package/tests/notification-integration.test.js +179 -0
- package/tests/org-scoping.test.js +687 -0
- package/tests/runner-controller.test.js +327 -0
- package/tests/runner-integration.test.js +231 -0
- package/tests/session-cookie-hmac.test.js +151 -0
- package/tests/snapshot-performance.test.js +247 -0
- package/tests/sse-events.test.js +107 -0
- package/tests/webhook-trigger.test.js +198 -0
- package/tests/workspace-volumes.test.js +312 -0
- package/tests/writeback-persistence.test.js +207 -0
package/src/web-ui.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createResource } from './resource-model.js';
|
|
2
|
+
import { toResourceYaml } from './identity-policy.js';
|
|
3
|
+
|
|
4
|
+
export function createPullRequestReviewModel({ pullRequest, changedFiles = [], pipelineRuns = [] }) {
|
|
5
|
+
return { layout: 'three-pane-review', panes: ['file-tree', 'diff-and-comments', 'conversation-ci'], keyboardShortcuts: ['j/k file navigation', 'n/p comment navigation', 'a add suggestion', 'm merge'], pullRequest, changedFiles, pipelineRuns, yaml: toResourceYaml(pullRequest) };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function createFailingRunModel({ pipeline, jobs }) {
|
|
9
|
+
const failedJobs = jobs.filter((job) => job.status.phase === 'Failed');
|
|
10
|
+
return { layout: 'live-run-debugger', stream: 'sse', pipeline, failedJobs, actions: ['copy failure', 'find similar runs', 'rerun from step'], similarRunSelector: failedJobs.map((job) => job.metadata.labels).filter(Boolean) };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createRunnerPoolEditor(pool) {
|
|
14
|
+
return { layout: 'split-form-yaml', fields: ['image', 'resources', 'nodeSelector', 'warmReplicas', 'maxReplicas', 'trustTier', 'cache'], resource: pool, yaml: toResourceYaml(pool), saveModes: ['apply', 'copy kubectl', 'open platform-config PR'] };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createWebhookInspector({ subscription, deliveries }) {
|
|
18
|
+
return { layout: 'webhook-inspector', subscription, deliveries, columns: ['phase', 'latency', 'attempts', 'response', 'signature'], actions: ['send test delivery', 'inspect headers/body/response', 'replay'] };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createPolicyRolloutModel(policy) {
|
|
22
|
+
return { layout: 'policy-authoring', modes: ['template', 'CEL/raw'], rollout: ['preview', 'audit', 'enforce'], policy, yaml: toResourceYaml(policy) };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createTriageView({ name, namespace = 'krate-org-default', organizationRef = 'default', selector }) {
|
|
26
|
+
return createResource('View', { name, namespace, labels: { purpose: 'triage' } }, { organizationRef, selector, columns: ['kind', 'repository', 'priority', 'assignee', 'status'], shareable: true }, { saved: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createDashboard({ repositories, pullRequests, pipelines, runnerPools, webhookDeliveries }) {
|
|
30
|
+
return {
|
|
31
|
+
product: 'Krate',
|
|
32
|
+
principles: ['Kubernetes is the backend', 'CRDs are contracts', 'GitOps transparency'],
|
|
33
|
+
repositories,
|
|
34
|
+
pullRequests,
|
|
35
|
+
pipelines,
|
|
36
|
+
runnerPools,
|
|
37
|
+
webhookDeliveries,
|
|
38
|
+
excellentFlows: ['Open and review a PR', 'Debug a failing run', 'Configure a runner pool', 'Add a webhook and verify it works', 'Write a PR policy with audit-to-enforce rollout', 'Cross-repo triage with saved filters']
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { createAgentAdapterController, validateAgentAdapter, createResource, AGENT_ADAPTER_CONTROLLER_BOUNDARY } from '../src/index.js';
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Acceptance criteria: Slice 1.2a — Agent Adapter Controller
|
|
7
|
+
//
|
|
8
|
+
// An AgentAdapter defines how an agent connects to a model provider.
|
|
9
|
+
// It specifies adapter name/type, supported transport protocols
|
|
10
|
+
// (stdio, http, websocket), a capabilities matrix, and a health check
|
|
11
|
+
// endpoint/method.
|
|
12
|
+
//
|
|
13
|
+
// All tests in this file are expected to FAIL until the controller is
|
|
14
|
+
// implemented and exported from src/index.js.
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
const VALID_TRANSPORTS = ['stdio', 'http', 'websocket', 'unix'];
|
|
18
|
+
const VALID_ADAPTER_TYPES = ['subprocess', 'remote', 'programmatic'];
|
|
19
|
+
|
|
20
|
+
function makeAdapter(name, overrides = {}) {
|
|
21
|
+
return createResource('AgentAdapter', { name, namespace: 'krate-org-default' }, {
|
|
22
|
+
organizationRef: 'default',
|
|
23
|
+
adapterType: 'subprocess',
|
|
24
|
+
transport: 'stdio',
|
|
25
|
+
capabilities: ['tool-use', 'streaming'],
|
|
26
|
+
...overrides
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// 1. Factory and shape
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
test('createAgentAdapterController returns a controller with validate method', () => {
|
|
35
|
+
const controller = createAgentAdapterController();
|
|
36
|
+
assert.ok(controller, 'controller must be truthy');
|
|
37
|
+
assert.equal(typeof controller.validate, 'function', 'controller must expose a validate method');
|
|
38
|
+
assert.equal(controller.role, 'agent-adapter-controller', 'controller must declare its role');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// 2. validate — happy path
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
test('validate accepts a valid adapter with name, type, and transport', () => {
|
|
46
|
+
const controller = createAgentAdapterController();
|
|
47
|
+
const adapter = makeAdapter('claude-code-adapter');
|
|
48
|
+
const result = controller.validate(adapter);
|
|
49
|
+
|
|
50
|
+
assert.equal(result.valid, true, 'valid adapter must pass validation');
|
|
51
|
+
assert.ok(Array.isArray(result.errors), 'result must contain an errors array');
|
|
52
|
+
assert.equal(result.errors.length, 0, 'errors array must be empty for a valid adapter');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// 3. validate — missing name
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
test('validate rejects adapter with missing name', () => {
|
|
60
|
+
const controller = createAgentAdapterController();
|
|
61
|
+
// Build a raw object without metadata.name to bypass createResource guard
|
|
62
|
+
const adapter = {
|
|
63
|
+
apiVersion: 'krate.a5c.ai/v1alpha1',
|
|
64
|
+
kind: 'AgentAdapter',
|
|
65
|
+
metadata: { namespace: 'krate-org-default', labels: {}, annotations: {} },
|
|
66
|
+
spec: { organizationRef: 'default', adapterType: 'subprocess', transport: 'stdio', capabilities: ['tool-use'] },
|
|
67
|
+
status: {}
|
|
68
|
+
};
|
|
69
|
+
const result = controller.validate(adapter);
|
|
70
|
+
|
|
71
|
+
assert.equal(result.valid, false, 'adapter without a name must fail validation');
|
|
72
|
+
assert.ok(result.errors.length > 0, 'errors array must not be empty');
|
|
73
|
+
assert.ok(
|
|
74
|
+
result.errors.some((e) => /name/i.test(e)),
|
|
75
|
+
'at least one error must mention "name"'
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// 4. validate — invalid transport type
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
test('validate rejects adapter with invalid transport type', () => {
|
|
84
|
+
const controller = createAgentAdapterController();
|
|
85
|
+
const adapter = makeAdapter('bad-transport-adapter', { transport: 'grpc' });
|
|
86
|
+
const result = controller.validate(adapter);
|
|
87
|
+
|
|
88
|
+
assert.equal(result.valid, false, 'adapter with unsupported transport must fail validation');
|
|
89
|
+
assert.ok(result.errors.length > 0, 'errors array must not be empty');
|
|
90
|
+
assert.ok(
|
|
91
|
+
result.errors.some((e) => /transport/i.test(e)),
|
|
92
|
+
'at least one error must mention "transport"'
|
|
93
|
+
);
|
|
94
|
+
// Confirm that valid transports are enumerated in the error message
|
|
95
|
+
assert.ok(
|
|
96
|
+
result.errors.some((e) => VALID_TRANSPORTS.some((t) => e.includes(t))),
|
|
97
|
+
'error must enumerate valid transport types'
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// 5. validate — empty capabilities array
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
test('validate rejects adapter with empty capabilities array', () => {
|
|
106
|
+
const controller = createAgentAdapterController();
|
|
107
|
+
const adapter = makeAdapter('no-caps-adapter', { capabilities: [] });
|
|
108
|
+
const result = controller.validate(adapter);
|
|
109
|
+
|
|
110
|
+
assert.equal(result.valid, false, 'adapter with empty capabilities must fail validation');
|
|
111
|
+
assert.ok(result.errors.length > 0, 'errors array must not be empty');
|
|
112
|
+
assert.ok(
|
|
113
|
+
result.errors.some((e) => /capabilities/i.test(e)),
|
|
114
|
+
'at least one error must mention "capabilities"'
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// 6. getCapabilities — capabilities matrix
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
test('getCapabilities returns the capabilities matrix for an adapter', () => {
|
|
123
|
+
const controller = createAgentAdapterController();
|
|
124
|
+
const adapter = makeAdapter('full-adapter', {
|
|
125
|
+
capabilities: ['tool-use', 'streaming', 'file-access']
|
|
126
|
+
});
|
|
127
|
+
const matrix = controller.getCapabilities(adapter);
|
|
128
|
+
|
|
129
|
+
assert.ok(matrix, 'getCapabilities must return a value');
|
|
130
|
+
assert.ok(Array.isArray(matrix.supported), 'matrix must have a supported array');
|
|
131
|
+
assert.ok(matrix.supported.includes('tool-use'), 'supported must include tool-use');
|
|
132
|
+
assert.ok(matrix.supported.includes('streaming'), 'supported must include streaming');
|
|
133
|
+
assert.ok(matrix.supported.includes('file-access'), 'supported must include file-access');
|
|
134
|
+
assert.equal(matrix.adapterName, adapter.metadata.name, 'matrix must carry the adapter name');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// 7. getSupportedTransports — transport enumeration
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
test('getSupportedTransports returns the transport types', () => {
|
|
142
|
+
const controller = createAgentAdapterController();
|
|
143
|
+
const transports = controller.getSupportedTransports();
|
|
144
|
+
|
|
145
|
+
assert.ok(Array.isArray(transports), 'getSupportedTransports must return an array');
|
|
146
|
+
assert.equal(transports.length, 4, 'exactly four transport types must be supported');
|
|
147
|
+
assert.ok(transports.includes('stdio'), 'transports must include stdio');
|
|
148
|
+
assert.ok(transports.includes('http'), 'transports must include http');
|
|
149
|
+
assert.ok(transports.includes('websocket'), 'transports must include websocket');
|
|
150
|
+
assert.ok(transports.includes('unix'), 'transports must include unix');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// 8. healthCheck — stub result when no endpoint configured
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
test('healthCheck returns a result with status "unknown" when no endpoint configured', async () => {
|
|
158
|
+
const controller = createAgentAdapterController();
|
|
159
|
+
const adapter = makeAdapter('no-endpoint-adapter');
|
|
160
|
+
// Adapter has no healthEndpoint in spec
|
|
161
|
+
const result = await controller.healthCheck(adapter);
|
|
162
|
+
|
|
163
|
+
assert.ok(result, 'healthCheck must return a result');
|
|
164
|
+
assert.equal(result.status, 'unknown', 'status must be "unknown" when no endpoint is configured');
|
|
165
|
+
assert.equal(result.adapterName, adapter.metadata.name, 'result must carry the adapter name');
|
|
166
|
+
assert.equal(result.reason, 'no-endpoint', 'reason must be "no-endpoint"');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// 9. validateAgentAdapter — standalone export follows existing pattern
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
test('validateAgentAdapter exported from controller follows existing pattern', () => {
|
|
174
|
+
assert.equal(typeof validateAgentAdapter, 'function', 'validateAgentAdapter must be a named export');
|
|
175
|
+
|
|
176
|
+
const adapter = makeAdapter('standalone-validate-adapter');
|
|
177
|
+
const result = validateAgentAdapter(adapter);
|
|
178
|
+
|
|
179
|
+
assert.ok(result, 'validateAgentAdapter must return a result');
|
|
180
|
+
assert.ok('valid' in result, 'result must have a valid property');
|
|
181
|
+
assert.ok(Array.isArray(result.errors), 'result must have an errors array');
|
|
182
|
+
assert.equal(result.valid, true, 'a fully-specified adapter must pass standalone validation');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// 10. validate — missing adapterType
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
test('validate rejects adapter with missing adapterType', () => {
|
|
190
|
+
const controller = createAgentAdapterController();
|
|
191
|
+
const adapter = makeAdapter('no-type-adapter', { adapterType: undefined });
|
|
192
|
+
// Remove adapterType from spec entirely
|
|
193
|
+
delete adapter.spec.adapterType;
|
|
194
|
+
const result = controller.validate(adapter);
|
|
195
|
+
|
|
196
|
+
assert.equal(result.valid, false, 'adapter without adapterType must fail validation');
|
|
197
|
+
assert.ok(result.errors.length > 0, 'errors array must not be empty');
|
|
198
|
+
assert.ok(
|
|
199
|
+
result.errors.some((e) => /adapterType/i.test(e)),
|
|
200
|
+
'at least one error must mention "adapterType"'
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// 11. validate — invalid adapterType ('garbage')
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
test('validate rejects adapter with invalid adapterType', () => {
|
|
209
|
+
const controller = createAgentAdapterController();
|
|
210
|
+
const adapter = makeAdapter('bad-type-adapter', { adapterType: 'garbage' });
|
|
211
|
+
const result = controller.validate(adapter);
|
|
212
|
+
|
|
213
|
+
assert.equal(result.valid, false, 'adapter with unsupported adapterType must fail validation');
|
|
214
|
+
assert.ok(result.errors.length > 0, 'errors array must not be empty');
|
|
215
|
+
assert.ok(
|
|
216
|
+
result.errors.some((e) => /adapterType/i.test(e)),
|
|
217
|
+
'at least one error must mention "adapterType"'
|
|
218
|
+
);
|
|
219
|
+
assert.ok(
|
|
220
|
+
result.errors.some((e) => VALID_ADAPTER_TYPES.some((t) => e.includes(t))),
|
|
221
|
+
'error must enumerate valid adapter types'
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// 12. validate — unix transport is valid
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
test('validate accepts adapter with unix transport', () => {
|
|
230
|
+
const controller = createAgentAdapterController();
|
|
231
|
+
const adapter = makeAdapter('unix-transport-adapter', { transport: 'unix' });
|
|
232
|
+
const result = controller.validate(adapter);
|
|
233
|
+
|
|
234
|
+
assert.equal(result.valid, true, 'adapter with unix transport must pass validation');
|
|
235
|
+
assert.equal(result.errors.length, 0, 'errors array must be empty');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
// 13. healthCheck returns 'not-implemented' when healthEndpoint IS configured
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
test('healthCheck performs real HTTP check when healthEndpoint is configured', async () => {
|
|
243
|
+
// Use a mock fetch to avoid real network calls in unit tests
|
|
244
|
+
const mockFetch = async () => ({ ok: true, status: 200 });
|
|
245
|
+
const controller = createAgentAdapterController({ fetch: mockFetch });
|
|
246
|
+
const adapter = makeAdapter('endpoint-adapter', { healthEndpoint: 'http://localhost:9090/health' });
|
|
247
|
+
const result = await controller.healthCheck(adapter);
|
|
248
|
+
|
|
249
|
+
assert.ok(result, 'healthCheck must return a result');
|
|
250
|
+
assert.equal(result.status, 'healthy', 'status must be "healthy" when endpoint fetch succeeds');
|
|
251
|
+
assert.equal(result.adapterName, adapter.metadata.name, 'result must carry the adapter name');
|
|
252
|
+
assert.ok(typeof result.latencyMs === 'number', 'result must include latencyMs');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
// 14. validate / getCapabilities / healthCheck reject null resource
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
test('validate rejects null resource with a clear error', () => {
|
|
260
|
+
const controller = createAgentAdapterController();
|
|
261
|
+
const result = controller.validate(null);
|
|
262
|
+
|
|
263
|
+
assert.equal(result.valid, false, 'null resource must fail validation');
|
|
264
|
+
assert.ok(result.errors.length > 0, 'errors array must not be empty');
|
|
265
|
+
assert.ok(
|
|
266
|
+
result.errors.some((e) => /null|undefined/i.test(e)),
|
|
267
|
+
'error must mention null or undefined'
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('getCapabilities throws on null resource', () => {
|
|
272
|
+
const controller = createAgentAdapterController();
|
|
273
|
+
assert.throws(
|
|
274
|
+
() => controller.getCapabilities(null),
|
|
275
|
+
/null|undefined/i,
|
|
276
|
+
'getCapabilities must throw on null resource'
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('healthCheck rejects on null resource', async () => {
|
|
281
|
+
const controller = createAgentAdapterController();
|
|
282
|
+
await assert.rejects(
|
|
283
|
+
() => controller.healthCheck(null),
|
|
284
|
+
/null|undefined/i,
|
|
285
|
+
'healthCheck must reject on null resource'
|
|
286
|
+
);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
// 15. getSupportedAdapterTypes — adapter type enumeration
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
test('getSupportedAdapterTypes returns the valid adapter types array', () => {
|
|
294
|
+
const controller = createAgentAdapterController();
|
|
295
|
+
const types = controller.getSupportedAdapterTypes();
|
|
296
|
+
|
|
297
|
+
assert.ok(Array.isArray(types), 'getSupportedAdapterTypes must return an array');
|
|
298
|
+
assert.equal(types.length, VALID_ADAPTER_TYPES.length, 'must return all valid adapter types');
|
|
299
|
+
assert.ok(types.includes('subprocess'), 'adapter types must include subprocess');
|
|
300
|
+
assert.ok(types.includes('remote'), 'adapter types must include remote');
|
|
301
|
+
assert.ok(types.includes('programmatic'), 'adapter types must include programmatic');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
// 16. getCapabilities with undefined spec (resource with no spec key)
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
test('getCapabilities handles resource with no spec key gracefully', () => {
|
|
309
|
+
const controller = createAgentAdapterController();
|
|
310
|
+
const resource = {
|
|
311
|
+
apiVersion: 'krate.a5c.ai/v1alpha1',
|
|
312
|
+
kind: 'AgentAdapter',
|
|
313
|
+
metadata: { name: 'spec-less-adapter', namespace: 'krate-org-default', labels: {}, annotations: {} },
|
|
314
|
+
status: {}
|
|
315
|
+
// no spec key at all
|
|
316
|
+
};
|
|
317
|
+
const matrix = controller.getCapabilities(resource);
|
|
318
|
+
|
|
319
|
+
assert.ok(matrix, 'getCapabilities must return a value even with no spec');
|
|
320
|
+
assert.ok(Array.isArray(matrix.supported), 'supported must be an array');
|
|
321
|
+
assert.equal(matrix.supported.length, 0, 'supported must be empty when spec is absent');
|
|
322
|
+
assert.equal(matrix.adapterName, 'spec-less-adapter', 'adapterName must still be populated');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
// 17. validate — simultaneous adapterType AND transport errors accumulate
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
test('validate accumulates all errors when both adapterType and transport are invalid', () => {
|
|
330
|
+
const controller = createAgentAdapterController();
|
|
331
|
+
const adapter = makeAdapter('double-bad-adapter', { adapterType: 'bad-type', transport: 'grpc' });
|
|
332
|
+
const result = controller.validate(adapter);
|
|
333
|
+
|
|
334
|
+
assert.equal(result.valid, false, 'adapter with multiple invalid fields must fail validation');
|
|
335
|
+
assert.ok(
|
|
336
|
+
result.errors.some((e) => /adapterType/i.test(e)),
|
|
337
|
+
'errors must include an adapterType error'
|
|
338
|
+
);
|
|
339
|
+
assert.ok(
|
|
340
|
+
result.errors.some((e) => /transport/i.test(e)),
|
|
341
|
+
'errors must include a transport error'
|
|
342
|
+
);
|
|
343
|
+
assert.ok(result.errors.length >= 2, 'must accumulate at least two errors');
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
// 18. BOUNDARY object export
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
|
|
350
|
+
test('AGENT_ADAPTER_CONTROLLER_BOUNDARY is exported and has correct role', () => {
|
|
351
|
+
assert.ok(AGENT_ADAPTER_CONTROLLER_BOUNDARY, 'BOUNDARY must be exported');
|
|
352
|
+
assert.equal(
|
|
353
|
+
AGENT_ADAPTER_CONTROLLER_BOUNDARY.role,
|
|
354
|
+
'agent-adapter-controller',
|
|
355
|
+
'BOUNDARY role must be "agent-adapter-controller"'
|
|
356
|
+
);
|
|
357
|
+
assert.ok(
|
|
358
|
+
Array.isArray(AGENT_ADAPTER_CONTROLLER_BOUNDARY.owns),
|
|
359
|
+
'BOUNDARY must declare owned concerns'
|
|
360
|
+
);
|
|
361
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { createAgentApprovalController, createResource } from '../src/index.js';
|
|
4
|
+
|
|
5
|
+
function makeApproval(name, dispatchRun, action, phase = 'Pending', extra = {}) {
|
|
6
|
+
const approval = createResource('AgentApproval', { name, namespace: 'krate-org-default' }, {
|
|
7
|
+
organizationRef: 'default',
|
|
8
|
+
dispatchRun,
|
|
9
|
+
action,
|
|
10
|
+
requestedBy: 'agent-stack-1',
|
|
11
|
+
...extra
|
|
12
|
+
});
|
|
13
|
+
approval.status = { phase, createdAt: new Date().toISOString(), ...extra.status };
|
|
14
|
+
return approval;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test('Create approval request returns resource with phase=Pending', () => {
|
|
18
|
+
const controller = createAgentApprovalController();
|
|
19
|
+
const result = controller.createApprovalRequest({
|
|
20
|
+
dispatchRun: 'run-1',
|
|
21
|
+
action: 'tool-use',
|
|
22
|
+
requestedBy: 'agent-stack-1',
|
|
23
|
+
context: 'Needs filesystem write access',
|
|
24
|
+
namespace: 'krate-org-default',
|
|
25
|
+
organizationRef: 'default',
|
|
26
|
+
resources: {}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
assert.equal(result.error, false, 'Should succeed');
|
|
30
|
+
assert.equal(result.duplicate, false, 'Should not be a duplicate');
|
|
31
|
+
assert.ok(result.approval, 'Should return an approval resource');
|
|
32
|
+
assert.equal(result.approval.kind, 'AgentApproval');
|
|
33
|
+
assert.equal(result.approval.status.phase, 'Pending');
|
|
34
|
+
assert.equal(result.approval.spec.action, 'tool-use');
|
|
35
|
+
assert.equal(result.approval.spec.dispatchRun, 'run-1');
|
|
36
|
+
assert.equal(result.approval.spec.requestedBy, 'agent-stack-1');
|
|
37
|
+
assert.ok(result.approval.status.createdAt, 'Should have createdAt timestamp');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('Record approve sets phase=Approved and decidedBy', () => {
|
|
41
|
+
const controller = createAgentApprovalController();
|
|
42
|
+
const existing = makeApproval('approval-1', 'run-1', 'tool-use', 'Pending');
|
|
43
|
+
const result = controller.recordDecision({
|
|
44
|
+
approvalName: 'approval-1',
|
|
45
|
+
decision: 'approve',
|
|
46
|
+
decidedBy: 'owner',
|
|
47
|
+
reason: 'Looks safe',
|
|
48
|
+
resources: { AgentApproval: [existing] }
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
assert.equal(result.error, false, 'Should succeed');
|
|
52
|
+
assert.equal(result.approval.status.phase, 'Approved');
|
|
53
|
+
assert.equal(result.approval.status.decidedBy, 'owner');
|
|
54
|
+
assert.equal(result.approval.status.reason, 'Looks safe');
|
|
55
|
+
assert.ok(result.approval.status.decidedAt, 'Should have decidedAt timestamp');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('Record deny sets phase=Denied', () => {
|
|
59
|
+
const controller = createAgentApprovalController();
|
|
60
|
+
const existing = makeApproval('approval-2', 'run-1', 'secret-access', 'Pending');
|
|
61
|
+
const result = controller.recordDecision({
|
|
62
|
+
approvalName: 'approval-2',
|
|
63
|
+
decision: 'deny',
|
|
64
|
+
decidedBy: 'admin',
|
|
65
|
+
reason: 'Sensitive secrets not allowed',
|
|
66
|
+
resources: { AgentApproval: [existing] }
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
assert.equal(result.error, false, 'Should succeed');
|
|
70
|
+
assert.equal(result.approval.status.phase, 'Denied');
|
|
71
|
+
assert.equal(result.approval.status.decidedBy, 'admin');
|
|
72
|
+
assert.equal(result.approval.status.reason, 'Sensitive secrets not allowed');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('isActionApproved returns true after approval', () => {
|
|
76
|
+
const controller = createAgentApprovalController();
|
|
77
|
+
const approved = makeApproval('approval-3', 'run-2', 'write-back', 'Approved', { status: { decidedBy: 'owner', decidedAt: new Date().toISOString() } });
|
|
78
|
+
const result = controller.isActionApproved({
|
|
79
|
+
dispatchRun: 'run-2',
|
|
80
|
+
action: 'write-back',
|
|
81
|
+
resources: { AgentApproval: [approved] }
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
assert.equal(result.approved, true, 'Should be approved');
|
|
85
|
+
assert.ok(result.approval, 'Should return the approval resource');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('isActionApproved returns false when still pending', () => {
|
|
89
|
+
const controller = createAgentApprovalController();
|
|
90
|
+
const pending = makeApproval('approval-4', 'run-3', 'release', 'Pending');
|
|
91
|
+
const result = controller.isActionApproved({
|
|
92
|
+
dispatchRun: 'run-3',
|
|
93
|
+
action: 'release',
|
|
94
|
+
resources: { AgentApproval: [pending] }
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
assert.equal(result.approved, false, 'Should not be approved');
|
|
98
|
+
assert.equal(result.reason, 'Approval is still pending');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('Duplicate pending request returns existing approval', () => {
|
|
102
|
+
const controller = createAgentApprovalController();
|
|
103
|
+
const existing = makeApproval('approval-5', 'run-4', 'tool-use', 'Pending');
|
|
104
|
+
const result = controller.createApprovalRequest({
|
|
105
|
+
dispatchRun: 'run-4',
|
|
106
|
+
action: 'tool-use',
|
|
107
|
+
requestedBy: 'agent-stack-1',
|
|
108
|
+
resources: { AgentApproval: [existing] }
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
assert.equal(result.error, false, 'Should succeed');
|
|
112
|
+
assert.equal(result.duplicate, true, 'Should be flagged as duplicate');
|
|
113
|
+
assert.equal(result.approval.metadata.name, 'approval-5', 'Should return the existing approval');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('Invalid action type returns error', () => {
|
|
117
|
+
const controller = createAgentApprovalController();
|
|
118
|
+
const result = controller.createApprovalRequest({
|
|
119
|
+
dispatchRun: 'run-5',
|
|
120
|
+
action: 'invalid-action',
|
|
121
|
+
requestedBy: 'agent-stack-1',
|
|
122
|
+
resources: {}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
assert.equal(result.error, true, 'Should fail');
|
|
126
|
+
assert.equal(result.reason, 'invalid-action');
|
|
127
|
+
assert.ok(result.message.includes('invalid-action'), 'Message should mention the invalid action');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('Already decided approval returns error on re-decide', () => {
|
|
131
|
+
const controller = createAgentApprovalController();
|
|
132
|
+
const decided = makeApproval('approval-6', 'run-6', 'escalation', 'Approved', { status: { decidedBy: 'admin', decidedAt: new Date().toISOString() } });
|
|
133
|
+
const result = controller.recordDecision({
|
|
134
|
+
approvalName: 'approval-6',
|
|
135
|
+
decision: 'deny',
|
|
136
|
+
decidedBy: 'other-admin',
|
|
137
|
+
resources: { AgentApproval: [decided] }
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
assert.equal(result.error, true, 'Should fail');
|
|
141
|
+
assert.equal(result.reason, 'already-decided');
|
|
142
|
+
assert.ok(result.message.includes('already been decided'), 'Message should indicate already decided');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('listPendingApprovals filters by org and pending status', () => {
|
|
146
|
+
const controller = createAgentApprovalController();
|
|
147
|
+
const pending1 = makeApproval('p1', 'run-7', 'tool-use', 'Pending');
|
|
148
|
+
const pending2 = makeApproval('p2', 'run-8', 'release', 'Pending');
|
|
149
|
+
const approved = makeApproval('p3', 'run-9', 'write-back', 'Approved');
|
|
150
|
+
|
|
151
|
+
const result = controller.listPendingApprovals({
|
|
152
|
+
organizationRef: 'default',
|
|
153
|
+
resources: { AgentApproval: [pending1, pending2, approved] }
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
assert.equal(result.length, 2, 'Should return only pending approvals');
|
|
157
|
+
assert.ok(result.every(a => a.status.phase === 'Pending'), 'All should be Pending');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('listApprovalsForRun filters by dispatch run', () => {
|
|
161
|
+
const controller = createAgentApprovalController();
|
|
162
|
+
const a1 = makeApproval('r1', 'run-10', 'tool-use', 'Pending');
|
|
163
|
+
const a2 = makeApproval('r2', 'run-10', 'secret-access', 'Approved');
|
|
164
|
+
const a3 = makeApproval('r3', 'run-11', 'release', 'Pending');
|
|
165
|
+
|
|
166
|
+
const result = controller.listApprovalsForRun({
|
|
167
|
+
dispatchRun: 'run-10',
|
|
168
|
+
resources: { AgentApproval: [a1, a2, a3] }
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
assert.equal(result.length, 2, 'Should return approvals for run-10 only');
|
|
172
|
+
assert.ok(result.every(a => a.spec.dispatchRun === 'run-10'), 'All should belong to run-10');
|
|
173
|
+
});
|