@meltstudio/meltctl 4.27.0 → 4.28.1
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/dist/commands/audit.js +30 -5
- package/dist/commands/audit.test.d.ts +1 -0
- package/dist/commands/audit.test.js +250 -0
- package/dist/commands/coins.test.d.ts +1 -0
- package/dist/commands/coins.test.js +133 -0
- package/dist/commands/feedback.test.d.ts +1 -0
- package/dist/commands/feedback.test.js +242 -0
- package/dist/commands/init.js +21 -4
- package/dist/commands/init.test.d.ts +1 -0
- package/dist/commands/init.test.js +476 -0
- package/dist/commands/login.test.d.ts +1 -0
- package/dist/commands/login.test.js +174 -0
- package/dist/commands/logout.test.d.ts +1 -0
- package/dist/commands/logout.test.js +59 -0
- package/dist/commands/plan.test.d.ts +1 -0
- package/dist/commands/plan.test.js +283 -0
- package/dist/commands/standup.test.d.ts +1 -0
- package/dist/commands/standup.test.js +252 -0
- package/dist/commands/templates.test.d.ts +1 -0
- package/dist/commands/templates.test.js +89 -0
- package/dist/commands/version.test.d.ts +1 -0
- package/dist/commands/version.test.js +86 -0
- package/dist/index.js +1 -1
- package/dist/utils/api.test.d.ts +1 -0
- package/dist/utils/api.test.js +96 -0
- package/dist/utils/auth.test.d.ts +1 -0
- package/dist/utils/auth.test.js +165 -0
- package/dist/utils/banner.test.d.ts +1 -0
- package/dist/utils/banner.test.js +34 -0
- package/dist/utils/git.test.d.ts +1 -0
- package/dist/utils/git.test.js +184 -0
- package/dist/utils/package-manager.test.d.ts +1 -0
- package/dist/utils/package-manager.test.js +76 -0
- package/dist/utils/templates.test.d.ts +1 -0
- package/dist/utils/templates.test.js +50 -0
- package/dist/utils/version-check.test.d.ts +1 -0
- package/dist/utils/version-check.test.js +135 -0
- package/package.json +2 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
vi.mock('fs-extra', () => ({
|
|
3
|
+
default: {
|
|
4
|
+
ensureDir: vi.fn(),
|
|
5
|
+
writeFile: vi.fn(),
|
|
6
|
+
},
|
|
7
|
+
}));
|
|
8
|
+
vi.mock('../utils/auth.js', () => ({
|
|
9
|
+
isAuthenticated: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
vi.mock('../utils/templates.js', () => ({
|
|
12
|
+
fetchTemplates: vi.fn(),
|
|
13
|
+
}));
|
|
14
|
+
import fs from 'fs-extra';
|
|
15
|
+
import { isAuthenticated } from '../utils/auth.js';
|
|
16
|
+
import { fetchTemplates } from '../utils/templates.js';
|
|
17
|
+
import { templatesCommand } from './templates.js';
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
vi.clearAllMocks();
|
|
20
|
+
vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
21
|
+
vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
22
|
+
vi.spyOn(process, 'exit').mockImplementation((code) => {
|
|
23
|
+
throw new Error(`process.exit(${code})`);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
describe('templatesCommand', () => {
|
|
27
|
+
it('exits when not authenticated', async () => {
|
|
28
|
+
;
|
|
29
|
+
isAuthenticated.mockResolvedValue(false);
|
|
30
|
+
await expect(templatesCommand()).rejects.toThrow('process.exit(1)');
|
|
31
|
+
expect(console.error).toHaveBeenCalled();
|
|
32
|
+
expect(fetchTemplates).not.toHaveBeenCalled();
|
|
33
|
+
});
|
|
34
|
+
it('fetches and writes templates to temp dir', async () => {
|
|
35
|
+
;
|
|
36
|
+
isAuthenticated.mockResolvedValue(true);
|
|
37
|
+
fetchTemplates.mockResolvedValue({
|
|
38
|
+
'AGENTS.md': '# Agents\nContent here',
|
|
39
|
+
'.claude/skills/setup.md': '# Setup skill',
|
|
40
|
+
});
|
|
41
|
+
await templatesCommand();
|
|
42
|
+
// Should have called ensureDir for the temp dir and subdirectories
|
|
43
|
+
expect(fs.ensureDir).toHaveBeenCalled();
|
|
44
|
+
// Should have written both files
|
|
45
|
+
expect(fs.writeFile).toHaveBeenCalledTimes(2);
|
|
46
|
+
// First file
|
|
47
|
+
const writeFileCalls = fs.writeFile.mock.calls;
|
|
48
|
+
const filePaths = writeFileCalls.map((c) => c[0]);
|
|
49
|
+
expect(filePaths.some((p) => p.endsWith('AGENTS.md'))).toBe(true);
|
|
50
|
+
expect(filePaths.some((p) => p.endsWith('setup.md'))).toBe(true);
|
|
51
|
+
// Verify content
|
|
52
|
+
const agentsCall = writeFileCalls.find((c) => c[0].endsWith('AGENTS.md'));
|
|
53
|
+
expect(agentsCall[1]).toBe('# Agents\nContent here');
|
|
54
|
+
// Should print the temp dir path
|
|
55
|
+
expect(console.log).toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
it('exits with error when session expired', async () => {
|
|
58
|
+
;
|
|
59
|
+
isAuthenticated.mockResolvedValue(true);
|
|
60
|
+
fetchTemplates.mockRejectedValue(new Error('Session expired'));
|
|
61
|
+
await expect(templatesCommand()).rejects.toThrow('process.exit(1)');
|
|
62
|
+
const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
|
|
63
|
+
expect(errorCalls.some((msg) => msg.includes('expired'))).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
it('exits with error on fetch/network error', async () => {
|
|
66
|
+
;
|
|
67
|
+
isAuthenticated.mockResolvedValue(true);
|
|
68
|
+
fetchTemplates.mockRejectedValue(new Error('fetch failed'));
|
|
69
|
+
await expect(templatesCommand()).rejects.toThrow('process.exit(1)');
|
|
70
|
+
const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
|
|
71
|
+
expect(errorCalls.some((msg) => msg.includes('connection'))).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
it('exits with generic error for unknown failures', async () => {
|
|
74
|
+
;
|
|
75
|
+
isAuthenticated.mockResolvedValue(true);
|
|
76
|
+
fetchTemplates.mockRejectedValue(new Error('Something weird'));
|
|
77
|
+
await expect(templatesCommand()).rejects.toThrow('process.exit(1)');
|
|
78
|
+
const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
|
|
79
|
+
expect(errorCalls.some((msg) => msg.includes('Something weird'))).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
it('exits with generic message for non-Error thrown values', async () => {
|
|
82
|
+
;
|
|
83
|
+
isAuthenticated.mockResolvedValue(true);
|
|
84
|
+
fetchTemplates.mockRejectedValue('string error');
|
|
85
|
+
await expect(templatesCommand()).rejects.toThrow('process.exit(1)');
|
|
86
|
+
const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
|
|
87
|
+
expect(errorCalls.some((msg) => msg.includes('Unknown error'))).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
vi.mock('../utils/version-check.js', () => ({
|
|
3
|
+
getCurrentCliVersion: vi.fn(),
|
|
4
|
+
getLatestCliVersion: vi.fn(),
|
|
5
|
+
compareVersions: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
vi.mock('../utils/package-manager.js', () => ({
|
|
8
|
+
getUpdateInstructions: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
import { getCurrentCliVersion, getLatestCliVersion, compareVersions, } from '../utils/version-check.js';
|
|
11
|
+
import { getUpdateInstructions } from '../utils/package-manager.js';
|
|
12
|
+
import { versionCheckCommand } from './version.js';
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
vi.clearAllMocks();
|
|
15
|
+
vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
16
|
+
vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
17
|
+
vi.spyOn(process, 'exit').mockImplementation((code) => {
|
|
18
|
+
throw new Error(`process.exit(${code})`);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
describe('versionCheckCommand', () => {
|
|
22
|
+
it('displays up-to-date message when versions match', async () => {
|
|
23
|
+
;
|
|
24
|
+
getCurrentCliVersion.mockResolvedValue('4.26.0');
|
|
25
|
+
getLatestCliVersion.mockResolvedValue('4.26.0');
|
|
26
|
+
compareVersions.mockReturnValue(false);
|
|
27
|
+
await versionCheckCommand();
|
|
28
|
+
const logCalls = console.log.mock.calls.map((c) => c[0]);
|
|
29
|
+
expect(logCalls.some((msg) => typeof msg === 'string' && msg.includes('up to date'))).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
it('displays update available when newer version exists', async () => {
|
|
32
|
+
;
|
|
33
|
+
getCurrentCliVersion.mockResolvedValue('4.25.0');
|
|
34
|
+
getLatestCliVersion.mockResolvedValue('4.26.0');
|
|
35
|
+
compareVersions.mockReturnValue(true);
|
|
36
|
+
getUpdateInstructions.mockReturnValue([
|
|
37
|
+
'npm install -g @meltstudio/meltctl@latest',
|
|
38
|
+
'',
|
|
39
|
+
'Or with yarn:',
|
|
40
|
+
' yarn global add @meltstudio/meltctl@latest',
|
|
41
|
+
]);
|
|
42
|
+
await versionCheckCommand();
|
|
43
|
+
const logCalls = console.log.mock.calls.map((c) => c[0]);
|
|
44
|
+
expect(logCalls.some((msg) => typeof msg === 'string' && msg.includes('Update available'))).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
it('shows update instructions when update is available', async () => {
|
|
47
|
+
;
|
|
48
|
+
getCurrentCliVersion.mockResolvedValue('4.25.0');
|
|
49
|
+
getLatestCliVersion.mockResolvedValue('4.26.0');
|
|
50
|
+
compareVersions.mockReturnValue(true);
|
|
51
|
+
getUpdateInstructions.mockReturnValue(['npm install -g @meltstudio/meltctl@latest']);
|
|
52
|
+
await versionCheckCommand();
|
|
53
|
+
expect(getUpdateInstructions).toHaveBeenCalled();
|
|
54
|
+
const logCalls = console.log.mock.calls.map((c) => c[0]);
|
|
55
|
+
expect(logCalls.some((msg) => typeof msg === 'string' && msg.includes('To update, run'))).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
it('shows warning when unable to check for updates (network error)', async () => {
|
|
58
|
+
;
|
|
59
|
+
getCurrentCliVersion.mockResolvedValue('4.26.0');
|
|
60
|
+
getLatestCliVersion.mockResolvedValue(null);
|
|
61
|
+
await versionCheckCommand();
|
|
62
|
+
const logCalls = console.log.mock.calls.map((c) => c[0]);
|
|
63
|
+
expect(logCalls.some((msg) => typeof msg === 'string' && msg.includes('Unable to check for updates'))).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
it('shows current version when network check fails', async () => {
|
|
66
|
+
;
|
|
67
|
+
getCurrentCliVersion.mockResolvedValue('4.26.0');
|
|
68
|
+
getLatestCliVersion.mockResolvedValue(null);
|
|
69
|
+
await versionCheckCommand();
|
|
70
|
+
const logCalls = console.log.mock.calls.map((c) => c[0]);
|
|
71
|
+
expect(logCalls.some((msg) => typeof msg === 'string' && msg.includes('4.26.0'))).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
it('exits with error when getCurrentCliVersion throws', async () => {
|
|
74
|
+
;
|
|
75
|
+
getCurrentCliVersion.mockRejectedValue(new Error('Cannot read package.json'));
|
|
76
|
+
await expect(versionCheckCommand()).rejects.toThrow('process.exit(1)');
|
|
77
|
+
expect(console.error).toHaveBeenCalled();
|
|
78
|
+
});
|
|
79
|
+
it('exits with error when getLatestCliVersion throws', async () => {
|
|
80
|
+
;
|
|
81
|
+
getCurrentCliVersion.mockResolvedValue('4.26.0');
|
|
82
|
+
getLatestCliVersion.mockRejectedValue(new Error('Unexpected error'));
|
|
83
|
+
await expect(versionCheckCommand()).rejects.toThrow('process.exit(1)');
|
|
84
|
+
expect(console.error).toHaveBeenCalled();
|
|
85
|
+
});
|
|
86
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -102,7 +102,7 @@ audit
|
|
|
102
102
|
audit
|
|
103
103
|
.command('list')
|
|
104
104
|
.description('list submitted audits (Team Managers only)')
|
|
105
|
-
.option('--type <type>', 'filter by type (audit, ux-audit)')
|
|
105
|
+
.option('--type <type>', 'filter by type (audit, ux-audit, security-audit)')
|
|
106
106
|
.option('--repository <repo>', 'filter by repository (owner/repo)')
|
|
107
107
|
.option('--latest', 'show only the latest audit per project and type')
|
|
108
108
|
.option('--limit <n>', 'max results (default 50, max 200)')
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
vi.mock('./auth.js', () => ({
|
|
3
|
+
getStoredAuth: vi.fn(),
|
|
4
|
+
API_BASE: 'https://test-api.example.com',
|
|
5
|
+
}));
|
|
6
|
+
import { getStoredAuth, API_BASE } from './auth.js';
|
|
7
|
+
import { getToken, tokenFetch } from './api.js';
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
vi.clearAllMocks();
|
|
10
|
+
vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
11
|
+
vi.spyOn(process, 'exit').mockImplementation((code) => {
|
|
12
|
+
throw new Error(`process.exit(${code})`);
|
|
13
|
+
});
|
|
14
|
+
delete process.env['MELTCTL_TOKEN'];
|
|
15
|
+
});
|
|
16
|
+
describe('getToken', () => {
|
|
17
|
+
it('returns env token when MELTCTL_TOKEN is set', async () => {
|
|
18
|
+
process.env['MELTCTL_TOKEN'] = 'env-token-123';
|
|
19
|
+
const token = await getToken();
|
|
20
|
+
expect(token).toBe('env-token-123');
|
|
21
|
+
expect(getStoredAuth).not.toHaveBeenCalled();
|
|
22
|
+
});
|
|
23
|
+
it('returns stored auth token when valid', async () => {
|
|
24
|
+
;
|
|
25
|
+
getStoredAuth.mockResolvedValue({
|
|
26
|
+
token: 'stored-token',
|
|
27
|
+
email: 'dev@meltstudio.co',
|
|
28
|
+
expiresAt: '2099-12-31T00:00:00Z',
|
|
29
|
+
});
|
|
30
|
+
const token = await getToken();
|
|
31
|
+
expect(token).toBe('stored-token');
|
|
32
|
+
});
|
|
33
|
+
it('exits when no auth is available', async () => {
|
|
34
|
+
;
|
|
35
|
+
getStoredAuth.mockResolvedValue(undefined);
|
|
36
|
+
await expect(getToken()).rejects.toThrow('process.exit(1)');
|
|
37
|
+
expect(console.error).toHaveBeenCalled();
|
|
38
|
+
});
|
|
39
|
+
it('exits when stored token is expired', async () => {
|
|
40
|
+
;
|
|
41
|
+
getStoredAuth.mockResolvedValue({
|
|
42
|
+
token: 'expired-token',
|
|
43
|
+
email: 'dev@meltstudio.co',
|
|
44
|
+
expiresAt: '2020-01-01T00:00:00Z',
|
|
45
|
+
});
|
|
46
|
+
await expect(getToken()).rejects.toThrow('process.exit(1)');
|
|
47
|
+
expect(console.error).toHaveBeenCalled();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe('tokenFetch', () => {
|
|
51
|
+
it('makes request with bearer token', async () => {
|
|
52
|
+
const mockResponse = { status: 200, ok: true };
|
|
53
|
+
const fetchMock = vi.fn().mockResolvedValue(mockResponse);
|
|
54
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
55
|
+
const result = await tokenFetch('my-token', '/audits');
|
|
56
|
+
expect(result).toBe(mockResponse);
|
|
57
|
+
expect(fetchMock).toHaveBeenCalledWith(`${API_BASE}/audits`, expect.objectContaining({
|
|
58
|
+
headers: expect.objectContaining({
|
|
59
|
+
Authorization: 'Bearer my-token',
|
|
60
|
+
}),
|
|
61
|
+
}));
|
|
62
|
+
vi.unstubAllGlobals();
|
|
63
|
+
});
|
|
64
|
+
it('merges custom options and headers', async () => {
|
|
65
|
+
const fetchMock = vi.fn().mockResolvedValue({ status: 200, ok: true });
|
|
66
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
67
|
+
await tokenFetch('tok', '/test', {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: { 'Content-Type': 'application/json' },
|
|
70
|
+
body: '{}',
|
|
71
|
+
});
|
|
72
|
+
expect(fetchMock).toHaveBeenCalledWith(`${API_BASE}/test`, expect.objectContaining({
|
|
73
|
+
method: 'POST',
|
|
74
|
+
body: '{}',
|
|
75
|
+
headers: expect.objectContaining({
|
|
76
|
+
Authorization: 'Bearer tok',
|
|
77
|
+
'Content-Type': 'application/json',
|
|
78
|
+
}),
|
|
79
|
+
}));
|
|
80
|
+
vi.unstubAllGlobals();
|
|
81
|
+
});
|
|
82
|
+
it('throws when API returns 401', async () => {
|
|
83
|
+
const fetchMock = vi.fn().mockResolvedValue({ status: 401, ok: false });
|
|
84
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
85
|
+
await expect(tokenFetch('bad-token', '/test')).rejects.toThrow('Authentication failed');
|
|
86
|
+
vi.unstubAllGlobals();
|
|
87
|
+
});
|
|
88
|
+
it('returns response for non-401 errors without throwing', async () => {
|
|
89
|
+
const mockResponse = { status: 500, ok: false, statusText: 'Internal Server Error' };
|
|
90
|
+
const fetchMock = vi.fn().mockResolvedValue(mockResponse);
|
|
91
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
92
|
+
const result = await tokenFetch('tok', '/test');
|
|
93
|
+
expect(result.status).toBe(500);
|
|
94
|
+
vi.unstubAllGlobals();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
vi.mock('fs-extra', () => ({
|
|
3
|
+
default: {
|
|
4
|
+
pathExists: vi.fn(),
|
|
5
|
+
readJson: vi.fn(),
|
|
6
|
+
ensureDir: vi.fn(),
|
|
7
|
+
writeJson: vi.fn(),
|
|
8
|
+
remove: vi.fn(),
|
|
9
|
+
},
|
|
10
|
+
}));
|
|
11
|
+
import fs from 'fs-extra';
|
|
12
|
+
import { getStoredAuth, storeAuth, clearAuth, isAuthenticated, authenticatedFetch, API_BASE, } from './auth.js';
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
vi.clearAllMocks();
|
|
15
|
+
});
|
|
16
|
+
describe('API_BASE', () => {
|
|
17
|
+
it('has a default value', () => {
|
|
18
|
+
expect(API_BASE).toBeTruthy();
|
|
19
|
+
expect(typeof API_BASE).toBe('string');
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
describe('getStoredAuth', () => {
|
|
23
|
+
it('returns undefined when auth file does not exist', async () => {
|
|
24
|
+
;
|
|
25
|
+
fs.pathExists.mockResolvedValue(false);
|
|
26
|
+
const result = await getStoredAuth();
|
|
27
|
+
expect(result).toBeUndefined();
|
|
28
|
+
});
|
|
29
|
+
it('returns stored auth data when file exists', async () => {
|
|
30
|
+
const mockAuth = { token: 'abc', email: 'dev@meltstudio.co', expiresAt: '2026-12-31' };
|
|
31
|
+
fs.pathExists.mockResolvedValue(true);
|
|
32
|
+
fs.readJson.mockResolvedValue(mockAuth);
|
|
33
|
+
const result = await getStoredAuth();
|
|
34
|
+
expect(result).toEqual(mockAuth);
|
|
35
|
+
});
|
|
36
|
+
it('returns undefined when readJson throws', async () => {
|
|
37
|
+
;
|
|
38
|
+
fs.pathExists.mockResolvedValue(true);
|
|
39
|
+
fs.readJson.mockRejectedValue(new Error('parse error'));
|
|
40
|
+
const result = await getStoredAuth();
|
|
41
|
+
expect(result).toBeUndefined();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
describe('storeAuth', () => {
|
|
45
|
+
it('creates directory and writes auth file', async () => {
|
|
46
|
+
const auth = { token: 'tok', email: 'a@meltstudio.co', expiresAt: '2026-12-31' };
|
|
47
|
+
await storeAuth(auth);
|
|
48
|
+
expect(fs.ensureDir).toHaveBeenCalled();
|
|
49
|
+
expect(fs.writeJson).toHaveBeenCalledWith(expect.stringContaining('auth.json'), auth, {
|
|
50
|
+
spaces: 2,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe('clearAuth', () => {
|
|
55
|
+
it('removes auth file when it exists', async () => {
|
|
56
|
+
;
|
|
57
|
+
fs.pathExists.mockResolvedValue(true);
|
|
58
|
+
await clearAuth();
|
|
59
|
+
expect(fs.remove).toHaveBeenCalled();
|
|
60
|
+
});
|
|
61
|
+
it('does nothing when auth file does not exist', async () => {
|
|
62
|
+
;
|
|
63
|
+
fs.pathExists.mockResolvedValue(false);
|
|
64
|
+
await clearAuth();
|
|
65
|
+
expect(fs.remove).not.toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe('isAuthenticated', () => {
|
|
69
|
+
it('returns false when no stored auth', async () => {
|
|
70
|
+
;
|
|
71
|
+
fs.pathExists.mockResolvedValue(false);
|
|
72
|
+
expect(await isAuthenticated()).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
it('returns false when token is expired', async () => {
|
|
75
|
+
;
|
|
76
|
+
fs.pathExists.mockResolvedValue(true);
|
|
77
|
+
fs.readJson.mockResolvedValue({
|
|
78
|
+
token: 'tok',
|
|
79
|
+
email: 'a@meltstudio.co',
|
|
80
|
+
expiresAt: '2020-01-01T00:00:00Z',
|
|
81
|
+
});
|
|
82
|
+
expect(await isAuthenticated()).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
it('returns true when token is valid and not expired', async () => {
|
|
85
|
+
;
|
|
86
|
+
fs.pathExists.mockResolvedValue(true);
|
|
87
|
+
fs.readJson.mockResolvedValue({
|
|
88
|
+
token: 'tok',
|
|
89
|
+
email: 'a@meltstudio.co',
|
|
90
|
+
expiresAt: '2099-12-31T00:00:00Z',
|
|
91
|
+
});
|
|
92
|
+
expect(await isAuthenticated()).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
describe('authenticatedFetch', () => {
|
|
96
|
+
it('throws when not authenticated', async () => {
|
|
97
|
+
;
|
|
98
|
+
fs.pathExists.mockResolvedValue(false);
|
|
99
|
+
await expect(authenticatedFetch('/test')).rejects.toThrow('Not authenticated');
|
|
100
|
+
});
|
|
101
|
+
it('throws when session expired', async () => {
|
|
102
|
+
;
|
|
103
|
+
fs.pathExists.mockResolvedValue(true);
|
|
104
|
+
fs.readJson.mockResolvedValue({
|
|
105
|
+
token: 'tok',
|
|
106
|
+
email: 'a@meltstudio.co',
|
|
107
|
+
expiresAt: '2020-01-01T00:00:00Z',
|
|
108
|
+
});
|
|
109
|
+
await expect(authenticatedFetch('/test')).rejects.toThrow('expired');
|
|
110
|
+
});
|
|
111
|
+
it('makes authenticated request with bearer token', async () => {
|
|
112
|
+
;
|
|
113
|
+
fs.pathExists.mockResolvedValue(true);
|
|
114
|
+
fs.readJson.mockResolvedValue({
|
|
115
|
+
token: 'my-jwt',
|
|
116
|
+
email: 'a@meltstudio.co',
|
|
117
|
+
expiresAt: '2099-12-31T00:00:00Z',
|
|
118
|
+
});
|
|
119
|
+
const mockResponse = { status: 200, ok: true };
|
|
120
|
+
const fetchMock = vi.fn().mockResolvedValue(mockResponse);
|
|
121
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
122
|
+
const result = await authenticatedFetch('/templates');
|
|
123
|
+
expect(result).toBe(mockResponse);
|
|
124
|
+
expect(fetchMock).toHaveBeenCalledWith(`${API_BASE}/templates`, expect.objectContaining({
|
|
125
|
+
headers: expect.objectContaining({
|
|
126
|
+
Authorization: 'Bearer my-jwt',
|
|
127
|
+
}),
|
|
128
|
+
}));
|
|
129
|
+
vi.unstubAllGlobals();
|
|
130
|
+
});
|
|
131
|
+
it('throws when API returns 401', async () => {
|
|
132
|
+
;
|
|
133
|
+
fs.pathExists.mockResolvedValue(true);
|
|
134
|
+
fs.readJson.mockResolvedValue({
|
|
135
|
+
token: 'expired-jwt',
|
|
136
|
+
email: 'a@meltstudio.co',
|
|
137
|
+
expiresAt: '2099-12-31T00:00:00Z',
|
|
138
|
+
});
|
|
139
|
+
const fetchMock = vi.fn().mockResolvedValue({ status: 401, ok: false });
|
|
140
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
141
|
+
await expect(authenticatedFetch('/test')).rejects.toThrow('expired');
|
|
142
|
+
vi.unstubAllGlobals();
|
|
143
|
+
});
|
|
144
|
+
it('merges custom headers with auth header', async () => {
|
|
145
|
+
;
|
|
146
|
+
fs.pathExists.mockResolvedValue(true);
|
|
147
|
+
fs.readJson.mockResolvedValue({
|
|
148
|
+
token: 'jwt',
|
|
149
|
+
email: 'a@meltstudio.co',
|
|
150
|
+
expiresAt: '2099-12-31T00:00:00Z',
|
|
151
|
+
});
|
|
152
|
+
const fetchMock = vi.fn().mockResolvedValue({ status: 200, ok: true });
|
|
153
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
154
|
+
await authenticatedFetch('/test', {
|
|
155
|
+
headers: { 'Content-Type': 'application/json' },
|
|
156
|
+
});
|
|
157
|
+
expect(fetchMock).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
|
|
158
|
+
headers: expect.objectContaining({
|
|
159
|
+
Authorization: 'Bearer jwt',
|
|
160
|
+
'Content-Type': 'application/json',
|
|
161
|
+
}),
|
|
162
|
+
}));
|
|
163
|
+
vi.unstubAllGlobals();
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
vi.mock('fs', () => ({
|
|
3
|
+
readFileSync: vi.fn().mockReturnValue(JSON.stringify({ version: '1.2.3' })),
|
|
4
|
+
}));
|
|
5
|
+
vi.mock('gradient-string', () => ({
|
|
6
|
+
default: vi.fn().mockReturnValue(vi.fn((str) => str)),
|
|
7
|
+
}));
|
|
8
|
+
import { printBanner } from './banner.js';
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
vi.clearAllMocks();
|
|
11
|
+
vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
12
|
+
});
|
|
13
|
+
describe('printBanner', () => {
|
|
14
|
+
it('does not throw', () => {
|
|
15
|
+
expect(() => printBanner()).not.toThrow();
|
|
16
|
+
});
|
|
17
|
+
it('calls console.log at least once', () => {
|
|
18
|
+
printBanner();
|
|
19
|
+
expect(console.log).toHaveBeenCalled();
|
|
20
|
+
});
|
|
21
|
+
it('prints output containing MELTCTL logo text', () => {
|
|
22
|
+
printBanner();
|
|
23
|
+
const logCalls = console.log.mock.calls.map((c) => c[0]);
|
|
24
|
+
const allOutput = logCalls.filter(Boolean).join('\n');
|
|
25
|
+
// The logo contains block characters for MELTCTL
|
|
26
|
+
expect(allOutput).toContain('███');
|
|
27
|
+
});
|
|
28
|
+
it('prints version info', () => {
|
|
29
|
+
printBanner();
|
|
30
|
+
const logCalls = console.log.mock.calls.map((c) => String(c[0] ?? ''));
|
|
31
|
+
const hasVersion = logCalls.some((msg) => msg.includes('v1.2.3') || msg.includes('1.2.3'));
|
|
32
|
+
expect(hasVersion).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|