crewly 1.11.6 → 1.12.0

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 (142) hide show
  1. package/config/skills/agent/onboarding/synthesize-hierarchy/SKILL.md +65 -0
  2. package/config/skills/agent/onboarding/synthesize-hierarchy/execute.sh +61 -0
  3. package/config/skills/agent/web-search/SKILL.md +70 -0
  4. package/config/skills/agent/web-search/execute.sh +170 -0
  5. package/config/skills/agent/web-search/skill.json +23 -0
  6. package/dist/backend/backend/src/constants.d.ts +12 -0
  7. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  8. package/dist/backend/backend/src/constants.js +12 -0
  9. package/dist/backend/backend/src/constants.js.map +1 -1
  10. package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts +22 -0
  11. package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts.map +1 -1
  12. package/dist/backend/backend/src/controllers/cloud/cloud.controller.js +58 -0
  13. package/dist/backend/backend/src/controllers/cloud/cloud.controller.js.map +1 -1
  14. package/dist/backend/backend/src/controllers/cloud/cloud.routes.d.ts.map +1 -1
  15. package/dist/backend/backend/src/controllers/cloud/cloud.routes.js +3 -1
  16. package/dist/backend/backend/src/controllers/cloud/cloud.routes.js.map +1 -1
  17. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.d.ts +27 -0
  18. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.d.ts.map +1 -1
  19. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.js +108 -0
  20. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.js.map +1 -1
  21. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.d.ts +6 -2
  22. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.d.ts.map +1 -1
  23. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.js +9 -3
  24. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.js.map +1 -1
  25. package/dist/backend/backend/src/index.d.ts.map +1 -1
  26. package/dist/backend/backend/src/index.js +36 -2
  27. package/dist/backend/backend/src/index.js.map +1 -1
  28. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts +18 -0
  29. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts.map +1 -1
  30. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js +24 -2
  31. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js.map +1 -1
  32. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts +102 -0
  33. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts.map +1 -0
  34. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js +164 -0
  35. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js.map +1 -0
  36. package/dist/backend/backend/src/services/fission/fission-guard.service.d.ts +21 -0
  37. package/dist/backend/backend/src/services/fission/fission-guard.service.d.ts.map +1 -1
  38. package/dist/backend/backend/src/services/fission/fission-guard.service.js +30 -0
  39. package/dist/backend/backend/src/services/fission/fission-guard.service.js.map +1 -1
  40. package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.d.ts +4 -0
  41. package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.d.ts.map +1 -1
  42. package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.js +8 -0
  43. package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.js.map +1 -1
  44. package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.d.ts +79 -58
  45. package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.d.ts.map +1 -1
  46. package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.js +140 -65
  47. package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.js.map +1 -1
  48. package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.d.ts +117 -0
  49. package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.d.ts.map +1 -0
  50. package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.js +189 -0
  51. package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.js.map +1 -0
  52. package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.d.ts.map +1 -1
  53. package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.js +1 -0
  54. package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.js.map +1 -1
  55. package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.d.ts.map +1 -1
  56. package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.js +2 -0
  57. package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.js.map +1 -1
  58. package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.d.ts.map +1 -1
  59. package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.js +17 -1
  60. package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.js.map +1 -1
  61. package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts +50 -0
  62. package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts.map +1 -1
  63. package/dist/backend/backend/src/services/reconciler/reconcile-rules.js +71 -0
  64. package/dist/backend/backend/src/services/reconciler/reconcile-rules.js.map +1 -1
  65. package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts +18 -0
  66. package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts.map +1 -1
  67. package/dist/backend/backend/src/services/reconciler/reconciler.service.js +75 -1
  68. package/dist/backend/backend/src/services/reconciler/reconciler.service.js.map +1 -1
  69. package/dist/backend/backend/src/services/session/pty/pty-session-backend.d.ts +115 -0
  70. package/dist/backend/backend/src/services/session/pty/pty-session-backend.d.ts.map +1 -1
  71. package/dist/backend/backend/src/services/session/pty/pty-session-backend.js +189 -3
  72. package/dist/backend/backend/src/services/session/pty/pty-session-backend.js.map +1 -1
  73. package/dist/backend/backend/src/services/session/pty/pty-session.d.ts +28 -0
  74. package/dist/backend/backend/src/services/session/pty/pty-session.d.ts.map +1 -1
  75. package/dist/backend/backend/src/services/session/pty/pty-session.js +61 -1
  76. package/dist/backend/backend/src/services/session/pty/pty-session.js.map +1 -1
  77. package/dist/backend/backend/src/services/template/template.service.d.ts.map +1 -1
  78. package/dist/backend/backend/src/services/template/template.service.js +67 -2
  79. package/dist/backend/backend/src/services/template/template.service.js.map +1 -1
  80. package/dist/backend/backend/src/services/v3/cascade-request-status.d.ts +19 -1
  81. package/dist/backend/backend/src/services/v3/cascade-request-status.d.ts.map +1 -1
  82. package/dist/backend/backend/src/services/v3/cascade-request-status.js +39 -2
  83. package/dist/backend/backend/src/services/v3/cascade-request-status.js.map +1 -1
  84. package/dist/backend/backend/src/services/v3/escalation-router.service.d.ts +41 -0
  85. package/dist/backend/backend/src/services/v3/escalation-router.service.d.ts.map +1 -1
  86. package/dist/backend/backend/src/services/v3/escalation-router.service.js +169 -0
  87. package/dist/backend/backend/src/services/v3/escalation-router.service.js.map +1 -1
  88. package/dist/backend/backend/src/services/v3/request-cascade.subscriber.d.ts +4 -1
  89. package/dist/backend/backend/src/services/v3/request-cascade.subscriber.d.ts.map +1 -1
  90. package/dist/backend/backend/src/services/v3/request-cascade.subscriber.js +21 -0
  91. package/dist/backend/backend/src/services/v3/request-cascade.subscriber.js.map +1 -1
  92. package/dist/backend/backend/src/types/intent-task.types.d.ts.map +1 -1
  93. package/dist/backend/backend/src/types/intent-task.types.js +8 -0
  94. package/dist/backend/backend/src/types/intent-task.types.js.map +1 -1
  95. package/dist/backend/backend/src/types/v2/request.types.d.ts +1 -1
  96. package/dist/backend/backend/src/types/v2/request.types.d.ts.map +1 -1
  97. package/dist/backend/backend/src/types/v2/request.types.js +1 -0
  98. package/dist/backend/backend/src/types/v2/request.types.js.map +1 -1
  99. package/dist/cli/backend/src/constants.d.ts +12 -0
  100. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  101. package/dist/cli/backend/src/constants.js +12 -0
  102. package/dist/cli/backend/src/constants.js.map +1 -1
  103. package/package.json +9 -3
  104. package/packages/crewly-agent/README.md +27 -0
  105. package/packages/crewly-agent/bin/crewly-agent +33 -0
  106. package/packages/crewly-agent/package.json +39 -0
  107. package/packages/crewly-agent/src/cli.ts +168 -0
  108. package/packages/crewly-agent/src/runtime/agent-runner.service.test.ts +2355 -0
  109. package/packages/crewly-agent/src/runtime/agent-runner.service.ts +1827 -0
  110. package/packages/crewly-agent/src/runtime/agent-stream.service.test.ts +153 -0
  111. package/packages/crewly-agent/src/runtime/agent-stream.service.ts +225 -0
  112. package/packages/crewly-agent/src/runtime/agent-worker.test.ts +171 -0
  113. package/packages/crewly-agent/src/runtime/agent-worker.ts +193 -0
  114. package/packages/crewly-agent/src/runtime/api-client.ts +143 -0
  115. package/packages/crewly-agent/src/runtime/approval-queue.service.ts +307 -0
  116. package/packages/crewly-agent/src/runtime/audit-log.service.test.ts +208 -0
  117. package/packages/crewly-agent/src/runtime/audit-log.service.ts +332 -0
  118. package/packages/crewly-agent/src/runtime/audit-trail.service.test.ts +178 -0
  119. package/packages/crewly-agent/src/runtime/audit-trail.service.ts +151 -0
  120. package/packages/crewly-agent/src/runtime/auditor-tools.test.ts +274 -0
  121. package/packages/crewly-agent/src/runtime/auditor-tools.ts +311 -0
  122. package/packages/crewly-agent/src/runtime/cloud-config.ts +67 -0
  123. package/packages/crewly-agent/src/runtime/deepseek-sse-transform.test.ts +165 -0
  124. package/packages/crewly-agent/src/runtime/deepseek-sse-transform.ts +168 -0
  125. package/packages/crewly-agent/src/runtime/env-isolation.service.ts +246 -0
  126. package/packages/crewly-agent/src/runtime/in-process-log-buffer.test.ts +280 -0
  127. package/packages/crewly-agent/src/runtime/in-process-log-buffer.ts +317 -0
  128. package/packages/crewly-agent/src/runtime/index.ts +38 -0
  129. package/packages/crewly-agent/src/runtime/mcp-tool-bridge.test.ts +352 -0
  130. package/packages/crewly-agent/src/runtime/mcp-tool-bridge.ts +244 -0
  131. package/packages/crewly-agent/src/runtime/model-manager.test.ts +326 -0
  132. package/packages/crewly-agent/src/runtime/model-manager.ts +363 -0
  133. package/packages/crewly-agent/src/runtime/output-filter.service.ts +175 -0
  134. package/packages/crewly-agent/src/runtime/prompt-guard.service.ts +303 -0
  135. package/packages/crewly-agent/src/runtime/rate-limiter.test.ts +228 -0
  136. package/packages/crewly-agent/src/runtime/rate-limiter.ts +353 -0
  137. package/packages/crewly-agent/src/runtime/tool-registry.test.ts +2510 -0
  138. package/packages/crewly-agent/src/runtime/tool-registry.ts +2104 -0
  139. package/packages/crewly-agent/src/runtime/types.test.ts +519 -0
  140. package/packages/crewly-agent/src/runtime/types.ts +637 -0
  141. package/packages/crewly-agent/src/runtime/web-search.tool.test.ts +131 -0
  142. package/packages/crewly-agent/src/runtime/web-search.tool.ts +140 -0
