@cachly-dev/mcp-server 0.10.26 → 0.10.40

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 (76) hide show
  1. package/.env.test.example +23 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +50 -0
  3. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +25 -0
  5. package/.github/workflows/publish.yml +72 -0
  6. package/CHANGELOG.md +252 -0
  7. package/LAUNCH_TEXTS.md +182 -0
  8. package/LICENSE +202 -0
  9. package/README.md +142 -112
  10. package/dist/__tests__/auth.test.d.ts +1 -0
  11. package/dist/__tests__/auth.test.js +107 -0
  12. package/dist/__tests__/auth.test.js.map +1 -0
  13. package/dist/__tests__/e2e/api-auth.test.d.ts +7 -0
  14. package/dist/__tests__/e2e/api-auth.test.js +162 -0
  15. package/dist/__tests__/e2e/api-auth.test.js.map +1 -0
  16. package/dist/__tests__/e2e/api-health.test.d.ts +7 -0
  17. package/dist/__tests__/e2e/api-health.test.js +95 -0
  18. package/dist/__tests__/e2e/api-health.test.js.map +1 -0
  19. package/dist/__tests__/e2e/cli.test.d.ts +10 -0
  20. package/dist/__tests__/e2e/cli.test.js +204 -0
  21. package/dist/__tests__/e2e/cli.test.js.map +1 -0
  22. package/dist/__tests__/e2e/config.d.ts +35 -0
  23. package/dist/__tests__/e2e/config.js +53 -0
  24. package/dist/__tests__/e2e/config.js.map +1 -0
  25. package/dist/__tests__/e2e/funnel.test.d.ts +13 -0
  26. package/dist/__tests__/e2e/funnel.test.js +90 -0
  27. package/dist/__tests__/e2e/funnel.test.js.map +1 -0
  28. package/dist/__tests__/e2e/mcp-tools.test.d.ts +7 -0
  29. package/dist/__tests__/e2e/mcp-tools.test.js +129 -0
  30. package/dist/__tests__/e2e/mcp-tools.test.js.map +1 -0
  31. package/dist/__tests__/e2e/provisioning.test.d.ts +15 -0
  32. package/dist/__tests__/e2e/provisioning.test.js +123 -0
  33. package/dist/__tests__/e2e/provisioning.test.js.map +1 -0
  34. package/dist/handlers/brain.js +119 -82
  35. package/dist/handlers/brain.js.map +1 -1
  36. package/dist/handlers/fedbrain.js +93 -44
  37. package/dist/handlers/fedbrain.js.map +1 -1
  38. package/dist/handlers/roadmap.js +8 -2
  39. package/dist/handlers/roadmap.js.map +1 -1
  40. package/dist/handlers/syndicate.js +15 -7
  41. package/dist/handlers/syndicate.js.map +1 -1
  42. package/dist/handlers/team.js +62 -50
  43. package/dist/handlers/team.js.map +1 -1
  44. package/dist/index.js +1264 -234
  45. package/dist/index.js.map +1 -1
  46. package/dist/tools.d.ts +3 -54
  47. package/dist/tools.js +8 -62
  48. package/dist/tools.js.map +1 -1
  49. package/eslint.config.js +16 -0
  50. package/glama.json +36 -0
  51. package/llms.txt +155 -0
  52. package/package.json +38 -14
  53. package/scripts/e2e-run.sh +160 -0
  54. package/scripts/postinstall.js +12 -12
  55. package/server.json +3 -9
  56. package/smithery.yaml +15 -8
  57. package/src/__tests__/auth.test.ts +147 -0
  58. package/src/__tests__/e2e/api-auth.test.ts +182 -0
  59. package/src/__tests__/e2e/api-health.test.ts +103 -0
  60. package/src/__tests__/e2e/cli.test.ts +228 -0
  61. package/src/__tests__/e2e/config.ts +53 -0
  62. package/src/__tests__/e2e/funnel.test.ts +99 -0
  63. package/src/__tests__/e2e/mcp-tools.test.ts +191 -0
  64. package/src/__tests__/e2e/provisioning.test.ts +128 -0
  65. package/src/handlers/brain.ts +118 -78
  66. package/src/handlers/fedbrain.ts +88 -38
  67. package/src/handlers/roadmap.ts +7 -2
  68. package/src/handlers/syndicate.ts +13 -7
  69. package/src/handlers/team.ts +57 -33
  70. package/src/index.ts +1312 -257
  71. package/src/tools.ts +8 -66
  72. package/vitest.config.ts +12 -0
  73. package/dist/__tests__/brain-predict-failures.test.d.ts +0 -17
  74. package/dist/__tests__/brain-predict-failures.test.js +0 -339
  75. package/dist/__tests__/brain-predict-failures.test.js.map +0 -1
  76. package/src/__tests__/brain-predict-failures.test.ts +0 -430
