@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.
Files changed (295) hide show
  1. package/Dockerfile +31 -0
  2. package/README.md +187 -0
  3. package/bin/kradle-demo.mjs +23 -0
  4. package/bin/kradle-server.mjs +14 -0
  5. package/dist/kradle-controller-ui.json +3482 -0
  6. package/dist/kradle-lifecycle.json +201 -0
  7. package/dist/kradle-runtime-snapshot.json +3125 -0
  8. package/dist/kradle-summary.json +724 -0
  9. package/docs/README.md +61 -0
  10. package/docs/agents/README.md +83 -0
  11. package/docs/agents/acceptance-test-matrix.md +193 -0
  12. package/docs/agents/agent-mux-adapter-contract.md +167 -0
  13. package/docs/agents/agent-mux-source-map.md +310 -0
  14. package/docs/agents/agent-run-memory-import-spec.md +256 -0
  15. package/docs/agents/agent-stack-management-spec.md +421 -0
  16. package/docs/agents/api-contract-spec.md +309 -0
  17. package/docs/agents/artifacts-writeback-spec.md +145 -0
  18. package/docs/agents/chart-packaging-spec.md +128 -0
  19. package/docs/agents/ci-orchestration-spec.md +140 -0
  20. package/docs/agents/context-assembly-spec.md +219 -0
  21. package/docs/agents/controller-reconciliation-spec.md +255 -0
  22. package/docs/agents/crd-schema-spec.md +315 -0
  23. package/docs/agents/decision-log-open-questions.md +169 -0
  24. package/docs/agents/developer-implementation-checklist.md +329 -0
  25. package/docs/agents/dispatching-design.md +262 -0
  26. package/docs/agents/gaps-agent-mux-to-kradle-crds.md +298 -0
  27. package/docs/agents/glossary.md +66 -0
  28. package/docs/agents/implementation-blueprint.md +324 -0
  29. package/docs/agents/implementation-rollout-slices.md +251 -0
  30. package/docs/agents/memory-context-integration-spec.md +194 -0
  31. package/docs/agents/memory-ontology-schema-spec.md +253 -0
  32. package/docs/agents/memory-operations-runbook.md +121 -0
  33. package/docs/agents/mvp-vertical-slice-spec.md +146 -0
  34. package/docs/agents/observability-audit-spec.md +265 -0
  35. package/docs/agents/operator-runbook.md +174 -0
  36. package/docs/agents/org-memory-api-payload-examples.md +333 -0
  37. package/docs/agents/org-memory-controller-sequence-spec.md +181 -0
  38. package/docs/agents/org-memory-e2e-fixture-plan.md +161 -0
  39. package/docs/agents/org-memory-ui-implementation-map.md +114 -0
  40. package/docs/agents/org-memory-vertical-slice-spec.md +168 -0
  41. package/docs/agents/org-resource-model-delta-spec.md +111 -0
  42. package/docs/agents/org-route-resource-model-spec.md +183 -0
  43. package/docs/agents/org-scoping-namespace-spec.md +114 -0
  44. package/docs/agents/rbac-secrets-management-spec.md +406 -0
  45. package/docs/agents/repository-page-integration-spec.md +255 -0
  46. package/docs/agents/resource-contract-examples.md +808 -0
  47. package/docs/agents/resource-relationship-map.md +190 -0
  48. package/docs/agents/security-threat-model.md +188 -0
  49. package/docs/agents/shared-memory-company-brain-spec.md +358 -0
  50. package/docs/agents/storage-migration-spec.md +168 -0
  51. package/docs/agents/subagent-orchestration-spec.md +152 -0
  52. package/docs/agents/system-overview.md +88 -0
  53. package/docs/agents/tools-mcp-skills-spec.md +189 -0
  54. package/docs/agents/traceability-matrix.md +79 -0
  55. package/docs/agents/ui-flow-spec.md +211 -0
  56. package/docs/agents/ui-ux-system-spec.md +426 -0
  57. package/docs/agents/workspace-lifecycle-spec.md +166 -0
  58. package/docs/architecture-spec.md +78 -0
  59. package/docs/architecture-v2.md +2759 -0
  60. package/docs/components/control-plane.md +78 -0
  61. package/docs/components/data-plane.md +69 -0
  62. package/docs/components/hooks-events.md +67 -0
  63. package/docs/components/identity-rbac-policy.md +73 -0
  64. package/docs/components/kubevela-oam.md +70 -0
  65. package/docs/components/operations-publishing.md +81 -0
  66. package/docs/components/runners-ci.md +66 -0
  67. package/docs/components/web-ui.md +94 -0
  68. package/docs/crd-behaviors-and-relationships.md +3926 -0
  69. package/docs/external/README.md +47 -0
  70. package/docs/external/bidirectional-sync-design.md +134 -0
  71. package/docs/external/cicd-interface.md +64 -0
  72. package/docs/external/external-backend-controllers.md +170 -0
  73. package/docs/external/external-backend-crds.md +234 -0
  74. package/docs/external/external-backend-ui-spec.md +151 -0
  75. package/docs/external/external-backend-ux-flows.md +115 -0
  76. package/docs/external/external-object-mapping.md +125 -0
  77. package/docs/external/git-forge-interface.md +68 -0
  78. package/docs/external/github-integration-design.md +151 -0
  79. package/docs/external/issue-tracking-interface.md +66 -0
  80. package/docs/external/provider-capability-manifests.md +204 -0
  81. package/docs/external/provider-catalog.md +139 -0
  82. package/docs/external/provider-rollout-testing.md +78 -0
  83. package/docs/external/research-results.md +48 -0
  84. package/docs/external/security-auth-permissions.md +81 -0
  85. package/docs/external/sync-state-machines.md +108 -0
  86. package/docs/external/unified-external-backend-model.md +107 -0
  87. package/docs/external/user-facing-changes.md +67 -0
  88. package/docs/gaps.md +161 -0
  89. package/docs/install.md +94 -0
  90. package/docs/integration-and-design-decisions.md +1530 -0
  91. package/docs/kradle-design.md +334 -0
  92. package/docs/local-minikube.md +55 -0
  93. package/docs/ontology/README.md +32 -0
  94. package/docs/ontology/bounded-contexts.md +29 -0
  95. package/docs/ontology/events-and-hooks.md +32 -0
  96. package/docs/ontology/oam-kubevela.md +32 -0
  97. package/docs/ontology/operations-and-release.md +25 -0
  98. package/docs/ontology/personas-and-actors.md +32 -0
  99. package/docs/ontology/policies-and-invariants.md +33 -0
  100. package/docs/ontology/problem-space.md +30 -0
  101. package/docs/ontology/resource-contracts.md +40 -0
  102. package/docs/ontology/resource-taxonomy.md +42 -0
  103. package/docs/ontology/runners-and-ci.md +29 -0
  104. package/docs/ontology/solution-space.md +24 -0
  105. package/docs/ontology/storage-and-data-boundaries.md +29 -0
  106. package/docs/ontology/validation-matrix.md +24 -0
  107. package/docs/ontology/web-ui-excellent-flows.md +32 -0
  108. package/docs/ontology/workflows.md +39 -0
  109. package/docs/ontology/world.md +35 -0
  110. package/docs/openapi.yaml +1291 -0
  111. package/docs/product-requirements.md +62 -0
  112. package/docs/requirements-v2.md +235 -0
  113. package/docs/roadmap-mvp.md +87 -0
  114. package/docs/sdk-api-reference.md +1108 -0
  115. package/docs/system-requirements.md +90 -0
  116. package/docs/system-spec-v2.md +1230 -0
  117. package/docs/tests/README.md +53 -0
  118. package/docs/tests/agent-qa-plan.md +63 -0
  119. package/docs/tests/browser-ui-tests.md +62 -0
  120. package/docs/tests/ci-quality-gates.md +48 -0
  121. package/docs/tests/coverage-model.md +64 -0
  122. package/docs/tests/e2e-scenario-tests.md +53 -0
  123. package/docs/tests/fixtures-test-data.md +63 -0
  124. package/docs/tests/observability-reliability-tests.md +54 -0
  125. package/docs/tests/product-test-matrix.md +145 -0
  126. package/docs/tests/qa-adoption-roadmap.md +130 -0
  127. package/docs/tests/qa-automation-plan.md +101 -0
  128. package/docs/tests/security-compliance-tests.md +57 -0
  129. package/docs/tests/test-framework-tools.md +88 -0
  130. package/docs/tests/test-suite-layout.md +121 -0
  131. package/docs/tests/unit-integration-tests.md +48 -0
  132. package/docs/todo-kyverno +714 -0
  133. package/docs/todos.md +4 -0
  134. package/docs/user-stories.md +78 -0
  135. package/docs/web-console-spec.md +533 -0
  136. package/examples/minikube-demo.yaml +190 -0
  137. package/examples/oam-application.yaml +23 -0
  138. package/examples/policy-kyverno-pr-title.yaml +18 -0
  139. package/package.json +66 -0
  140. package/scripts/build.mjs +29 -0
  141. package/scripts/setup-minikube.mjs +65 -0
  142. package/scripts/smoke.mjs +37 -0
  143. package/scripts/validate-doc-coverage.mjs +152 -0
  144. package/scripts/validate-package.mjs +95 -0
  145. package/scripts/validate-ui.mjs +305 -0
  146. package/src/agent-adapter-controller.js +169 -0
  147. package/src/agent-approval-controller.js +170 -0
  148. package/src/agent-context-bundles.js +242 -0
  149. package/src/agent-dispatch-controller.js +549 -0
  150. package/src/agent-gateway-config-controller.js +147 -0
  151. package/src/agent-identity-migration.js +115 -0
  152. package/src/agent-memory-controller.js +357 -0
  153. package/src/agent-memory-import.js +327 -0
  154. package/src/agent-memory-query.js +292 -0
  155. package/src/agent-memory-repository-source-controller.js +255 -0
  156. package/src/agent-mux-client.js +589 -0
  157. package/src/agent-permission-review.js +250 -0
  158. package/src/agent-persona-controller.js +135 -0
  159. package/src/agent-project-controller.js +117 -0
  160. package/src/agent-prompt-composition.js +55 -0
  161. package/src/agent-provider-config-controller.js +151 -0
  162. package/src/agent-secret-config-grant-controller.js +282 -0
  163. package/src/agent-session-transcript-controller.js +189 -0
  164. package/src/agent-stack-controller.js +421 -0
  165. package/src/agent-subagent-controller.js +160 -0
  166. package/src/agent-transport-binding-controller.js +121 -0
  167. package/src/agent-trigger-controller.js +387 -0
  168. package/src/agent-workspace-controller.js +702 -0
  169. package/src/agent-writeback-controller.js +302 -0
  170. package/src/api-controller.js +621 -0
  171. package/src/argocd-gitops.js +43 -0
  172. package/src/artifact-registry-controller.js +542 -0
  173. package/src/assistant-runtime.js +284 -0
  174. package/src/async-controller.js +207 -0
  175. package/src/audit-controller.js +191 -0
  176. package/src/auth.js +310 -0
  177. package/src/component-catalog.js +41 -0
  178. package/src/control-plane.js +136 -0
  179. package/src/controller-client.js +112 -0
  180. package/src/controller-ui.js +620 -0
  181. package/src/data-plane.js +179 -0
  182. package/src/event-bus.js +397 -0
  183. package/src/external/conflict-controller.js +225 -0
  184. package/src/external/github/auth.js +96 -0
  185. package/src/external/github/cicd.js +180 -0
  186. package/src/external/github/git-forge.js +240 -0
  187. package/src/external/github/index.js +144 -0
  188. package/src/external/github/issue-tracking.js +163 -0
  189. package/src/external/provider-adapter.js +161 -0
  190. package/src/external/provider-resource-factory.js +221 -0
  191. package/src/external/sync-controller.js +235 -0
  192. package/src/external/webhook-controller.js +144 -0
  193. package/src/external/write-controller.js +283 -0
  194. package/src/gitea-backend.js +131 -0
  195. package/src/gitea-service.js +173 -0
  196. package/src/handoff.js +98 -0
  197. package/src/health-probes.js +134 -0
  198. package/src/hooks-events.js +63 -0
  199. package/src/hooks-lifecycle.js +117 -0
  200. package/src/http-server.js +409 -0
  201. package/src/identity-policy.js +86 -0
  202. package/src/index.js +71 -0
  203. package/src/jitsi-agent-bridge.js +141 -0
  204. package/src/jitsi-meeting-controller.js +291 -0
  205. package/src/jitsi-sync-controller.js +198 -0
  206. package/src/kradle-inference-service-controller.js +246 -0
  207. package/src/kubernetes-controller-async.js +531 -0
  208. package/src/kubernetes-controller.js +904 -0
  209. package/src/kubernetes-resource-gateway.js +48 -0
  210. package/src/model-route-controller.js +364 -0
  211. package/src/notification-controller.js +178 -0
  212. package/src/operations.js +112 -0
  213. package/src/org-scoping.js +5 -0
  214. package/src/resource-model.js +282 -0
  215. package/src/runner-controller.js +272 -0
  216. package/src/runners-ci.js +48 -0
  217. package/src/runtime.js +196 -0
  218. package/src/snapshot-cache.js +157 -0
  219. package/src/virtual-model-controller.js +538 -0
  220. package/src/virtual-model-hook-bridge.js +200 -0
  221. package/src/web-ui.js +40 -0
  222. package/tests/agent-adapter-controller.test.js +361 -0
  223. package/tests/agent-approval-controller.test.js +173 -0
  224. package/tests/agent-context-bundles.test.js +278 -0
  225. package/tests/agent-dispatch-controller.test.js +679 -0
  226. package/tests/agent-gateway-config-controller.test.js +386 -0
  227. package/tests/agent-identity-migration.test.js +87 -0
  228. package/tests/agent-memory-controller.test.js +461 -0
  229. package/tests/agent-memory-import-snapshot.test.js +477 -0
  230. package/tests/agent-memory-query.test.js +404 -0
  231. package/tests/agent-memory-repository-source.test.js +514 -0
  232. package/tests/agent-mux-client.test.js +389 -0
  233. package/tests/agent-mux-integration.test.js +971 -0
  234. package/tests/agent-permission-review-v2.test.js +317 -0
  235. package/tests/agent-permission-review.test.js +209 -0
  236. package/tests/agent-persona-controller.test.js +127 -0
  237. package/tests/agent-project-controller.test.js +302 -0
  238. package/tests/agent-prompt-composition.test.js +76 -0
  239. package/tests/agent-provider-config-controller.test.js +376 -0
  240. package/tests/agent-resources.test.js +303 -0
  241. package/tests/agent-secret-config-grant.test.js +231 -0
  242. package/tests/agent-session-transcript-controller.test.js +499 -0
  243. package/tests/agent-stack-controller.test.js +283 -0
  244. package/tests/agent-subagent-controller.test.js +201 -0
  245. package/tests/agent-transport-binding-controller.test.js +294 -0
  246. package/tests/agent-trigger-controller.test.js +271 -0
  247. package/tests/agent-trigger-routes.test.js +190 -0
  248. package/tests/agent-trigger-sources.test.js +245 -0
  249. package/tests/agent-workspace-controller.test.js +181 -0
  250. package/tests/agent-writeback.test.js +292 -0
  251. package/tests/approval-persistence.test.js +171 -0
  252. package/tests/artifact-registry.test.js +511 -0
  253. package/tests/assistant-runtime.test.js +506 -0
  254. package/tests/async-controller.test.js +252 -0
  255. package/tests/audit-controller.test.js +227 -0
  256. package/tests/codespace-controller.test.js +318 -0
  257. package/tests/controller-client.test.js +133 -0
  258. package/tests/deployment.test.js +527 -0
  259. package/tests/e2e/lifecycle.test.js +120 -0
  260. package/tests/event-bus-integration.test.js +355 -0
  261. package/tests/external-github-forge.test.js +560 -0
  262. package/tests/external-github-issues-cicd.test.js +520 -0
  263. package/tests/external-integration.test.js +470 -0
  264. package/tests/external-persistence.test.js +415 -0
  265. package/tests/external-provider-adapter.test.js +365 -0
  266. package/tests/external-resource-model.test.js +223 -0
  267. package/tests/external-webhook-sync.test.js +287 -0
  268. package/tests/external-write-conflict.test.js +353 -0
  269. package/tests/gitea-service.test.js +253 -0
  270. package/tests/health-check-real.test.js +165 -0
  271. package/tests/health-probes.test.js +90 -0
  272. package/tests/hooks-lifecycle.test.js +364 -0
  273. package/tests/integration/full-flow.test.js +266 -0
  274. package/tests/jitsi-agent-bridge.test.js +119 -0
  275. package/tests/jitsi-helm-integration.test.js +77 -0
  276. package/tests/jitsi-meeting-controller.test.js +170 -0
  277. package/tests/jitsi-resource-model.test.js +73 -0
  278. package/tests/jitsi-sync-controller.test.js +112 -0
  279. package/tests/kradle-inference-service.test.js +689 -0
  280. package/tests/kradle.test.js +779 -0
  281. package/tests/memory-search-wiring.test.js +270 -0
  282. package/tests/model-route-controller.test.js +733 -0
  283. package/tests/notification-controller.test.js +196 -0
  284. package/tests/notification-integration.test.js +179 -0
  285. package/tests/org-scoping.test.js +687 -0
  286. package/tests/runner-controller.test.js +327 -0
  287. package/tests/runner-integration.test.js +231 -0
  288. package/tests/session-cookie-hmac.test.js +151 -0
  289. package/tests/snapshot-performance.test.js +315 -0
  290. package/tests/sse-events.test.js +107 -0
  291. package/tests/virtual-model-controller.test.js +877 -0
  292. package/tests/virtual-model-hook-bridge.test.js +384 -0
  293. package/tests/webhook-trigger.test.js +198 -0
  294. package/tests/workspace-volumes.test.js +312 -0
  295. package/tests/writeback-persistence.test.js +207 -0
