@mintlify/cli 4.0.1733 → 4.0.1735
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/__test__/agent-claim.test.ts +17 -22
- package/__test__/agent-credentials.test.ts +91 -4
- package/__test__/deploy.test.ts +4 -6
- package/__test__/validateCommand.test.ts +141 -0
- package/__test__/validateEngine.test.ts +46 -0
- package/__test__/validateRules.test.ts +500 -0
- package/bin/agent-claim.js +3 -10
- package/bin/agent-credentials.js +75 -6
- package/bin/cli.js +22 -1
- package/bin/deploy.js +1 -8
- package/bin/resolveFiles.js +3 -1
- package/bin/tsconfig.build.tsbuildinfo +1 -1
- package/bin/validate/command.js +58 -0
- package/bin/validate/engine.js +76 -0
- package/bin/validate/files.js +148 -0
- package/bin/validate/mdx.js +201 -0
- package/bin/validate/output.js +28 -0
- package/bin/validate/rules/content/voidTags.js +35 -0
- package/bin/validate/rules/index.js +3 -0
- package/bin/validate/rules/parseError.js +7 -0
- package/bin/validate/site.js +55 -0
- package/bin/validate/types.js +1 -0
- package/package.json +8 -8
- package/src/agent-claim.ts +1 -10
- package/src/agent-credentials.ts +53 -10
- package/src/cli.tsx +24 -1
- package/src/deploy.ts +2 -9
- package/src/resolveFiles.ts +3 -1
- package/src/validate/command.tsx +71 -0
- package/src/validate/engine.ts +93 -0
- package/src/validate/files.ts +212 -0
- package/src/validate/mdx.ts +252 -0
- package/src/validate/output.tsx +56 -0
- package/src/validate/rules/content/voidTags.ts +35 -0
- package/src/validate/rules/index.ts +5 -0
- package/src/validate/rules/parseError.ts +9 -0
- package/src/validate/site.ts +52 -0
- package/src/validate/types.ts +88 -0
|
@@ -54,7 +54,7 @@ afterEach(() => vi.unstubAllGlobals());
|
|
|
54
54
|
|
|
55
55
|
describe('claim command', () => {
|
|
56
56
|
it('completes the existing claim, saves the new identity, and signs up with a fresh token', async () => {
|
|
57
|
-
expect(await claimDeployment(options
|
|
57
|
+
expect(await claimDeployment(options)).toContain('successfully');
|
|
58
58
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
59
59
|
`${issuer}/agent/identity/claim/complete`,
|
|
60
60
|
expect.objectContaining({
|
|
@@ -83,11 +83,11 @@ describe('claim command', () => {
|
|
|
83
83
|
fetchMock.mockImplementation((url, init) =>
|
|
84
84
|
url.endsWith('/signup') ? response({ code: 'unavailable' }, 503) : implementation(url, init)
|
|
85
85
|
);
|
|
86
|
-
await expect(claimDeployment(options
|
|
86
|
+
await expect(claimDeployment(options)).rejects.toThrow('503');
|
|
87
87
|
const saved = vi.mocked(saveAgentCredentials).mock.calls[0]![2];
|
|
88
88
|
vi.mocked(loadAgentCredentials).mockResolvedValue(saved);
|
|
89
89
|
fetchMock.mockClear().mockImplementation(implementation);
|
|
90
|
-
expect(await claimDeployment(options
|
|
90
|
+
expect(await claimDeployment(options)).toContain('successfully');
|
|
91
91
|
expect(fetchMock.mock.calls.some(([url]) => url.endsWith('/complete'))).toBe(false);
|
|
92
92
|
});
|
|
93
93
|
it('does not save or sign up after an invalid code', async () => {
|
|
@@ -97,25 +97,22 @@ describe('claim command', () => {
|
|
|
97
97
|
? response({ code: 'invalid_user_code' }, 400)
|
|
98
98
|
: implementation(url, init)
|
|
99
99
|
);
|
|
100
|
-
await expect(claimDeployment(options
|
|
100
|
+
await expect(claimDeployment(options)).rejects.toThrow('invalid_user_code');
|
|
101
101
|
expect(saveAgentCredentials).not.toHaveBeenCalled();
|
|
102
102
|
expect(trackEvent).not.toHaveBeenCalled();
|
|
103
103
|
expect(fetchMock.mock.calls.some(([url]) => url.endsWith('/signup'))).toBe(false);
|
|
104
104
|
});
|
|
105
|
-
it('
|
|
106
|
-
vi.mocked(saveAgentCredentials).mockRejectedValue(new Error('
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
expect(output).toContain('successfully');
|
|
110
|
-
expect(warn).toHaveBeenCalled();
|
|
111
|
-
expect(JSON.stringify([output, warn.mock.calls])).not.toContain('secret-claim');
|
|
112
|
-
expect(JSON.stringify([output, warn.mock.calls])).not.toContain('claimed-assertion');
|
|
105
|
+
it('fails before signup when credentials cannot be saved', async () => {
|
|
106
|
+
vi.mocked(saveAgentCredentials).mockRejectedValue(new Error('disk full'));
|
|
107
|
+
await expect(claimDeployment(options)).rejects.toThrow('disk full');
|
|
108
|
+
expect(fetchMock.mock.calls.some(([url]) => url.endsWith('/signup'))).toBe(false);
|
|
113
109
|
});
|
|
114
110
|
it('creates only a claim link for --email', async () => {
|
|
115
|
-
const output = await claimDeployment(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
111
|
+
const output = await claimDeployment({
|
|
112
|
+
apiUrl,
|
|
113
|
+
directory: '/docs',
|
|
114
|
+
email: 'person@example.com',
|
|
115
|
+
});
|
|
119
116
|
expect(new URL(output.match(/https?:\/\/\S+/)![0]).searchParams.get('registration')).toBe(
|
|
120
117
|
'reg'
|
|
121
118
|
);
|
|
@@ -128,15 +125,13 @@ describe('claim command', () => {
|
|
|
128
125
|
});
|
|
129
126
|
it('fails before network calls when credentials are missing', async () => {
|
|
130
127
|
vi.mocked(loadAgentCredentials).mockResolvedValue(null);
|
|
131
|
-
await expect(claimDeployment(options
|
|
128
|
+
await expect(claimDeployment(options)).rejects.toThrow('No saved agent registration');
|
|
132
129
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
133
130
|
});
|
|
134
131
|
it.each([{ code: undefined }, { code: ' ', email: undefined }, { email: 'person@example.com' }])(
|
|
135
132
|
'rejects missing or conflicting arguments: %o',
|
|
136
133
|
async (override) => {
|
|
137
|
-
await expect(claimDeployment({ ...options, ...override }
|
|
138
|
-
'Provide either'
|
|
139
|
-
);
|
|
134
|
+
await expect(claimDeployment({ ...options, ...override })).rejects.toThrow('Provide either');
|
|
140
135
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
141
136
|
}
|
|
142
137
|
);
|
|
@@ -145,7 +140,7 @@ describe('claim command', () => {
|
|
|
145
140
|
...original,
|
|
146
141
|
issuer: 'https://other.example.com',
|
|
147
142
|
});
|
|
148
|
-
await expect(claimDeployment(options
|
|
143
|
+
await expect(claimDeployment(options)).rejects.toThrow('another issuer');
|
|
149
144
|
expect(fetchMock.mock.calls).toHaveLength(2);
|
|
150
145
|
});
|
|
151
146
|
it('does not report success if signup returns an anonymous identity', async () => {
|
|
@@ -155,6 +150,6 @@ describe('claim command', () => {
|
|
|
155
150
|
? response({ orgId: 'org', claimed: false })
|
|
156
151
|
: implementation(url, init)
|
|
157
152
|
);
|
|
158
|
-
await expect(claimDeployment(options
|
|
153
|
+
await expect(claimDeployment(options)).rejects.toThrow('unexpected response');
|
|
159
154
|
});
|
|
160
155
|
});
|
|
@@ -1,8 +1,21 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
1
4
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
5
|
|
|
3
6
|
const entries = new Map<string, string>();
|
|
4
|
-
const keytar = vi.hoisted(() => ({
|
|
7
|
+
const keytar = vi.hoisted(() => ({
|
|
8
|
+
getPassword: vi.fn(),
|
|
9
|
+
setPassword: vi.fn(),
|
|
10
|
+
deletePassword: vi.fn(),
|
|
11
|
+
}));
|
|
5
12
|
vi.mock('../src/keyring.js', () => ({ getKeytar: async () => keytar }));
|
|
13
|
+
const configDir = vi.hoisted(() => ({ path: '' }));
|
|
14
|
+
vi.mock('../src/constants.js', () => ({
|
|
15
|
+
get CONFIG_DIR() {
|
|
16
|
+
return configDir.path;
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
6
19
|
|
|
7
20
|
import { loadAgentCredentials, saveAgentCredentials } from '../src/agent-credentials.js';
|
|
8
21
|
|
|
@@ -17,12 +30,16 @@ describe('agent credentials', () => {
|
|
|
17
30
|
beforeEach(() => {
|
|
18
31
|
entries.clear();
|
|
19
32
|
vi.resetAllMocks();
|
|
33
|
+
configDir.path = fs.mkdtempSync(path.join(os.tmpdir(), 'mint-agent-'));
|
|
20
34
|
keytar.getPassword.mockImplementation(
|
|
21
35
|
async (service, account) => entries.get(`${service}/${account}`) ?? null
|
|
22
36
|
);
|
|
23
37
|
keytar.setPassword.mockImplementation(async (service, account, value) => {
|
|
24
38
|
entries.set(`${service}/${account}`, value);
|
|
25
39
|
});
|
|
40
|
+
keytar.deletePassword.mockImplementation(async (service, account) =>
|
|
41
|
+
entries.delete(`${service}/${account}`)
|
|
42
|
+
);
|
|
26
43
|
});
|
|
27
44
|
|
|
28
45
|
it('isolates projects and environments from each other and human credentials', async () => {
|
|
@@ -45,12 +62,82 @@ describe('agent credentials', () => {
|
|
|
45
62
|
expect(entries.has('mintlify-agent/https://api.example.com:reg_1')).toBe(true);
|
|
46
63
|
});
|
|
47
64
|
|
|
48
|
-
it('
|
|
65
|
+
it('falls back to a local file when the keyring cannot be written or read', async () => {
|
|
66
|
+
keytar.setPassword.mockRejectedValue(new Error('locked'));
|
|
67
|
+
await saveAgentCredentials('https://api.example.com', '/docs', credentials);
|
|
68
|
+
expect(entries.size).toBe(0);
|
|
69
|
+
const file = path.join(configDir.path, 'agent-credentials.json');
|
|
70
|
+
if (process.platform !== 'win32') expect(fs.statSync(file).mode & 0o777).toBe(0o600);
|
|
71
|
+
keytar.getPassword.mockRejectedValue(new Error('locked'));
|
|
72
|
+
expect(await loadAgentCredentials('https://api.example.com', '/docs')).toEqual(credentials);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('removes stale fallback entries once the keyring is writable again', async () => {
|
|
49
76
|
keytar.setPassword.mockRejectedValueOnce(new Error('locked'));
|
|
77
|
+
await saveAgentCredentials('https://api.example.com', '/docs', credentials);
|
|
78
|
+
await saveAgentCredentials('https://api.example.com', '/docs', {
|
|
79
|
+
...credentials,
|
|
80
|
+
assertion: 'next',
|
|
81
|
+
});
|
|
82
|
+
expect((await loadAgentCredentials('https://api.example.com', '/docs'))?.assertion).toBe(
|
|
83
|
+
'next'
|
|
84
|
+
);
|
|
85
|
+
const file = path.join(configDir.path, 'agent-credentials.json');
|
|
86
|
+
expect(JSON.parse(fs.readFileSync(file, 'utf-8'))).toEqual({});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('reads the keyring before the fallback file', async () => {
|
|
90
|
+
await saveAgentCredentials('https://api.example.com', '/docs', credentials);
|
|
91
|
+
fs.writeFileSync(
|
|
92
|
+
path.join(configDir.path, 'agent-credentials.json'),
|
|
93
|
+
JSON.stringify({
|
|
94
|
+
'https://api.example.com:reg_1': JSON.stringify({ ...credentials, assertion: 'stale' }),
|
|
95
|
+
})
|
|
96
|
+
);
|
|
97
|
+
expect(await loadAgentCredentials('https://api.example.com', '/docs')).toEqual(credentials);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('ignores a corrupt fallback file when the keyring has the credentials', async () => {
|
|
101
|
+
await saveAgentCredentials('https://api.example.com', '/docs', credentials);
|
|
102
|
+
fs.writeFileSync(path.join(configDir.path, 'agent-credentials.json'), '{');
|
|
103
|
+
expect(await loadAgentCredentials('https://api.example.com', '/docs')).toEqual(credentials);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('drops the stale keyring entry when a write falls back to the file', async () => {
|
|
107
|
+
await saveAgentCredentials('https://api.example.com', '/docs', credentials);
|
|
108
|
+
keytar.setPassword.mockRejectedValueOnce(new Error('locked'));
|
|
109
|
+
await saveAgentCredentials('https://api.example.com', '/docs', {
|
|
110
|
+
...credentials,
|
|
111
|
+
assertion: 'next',
|
|
112
|
+
});
|
|
113
|
+
expect((await loadAgentCredentials('https://api.example.com', '/docs'))?.assertion).toBe(
|
|
114
|
+
'next'
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('succeeds when the keyring write works but fallback cleanup fails', async () => {
|
|
119
|
+
keytar.setPassword.mockRejectedValueOnce(new Error('locked'));
|
|
120
|
+
await saveAgentCredentials('https://api.example.com', '/docs', credentials);
|
|
121
|
+
fs.chmodSync(configDir.path, 0o500);
|
|
122
|
+
await expect(
|
|
123
|
+
saveAgentCredentials('https://api.example.com', '/docs', {
|
|
124
|
+
...credentials,
|
|
125
|
+
assertion: 'next',
|
|
126
|
+
})
|
|
127
|
+
).resolves.toBeUndefined();
|
|
128
|
+
fs.chmodSync(configDir.path, 0o700);
|
|
129
|
+
expect((await loadAgentCredentials('https://api.example.com', '/docs'))?.assertion).toBe(
|
|
130
|
+
'next'
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('fails when neither the keyring nor the local file can be written', async () => {
|
|
135
|
+
keytar.setPassword.mockRejectedValue(new Error('locked'));
|
|
136
|
+
configDir.path = path.join(configDir.path, 'not-a-directory');
|
|
137
|
+
fs.writeFileSync(configDir.path, '');
|
|
50
138
|
await expect(
|
|
51
139
|
saveAgentCredentials('https://api.example.com', '/docs', credentials)
|
|
52
|
-
).rejects.toThrow(
|
|
53
|
-
expect(keytar.setPassword).toHaveBeenCalledTimes(1);
|
|
140
|
+
).rejects.toThrow();
|
|
54
141
|
});
|
|
55
142
|
|
|
56
143
|
it('preserves read failures instead of treating them as an absent registration', async () => {
|
package/__test__/deploy.test.ts
CHANGED
|
@@ -82,12 +82,10 @@ describe('mint deploy', () => {
|
|
|
82
82
|
const result = await prepareAgentDeployment({ ...options, apiUrl }, warn);
|
|
83
83
|
expect(formatDeployment(result, apiUrl)).toContain(`Site: ${siteUrl}`);
|
|
84
84
|
});
|
|
85
|
-
it('
|
|
86
|
-
vi.mocked(storage.saveAgentCredentials).mockRejectedValue(new Error('
|
|
87
|
-
|
|
88
|
-
expect(
|
|
89
|
-
expect(warn).toHaveBeenCalledWith(expect.stringContaining('Continuing for this run'));
|
|
90
|
-
expect(formatDeployment(result, options.apiUrl)).toContain('https://allocated.mintlify.site');
|
|
85
|
+
it('fails before provisioning when credentials cannot be saved', async () => {
|
|
86
|
+
vi.mocked(storage.saveAgentCredentials).mockRejectedValue(new Error('disk full'));
|
|
87
|
+
await expect(prepareAgentDeployment(options, warn)).rejects.toThrow('disk full');
|
|
88
|
+
expect(deployment.provisionAgentSite).not.toHaveBeenCalled();
|
|
91
89
|
});
|
|
92
90
|
it('persists the exchanged access token for later commands', async () => {
|
|
93
91
|
await prepareAgentDeployment(options, warn);
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { addLog, validateBuild } from '@mintlify/previewing';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { mockProcessExit } from 'vitest-mock-process';
|
|
5
|
+
|
|
6
|
+
import { runCommand } from './utils.js';
|
|
7
|
+
|
|
8
|
+
const { dir } = vi.hoisted(() => ({
|
|
9
|
+
dir: `${process.env.TMPDIR ?? '/tmp'}/validate-command-${process.pid}`,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock('../src/helpers.js', async (importOriginal) => {
|
|
13
|
+
const mod = await importOriginal<typeof import('../src/helpers.js')>();
|
|
14
|
+
return { ...mod, CMD_EXEC_PATH: dir, autoUpgradeIfNeeded: vi.fn() };
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
vi.mock('@mintlify/previewing', async (importOriginal) => {
|
|
18
|
+
const mod = await importOriginal<typeof import('@mintlify/previewing')>();
|
|
19
|
+
return { ...mod, addLog: vi.fn(), validateBuild: vi.fn() };
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const processExitMock = mockProcessExit();
|
|
23
|
+
|
|
24
|
+
const HOME = '---\ntitle: Home\n---\n\nBody\n';
|
|
25
|
+
|
|
26
|
+
const writeSite = (files: Record<string, string>, config: Record<string, unknown> = {}) => {
|
|
27
|
+
fs.emptyDirSync(dir);
|
|
28
|
+
fs.writeJSONSync(path.join(dir, 'docs.json'), {
|
|
29
|
+
theme: 'mint',
|
|
30
|
+
name: 'Test',
|
|
31
|
+
colors: { primary: '#000000' },
|
|
32
|
+
navigation: { pages: ['index'] },
|
|
33
|
+
...config,
|
|
34
|
+
});
|
|
35
|
+
for (const [file, content] of Object.entries(files)) {
|
|
36
|
+
fs.outputFileSync(path.join(dir, file), content);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const logged = () => JSON.stringify(vi.mocked(addLog).mock.calls);
|
|
41
|
+
|
|
42
|
+
describe('mint validate', () => {
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
vi.clearAllMocks();
|
|
45
|
+
vi.mocked(validateBuild).mockResolvedValue(undefined as never);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterAll(() => {
|
|
49
|
+
fs.removeSync(dir);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('logs lint findings by file and exits 1 before the build', async () => {
|
|
53
|
+
writeSite({
|
|
54
|
+
'index.mdx': '---\ntitle: Home\n---\n\nline<br>\n',
|
|
55
|
+
'other.mdx': '---\ntitle: Other\n---\n\nText {broken\n',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await runCommand('validate');
|
|
59
|
+
|
|
60
|
+
const text = logged();
|
|
61
|
+
expect(text).toContain('index.mdx');
|
|
62
|
+
expect(text).toContain('must be self-closed');
|
|
63
|
+
expect(text).toContain('other.mdx');
|
|
64
|
+
expect(text).toContain('other checks were skipped for this file');
|
|
65
|
+
expect(text).toContain('2 errors');
|
|
66
|
+
expect(validateBuild).not.toHaveBeenCalled();
|
|
67
|
+
expect(processExitMock).toHaveBeenCalledWith(1);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('continues into the build when the lint pass is clean', async () => {
|
|
71
|
+
writeSite({ 'index.mdx': HOME });
|
|
72
|
+
|
|
73
|
+
await runCommand('validate');
|
|
74
|
+
|
|
75
|
+
expect(validateBuild).toHaveBeenCalledWith(
|
|
76
|
+
expect.objectContaining({ packageName: 'mint' }),
|
|
77
|
+
expect.any(Function)
|
|
78
|
+
);
|
|
79
|
+
expect(processExitMock).not.toHaveBeenCalled();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('keeps --disable-openapi working', async () => {
|
|
83
|
+
writeSite({ 'index.mdx': HOME }, { api: { openapi: 'missing.json' } });
|
|
84
|
+
|
|
85
|
+
await runCommand('validate', '--disable-openapi');
|
|
86
|
+
|
|
87
|
+
expect(validateBuild).toHaveBeenCalled();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('turns rules and groups on and off', async () => {
|
|
91
|
+
writeSite({ 'index.mdx': '---\ntitle: Home\n---\n\nline<br>\n' });
|
|
92
|
+
|
|
93
|
+
await runCommand('validate', '--disable', 'content');
|
|
94
|
+
expect(logged()).toContain('parse-error');
|
|
95
|
+
expect(logged()).not.toContain('void-tags');
|
|
96
|
+
|
|
97
|
+
vi.clearAllMocks();
|
|
98
|
+
await runCommand('validate', '--disable', 'all', '--enable', 'void-tags');
|
|
99
|
+
expect(logged()).toContain('void-tags');
|
|
100
|
+
expect(processExitMock).toHaveBeenCalledWith(1);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('rejects unknown rules and disabling parse-error', async () => {
|
|
104
|
+
writeSite({ 'index.mdx': HOME });
|
|
105
|
+
|
|
106
|
+
await runCommand('validate', '--disable', 'nope');
|
|
107
|
+
expect(logged()).toContain('unknown rule or group');
|
|
108
|
+
expect(processExitMock).toHaveBeenLastCalledWith(1);
|
|
109
|
+
|
|
110
|
+
await runCommand('validate', '--disable', 'parse-error');
|
|
111
|
+
expect(logged()).toContain("parse-error always runs and can't be disabled");
|
|
112
|
+
expect(processExitMock).toHaveBeenLastCalledWith(1);
|
|
113
|
+
expect(validateBuild).not.toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('limits page findings to --files', async () => {
|
|
117
|
+
writeSite(
|
|
118
|
+
{
|
|
119
|
+
'index.mdx': '---\ntitle: Home\n---\n\nline<br>\n',
|
|
120
|
+
'other.mdx': '---\ntitle: Other\n---\n\nline<br>\n\nmore<hr>\n',
|
|
121
|
+
},
|
|
122
|
+
{ navigation: { pages: ['index', 'other'] } }
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
await runCommand('validate', '--files', 'other.mdx');
|
|
126
|
+
|
|
127
|
+
expect(logged()).toContain('other.mdx');
|
|
128
|
+
expect(logged()).not.toContain('index.mdx');
|
|
129
|
+
expect(processExitMock).toHaveBeenCalledWith(1);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('rejects --files that match nothing', async () => {
|
|
133
|
+
writeSite({ 'index.mdx': HOME });
|
|
134
|
+
|
|
135
|
+
await runCommand('validate', '--files', 'missing.mdx');
|
|
136
|
+
|
|
137
|
+
expect(logged()).toContain('No matching files');
|
|
138
|
+
expect(processExitMock).toHaveBeenCalledWith(1);
|
|
139
|
+
expect(validateBuild).not.toHaveBeenCalled();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { selectRules, UsageError } from '../src/validate/engine.js';
|
|
2
|
+
import { RULES } from '../src/validate/rules/index.js';
|
|
3
|
+
import { GROUPS } from '../src/validate/types.js';
|
|
4
|
+
|
|
5
|
+
const ids = (rules: { id: string }[]) => rules.map(({ id }) => id);
|
|
6
|
+
|
|
7
|
+
describe('selectRules', () => {
|
|
8
|
+
it('starts from every rule; --disable all empties the selection', () => {
|
|
9
|
+
expect(ids(selectRules({}))).toEqual(ids(RULES));
|
|
10
|
+
expect(ids(selectRules({ disable: ['all'] }))).toEqual([]);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('turns on the rules of an enabled group', () => {
|
|
14
|
+
const content = ids(selectRules({ disable: ['all'], enable: ['content'] }));
|
|
15
|
+
|
|
16
|
+
expect(content).toEqual(['void-tags']);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('applies --enable after --disable', () => {
|
|
20
|
+
expect(ids(selectRules({ disable: ['content'], enable: ['void-tags'] }))).toContain(
|
|
21
|
+
'void-tags'
|
|
22
|
+
);
|
|
23
|
+
expect(ids(selectRules({ disable: ['content'] }))).not.toContain('void-tags');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('rejects unknown names and naming parse-error', () => {
|
|
27
|
+
expect(() => selectRules({ enable: ['nope'] })).toThrow(UsageError);
|
|
28
|
+
expect(() => selectRules({ disable: ['parse-error'] })).toThrow(
|
|
29
|
+
"parse-error always runs and can't be disabled"
|
|
30
|
+
);
|
|
31
|
+
expect(() => selectRules({ enable: ['parse-error'] })).toThrow(
|
|
32
|
+
"parse-error always runs and can't be disabled"
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('gives every rule a known group, a severity and a description', () => {
|
|
37
|
+
for (const rule of RULES) {
|
|
38
|
+
expect(GROUPS).toContain(rule.group);
|
|
39
|
+
expect(['error', 'warning']).toContain(rule.severity);
|
|
40
|
+
expect(rule.description).not.toBe('');
|
|
41
|
+
expect(rule.id).not.toBe('parse-error');
|
|
42
|
+
expect(GROUPS as string[]).not.toContain(rule.id);
|
|
43
|
+
}
|
|
44
|
+
expect(new Set(ids(RULES)).size).toBe(RULES.length);
|
|
45
|
+
});
|
|
46
|
+
});
|