@cleocode/core 2026.4.72 → 2026.4.73

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cleocode/core",
3
- "version": "2026.4.72",
3
+ "version": "2026.4.73",
4
4
  "description": "CLEO core business logic kernel — tasks, sessions, memory, orchestration, lifecycle, with bundled SQLite store",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -76,13 +76,13 @@
76
76
  "write-file-atomic": "^7.0.1",
77
77
  "yaml": "^2.8.3",
78
78
  "zod": "^4.3.6",
79
- "@cleocode/adapters": "2026.4.72",
80
- "@cleocode/agents": "2026.4.72",
81
- "@cleocode/contracts": "2026.4.72",
82
- "@cleocode/caamp": "2026.4.72",
83
- "@cleocode/lafs": "2026.4.72",
84
- "@cleocode/nexus": "2026.4.72",
85
- "@cleocode/skills": "2026.4.72"
79
+ "@cleocode/adapters": "2026.4.73",
80
+ "@cleocode/agents": "2026.4.73",
81
+ "@cleocode/caamp": "2026.4.73",
82
+ "@cleocode/contracts": "2026.4.73",
83
+ "@cleocode/lafs": "2026.4.73",
84
+ "@cleocode/nexus": "2026.4.73",
85
+ "@cleocode/skills": "2026.4.73"
86
86
  },