@@ -0,0 +1,519 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ isModelProvider,
4
+ isModelConfig,
5
+ MODEL_PROVIDERS,
6
+ CREWLY_AGENT_DEFAULTS,
7
+ WRITE_TOOLS,
8
+ MODEL_CONTEXT_WINDOWS,
9
+ MODEL_OUTPUT_TOKEN_FLOORS,
10
+ SUPPORTED_MODELS,
11
+ resolveMaxOutputTokens,
12
+ } from './types.js';
13
+ import type {
14
+ ToolDefinition,
15
+ ToolSensitivity,
16
+ AuditEntry,
17
+ SecurityPolicy,
18
+ CompactionResult,
19
+ ContextBudgetStatus,
20
+ ToolCallbacks,
21
+ ApprovalCheckResult,
22
+ AuditLogFilters,
23
+ AgentRunResult,
24
+ } from './types.js';
25
+
26
+ describe('Crewly Agent Types', () => {
27
+ describe('MODEL_PROVIDERS', () => {
28
+ it('should contain all supported providers', () => {
29
+ expect(MODEL_PROVIDERS).toContain('anthropic');
30
+ expect(MODEL_PROVIDERS).toContain('openai');
31
+ expect(MODEL_PROVIDERS).toContain('google');
32
+ expect(MODEL_PROVIDERS).toContain('ollama');
33
+ expect(MODEL_PROVIDERS).toContain('deepseek');
34
+ expect(MODEL_PROVIDERS).toHaveLength(5);
35
+ });
36
+ });
37
+
38
+ describe('CREWLY_AGENT_DEFAULTS', () => {
39
+ it('should have sensible default values', () => {
40
+ expect(CREWLY_AGENT_DEFAULTS.MAX_STEPS).toBe(500);
41
+ expect(CREWLY_AGENT_DEFAULTS.API_BASE_URL).toBe('http://localhost:8787');
42
+ expect(CREWLY_AGENT_DEFAULTS.MAX_HISTORY_MESSAGES).toBe(100);
43
+ expect(CREWLY_AGENT_DEFAULTS.COMPACTION_THRESHOLD).toBe(0.8);
44
+ expect(CREWLY_AGENT_DEFAULTS.API_TIMEOUT_MS).toBe(30000);
45
+ });
46
+
47
+ it('should have a valid default model config', () => {
48
+ expect(CREWLY_AGENT_DEFAULTS.DEFAULT_MODEL.provider).toBe('google');
49
+ expect(CREWLY_AGENT_DEFAULTS.DEFAULT_MODEL.modelId).toBeTruthy();
50
+ expect(typeof CREWLY_AGENT_DEFAULTS.DEFAULT_MODEL.temperature).toBe('number');
51
+ expect(typeof CREWLY_AGENT_DEFAULTS.DEFAULT_MODEL.maxTokens).toBe('number');
52
+ });
53
+
54
+ it('should have a valid default security policy', () => {
55
+ const policy = CREWLY_AGENT_DEFAULTS.SECURITY_POLICY;
56
+ expect(policy.auditEnabled).toBe(true);
57
+ expect(policy.requireApproval).toEqual([]);
58
+ expect(policy.blockedTools).toEqual([]);
59
+ expect(policy.maxAuditEntries).toBe(500);
60
+ expect(policy.readOnlyMode).toBe(false);
61
+ });
62
+
63
+ it('should have valid default security guardrails', () => {
64
+ const guardrails = CREWLY_AGENT_DEFAULTS.SECURITY_GUARDRAILS;
65
+ expect(guardrails.outputFilterEnabled).toBe(true);
66
+ expect(guardrails.envIsolationEnabled).toBe(true);
67
+ expect(guardrails.promptGuardEnabled).toBe(true);
68
+ expect(guardrails.explicitEnvVars).toEqual([]);
69
+ });
70
+
71
+ /**
72
+ * P3 #7 — defensive guard: MODEL_TIMEOUT_MS must be Object.frozen at module
73
+ * load. `as const` is compile-time only and the `Record<string, number>` cast
74
+ * widens it back to mutable; without freeze, runtime writes like
75
+ * `MODEL_TIMEOUT_MS['x'] = 999` silently mutate global config table.
76
+ * Catches future regressions if anyone removes the freeze wrapper.
77
+ */
78
+ it('MODEL_TIMEOUT_MS is Object.frozen (defensive guard against runtime mutation)', () => {
79
+ expect(Object.isFrozen(CREWLY_AGENT_DEFAULTS.MODEL_TIMEOUT_MS)).toBe(true);
80
+ // In strict mode (which Node ESM modules use by default) writes to a
81
+ // frozen object throw TypeError. Without strict mode they would fail
82
+ // silently. Jest runs with strict mode under Node ESM.
83
+ expect(() => {
84
+ (CREWLY_AGENT_DEFAULTS.MODEL_TIMEOUT_MS as Record<string, number>)['test-mutation'] = 999;
85
+ }).toThrow(TypeError);
86
+ // And the existing entry is untouched.
87
+ expect(CREWLY_AGENT_DEFAULTS.MODEL_TIMEOUT_MS['deepseek-reasoner']).toBe(600_000);
88
+ expect(CREWLY_AGENT_DEFAULTS.MODEL_TIMEOUT_MS['test-mutation']).toBeUndefined();
89
+ });
90
+ });
91
+
92
+ describe('isModelProvider', () => {
93
+ it('should return true for valid providers', () => {
94
+ expect(isModelProvider('anthropic')).toBe(true);
95
+ expect(isModelProvider('openai')).toBe(true);
96
+ expect(isModelProvider('google')).toBe(true);
97
+ expect(isModelProvider('ollama')).toBe(true);
98
+ });
99
+
100
+ it('should return false for invalid providers', () => {
101
+ expect(isModelProvider('azure')).toBe(false);
102
+ expect(isModelProvider('')).toBe(false);
103
+ expect(isModelProvider('ANTHROPIC')).toBe(false);
104
+ });
105
+ });
106
+
107
+ describe('isModelConfig', () => {
108
+ it('should return true for valid configs', () => {
109
+ expect(isModelConfig({ provider: 'anthropic', modelId: 'claude-sonnet-4-20250514' })).toBe(true);
110
+ expect(isModelConfig({ provider: 'openai', modelId: 'gpt-4o', temperature: 0.5 })).toBe(true);
111
+ expect(isModelConfig({ provider: 'google', modelId: 'gemini-2.0-flash', maxTokens: 4096 })).toBe(true);
112
+ expect(isModelConfig({ provider: 'ollama', modelId: 'llama3.3:70b' })).toBe(true);
113
+ });
114
+
115
+ it('should return false for invalid configs', () => {
116
+ expect(isModelConfig(null)).toBe(false);
117
+ expect(isModelConfig(undefined)).toBe(false);
118
+ expect(isModelConfig(42)).toBe(false);
119
+ expect(isModelConfig('string')).toBe(false);
120
+ expect(isModelConfig({})).toBe(false);
121
+ expect(isModelConfig({ provider: 'anthropic' })).toBe(false);
122
+ expect(isModelConfig({ modelId: 'gpt-4o' })).toBe(false);
123
+ expect(isModelConfig({ provider: 'invalid', modelId: 'test' })).toBe(false);
124
+ expect(isModelConfig({ provider: 'anthropic', modelId: '' })).toBe(false);
125
+ });
126
+ });
127
+
128
+ describe('ToolDefinition', () => {
129
+ it('should be usable as a type for tool objects', () => {
130
+ const tool: ToolDefinition = {
131
+ description: 'A test tool',
132
+ inputSchema: { parse: () => ({}) } as any,
133
+ execute: async () => ({ result: 'ok' }),
134
+ };
135
+ expect(tool.description).toBe('A test tool');
136
+ expect(typeof tool.execute).toBe('function');
137
+ });
138
+
139
+ it('should support optional sensitivity field', () => {
140
+ const tool: ToolDefinition = {
141
+ description: 'A sensitive tool',
142
+ inputSchema: { parse: () => ({}) } as any,
143
+ execute: async () => ({ result: 'ok' }),
144
+ sensitivity: 'destructive',
145
+ };
146
+ expect(tool.sensitivity).toBe('destructive');
147
+ });
148
+ });
149
+
150
+ describe('ToolSensitivity', () => {
151
+ it('should accept valid sensitivity values', () => {
152
+ const safe: ToolSensitivity = 'safe';
153
+ const sensitive: ToolSensitivity = 'sensitive';
154
+ const destructive: ToolSensitivity = 'destructive';
155
+ expect(safe).toBe('safe');
156
+ expect(sensitive).toBe('sensitive');
157
+ expect(destructive).toBe('destructive');
158
+ });
159
+ });
160
+
161
+ describe('AuditEntry', () => {
162
+ it('should be constructible with required fields', () => {
163
+ const entry: AuditEntry = {
164
+ timestamp: '2026-03-12T00:00:00.000Z',
165
+ toolName: 'edit_file',
166
+ sensitivity: 'destructive',
167
+ args: { file_path: '/test.ts' },
168
+ success: true,
169
+ durationMs: 42,
170
+ };
171
+ expect(entry.toolName).toBe('edit_file');
172
+ expect(entry.sensitivity).toBe('destructive');
173
+ expect(entry.error).toBeUndefined();
174
+ expect(entry.sessionName).toBeUndefined();
175
+ });
176
+
177
+ it('should support optional error field', () => {
178
+ const entry: AuditEntry = {
179
+ timestamp: '2026-03-12T00:00:00.000Z',
180
+ toolName: 'write_file',
181
+ sensitivity: 'destructive',
182
+ args: {},
183
+ success: false,
184
+ error: 'EACCES',
185
+ durationMs: 5,
186
+ };
187
+ expect(entry.error).toBe('EACCES');
188
+ });
189
+
190
+ it('should support optional sessionName field', () => {
191
+ const entry: AuditEntry = {
192
+ timestamp: '2026-03-12T00:00:00.000Z',
193
+ sessionName: 'agent-session-abc',
194
+ toolName: 'delegate_task',
195
+ sensitivity: 'sensitive',
196
+ args: {},
197
+ success: true,
198
+ durationMs: 100,
199
+ };
200
+ expect(entry.sessionName).toBe('agent-session-abc');
201
+ });
202
+ });
203
+
204
+ describe('SecurityPolicy', () => {
205
+ it('should be constructible with all fields', () => {
206
+ const policy: SecurityPolicy = {
207
+ auditEnabled: true,
208
+ requireApproval: ['destructive'],
209
+ blockedTools: ['stop_agent'],
210
+ maxAuditEntries: 100,
211
+ readOnlyMode: false,
212
+ };
213
+ expect(policy.auditEnabled).toBe(true);
214
+ expect(policy.requireApproval).toContain('destructive');
215
+ expect(policy.blockedTools).toContain('stop_agent');
216
+ expect(policy.readOnlyMode).toBe(false);
217
+ });
218
+
219
+ it('should support readOnlyMode', () => {
220
+ const policy: SecurityPolicy = {
221
+ auditEnabled: true,
222
+ requireApproval: [],
223
+ blockedTools: [],
224
+ maxAuditEntries: 500,
225
+ readOnlyMode: true,
226
+ };
227
+ expect(policy.readOnlyMode).toBe(true);
228
+ });
229
+ });
230
+
231
+ describe('CompactionResult', () => {
232
+ it('should represent a successful compaction', () => {
233
+ const result: CompactionResult = {
234
+ compacted: true,
235
+ messagesBefore: 50,
236
+ messagesAfter: 11,
237
+ };
238
+ expect(result.compacted).toBe(true);
239
+ expect(result.reason).toBeUndefined();
240
+ });
241
+
242
+ it('should represent a skipped compaction with reason', () => {
243
+ const result: CompactionResult = {
244
+ compacted: false,
245
+ messagesBefore: 5,
246
+ messagesAfter: 5,
247
+ reason: 'Too few messages to compact',
248
+ };
249
+ expect(result.compacted).toBe(false);
250
+ expect(result.reason).toBeTruthy();
251
+ });
252
+ });
253
+
254
+ describe('ToolCallbacks', () => {
255
+ it('should be constructible with optional fields', () => {
256
+ const callbacks: ToolCallbacks = {};
257
+ expect(callbacks.onCompactMemory).toBeUndefined();
258
+ expect(callbacks.onAuditLog).toBeUndefined();
259
+ expect(callbacks.onCheckApproval).toBeUndefined();
260
+ expect(callbacks.onGetAuditLog).toBeUndefined();
261
+ });
262
+
263
+ it('should accept callback functions', () => {
264
+ const callbacks: ToolCallbacks = {
265
+ onCompactMemory: async () => ({ compacted: true, messagesBefore: 50, messagesAfter: 11 }),
266
+ onAuditLog: () => {},
267
+ onCheckApproval: () => ({ allowed: true }),
268
+ onGetAuditLog: () => [],
269
+ };
270
+ expect(typeof callbacks.onCompactMemory).toBe('function');
271
+ expect(typeof callbacks.onAuditLog).toBe('function');
272
+ expect(typeof callbacks.onCheckApproval).toBe('function');
273
+ expect(typeof callbacks.onGetAuditLog).toBe('function');
274
+ });
275
+ });
276
+
277
+ describe('ApprovalCheckResult', () => {
278
+ it('should represent an allowed result', () => {
279
+ const result: ApprovalCheckResult = { allowed: true };
280
+ expect(result.allowed).toBe(true);
281
+ expect(result.reason).toBeUndefined();
282
+ expect(result.blocked).toBeUndefined();
283
+ });
284
+
285
+ it('should represent a blocked tool', () => {
286
+ const result: ApprovalCheckResult = {
287
+ allowed: false,
288
+ blocked: true,
289
+ reason: 'Tool is blocked by security policy',
290
+ };
291
+ expect(result.allowed).toBe(false);
292
+ expect(result.blocked).toBe(true);
293
+ expect(result.reason).toBeTruthy();
294
+ });
295
+
296
+ it('should represent a tool requiring approval', () => {
297
+ const result: ApprovalCheckResult = {
298
+ allowed: false,
299
+ blocked: false,
300
+ reason: 'Tool requires approval for destructive operations',
301
+ };
302
+ expect(result.allowed).toBe(false);
303
+ expect(result.blocked).toBe(false);
304
+ expect(result.reason).toContain('approval');
305
+ });
306
+ });
307
+
308
+ describe('AuditLogFilters', () => {
309
+ it('should be constructible with required limit', () => {
310
+ const filters: AuditLogFilters = { limit: 50 };
311
+ expect(filters.limit).toBe(50);
312
+ expect(filters.sensitivity).toBeUndefined();
313
+ expect(filters.toolName).toBeUndefined();
314
+ });
315
+
316
+ it('should support optional filters', () => {
317
+ const filters: AuditLogFilters = {
318
+ limit: 10,
319
+ sensitivity: 'destructive',
320
+ toolName: 'edit_file',
321
+ };
322
+ expect(filters.sensitivity).toBe('destructive');
323
+ expect(filters.toolName).toBe('edit_file');
324
+ });
325
+ });
326
+
327
+ describe('WRITE_TOOLS', () => {
328
+ it('should be a non-empty readonly array', () => {
329
+ expect(WRITE_TOOLS.length).toBeGreaterThan(0);
330
+ expect(Array.isArray(WRITE_TOOLS)).toBe(true);
331
+ });
332
+
333
+ it('should contain all file-modifying tools', () => {
334
+ expect(WRITE_TOOLS).toContain('edit_file');
335
+ expect(WRITE_TOOLS).toContain('write_file');
336
+ });
337
+
338
+ it('should contain agent lifecycle tools', () => {
339
+ expect(WRITE_TOOLS).toContain('start_agent');
340
+ expect(WRITE_TOOLS).toContain('stop_agent');
341
+ expect(WRITE_TOOLS).toContain('handle_agent_failure');
342
+ });
343
+
344
+ it('should not contain read-only tools', () => {
345
+ expect(WRITE_TOOLS).not.toContain('read_file');
346
+ expect(WRITE_TOOLS).not.toContain('get_team_status');
347
+ expect(WRITE_TOOLS).not.toContain('recall_memory');
348
+ });
349
+ });
350
+
351
+ describe('MODEL_CONTEXT_WINDOWS', () => {
352
+ it('should have a default entry', () => {
353
+ expect(MODEL_CONTEXT_WINDOWS.default).toBeDefined();
354
+ expect(MODEL_CONTEXT_WINDOWS.default).toBeGreaterThan(0);
355
+ });
356
+
357
+ it('should include known Anthropic models', () => {
358
+ expect(MODEL_CONTEXT_WINDOWS['claude-opus-4-20250514']).toBe(200_000);
359
+ expect(MODEL_CONTEXT_WINDOWS['claude-sonnet-4-20250514']).toBe(200_000);
360
+ });
361
+
362
+ it('should include known Google models', () => {
363
+ expect(MODEL_CONTEXT_WINDOWS['gemini-2.0-flash']).toBe(1_000_000);
364
+ });
365
+
366
+ it('should include known OpenAI models', () => {
367
+ expect(MODEL_CONTEXT_WINDOWS['gpt-4o']).toBe(128_000);
368
+ });
369
+
370
+ it('should include DeepSeek models with the real 64k window (B3)', () => {
371
+ // Without these entries the lookup falls back to `default: 128_000`,
372
+ // which would let Crewly's compaction trigger (0.8 × budget) skip past
373
+ // DeepSeek's real 64k cap and let the API 4xx with context_length_exceeded.
374
+ expect(MODEL_CONTEXT_WINDOWS['deepseek-chat']).toBe(64_000);
375
+ expect(MODEL_CONTEXT_WINDOWS['deepseek-reasoner']).toBe(64_000);
376
+ });
377
+ });
378
+
379
+ describe('SUPPORTED_MODELS (B2)', () => {
380
+ it('should include both DeepSeek model variants for the model picker', () => {
381
+ const ids = SUPPORTED_MODELS.map((m) => m.id);
382
+ expect(ids).toContain('deepseek/deepseek-chat');
383
+ expect(ids).toContain('deepseek/deepseek-reasoner');
384
+ });
385
+
386
+ it('should tag DeepSeek entries with the deepseek provider', () => {
387
+ const chat = SUPPORTED_MODELS.find((m) => m.id === 'deepseek/deepseek-chat');
388
+ const reasoner = SUPPORTED_MODELS.find((m) => m.id === 'deepseek/deepseek-reasoner');
389
+ expect(chat?.provider).toBe('deepseek');
390
+ expect(reasoner?.provider).toBe('deepseek');
391
+ expect(chat?.label).toBeTruthy();
392
+ expect(reasoner?.label).toBeTruthy();
393
+ });
394
+ });
395
+
396
+ describe('MODEL_OUTPUT_TOKEN_FLOORS / resolveMaxOutputTokens (N5)', () => {
397
+ it('should declare a 1024-token floor only for deepseek-reasoner', () => {
398
+ // R1 mixes reasoning_tokens into the same max_tokens budget as
399
+ // completion_tokens, so a low limit silently produces empty content.
400
+ // See live smoke test D in the gap-list spec (2026-05-03).
401
+ expect(MODEL_OUTPUT_TOKEN_FLOORS['deepseek-reasoner']).toBe(1024);
402
+ expect(MODEL_OUTPUT_TOKEN_FLOORS['deepseek-chat']).toBeUndefined();
403
+ expect(MODEL_OUTPUT_TOKEN_FLOORS['gpt-4o']).toBeUndefined();
404
+ });
405
+
406
+ it('should clamp deepseek-reasoner maxTokens up to the 1024 floor', () => {
407
+ expect(
408
+ resolveMaxOutputTokens({ provider: 'deepseek', modelId: 'deepseek-reasoner', maxTokens: 30 })
409
+ ).toBe(1024);
410
+ expect(
411
+ resolveMaxOutputTokens({ provider: 'deepseek', modelId: 'deepseek-reasoner', maxTokens: 500 })
412
+ ).toBe(1024);
413
+ });
414
+
415
+ it('should pass through user-set maxTokens when above the floor', () => {
416
+ expect(
417
+ resolveMaxOutputTokens({ provider: 'deepseek', modelId: 'deepseek-reasoner', maxTokens: 8192 })
418
+ ).toBe(8192);
419
+ });
420
+
421
+ it('should not clamp models without a floor', () => {
422
+ expect(
423
+ resolveMaxOutputTokens({ provider: 'deepseek', modelId: 'deepseek-chat', maxTokens: 30 })
424
+ ).toBe(30);
425
+ expect(
426
+ resolveMaxOutputTokens({ provider: 'openai', modelId: 'gpt-4o', maxTokens: 100 })
427
+ ).toBe(100);
428
+ });
429
+
430
+ it('should fall back to the runtime default when maxTokens is undefined', () => {
431
+ const fallback = CREWLY_AGENT_DEFAULTS.DEFAULT_MODEL.maxTokens ?? 0;
432
+ expect(
433
+ resolveMaxOutputTokens({ provider: 'openai', modelId: 'gpt-4o' })
434
+ ).toBe(fallback);
435
+ });
436
+
437
+ it('should still apply the floor when maxTokens is undefined for deepseek-reasoner', () => {
438
+ // CREWLY_AGENT_DEFAULTS.DEFAULT_MODEL.maxTokens=8192 already clears the
439
+ // 1024 floor, so the result is the default itself — but the contract
440
+ // remains: `result >= floor`.
441
+ const result = resolveMaxOutputTokens({ provider: 'deepseek', modelId: 'deepseek-reasoner' });
442
+ expect(result).toBeGreaterThanOrEqual(1024);
443
+ });
444
+ });
445
+
446
+ describe('ContextBudgetStatus type', () => {
447
+ it('should accept valid normal status', () => {
448
+ const status: ContextBudgetStatus = {
449
+ totalTokensUsed: 5000,
450
+ contextWindowSize: 200000,
451
+ usagePercent: 0.025,
452
+ level: 'normal',
453
+ messageCount: 10,
454
+ compactionPending: false,
455
+ summary: '2.5% of context budget used',
456
+ };
457
+ expect(status.level).toBe('normal');
458
+ expect(status.compactionPending).toBe(false);
459
+ });
460
+
461
+ it('should accept valid critical status', () => {
462
+ const status: ContextBudgetStatus = {
463
+ totalTokensUsed: 180000,
464
+ contextWindowSize: 200000,
465
+ usagePercent: 0.9,
466
+ level: 'critical',
467
+ messageCount: 95,
468
+ compactionPending: true,
469
+ summary: '90.0% — CRITICAL',
470
+ };
471
+ expect(status.level).toBe('critical');
472
+ expect(status.compactionPending).toBe(true);
473
+ });
474
+ });
475
+
476
+ describe('AgentRunResult budgetWarning', () => {
477
+ it('should accept result with budgetWarning', () => {
478
+ const result: AgentRunResult = {
479
+ text: 'Done',
480
+ steps: 1,
481
+ usage: { input: 100, output: 50 },
482
+ toolCalls: [],
483
+ finishReason: 'stop',
484
+ budgetWarning: 'WARNING: approaching compaction threshold',
485
+ };
486
+ expect(result.budgetWarning).toContain('WARNING');
487
+ });
488
+
489
+ it('should accept result without budgetWarning', () => {
490
+ const result: AgentRunResult = {
491
+ text: 'Done',
492
+ steps: 1,
493
+ usage: { input: 100, output: 50 },
494
+ toolCalls: [],
495
+ finishReason: 'stop',
496
+ };
497
+ expect(result.budgetWarning).toBeUndefined();
498
+ });
499
+ });
500
+
501
+ describe('ToolCallbacks onGetContextBudget', () => {
502
+ it('should accept callbacks with onGetContextBudget', () => {
503
+ const callbacks: ToolCallbacks = {
504
+ onGetContextBudget: () => ({
505
+ totalTokensUsed: 0,
506
+ contextWindowSize: 200000,
507
+ usagePercent: 0,
508
+ level: 'normal',
509
+ messageCount: 0,
510
+ compactionPending: false,
511
+ summary: '0%',
512
+ }),
513
+ };
514
+ expect(callbacks.onGetContextBudget).toBeDefined();
515
+ const result = callbacks.onGetContextBudget!();
516
+ expect(result.level).toBe('normal');
517
+ });
518
+ });
519
+ });