@@ -0,0 +1,107 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ErrorCode } from '@modelcontextprotocol/sdk/types.js';
3
+ import { jwtExpiryMs, checkJwt, handleApiError } from '../auth.js';
4
+ function makeJwt(payload) {
5
+ const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
6
+ const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
7
+ return `${header}.${body}.fakesig`;
8
+ }
9
+ const nowSec = () => Math.floor(Date.now() / 1000);
10
+ describe('jwtExpiryMs', () => {
11
+ it('returns null for an empty string', () => {
12
+ expect(jwtExpiryMs('')).toBeNull();
13
+ });
14
+ it('returns null for a plain (non-JWT) string', () => {
15
+ expect(jwtExpiryMs('not-a-token')).toBeNull();
16
+ });
17
+ it('returns null when the JWT has only two parts', () => {
18
+ expect(jwtExpiryMs('header.payload')).toBeNull();
19
+ });
20
+ it('returns null when the JWT has four parts', () => {
21
+ expect(jwtExpiryMs('a.b.c.d')).toBeNull();
22
+ });
23
+ it('returns null when the payload base64 is invalid', () => {
24
+ expect(jwtExpiryMs('header.!!!.sig')).toBeNull();
25
+ });
26
+ it('returns null when the payload has no exp field', () => {
27
+ expect(jwtExpiryMs(makeJwt({ sub: 'user-123', iat: nowSec() }))).toBeNull();
28
+ });
29
+ it('returns null when exp is 0', () => {
30
+ expect(jwtExpiryMs(makeJwt({ exp: 0 }))).toBeNull();
31
+ });
32
+ it('converts exp (seconds) to milliseconds', () => {
33
+ const expSec = nowSec() + 3600;
34
+ expect(jwtExpiryMs(makeJwt({ exp: expSec }))).toBe(expSec * 1000);
35
+ });
36
+ it('returns a past timestamp for an expired token', () => {
37
+ expect(jwtExpiryMs(makeJwt({ exp: nowSec() - 300 }))).toBeLessThan(Date.now());
38
+ });
39
+ it('returns a future timestamp for a valid token', () => {
40
+ expect(jwtExpiryMs(makeJwt({ exp: nowSec() + 86400 }))).toBeGreaterThan(Date.now());
41
+ });
42
+ it('handles a token expiring in less than 60 minutes', () => {
43
+ const expSec = nowSec() + 30 * 60;
44
+ const result = jwtExpiryMs(makeJwt({ exp: expSec }));
45
+ const minsLeft = Math.floor((result - Date.now()) / 60_000);
46
+ expect(minsLeft).toBeGreaterThanOrEqual(29);
47
+ expect(minsLeft).toBeLessThanOrEqual(30);
48
+ });
49
+ it('ignores extra claims', () => {
50
+ const expSec = nowSec() + 3600;
51
+ expect(jwtExpiryMs(makeJwt({ sub: 'u', iat: nowSec(), exp: expSec, plan: 'free' }))).toBe(expSec * 1000);
52
+ });
53
+ it('handles padding-free base64url payloads', () => {
54
+ // base64url (no =) — Buffer.from handles this natively
55
+ const expSec = nowSec() + 7200;
56
+ expect(jwtExpiryMs(makeJwt({ exp: expSec, x: 'a' }))).toBe(expSec * 1000);
57
+ });
58
+ });
59
+ describe('checkJwt', () => {
60
+ it('throws InvalidRequest when jwt is empty', () => {
61
+ expect(() => checkJwt('')).toThrowError(expect.objectContaining({ code: ErrorCode.InvalidRequest, message: expect.stringContaining('CACHLY_JWT env var not set') }));
62
+ });
63
+ it('throws with setup URL when jwt is missing', () => {
64
+ expect(() => checkJwt('')).toThrowError(expect.objectContaining({ message: expect.stringContaining('cachly.dev/setup-ai') }));
65
+ });
66
+ it('throws InvalidRequest with expiry timestamp for an expired token', () => {
67
+ const expSec = nowSec() - 60;
68
+ const token = makeJwt({ exp: expSec });
69
+ expect(() => checkJwt(token)).toThrowError(expect.objectContaining({
70
+ code: ErrorCode.InvalidRequest,
71
+ message: expect.stringContaining('expired at'),
72
+ }));
73
+ });
74
+ it('does not throw for a valid non-expired token', () => {
75
+ expect(() => checkJwt(makeJwt({ exp: nowSec() + 3600 }))).not.toThrow();
76
+ });
77
+ it('does not throw for a token with no exp claim', () => {
78
+ expect(() => checkJwt(makeJwt({ sub: 'user' }))).not.toThrow();
79
+ });
80
+ });
81
+ describe('handleApiError', () => {
82
+ it('throws InvalidRequest for 401 with the detail message', () => {
83
+ expect(() => handleApiError(401, 'token expired')).toThrowError(expect.objectContaining({
84
+ code: ErrorCode.InvalidRequest,
85
+ message: expect.stringContaining('Authentication failed (401): token expired'),
86
+ }));
87
+ });
88
+ it('throws InvalidRequest for 403 with the detail message', () => {
89
+ expect(() => handleApiError(403, 'plan limit reached')).toThrowError(expect.objectContaining({
90
+ code: ErrorCode.InvalidRequest,
91
+ message: expect.stringContaining('Access denied (403): plan limit reached'),
92
+ }));
93
+ });
94
+ it('throws InternalError for other status codes', () => {
95
+ expect(() => handleApiError(500, 'server crash')).toThrowError(expect.objectContaining({
96
+ code: ErrorCode.InternalError,
97
+ message: expect.stringContaining('cachly API error 500: server crash'),
98
+ }));
99
+ });
100
+ it('includes the cachly.dev/setup-ai link for 401 errors', () => {
101
+ expect(() => handleApiError(401, 'Unauthorized')).toThrowError(expect.objectContaining({ message: expect.stringContaining('cachly.dev/setup-ai') }));
102
+ });
103
+ it('includes account access hint for 403 errors', () => {
104
+ expect(() => handleApiError(403, 'Forbidden')).toThrowError(expect.objectContaining({ message: expect.stringContaining('access to this resource') }));
105
+ });
106
+ });
107
+ //# sourceMappingURL=auth.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.test.js","sourceRoot":"","sources":["../../src/__tests__/auth.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * E2E: Authenticated API endpoints
3
+ *
4
+ * Requires: E2E_JWT, E2E_INSTANCE_ID
5
+ * Run: npx vitest run src/__tests__/e2e/api-auth.test.ts
6
+ */
7
+ export {};
@@ -0,0 +1,162 @@
1
+ /**
2
+ * E2E: Authenticated API endpoints
3
+ *
4
+ * Requires: E2E_JWT, E2E_INSTANCE_ID
5
+ * Run: npx vitest run src/__tests__/e2e/api-auth.test.ts
6
+ */
7
+ import { describe, it, expect, beforeAll } from 'vitest';
8
+ import { cfg, requireAuth, apiGet, apiPost } from './config.js';
9
+ beforeAll(() => requireAuth());
10
+ describe('Auth guard', () => {
11
+ it('GET /api/v1/instances without token returns 401', async () => {
12
+ const { status } = await apiGet('/api/v1/instances', '');
13
+ expect(status).toBe(401);
14
+ });
15
+ it('GET /api/v1/instances with invalid token returns 401', async () => {
16
+ const { status } = await apiGet('/api/v1/instances', 'not-a-real-token');
17
+ expect(status).toBe(401);
18
+ });
19
+ it('GET /api/v1/instances with valid token returns 200', async () => {
20
+ const { status } = await apiGet('/api/v1/instances');
21
+ expect(status).toBe(200);
22
+ });
23
+ });
24
+ describe('Instances API', () => {
25
+ // API returns { data: Instance[], count: number }
26
+ it('returns object with data array', async () => {
27
+ const { body } = await apiGet('/api/v1/instances');
28
+ const b = body;
29
+ expect(Array.isArray(b.data)).toBe(true);
30
+ expect(typeof b.count).toBe('number');
31
+ });
32
+ it('each instance has required fields', async () => {
33
+ const { body } = await apiGet('/api/v1/instances');
34
+ const instances = (body.data) ?? [];
35
+ if (instances.length === 0)
36
+ return; // fresh account — skip
37
+ for (const inst of instances) {
38
+ expect(typeof inst.id).toBe('string');
39
+ expect(typeof inst.name).toBe('string');
40
+ expect(typeof inst.status).toBe('string');
41
+ expect(typeof inst.tier).toBe('string');
42
+ }
43
+ });
44
+ it('test instance is present and running', async () => {
45
+ const { body } = await apiGet('/api/v1/instances');
46
+ const instances = (body.data) ?? [];
47
+ const found = instances.find((i) => i.id === cfg.instanceId);
48
+ expect(found).toBeDefined();
49
+ expect(found.status).toBe('running');
50
+ });
51
+ });
52
+ describe('Single instance GET', () => {
53
+ it('returns 200 for test instance', async () => {
54
+ const { status } = await apiGet(`/api/v1/instances/${cfg.instanceId}`);
55
+ expect(status).toBe(200);
56
+ });
57
+ it('returns 404 for non-existent instance', async () => {
58
+ const { status } = await apiGet('/api/v1/instances/00000000-0000-0000-0000-000000000000');
59
+ expect([403, 404]).toContain(status);
60
+ });
61
+ it('instance body has id, status running, tier, name', async () => {
62
+ const { body } = await apiGet(`/api/v1/instances/${cfg.instanceId}`);
63
+ const inst = body;
64
+ expect(inst.id).toBe(cfg.instanceId);
65
+ expect(inst.status).toBe('running');
66
+ expect(typeof inst.tier).toBe('string');
67
+ expect(typeof inst.name).toBe('string');
68
+ });
69
+ });
70
+ describe('Connection endpoint', () => {
71
+ it('returns 200 with connection details for running instance', async () => {
72
+ const { status, body } = await apiGet(`/api/v1/instances/${cfg.instanceId}/connection`);
73
+ expect(status).toBe(200);
74
+ const conn = body;
75
+ expect(typeof conn.host).toBe('string');
76
+ expect(typeof conn.port).toBe('number');
77
+ expect(typeof conn.password).toBe('string');
78
+ expect(conn.host.length).toBeGreaterThan(3);
79
+ });
80
+ it('connection host is not localhost (prod/dev has real host)', async () => {
81
+ const { body } = await apiGet(`/api/v1/instances/${cfg.instanceId}/connection`);
82
+ const conn = body;
83
+ expect(String(conn.host)).not.toBe('localhost');
84
+ expect(String(conn.host)).not.toBe('127.0.0.1');
85
+ });
86
+ it('port is within valid Redis range', async () => {
87
+ const { body } = await apiGet(`/api/v1/instances/${cfg.instanceId}/connection`);
88
+ const conn = body;
89
+ expect(Number(conn.port)).toBeGreaterThan(1023);
90
+ expect(Number(conn.port)).toBeLessThan(65536);
91
+ });
92
+ });
93
+ describe('API keys', () => {
94
+ // API returns { keys: APIKey[], count: number }
95
+ it('GET /api/v1/api-keys returns 200 with keys array', async () => {
96
+ const { status, body } = await apiGet('/api/v1/api-keys');
97
+ expect(status).toBe(200);
98
+ const b = body;
99
+ expect(Array.isArray(b.keys)).toBe(true);
100
+ });
101
+ it('api-keys have id and instance_id fields', async () => {
102
+ const { body } = await apiGet('/api/v1/api-keys');
103
+ const keys = (body.keys) ?? [];
104
+ if (keys.length === 0)
105
+ return;
106
+ for (const k of keys) {
107
+ expect(typeof k.id).toBe('string');
108
+ expect(typeof k.instance_id).toBe('string');
109
+ }
110
+ });
111
+ });
112
+ describe('Brain Stats endpoint', () => {
113
+ it('GET /api/v1/instances/:id/brain-stats returns 200', async () => {
114
+ const { status } = await apiGet(`/api/v1/instances/${cfg.instanceId}/brain-stats`);
115
+ expect(status).toBe(200);
116
+ });
117
+ it('brain-stats body has brain_level and brain_level_name', async () => {
118
+ const { body } = await apiGet(`/api/v1/instances/${cfg.instanceId}/brain-stats`);
119
+ const stats = body;
120
+ expect(typeof stats.brain_level).toBe('number');
121
+ expect(stats.brain_level).toBeGreaterThanOrEqual(1);
122
+ expect(typeof stats.brain_level_name).toBe('string');
123
+ expect(['Apprentice', 'Explorer', 'Expert', 'Architect', 'Oracle']).toContain(stats.brain_level_name);
124
+ });
125
+ it('brain-stats has hours_saved and brain_recall_count', async () => {
126
+ const { body } = await apiGet(`/api/v1/instances/${cfg.instanceId}/brain-stats`);
127
+ const stats = body;
128
+ expect(typeof stats.hours_saved).toBe('number');
129
+ expect(stats.hours_saved).toBeGreaterThanOrEqual(0);
130
+ expect(typeof stats.brain_recall_count).toBe('number');
131
+ expect(stats.brain_recall_count).toBeGreaterThanOrEqual(0);
132
+ });
133
+ it('brain-stats has standard search telemetry fields', async () => {
134
+ const { body } = await apiGet(`/api/v1/instances/${cfg.instanceId}/brain-stats`);
135
+ const stats = body;
136
+ expect(typeof stats.total_searches).toBe('number');
137
+ expect(typeof stats.avg_latency_ms).toBe('number');
138
+ expect(Array.isArray(stats.top_queries)).toBe(true);
139
+ });
140
+ });
141
+ describe('Provisioning status messages', () => {
142
+ it('non-existent instance connection returns 4xx, never 500', async () => {
143
+ const { status } = await apiGet('/api/v1/instances/00000000-0000-0000-0000-000000000001/connection');
144
+ expect([400, 403, 404, 422]).toContain(status);
145
+ });
146
+ });
147
+ describe('Auto-provision endpoint', () => {
148
+ it('POST /api/v1/instances/auto returns 200 or 201', async () => {
149
+ const { status } = await apiPost('/api/v1/instances/auto', {});
150
+ expect([200, 201]).toContain(status);
151
+ });
152
+ it('returns existing running instance without creating a new one', async () => {
153
+ const { body } = await apiPost('/api/v1/instances/auto', {});
154
+ const b = body;
155
+ if (b.instance) {
156
+ // Existing instance returned
157
+ expect(typeof b.instance.id).toBe('string');
158
+ expect(b.created).toBe(false);
159
+ }
160
+ });
161
+ });
162
+ //# sourceMappingURL=api-auth.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-auth.test.js","sourceRoot":"","sources":["../../../src/__tests__/e2e/api-auth.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACzD,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAEhE,SAAS,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;AAE/B,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;IAC1B,EAAE,CAAC,iDAAiD,EAAE,KAAK,IAAI,EAAE;QAC/D,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,mBAAmB,EAAE,EAAE,CAAC,CAAC;QACzD,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sDAAsD,EAAE,KAAK,IAAI,EAAE;QACpE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,mBAAmB,EAAE,kBAAkB,CAAC,CAAC;QACzE,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oDAAoD,EAAE,KAAK,IAAI,EAAE;QAClE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAC;QACrD,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,kDAAkD;IAClD,EAAE,CAAC,gCAAgC,EAAE,KAAK,IAAI,EAAE;QAC9C,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAC;QACnD,MAAM,CAAC,GAAG,IAA4C,CAAC;QACvD,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACzC,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACxC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mCAAmC,EAAE,KAAK,IAAI,EAAE;QACjD,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAC;QACnD,MAAM,SAAS,GAAG,CAAE,IAA6C,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QAC9E,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,CAAC,uBAAuB;QAC3D,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;YAC7B,MAAM,CAAC,OAAO,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACtC,MAAM,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACxC,MAAM,CAAC,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC1C,MAAM,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC1C,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,KAAK,IAAI,EAAE;QACpD,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAC;QACnD,MAAM,SAAS,GAAG,CAAE,IAA6C,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QAC9E,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,UAAU,CAAC,CAAC;QAC7D,MAAM,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;QAC5B,MAAM,CAAC,KAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACxC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;IACnC,EAAE,CAAC,+BAA+B,EAAE,KAAK,IAAI,EAAE;QAC7C,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,qBAAqB,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC;QACvE,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,KAAK,IAAI,EAAE;QACrD,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,wDAAwD,CAAC,CAAC;QAC1F,MAAM,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kDAAkD,EAAE,KAAK,IAAI,EAAE;QAChE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,qBAAqB,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC;QACrE,MAAM,IAAI,GAAG,IAA+B,CAAC;QAC7C,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACrC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACpC,MAAM,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACxC,MAAM,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;IACnC,EAAE,CAAC,0DAA0D,EAAE,KAAK,IAAI,EAAE;QACxE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,qBAAqB,GAAG,CAAC,UAAU,aAAa,CAAC,CAAC;QACxF,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACzB,MAAM,IAAI,GAAG,IAA+B,CAAC;QAC7C,MAAM,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACxC,MAAM,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACxC,MAAM,CAAC,OAAO,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC5C,MAAM,CAAE,IAAI,CAAC,IAAe,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;IAC1D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2DAA2D,EAAE,KAAK,IAAI,EAAE;QACzE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,qBAAqB,GAAG,CAAC,UAAU,aAAa,CAAC,CAAC;QAChF,MAAM,IAAI,GAAG,IAA+B,CAAC;QAC7C,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAChD,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kCAAkC,EAAE,KAAK,IAAI,EAAE;QAChD,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,qBAAqB,GAAG,CAAC,UAAU,aAAa,CAAC,CAAC;QAChF,MAAM,IAAI,GAAG,IAA+B,CAAC;QAC7C,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;QAChD,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;IAChD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,UAAU,EAAE,GAAG,EAAE;IACxB,gDAAgD;IAChD,EAAE,CAAC,kDAAkD,EAAE,KAAK,IAAI,EAAE;QAChE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAC;QAC1D,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACzB,MAAM,CAAC,GAAG,IAA4C,CAAC;QACvD,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,KAAK,IAAI,EAAE;QACvD,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAC;QAClD,MAAM,IAAI,GAAG,CAAE,IAA6C,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACzE,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAC9B,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;YACrB,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACnC,MAAM,CAAC,OAAO,CAAC,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC9C,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,EAAE,CAAC,mDAAmD,EAAE,KAAK,IAAI,EAAE;QACjE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,qBAAqB,GAAG,CAAC,UAAU,cAAc,CAAC,CAAC;QACnF,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uDAAuD,EAAE,KAAK,IAAI,EAAE;QACrE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,qBAAqB,GAAG,CAAC,UAAU,cAAc,CAAC,CAAC;QACjF,MAAM,KAAK,GAAG,IAA+B,CAAC;QAC9C,MAAM,CAAC,OAAO,KAAK,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAChD,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC;QACpD,MAAM,CAAC,OAAO,KAAK,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACrD,MAAM,CAAC,CAAC,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC;IACxG,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oDAAoD,EAAE,KAAK,IAAI,EAAE;QAClE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,qBAAqB,GAAG,CAAC,UAAU,cAAc,CAAC,CAAC;QACjF,MAAM,KAAK,GAAG,IAA+B,CAAC;QAC9C,MAAM,CAAC,OAAO,KAAK,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAChD,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC;QACpD,MAAM,CAAC,OAAO,KAAK,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACvD,MAAM,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kDAAkD,EAAE,KAAK,IAAI,EAAE;QAChE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,qBAAqB,GAAG,CAAC,UAAU,cAAc,CAAC,CAAC;QACjF,MAAM,KAAK,GAAG,IAA+B,CAAC;QAC9C,MAAM,CAAC,OAAO,KAAK,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACnD,MAAM,CAAC,OAAO,KAAK,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACnD,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,8BAA8B,EAAE,GAAG,EAAE;IAC5C,EAAE,CAAC,yDAAyD,EAAE,KAAK,IAAI,EAAE;QACvE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,mEAAmE,CAAC,CAAC;QACrG,MAAM,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;IACvC,EAAE,CAAC,gDAAgD,EAAE,KAAK,IAAI,EAAE;QAC9D,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,OAAO,CAAC,wBAAwB,EAAE,EAAE,CAAC,CAAC;QAC/D,MAAM,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8DAA8D,EAAE,KAAK,IAAI,EAAE;QAC5E,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,OAAO,CAAC,wBAAwB,EAAE,EAAE,CAAC,CAAC;QAC7D,MAAM,CAAC,GAAG,IAAwE,CAAC;QACnF,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;YACf,6BAA6B;YAC7B,MAAM,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC5C,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAChC,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * E2E: API health & public endpoints
3
+ *
4
+ * These tests require NO credentials — they hit publicly accessible endpoints.
5
+ * Run: E2E_API_URL=https://api.dev.cachly.dev npx vitest run src/__tests__/e2e/api-health.test.ts
6
+ */
7
+ export {};
@@ -0,0 +1,95 @@
1
+ /**
2
+ * E2E: API health & public endpoints
3
+ *
4
+ * These tests require NO credentials — they hit publicly accessible endpoints.
5
+ * Run: E2E_API_URL=https://api.dev.cachly.dev npx vitest run src/__tests__/e2e/api-health.test.ts
6
+ */
7
+ import { describe, it, expect } from 'vitest';
8
+ import { cfg } from './config.js';
9
+ describe('API health (no auth required)', () => {
10
+ it('GET /health returns 200', async () => {
11
+ const res = await fetch(`${cfg.apiUrl}/health`);
12
+ expect(res.status).toBe(200);
13
+ });
14
+ it('GET /health body contains status ok', async () => {
15
+ const res = await fetch(`${cfg.apiUrl}/health`);
16
+ const body = await res.json();
17
+ expect(body.status ?? body.ok ?? body.alive).toBeTruthy();
18
+ });
19
+ it('unknown route returns 404, not 500', async () => {
20
+ const res = await fetch(`${cfg.apiUrl}/api/v1/does-not-exist-xyz`);
21
+ expect(res.status).toBe(404);
22
+ });
23
+ });
24
+ describe('Device flow init (no auth required)', () => {
25
+ it('GET /api/v1/auth/device/code returns device_code and user_code', async () => {
26
+ const res = await fetch(`${cfg.apiUrl}/api/v1/auth/device/code`);
27
+ expect(res.status).toBe(200);
28
+ const body = await res.json();
29
+ expect(typeof body.device_code).toBe('string');
30
+ expect(typeof body.user_code).toBe('string');
31
+ expect(typeof body.verification_uri).toBe('string');
32
+ expect(body.verification_uri).toContain('cachly.dev');
33
+ });
34
+ it('device_code is at least 20 chars (not truncated)', async () => {
35
+ const res = await fetch(`${cfg.apiUrl}/api/v1/auth/device/code`);
36
+ const body = await res.json();
37
+ expect(body.device_code.length).toBeGreaterThanOrEqual(20);
38
+ });
39
+ it('expires_in is a positive number', async () => {
40
+ const res = await fetch(`${cfg.apiUrl}/api/v1/auth/device/code`);
41
+ const body = await res.json();
42
+ expect(Number(body.expires_in)).toBeGreaterThan(0);
43
+ });
44
+ });
45
+ describe('Badge endpoint (public, no auth)', () => {
46
+ it('returns 400 or 404 for an invalid instance id', async () => {
47
+ const res = await fetch(`${cfg.apiUrl}/api/v1/badge/not-a-real-uuid`);
48
+ expect([400, 404]).toContain(res.status);
49
+ });
50
+ it('returns badge JSON for a valid instance id', async () => {
51
+ if (!cfg.instanceId)
52
+ return; // skip if not configured
53
+ const res = await fetch(`${cfg.apiUrl}/api/v1/badge/${cfg.instanceId}`);
54
+ expect(res.status).toBe(200);
55
+ const body = await res.json();
56
+ expect(typeof body.schemaVersion).toBe('number');
57
+ expect(typeof body.label).toBe('string');
58
+ expect(typeof body.message).toBe('string');
59
+ });
60
+ it('badge message contains lesson count (numeric string)', async () => {
61
+ if (!cfg.instanceId)
62
+ return;
63
+ const res = await fetch(`${cfg.apiUrl}/api/v1/badge/${cfg.instanceId}`);
64
+ const body = await res.json();
65
+ expect(body.message).toMatch(/\d+/);
66
+ });
67
+ });
68
+ describe('Telemetry endpoint (public)', () => {
69
+ it('POST /api/v1/telemetry/mcp accepts anonymous event', async () => {
70
+ const res = await fetch(`${cfg.apiUrl}/api/v1/telemetry/mcp`, {
71
+ method: 'POST',
72
+ headers: { 'Content-Type': 'application/json' },
73
+ body: JSON.stringify({ event: 'e2e_test_ping', version: '0.0.0', editor: 'test', source: 'e2e' }),
74
+ });
75
+ expect([200, 202, 204]).toContain(res.status);
76
+ });
77
+ it('POST /api/v1/telemetry/mcp accepts jwt-attributed event', async () => {
78
+ if (!cfg.jwt)
79
+ return;
80
+ const res = await fetch(`${cfg.apiUrl}/api/v1/telemetry/mcp`, {
81
+ method: 'POST',
82
+ headers: { 'Content-Type': 'application/json' },
83
+ body: JSON.stringify({
84
+ event: 'e2e_test_ping_authed',
85
+ version: '0.0.0',
86
+ editor: 'test',
87
+ source: 'e2e',
88
+ jwt: cfg.jwt,
89
+ instance_id: cfg.instanceId,
90
+ }),
91
+ });
92
+ expect([200, 202, 204]).toContain(res.status);
93
+ });
94
+ });
95
+ //# sourceMappingURL=api-health.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-health.test.js","sourceRoot":"","sources":["../../../src/__tests__/e2e/api-health.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * E2E: CLI command smoke tests
3
+ *
4
+ * Spawns the actual CLI binary and verifies output + exit codes.
5
+ * Uses the built dist/ — run `npm run build` first.
6
+ *
7
+ * Requires: E2E_JWT, E2E_INSTANCE_ID (for health/badge/share)
8
+ * Run: npm run build && npx vitest run src/__tests__/e2e/cli.test.ts
9
+ */
10
+ export {};
@@ -0,0 +1,204 @@
1
+ /**
2
+ * E2E: CLI command smoke tests
3
+ *
4
+ * Spawns the actual CLI binary and verifies output + exit codes.
5
+ * Uses the built dist/ — run `npm run build` first.
6
+ *
7
+ * Requires: E2E_JWT, E2E_INSTANCE_ID (for health/badge/share)
8
+ * Run: npm run build && npx vitest run src/__tests__/e2e/cli.test.ts
9
+ */
10
+ import { describe, it, expect } from 'vitest';
11
+ import { spawnSync } from 'node:child_process';
12
+ import { join } from 'node:path';
13
+ import { cfg } from './config.js';
14
+ const BIN = join(import.meta.dirname, '../../../../dist/index.js');
15
+ const TIMEOUT_MS = 30_000;
16
+ function run(args, env = {}, timeoutMs = TIMEOUT_MS) {
17
+ const result = spawnSync(process.execPath, [BIN, ...args], {
18
+ env: {
19
+ ...process.env,
20
+ CACHLY_JWT: cfg.jwt,
21
+ CACHLY_BRAIN_INSTANCE_ID: cfg.instanceId,
22
+ CACHLY_API_URL: cfg.apiUrl,
23
+ ...env,
24
+ },
25
+ timeout: timeoutMs,
26
+ encoding: 'utf8',
27
+ });
28
+ return {
29
+ stdout: result.stdout ?? '',
30
+ stderr: result.stderr ?? '',
31
+ code: result.status,
32
+ };
33
+ }
34
+ describe('cachly (no args) — help output', () => {
35
+ it('exits 0', () => {
36
+ const { code } = run([]);
37
+ expect(code).toBe(0);
38
+ });
39
+ it('prints cachly branding', () => {
40
+ const { stdout } = run([]);
41
+ expect(stdout).toMatch(/cachly/i);
42
+ });
43
+ it('lists setup command', () => {
44
+ const { stdout } = run([]);
45
+ expect(stdout).toContain('setup');
46
+ });
47
+ it('lists demo command', () => {
48
+ const { stdout } = run([]);
49
+ expect(stdout).toContain('demo');
50
+ });
51
+ it('mentions 1–5 minutes (no false "30 seconds" promise)', () => {
52
+ const { stdout } = run([]);
53
+ // Must NOT contain "30 seconds" — we corrected this in v0.10.19+
54
+ expect(stdout).not.toContain('30 seconds');
55
+ expect(stdout).not.toMatch(/\b30s\b/);
56
+ });
57
+ it('mentions free forever', () => {
58
+ const { stdout } = run([]);
59
+ expect(stdout.toLowerCase()).toMatch(/free|kostenlos/);
60
+ });
61
+ });
62
+ describe('cachly upgrade', () => {
63
+ it('exits 0', () => {
64
+ const { code } = run(['upgrade']);
65
+ expect(code).toBe(0);
66
+ });
67
+ it('prints current version', () => {
68
+ const { stdout } = run(['upgrade']);
69
+ expect(stdout).toMatch(/\d+\.\d+\.\d+/);
70
+ });
71
+ it('version string is valid semver', () => {
72
+ const { stdout } = run(['upgrade']);
73
+ const match = stdout.match(/(\d+\.\d+\.\d+)/);
74
+ expect(match).not.toBeNull();
75
+ const parts = match[1].split('.').map(Number);
76
+ expect(parts).toHaveLength(3);
77
+ parts.forEach((p) => expect(Number.isInteger(p)).toBe(true));
78
+ });
79
+ it('does not crash on npm registry timeout (graceful error)', () => {
80
+ const { code } = run(['upgrade'], { CACHLY_API_URL: 'http://127.0.0.1:1' });
81
+ // Should exit 0 or 1 — never crash with unhandled exception
82
+ expect([0, 1]).toContain(code);
83
+ });
84
+ });
85
+ describe('cachly demo', () => {
86
+ it('exits 0 even without credentials', () => {
87
+ const { code } = run(['demo'], { CACHLY_JWT: '', CACHLY_BRAIN_INSTANCE_ID: '' }, 15_000);
88
+ expect(code).toBe(0);
89
+ });
90
+ it('prints Brain Preview header', () => {
91
+ const { stdout } = run(['demo'], { CACHLY_JWT: '', CACHLY_BRAIN_INSTANCE_ID: '' }, 15_000);
92
+ expect(stdout).toMatch(/brain|preview|cachly/i);
93
+ });
94
+ it('prints setup command at the end', () => {
95
+ const { stdout } = run(['demo'], { CACHLY_JWT: '', CACHLY_BRAIN_INSTANCE_ID: '' }, 15_000);
96
+ expect(stdout).toContain('setup');
97
+ });
98
+ it('does not crash with invalid git repo (fallback gracefully)', () => {
99
+ const { code } = run(['demo'], {
100
+ CACHLY_JWT: '',
101
+ CACHLY_BRAIN_INSTANCE_ID: '',
102
+ }, 15_000);
103
+ expect([0, 1]).toContain(code); // never 2+ (unhandled crash)
104
+ });
105
+ it('shows Brain Level (valid level name)', () => {
106
+ const { stdout } = run(['demo'], { CACHLY_JWT: '', CACHLY_BRAIN_INSTANCE_ID: '' }, 15_000);
107
+ // Demo must show a valid brain level — Apprentice for new projects is expected
108
+ expect(stdout).toMatch(/Apprentice|Explorer|Expert|Architect|Oracle/);
109
+ });
110
+ it('shows 1–5 minutes in the CTA (no "30 seconds")', () => {
111
+ const { stdout } = run(['demo'], { CACHLY_JWT: '', CACHLY_BRAIN_INSTANCE_ID: '' }, 15_000);
112
+ expect(stdout).toContain('1–5 minutes');
113
+ expect(stdout).not.toContain('30 seconds');
114
+ });
115
+ it('shows "never repeat" instead of "remember" for bug fixes', () => {
116
+ const { stdout } = run(['demo'], { CACHLY_JWT: '', CACHLY_BRAIN_INSTANCE_ID: '' }, 15_000);
117
+ // Copy should say "never repeat" not "remember" — old wording was weaker
118
+ if (stdout.includes('Bug fixes')) {
119
+ expect(stdout).toMatch(/never repeat/i);
120
+ }
121
+ });
122
+ });
123
+ describe('cachly health', () => {
124
+ it('exits 0 with valid credentials', () => {
125
+ if (!cfg.jwt || !cfg.instanceId)
126
+ return;
127
+ const { code } = run(['health']);
128
+ expect(code).toBe(0);
129
+ });
130
+ it('prints API status', () => {
131
+ if (!cfg.jwt || !cfg.instanceId)
132
+ return;
133
+ const { stdout } = run(['health']);
134
+ expect(stdout.toLowerCase()).toMatch(/api|status|ok|connected/);
135
+ });
136
+ it('prints editor detection results', () => {
137
+ if (!cfg.jwt || !cfg.instanceId)
138
+ return;
139
+ const { stdout } = run(['health']);
140
+ expect(stdout.toLowerCase()).toMatch(/editor|claude|cursor|copilot|windsurf/);
141
+ });
142
+ it('exits non-zero without JWT', () => {
143
+ const { code, stdout } = run(['health'], { CACHLY_JWT: '' });
144
+ // Either exits non-zero, or prints an error message
145
+ const hasError = code !== 0 || stdout.toLowerCase().includes('not set') || stdout.toLowerCase().includes('sign in');
146
+ expect(hasError).toBe(true);
147
+ });
148
+ });
149
+ describe('cachly badge', () => {
150
+ it('exits 0 with valid instance', () => {
151
+ if (!cfg.jwt || !cfg.instanceId)
152
+ return;
153
+ const { code } = run(['badge']);
154
+ expect(code).toBe(0);
155
+ });
156
+ it('outputs a markdown badge snippet', () => {
157
+ if (!cfg.jwt || !cfg.instanceId)
158
+ return;
159
+ const { stdout } = run(['badge']);
160
+ expect(stdout).toMatch(/\[!\[.*cachly.*\]\(.*\)\]\(.*\)/i);
161
+ });
162
+ it('badge URL points to api.cachly.dev', () => {
163
+ if (!cfg.jwt || !cfg.instanceId)
164
+ return;
165
+ const { stdout } = run(['badge']);
166
+ expect(stdout).toContain('api.cachly.dev');
167
+ });
168
+ });
169
+ describe('cachly share', () => {
170
+ it('exits 0 with valid credentials', () => {
171
+ if (!cfg.jwt || !cfg.instanceId)
172
+ return;
173
+ const { code } = run(['share']);
174
+ expect(code).toBe(0);
175
+ });
176
+ it('output contains UTM share URL', () => {
177
+ if (!cfg.jwt || !cfg.instanceId)
178
+ return;
179
+ const { stdout } = run(['share']);
180
+ expect(stdout).toContain('utm_source=x');
181
+ });
182
+ it('output contains tweet text', () => {
183
+ if (!cfg.jwt || !cfg.instanceId)
184
+ return;
185
+ const { stdout } = run(['share']);
186
+ expect(stdout.toLowerCase()).toMatch(/tweet|x\.com|twitter/i);
187
+ });
188
+ });
189
+ describe('cachly invite', () => {
190
+ it('exits 0 and prints invite link', () => {
191
+ if (!cfg.jwt || !cfg.instanceId)
192
+ return;
193
+ const { code, stdout } = run(['invite']);
194
+ expect(code).toBe(0);
195
+ expect(stdout).toMatch(/invite|join|cachly\.dev/i);
196
+ });
197
+ it('does NOT mention 30 seconds (1-5 minutes only)', () => {
198
+ if (!cfg.jwt || !cfg.instanceId)
199
+ return;
200
+ const { stdout } = run(['invite']);
201
+ expect(stdout).not.toContain('30 seconds');
202
+ });
203
+ });
204
+ //# sourceMappingURL=cli.test.js.map