@@ -0,0 +1,506 @@
1
+ import assert from 'node:assert/strict';
2
+ import { describe, it, beforeEach } from 'node:test';
3
+ import {
4
+ createAssistantRuntime,
5
+ defaultAssistantConfig,
6
+ defaultSystemPrompt,
7
+ callModel,
8
+ ASSISTANT_RUNTIME_BOUNDARY,
9
+ } from '../src/assistant-runtime.js';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Helpers
13
+ // ---------------------------------------------------------------------------
14
+
15
+ /** Create a mock fetch that returns a successful Anthropic Messages API response. */
16
+ function mockFetch(overrides = {}) {
17
+ const defaultResponse = {
18
+ content: [{ type: 'text', text: 'Hello from the assistant.' }],
19
+ usage: { input_tokens: 10, output_tokens: 5 },
20
+ stop_reason: 'end_turn',
21
+ };
22
+ const body = { ...defaultResponse, ...overrides };
23
+ return async (_url, _opts) => ({
24
+ ok: true,
25
+ json: async () => body,
26
+ });
27
+ }
28
+
29
+ /** Create a mock fetch that returns an API error. */
30
+ function mockFetchError(message = 'rate_limit_exceeded') {
31
+ return async () => ({
32
+ ok: false,
33
+ json: async () => ({ error: { message } }),
34
+ });
35
+ }
36
+
37
+ /** Create a mock fetch that throws a network error. */
38
+ function mockFetchNetworkError(msg = 'ECONNREFUSED') {
39
+ return async () => { throw new Error(msg); };
40
+ }
41
+
42
+ /** Create a mock controller with getResource. */
43
+ function mockController(resources = {}) {
44
+ return {
45
+ async getResource(kind, name) {
46
+ const key = `${kind}/${name}`;
47
+ if (resources[key]) return { resource: resources[key] };
48
+ throw new Error(`Resource ${key} not found`);
49
+ },
50
+ };
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // ASSISTANT_RUNTIME_BOUNDARY
55
+ // ---------------------------------------------------------------------------
56
+
57
+ describe('ASSISTANT_RUNTIME_BOUNDARY', () => {
58
+ it('declares the expected role', () => {
59
+ assert.equal(ASSISTANT_RUNTIME_BOUNDARY.role, 'assistant-runtime');
60
+ });
61
+
62
+ it('includes chat sessions in owns list', () => {
63
+ assert.ok(ASSISTANT_RUNTIME_BOUNDARY.owns.includes('chat sessions'));
64
+ });
65
+ });
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // defaultAssistantConfig
69
+ // ---------------------------------------------------------------------------
70
+
71
+ describe('defaultAssistantConfig', () => {
72
+ it('returns an object with expected fields', () => {
73
+ const config = defaultAssistantConfig();
74
+ assert.equal(config.baseAgent, 'kradle-assistant');
75
+ assert.ok(typeof config.provider === 'string' || config.provider === undefined);
76
+ assert.equal(typeof config.model, 'string');
77
+ assert.equal(typeof config.systemPrompt, 'string');
78
+ assert.equal(config.approvalMode, 'prompt');
79
+ });
80
+
81
+ it('returns a fresh object each call', () => {
82
+ const a = defaultAssistantConfig();
83
+ const b = defaultAssistantConfig();
84
+ assert.notEqual(a, b);
85
+ assert.deepEqual(a, b);
86
+ });
87
+ });
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // defaultSystemPrompt
91
+ // ---------------------------------------------------------------------------
92
+
93
+ describe('defaultSystemPrompt', () => {
94
+ it('returns a non-empty string mentioning Kradle', () => {
95
+ const prompt = defaultSystemPrompt();
96
+ assert.equal(typeof prompt, 'string');
97
+ assert.ok(prompt.length > 50);
98
+ assert.ok(prompt.includes('Kradle'));
99
+ });
100
+ });
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // createAssistantRuntime — factory
104
+ // ---------------------------------------------------------------------------
105
+
106
+ describe('createAssistantRuntime — factory', () => {
107
+ it('returns an object with all expected methods', () => {
108
+ const runtime = createAssistantRuntime();
109
+ assert.equal(runtime.role, 'assistant-runtime');
110
+ assert.equal(typeof runtime.resolveConfig, 'function');
111
+ assert.equal(typeof runtime.createSession, 'function');
112
+ assert.equal(typeof runtime.chat, 'function');
113
+ assert.equal(typeof runtime.getSession, 'function');
114
+ assert.equal(typeof runtime.listSessions, 'function');
115
+ assert.equal(typeof runtime.deleteSession, 'function');
116
+ assert.equal(typeof runtime.structuredCall, 'function');
117
+ });
118
+ });
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // createSession
122
+ // ---------------------------------------------------------------------------
123
+
124
+ describe('createAssistantRuntime — createSession', () => {
125
+ it('creates a session with a generated ID when none provided', () => {
126
+ const runtime = createAssistantRuntime();
127
+ const session = runtime.createSession();
128
+ assert.equal(typeof session.id, 'string');
129
+ assert.ok(session.id.length > 0);
130
+ assert.deepEqual(session.messages, []);
131
+ assert.equal(session.status, 'active');
132
+ assert.equal(session.stackRef, 'assistant');
133
+ });
134
+
135
+ it('creates a session with a custom ID', () => {
136
+ const runtime = createAssistantRuntime();
137
+ const session = runtime.createSession('my-session-123');
138
+ assert.equal(session.id, 'my-session-123');
139
+ });
140
+
141
+ it('creates a session with a custom stackRef', () => {
142
+ const runtime = createAssistantRuntime();
143
+ const session = runtime.createSession(undefined, 'review-agent');
144
+ assert.equal(session.stackRef, 'review-agent');
145
+ });
146
+
147
+ it('has a valid ISO createdAt timestamp', () => {
148
+ const runtime = createAssistantRuntime();
149
+ const session = runtime.createSession();
150
+ assert.ok(!isNaN(Date.parse(session.createdAt)));
151
+ });
152
+ });
153
+
154
+ // ---------------------------------------------------------------------------
155
+ // getSession
156
+ // ---------------------------------------------------------------------------
157
+
158
+ describe('createAssistantRuntime — getSession', () => {
159
+ it('returns a created session by ID', () => {
160
+ const runtime = createAssistantRuntime();
161
+ const session = runtime.createSession('s1');
162
+ const found = runtime.getSession('s1');
163
+ assert.deepEqual(found, session);
164
+ });
165
+
166
+ it('returns null for unknown session ID', () => {
167
+ const runtime = createAssistantRuntime();
168
+ assert.equal(runtime.getSession('nonexistent'), null);
169
+ });
170
+ });
171
+
172
+ // ---------------------------------------------------------------------------
173
+ // listSessions
174
+ // ---------------------------------------------------------------------------
175
+
176
+ describe('createAssistantRuntime — listSessions', () => {
177
+ it('returns empty array when no sessions exist', () => {
178
+ const runtime = createAssistantRuntime();
179
+ assert.deepEqual(runtime.listSessions(), []);
180
+ });
181
+
182
+ it('returns all created sessions', () => {
183
+ const runtime = createAssistantRuntime();
184
+ runtime.createSession('a');
185
+ runtime.createSession('b');
186
+ runtime.createSession('c');
187
+ const list = runtime.listSessions();
188
+ assert.equal(list.length, 3);
189
+ const ids = list.map(s => s.id);
190
+ assert.ok(ids.includes('a'));
191
+ assert.ok(ids.includes('b'));
192
+ assert.ok(ids.includes('c'));
193
+ });
194
+ });
195
+
196
+ // ---------------------------------------------------------------------------
197
+ // deleteSession
198
+ // ---------------------------------------------------------------------------
199
+
200
+ describe('createAssistantRuntime — deleteSession', () => {
201
+ it('deletes an existing session and returns true', () => {
202
+ const runtime = createAssistantRuntime();
203
+ runtime.createSession('del-me');
204
+ assert.equal(runtime.deleteSession('del-me'), true);
205
+ assert.equal(runtime.getSession('del-me'), null);
206
+ });
207
+
208
+ it('returns false for a nonexistent session', () => {
209
+ const runtime = createAssistantRuntime();
210
+ assert.equal(runtime.deleteSession('ghost'), false);
211
+ });
212
+ });
213
+
214
+ // ---------------------------------------------------------------------------
215
+ // resolveConfig
216
+ // ---------------------------------------------------------------------------
217
+
218
+ describe('createAssistantRuntime — resolveConfig', () => {
219
+ it('returns default config when no controller provided', async () => {
220
+ const runtime = createAssistantRuntime();
221
+ const config = await runtime.resolveConfig(null);
222
+ assert.equal(config.baseAgent, 'kradle-assistant');
223
+ assert.ok(typeof config.provider === 'string' || config.provider === undefined);
224
+ });
225
+
226
+ it('returns default config when controller throws', async () => {
227
+ const runtime = createAssistantRuntime();
228
+ const controller = mockController({}); // empty — will throw
229
+ const config = await runtime.resolveConfig(controller);
230
+ assert.equal(config.baseAgent, 'kradle-assistant');
231
+ });
232
+
233
+ it('reads spec from controller when stack exists', async () => {
234
+ const runtime = createAssistantRuntime({ stackName: 'my-stack' });
235
+ const controller = mockController({
236
+ 'AgentStack/my-stack': {
237
+ spec: {
238
+ baseAgent: 'codex',
239
+ provider: 'openai',
240
+ model: 'o3-pro',
241
+ systemPrompt: 'Custom prompt',
242
+ approvalMode: 'auto',
243
+ },
244
+ },
245
+ });
246
+ const config = await runtime.resolveConfig(controller);
247
+ assert.equal(config.baseAgent, 'codex');
248
+ assert.equal(config.provider, 'openai');
249
+ assert.equal(config.model, 'o3-pro');
250
+ assert.equal(config.approvalMode, 'auto');
251
+ });
252
+
253
+ it('falls back to default when resource has no spec', async () => {
254
+ const runtime = createAssistantRuntime();
255
+ const controller = mockController({
256
+ 'AgentStack/assistant': { spec: null },
257
+ });
258
+ const config = await runtime.resolveConfig(controller);
259
+ assert.equal(config.baseAgent, 'kradle-assistant');
260
+ });
261
+ });
262
+
263
+ // ---------------------------------------------------------------------------
264
+ // chat
265
+ // ---------------------------------------------------------------------------
266
+
267
+ describe('createAssistantRuntime — chat', () => {
268
+ it('throws when session does not exist', async () => {
269
+ const runtime = createAssistantRuntime({ fetchImpl: mockFetch() });
270
+ await assert.rejects(
271
+ () => runtime.chat('no-such-session', 'hello'),
272
+ { message: /not found/ }
273
+ );
274
+ });
275
+
276
+ it('adds user message and gets assistant response', async () => {
277
+ const runtime = createAssistantRuntime({
278
+ fetchImpl: mockFetch(),
279
+ apiKey: 'test-key',
280
+ });
281
+ const session = runtime.createSession('chat-1');
282
+ const response = await runtime.chat('chat-1', 'What is Kradle?');
283
+
284
+ assert.equal(response.content, 'Hello from the assistant.');
285
+ assert.equal(session.messages.length, 2);
286
+ assert.equal(session.messages[0].role, 'user');
287
+ assert.equal(session.messages[0].content, 'What is Kradle?');
288
+ assert.equal(session.messages[1].role, 'assistant');
289
+ assert.equal(session.messages[1].content, 'Hello from the assistant.');
290
+ });
291
+
292
+ it('messages accumulate across multiple chat calls', async () => {
293
+ const runtime = createAssistantRuntime({
294
+ fetchImpl: mockFetch(),
295
+ apiKey: 'test-key',
296
+ });
297
+ const session = runtime.createSession('multi');
298
+ await runtime.chat('multi', 'First message');
299
+ await runtime.chat('multi', 'Second message');
300
+
301
+ assert.equal(session.messages.length, 4);
302
+ assert.equal(session.messages[0].role, 'user');
303
+ assert.equal(session.messages[0].content, 'First message');
304
+ assert.equal(session.messages[1].role, 'assistant');
305
+ assert.equal(session.messages[2].role, 'user');
306
+ assert.equal(session.messages[2].content, 'Second message');
307
+ assert.equal(session.messages[3].role, 'assistant');
308
+ });
309
+
310
+ it('chat messages have timestamps', async () => {
311
+ const runtime = createAssistantRuntime({
312
+ fetchImpl: mockFetch(),
313
+ apiKey: 'test-key',
314
+ });
315
+ runtime.createSession('ts');
316
+ await runtime.chat('ts', 'hi');
317
+ const session = runtime.getSession('ts');
318
+ for (const msg of session.messages) {
319
+ assert.ok(!isNaN(Date.parse(msg.timestamp)), `timestamp should be valid ISO: ${msg.timestamp}`);
320
+ }
321
+ });
322
+
323
+ it('returns helpful error when no API key is set', async () => {
324
+ // Temporarily unset env vars
325
+ const origAnthropic = process.env.ANTHROPIC_API_KEY;
326
+ const origKradle = process.env.KRADLE_ASSISTANT_API_KEY;
327
+ delete process.env.ANTHROPIC_API_KEY;
328
+ delete process.env.KRADLE_ASSISTANT_API_KEY;
329
+
330
+ try {
331
+ const runtime = createAssistantRuntime({ apiKey: undefined });
332
+ const session = runtime.createSession('no-key');
333
+ const response = await runtime.chat('no-key', 'hello');
334
+ assert.ok(response.content.includes('API key not configured'));
335
+ } finally {
336
+ if (origAnthropic !== undefined) process.env.ANTHROPIC_API_KEY = origAnthropic;
337
+ if (origKradle !== undefined) process.env.KRADLE_ASSISTANT_API_KEY = origKradle;
338
+ }
339
+ });
340
+
341
+ it('handles model API error response', async () => {
342
+ const runtime = createAssistantRuntime({
343
+ fetchImpl: mockFetchError('overloaded'),
344
+ apiKey: 'test-key',
345
+ });
346
+ runtime.createSession('err');
347
+ const response = await runtime.chat('err', 'test');
348
+ assert.equal(response.content, 'overloaded');
349
+ });
350
+
351
+ it('handles network error gracefully', async () => {
352
+ const runtime = createAssistantRuntime({
353
+ fetchImpl: mockFetchNetworkError('ECONNREFUSED'),
354
+ apiKey: 'test-key',
355
+ });
356
+ runtime.createSession('net-err');
357
+ const response = await runtime.chat('net-err', 'test');
358
+ assert.ok(response.content.includes('ECONNREFUSED'));
359
+ });
360
+ });
361
+
362
+ // ---------------------------------------------------------------------------
363
+ // structuredCall
364
+ // ---------------------------------------------------------------------------
365
+
366
+ describe('createAssistantRuntime — structuredCall', () => {
367
+ it('returns model response for a string task', async () => {
368
+ const runtime = createAssistantRuntime({
369
+ fetchImpl: mockFetch({ content: [{ type: 'text', text: '{"result": "done"}' }] }),
370
+ apiKey: 'test-key',
371
+ });
372
+ const response = await runtime.structuredCall('Summarize the deployment');
373
+ assert.equal(response.content, '{"result": "done"}');
374
+ });
375
+
376
+ it('serializes object task to JSON', async () => {
377
+ let capturedBody;
378
+ const runtime = createAssistantRuntime({
379
+ fetchImpl: async (_url, opts) => {
380
+ capturedBody = JSON.parse(opts.body);
381
+ return { ok: true, json: async () => ({ content: [{ type: 'text', text: 'ok' }], usage: {} }) };
382
+ },
383
+ apiKey: 'test-key',
384
+ });
385
+ await runtime.structuredCall({ action: 'deploy', target: 'staging' });
386
+ const userMsg = capturedBody.messages.find(m => m.role === 'user');
387
+ assert.ok(userMsg.content.includes('"action"'));
388
+ assert.ok(userMsg.content.includes('"deploy"'));
389
+ });
390
+
391
+ it('supports custom systemPrompt override', async () => {
392
+ let capturedBody;
393
+ const runtime = createAssistantRuntime({
394
+ fetchImpl: async (_url, opts) => {
395
+ capturedBody = JSON.parse(opts.body);
396
+ return { ok: true, json: async () => ({ content: [{ type: 'text', text: 'ok' }], usage: {} }) };
397
+ },
398
+ apiKey: 'test-key',
399
+ });
400
+ await runtime.structuredCall('Do something', { systemPrompt: 'You are a deployment expert.' });
401
+ assert.equal(capturedBody.system, 'You are a deployment expert.');
402
+ });
403
+
404
+ it('returns helpful error when no API key is set', async () => {
405
+ const origAnthropic = process.env.ANTHROPIC_API_KEY;
406
+ const origKradle = process.env.KRADLE_ASSISTANT_API_KEY;
407
+ delete process.env.ANTHROPIC_API_KEY;
408
+ delete process.env.KRADLE_ASSISTANT_API_KEY;
409
+
410
+ try {
411
+ const runtime = createAssistantRuntime({ apiKey: undefined });
412
+ const response = await runtime.structuredCall('test');
413
+ assert.ok(response.content.includes('API key not configured'));
414
+ } finally {
415
+ if (origAnthropic !== undefined) process.env.ANTHROPIC_API_KEY = origAnthropic;
416
+ if (origKradle !== undefined) process.env.KRADLE_ASSISTANT_API_KEY = origKradle;
417
+ }
418
+ });
419
+
420
+ it('uses stack config from controller when provided', async () => {
421
+ let capturedBody;
422
+ const runtime = createAssistantRuntime({
423
+ stackName: 'custom',
424
+ fetchImpl: async (_url, opts) => {
425
+ capturedBody = JSON.parse(opts.body);
426
+ return { ok: true, json: async () => ({ content: [{ type: 'text', text: 'ok' }], usage: {} }) };
427
+ },
428
+ apiKey: 'test-key',
429
+ });
430
+ const controller = mockController({
431
+ 'AgentStack/custom': {
432
+ spec: {
433
+ provider: 'anthropic',
434
+ model: 'claude-opus-4-20250514',
435
+ systemPrompt: 'You are a code reviewer.',
436
+ },
437
+ },
438
+ });
439
+ await runtime.structuredCall('Review this PR', { controller });
440
+ assert.equal(capturedBody.model, 'claude-opus-4-20250514');
441
+ assert.equal(capturedBody.system, 'You are a code reviewer.');
442
+ });
443
+ });
444
+
445
+ // ---------------------------------------------------------------------------
446
+ // callModel — unit
447
+ // ---------------------------------------------------------------------------
448
+
449
+ describe('callModel', () => {
450
+ it('returns API key error when none set', async () => {
451
+ const origAnthropic = process.env.ANTHROPIC_API_KEY;
452
+ const origKradle = process.env.KRADLE_ASSISTANT_API_KEY;
453
+ delete process.env.ANTHROPIC_API_KEY;
454
+ delete process.env.KRADLE_ASSISTANT_API_KEY;
455
+
456
+ try {
457
+ const result = await callModel({
458
+ provider: 'anthropic',
459
+ model: 'claude-sonnet-4-20250514',
460
+ messages: [{ role: 'user', content: 'hi' }],
461
+ });
462
+ assert.ok(result.content.includes('API key not configured'));
463
+ } finally {
464
+ if (origAnthropic !== undefined) process.env.ANTHROPIC_API_KEY = origAnthropic;
465
+ if (origKradle !== undefined) process.env.KRADLE_ASSISTANT_API_KEY = origKradle;
466
+ }
467
+ });
468
+
469
+ it('extracts tool_use blocks into toolCalls', async () => {
470
+ const result = await callModel({
471
+ provider: 'anthropic',
472
+ model: 'test-model',
473
+ messages: [{ role: 'user', content: 'use a tool' }],
474
+ apiKey: 'test-key',
475
+ fetchImpl: async () => ({
476
+ ok: true,
477
+ json: async () => ({
478
+ content: [
479
+ { type: 'text', text: 'Sure, I will use a tool.' },
480
+ { type: 'tool_use', id: 'tu_1', name: 'get_status', input: {} },
481
+ ],
482
+ usage: { input_tokens: 5, output_tokens: 3 },
483
+ stop_reason: 'tool_use',
484
+ }),
485
+ }),
486
+ });
487
+ assert.equal(result.toolCalls.length, 1);
488
+ assert.equal(result.toolCalls[0].name, 'get_status');
489
+ assert.equal(result.stopReason, 'tool_use');
490
+ assert.ok(result.content.includes('Sure'));
491
+ });
492
+ });
493
+
494
+ // ---------------------------------------------------------------------------
495
+ // Session isolation
496
+ // ---------------------------------------------------------------------------
497
+
498
+ describe('createAssistantRuntime — session isolation', () => {
499
+ it('separate runtime instances have independent session stores', () => {
500
+ const r1 = createAssistantRuntime();
501
+ const r2 = createAssistantRuntime();
502
+ r1.createSession('shared-id');
503
+ assert.ok(r1.getSession('shared-id'));
504
+ assert.equal(r2.getSession('shared-id'), null);
505
+ });
506
+ });