87
87
  "engines": {
88
88
  "node": ">=24.0.0"
@@ -0,0 +1,351 @@
1
+ /**
2
+ * Gate runner tests — comprehensive coverage per gate kind.
3
+ *
4
+ * Tests `runGates()` from packages/core/src/tasks/gate-runner.ts
5
+ * ensuring each gate type (test, file, command, lint, http, manual)
6
+ * works correctly with contract validation.
7
+ *
8
+ * @task T784
9
+ * @epic T768
10
+ */
11
+
12
+ import { rm, writeFile } from 'node:fs/promises';
13
+ import { dirname, join, resolve } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+ import type {
16
+ CommandGate,
17
+ FileGate,
18
+ HttpGate,
19
+ LintGate,
20
+ ManualGate,
21
+ TestGate,
22
+ } from '@cleocode/contracts';
23
+ import { beforeAll, describe, expect, it } from 'vitest';
24
+ import { getProjectRoot } from '../../paths.js';
25
+ import { runGates } from '../gate-runner.js';
26
+
27
+ // ─── Setup ────────────────────────────────────────────────────────────────
28
+
29
+ const thisDir = dirname(fileURLToPath(import.meta.url));
30
+ const projectRoot = getProjectRoot();
31
+ const testDir = resolve(thisDir, '.gate-runner-test');
32
+
33
+ beforeAll(async () => {
34
+ // Create test directory
35
+ try {
36
+ await rm(testDir, { recursive: true, force: true });
37
+ } catch {
38
+ // Ignore
39
+ }
40
+ });
41
+
42
+ // ─── Gate Kind Tests ────────────────────────────────────────────────────────
43
+
44
+ describe('gate-runner — test gate', () => {
45
+ it('accepts a test gate with passing exit code', async () => {
46
+ const gates: TestGate[] = [
47
+ {
48
+ kind: 'test',
49
+ description: 'sample-test — validates passing test gate',
50
+ command: 'echo',
51
+ args: ['hello'],
52
+ expect: 'exit0',
53
+ },
54
+ ];
55
+
56
+ const results = await runGates(gates, { projectRoot });
57
+
58
+ expect(results).toHaveLength(1);
59
+ expect(results[0]).toMatchObject({
60
+ kind: 'test',
61
+ result: 'pass',
62
+ });
63
+ });
64
+
65
+ it('rejects a test gate with failing exit code', async () => {
66
+ const gates: TestGate[] = [
67
+ {
68
+ kind: 'test',
69
+ description: 'failing-test — validates failing test gate',
70
+ command: 'false',
71
+ expect: 'exit0',
72
+ },
73
+ ];
74
+
75
+ const results = await runGates(gates, { projectRoot });
76
+
77
+ expect(results).toHaveLength(1);
78
+ expect(results[0]).toMatchObject({
79
+ kind: 'test',
80
+ result: 'fail',
81
+ });
82
+ // Gate-runner emits `errorMessage` on fail (not free-text failureReason)
83
+ expect(results[0].errorMessage).toBeDefined();
84
+ });
85
+ });
86
+
87
+ describe('gate-runner — file gate', () => {
88
+ it('validates file existence', async () => {
89
+ // Use an existing file from the project
90
+ const existingFile = join(projectRoot, 'package.json');
91
+
92
+ const gates: FileGate[] = [
93
+ {
94
+ kind: 'file',
95
+ description: 'package-json-exists — validates file gate pass',
96
+ path: existingFile,
97
+ assertions: [{ type: 'exists' }],
98
+ },
99
+ ];
100
+
101
+ const results = await runGates(gates, { projectRoot });
102
+
103
+ expect(results).toHaveLength(1);
104
+ expect(results[0]).toMatchObject({
105
+ kind: 'file',
106
+ result: 'pass',
107
+ });
108
+ });
109
+
110
+ it('rejects when file does not exist', async () => {
111
+ const gates: FileGate[] = [
112
+ {
113
+ kind: 'file',
114
+ description: 'nonexistent — validates file gate fail',
115
+ path: '/tmp/this-does-not-exist-12345.txt',
116
+ assertions: [{ type: 'exists' }],
117
+ },
118
+ ];
119
+
120
+ const results = await runGates(gates, { projectRoot });
121
+
122
+ expect(results).toHaveLength(1);
123
+ expect(results[0]).toMatchObject({
124
+ kind: 'file',
125
+ result: 'fail',
126
+ });
127
+ });
128
+ });
129
+
130
+ describe('gate-runner — command gate', () => {
131
+ it('passes with successful command', async () => {
132
+ const gates: CommandGate[] = [
133
+ {
134
+ kind: 'command',
135
+ description: 'echo-test — validates command execution',
136
+ cmd: 'echo',
137
+ args: ['test output'],
138
+ exitCode: 0,
139
+ },
140
+ ];
141
+
142
+ const results = await runGates(gates, { projectRoot });
143
+
144
+ expect(results).toHaveLength(1);
145
+ expect(results[0]).toMatchObject({
146
+ kind: 'command',
147
+ result: 'pass',
148
+ });
149
+ });
150
+
151
+ it('fails with unexpected exit code', async () => {
152
+ const gates: CommandGate[] = [
153
+ {
154
+ kind: 'command',
155
+ description: 'false-command — validates exit-code rejection',
156
+ cmd: 'false',
157
+ exitCode: 0,
158
+ },
159
+ ];
160
+
161
+ const results = await runGates(gates, { projectRoot });
162
+
163
+ expect(results).toHaveLength(1);
164
+ expect(results[0]).toMatchObject({
165
+ kind: 'command',
166
+ result: 'fail',
167
+ });
168
+ });
169
+ });
170
+
171
+ describe('gate-runner — lint gate', () => {
172
+ it('skips lint gate gracefully when command not found', async () => {
173
+ // This test verifies that lint gates handle missing tools gracefully
174
+ const gates: LintGate[] = [
175
+ {
176
+ kind: 'lint',
177
+ description: 'biome-format — validates lint gate',
178
+ linter: 'biome',
179
+ paths: ['packages/core/src/tasks/gate-runner.ts'],
180
+ mode: 'format',
181
+ },
182
+ ];
183
+
184
+ const results = await runGates(gates, { projectRoot });
185
+
186
+ expect(results).toHaveLength(1);
187
+ expect(results[0]).toMatchObject({
188
+ kind: 'lint',
189
+ });
190
+ // Should either pass or fail depending on whether biome is configured
191
+ expect(['pass', 'fail', 'warn', 'skipped', 'error']).toContain(results[0].result);
192
+ });
193
+ });
194
+
195
+ describe('gate-runner — http gate', () => {
196
+ it('skips http gate when network unavailable', async () => {
197
+ const gates: HttpGate[] = [
198
+ {
199
+ kind: 'http',
200
+ description: 'health-check — validates http gate',
201
+ url: 'http://127.0.0.1:99999/health',
202
+ status: 200,
203
+ timeoutMs: 1000,
204
+ },
205
+ ];
206
+
207
+ const results = await runGates(gates, { projectRoot });
208
+
209
+ expect(results).toHaveLength(1);
210
+ expect(results[0]).toMatchObject({
211
+ kind: 'http',
212
+ });
213
+ // Should fail or skip depending on network configuration
214
+ expect(['fail', 'skipped', 'warn', 'error']).toContain(results[0].result);
215
+ });
216
+ });
217
+
218
+ describe('gate-runner — manual gate', () => {
219
+ it('returns skipped for manual gates by default', async () => {
220
+ const gates: ManualGate[] = [
221
+ {
222
+ kind: 'manual',
223
+ description: 'manual-review — validates manual gate',
224
+ prompt: 'Please review the implementation',
225
+ },
226
+ ];
227
+
228
+ const results = await runGates(gates, { projectRoot }, { skipManual: true });
229
+
230
+ expect(results).toHaveLength(1);
231
+ expect(results[0]).toMatchObject({
232
+ kind: 'manual',
233
+ result: 'skipped',
234
+ });
235
+ });
236
+
237
+ it('returns skipped for manual gates without skipManual flag', async () => {
238
+ const gates: ManualGate[] = [
239
+ {
240
+ kind: 'manual',
241
+ description: 'manual-review-2 — validates manual gate with accept',
242
+ prompt: 'Please review',
243
+ },
244
+ ];
245
+
246
+ const results = await runGates(gates, { projectRoot }, { skipManual: false });
247
+
248
+ expect(results).toHaveLength(1);
249
+ expect(results[0]).toMatchObject({
250
+ kind: 'manual',
251
+ result: 'skipped',
252
+ });
253
+ });
254
+ });
255
+
256
+ describe('gate-runner — multi-gate execution', () => {
257
+ it('runs multiple gates sequentially', async () => {
258
+ const gates = [
259
+ {
260
+ kind: 'test' as const,
261
+ description: 'test-1 — multi-gate test gate',
262
+ command: 'echo',
263
+ args: ['test1'],
264
+ expect: 'exit0' as const,
265
+ },
266
+ {
267
+ kind: 'command' as const,
268
+ description: 'cmd-1 — multi-gate command gate',
269
+ cmd: 'echo',
270
+ args: ['cmd1'],
271
+ exitCode: 0,
272
+ },
273
+ {
274
+ kind: 'manual' as const,
275
+ description: 'manual-1 — multi-gate manual gate',
276
+ prompt: 'Review test',
277
+ },
278
+ ] as const;
279
+
280
+ const results = await runGates(
281
+ gates as unknown as Parameters<typeof runGates>[0],
282
+ { projectRoot },
283
+ { skipManual: true },
284
+ );
285
+
286
+ expect(results).toHaveLength(3);
287
+ expect(results[0].result).toBe('pass');
288
+ expect(results[1].result).toBe('pass');
289
+ expect(results[2].result).toBe('skipped');
290
+ });
291
+
292
+ it('includes metadata in results', async () => {
293
+ const gates: TestGate[] = [
294
+ {
295
+ kind: 'test',
296
+ description: 'metadata-test — validates result metadata shape',
297
+ command: 'echo',
298
+ args: ['hello'],
299
+ expect: 'exit0',
300
+ },
301
+ ];
302
+
303
+ const results = await runGates(gates, { projectRoot });
304
+
305
+ expect(results).toHaveLength(1);
306
+ const result = results[0];
307
+
308
+ // AcceptanceGateResult contract shape (v2026.4.72):
309
+ // index, req, kind, result, durationMs, details, checkedAt, checkedBy
310
+ expect(result).toHaveProperty('kind');
311
+ expect(result).toHaveProperty('index');
312
+ expect(result).toHaveProperty('result');
313
+ expect(result).toHaveProperty('checkedAt');
314
+ expect(result).toHaveProperty('durationMs');
315
+ });
316
+ });
317
+
318
+ describe('gate-runner — integration with contract types', () => {
319
+ it('validates all gate kinds together', async () => {
320
+ const gates = [
321
+ {
322
+ kind: 'test' as const,
323
+ description: 'test-1 — integration test gate',
324
+ command: 'echo',
325
+ args: ['test'],
326
+ expect: 'exit0' as const,
327
+ },
328
+ {
329
+ kind: 'command' as const,
330
+ description: 'cmd-1 — integration command gate',
331
+ cmd: 'echo',
332
+ args: ['cmd'],
333
+ exitCode: 0,
334
+ },
335
+ {
336
+ kind: 'manual' as const,
337
+ description: 'manual-1 — integration manual gate',
338
+ prompt: 'Review manual',
339
+ },
340
+ ] as const;
341
+
342
+ const results = await runGates(
343
+ gates as unknown as Parameters<typeof runGates>[0],
344
+ { projectRoot },
345
+ { skipManual: true },
346
+ );
347
+
348
+ expect(results.length).toBeGreaterThanOrEqual(3);
349
+ expect(results.every((r) => r.result)).toBe(true);
350
+ });
351
+ });