Skip to content

Commit ff49369

Browse files
committed
Fixes + imrpovements + some tests.
1 parent 3ac8315 commit ff49369

File tree

19 files changed

+355
-41
lines changed

19 files changed

+355
-41
lines changed
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2+
import { ResolverType } from './runners';
3+
import * as fs from 'node:fs';
4+
import { CodifyResolver } from './index';
5+
import { LoginHelper } from '../../connect/login-helper.js';
6+
import { Reporter } from '../../ui/reporters/reporter.js';
7+
import { NoCodifyFileError, MultipleFilesError } from './errors.js';
8+
9+
vi.mock('node:fs', async () => {
10+
const { fs } = await import('memfs');
11+
return fs
12+
})
13+
14+
vi.mock('node:fs/promises', async () => {
15+
const { fs } = await import('memfs');
16+
return fs.promises;
17+
})
18+
19+
vi.mock('../../connect/login-helper.js');
20+
vi.mock('../../api/dashboard/index.js');
21+
vi.mock('../../ui/reporters/reporter.js');
22+
23+
24+
describe('Codify resolver tests', () => {
25+
beforeEach(() => {
26+
vi.clearAllMocks();
27+
});
28+
29+
describe('resolveFile', () => {
30+
it('Can resolve a local file', async () => {
31+
fs.writeFileSync('/codify.jsonc', '{}', { encoding: 'utf-8' });
32+
33+
const result = await CodifyResolver.resolveFile('/codify.jsonc', { resolverType: ResolverType.LOCAL});
34+
expect(result?.path).to.eq('/codify.jsonc');
35+
})
36+
37+
it('Should return null when no file is found and allowEmpty is true', async () => {
38+
const result = await CodifyResolver.resolveFile('/nonexistent.codify.jsonc', {
39+
resolverType: ResolverType.LOCAL,
40+
allowEmpty: true
41+
});
42+
expect(result).to.be.null;
43+
})
44+
45+
it('Should throw NoCodifyFileError when no file is found and allowEmpty is false', async () => {
46+
await expect(
47+
CodifyResolver.resolveFile('/nonexistent.codify.jsonc', {
48+
resolverType: ResolverType.LOCAL,
49+
allowEmpty: false
50+
})
51+
).rejects.toThrow(NoCodifyFileError);
52+
})
53+
54+
it('Should throw NoCodifyFileError by default when no file is found', async () => {
55+
await expect(
56+
CodifyResolver.resolveFile('/nonexistent.codify.jsonc', {
57+
resolverType: ResolverType.LOCAL
58+
})
59+
).rejects.toThrow(NoCodifyFileError);
60+
})
61+
62+
it('Should throw MultipleFilesError when multiple files are found without a reporter', async () => {
63+
fs.mkdirSync('/test-dir', { recursive: true });
64+
fs.writeFileSync('/test-dir/file1.codify.jsonc', '{}', { encoding: 'utf-8' });
65+
fs.writeFileSync('/test-dir/file2.codify.jsonc', '{}', { encoding: 'utf-8' });
66+
67+
await expect(
68+
CodifyResolver.resolveFile('/test-dir', {
69+
path: '/test-dir'
70+
})
71+
).rejects.toThrow(MultipleFilesError);
72+
})
73+
74+
it('Should prompt user when multiple files are found with a reporter', async () => {
75+
fs.mkdirSync('/test-dir', { recursive: true });
76+
fs.writeFileSync('/test-dir/file1.codify.jsonc', '{"name":"file1"}', { encoding: 'utf-8' });
77+
fs.writeFileSync('/test-dir/file2.codify.jsonc', '{"name":"file2"}', { encoding: 'utf-8' });
78+
79+
const mockReporter = {
80+
promptOptions: vi.fn().mockResolvedValue(0)
81+
} as unknown as Reporter;
82+
83+
const result = await CodifyResolver.resolveFile('test', {
84+
path: '/test-dir',
85+
reporter: mockReporter
86+
});
87+
88+
expect(mockReporter.promptOptions).toHaveBeenCalled();
89+
expect(result?.path).to.eq('/test-dir/file1.codify.jsonc');
90+
})
91+
92+
it('Should return the selected file when user chooses from multiple options', async () => {
93+
fs.mkdirSync('/test-dir', { recursive: true });
94+
fs.writeFileSync('/test-dir/file1.codify.jsonc', '{"name":"file1"}', { encoding: 'utf-8' });
95+
fs.writeFileSync('/test-dir/file2.codify.jsonc', '{"name":"file2"}', { encoding: 'utf-8' });
96+
97+
const mockReporter = {
98+
promptOptions: vi.fn().mockResolvedValue(1)
99+
} as unknown as Reporter;
100+
101+
const result = await CodifyResolver.resolveFile('test', {
102+
path: '/test-dir',
103+
reporter: mockReporter
104+
});
105+
106+
expect(result?.path).to.eq('/test-dir/file2.codify.jsonc');
107+
})
108+
})
109+
110+
describe('resolveAll', () => {
111+
it('Should resolve all files in a directory', async () => {
112+
fs.mkdirSync('/test-dir', { recursive: true });
113+
fs.writeFileSync('/test-dir/file1.codify.jsonc', '{}', { encoding: 'utf-8' });
114+
fs.writeFileSync('/test-dir/file2.codify.jsonc', '{}', { encoding: 'utf-8' });
115+
116+
const results = await CodifyResolver.resolveAll('test', {
117+
path: '/test-dir'
118+
});
119+
120+
expect(results).toHaveLength(2);
121+
expect(results[0].path).to.eq('/test-dir/file1.codify.jsonc');
122+
expect(results[1].path).to.eq('/test-dir/file2.codify.jsonc');
123+
})
124+
125+
it('Should return empty array when no files are found', async () => {
126+
const results = await CodifyResolver.resolveAll('test', {
127+
path: '/nonexistent-dir',
128+
allowEmpty: true
129+
});
130+
131+
expect(results).toHaveLength(0);
132+
})
133+
134+
it('Should resolve a single file', async () => {
135+
fs.writeFileSync('/single.codify.jsonc', '{}', { encoding: 'utf-8' });
136+
137+
const results = await CodifyResolver.resolveAll('/single.codify.jsonc', {
138+
resolverType: ResolverType.LOCAL
139+
});
140+
141+
expect(results).toHaveLength(1);
142+
expect(results[0].path).to.eq('/single.codify.jsonc');
143+
})
144+
})
145+
146+
describe('resolver type selection', () => {
147+
it('Should use specified resolver type when provided', async () => {
148+
fs.writeFileSync('/codify.jsonc', '{}', { encoding: 'utf-8' });
149+
150+
const result = await CodifyResolver.resolveFile('/codify.jsonc', {
151+
resolverType: ResolverType.LOCAL
152+
});
153+
154+
expect(result?.path).to.eq('/codify.jsonc');
155+
})
156+
157+
it('Should use path resolver when path is provided', async () => {
158+
fs.writeFileSync('/codify.jsonc', '{}', { encoding: 'utf-8' });
159+
160+
const result = await CodifyResolver.resolveFile('any-location', {
161+
path: '/codify.jsonc'
162+
});
163+
164+
expect(result?.path).to.eq('/codify.jsonc');
165+
})
166+
167+
it('Should prioritize resolverType over path', async () => {
168+
fs.writeFileSync('/codify.jsonc', '{}', { encoding: 'utf-8' });
169+
170+
const result = await CodifyResolver.resolveFile('/codify.jsonc', {
171+
resolverType: ResolverType.LOCAL,
172+
path: '/some-other-path'
173+
});
174+
175+
expect(result?.path).to.eq('/codify.jsonc');
176+
})
177+
})
178+
179+
describe('file type detection', () => {
180+
it('Should detect JSONC file type', async () => {
181+
fs.writeFileSync('/codify.jsonc', '{}', { encoding: 'utf-8' });
182+
183+
const result = await CodifyResolver.resolveFile('/codify.jsonc', {
184+
resolverType: ResolverType.LOCAL
185+
});
186+
187+
expect(result?.fileType).to.eq('jsonc');
188+
})
189+
190+
it('Should detect JSON file type', async () => {
191+
fs.writeFileSync('/codify.json', '{}', { encoding: 'utf-8' });
192+
193+
const result = await CodifyResolver.resolveFile('/codify.json', {
194+
resolverType: ResolverType.LOCAL
195+
});
196+
197+
expect(result?.fileType).to.eq('json');
198+
})
199+
200+
it('Should detect JSON5 file type', async () => {
201+
fs.writeFileSync('/codify.json5', '{}', { encoding: 'utf-8' });
202+
203+
const result = await CodifyResolver.resolveFile('/codify.json5', {
204+
resolverType: ResolverType.LOCAL
205+
});
206+
207+
expect(result?.fileType).to.eq('json5');
208+
})
209+
210+
it('Should detect YAML file type', async () => {
211+
fs.writeFileSync('/codify.yaml', '{}', { encoding: 'utf-8' });
212+
213+
const result = await CodifyResolver.resolveFile('/codify.yaml', {
214+
resolverType: ResolverType.LOCAL
215+
});
216+
217+
expect(result?.fileType).to.eq('yaml');
218+
})
219+
})
220+
221+
describe('file content handling', () => {
222+
it('Should preserve file contents', async () => {
223+
const testContent = '{ "test": "value" }';
224+
fs.writeFileSync('/codify.jsonc', testContent, { encoding: 'utf-8' });
225+
226+
const result = await CodifyResolver.resolveFile('/codify.jsonc', {
227+
resolverType: ResolverType.LOCAL
228+
});
229+
230+
expect(result?.contents).to.eq(testContent);
231+
})
232+
233+
it('Should handle empty files', async () => {
234+
fs.writeFileSync('/codify.jsonc', '', { encoding: 'utf-8' });
235+
236+
const result = await CodifyResolver.resolveFile('/codify.jsonc', {
237+
resolverType: ResolverType.LOCAL
238+
});
239+
240+
expect(result?.contents).to.eq('');
241+
})
242+
243+
it('Should handle files with special characters', async () => {
244+
const testContent = '{ "test": "value with special chars: @#$%^&*()" }';
245+
fs.writeFileSync('/codify.jsonc', testContent, { encoding: 'utf-8' });
246+
247+
const result = await CodifyResolver.resolveFile('/codify.jsonc', {
248+
resolverType: ResolverType.LOCAL
249+
});
250+
251+
expect(result?.contents).to.eq(testContent);
252+
})
253+
})
254+
255+
describe('directory handling', () => {
256+
it('Should resolve all codify files in a directory', async () => {
257+
fs.mkdirSync('/test-dir', { recursive: true });
258+
fs.writeFileSync('/test-dir/config.codify.jsonc', '{}', { encoding: 'utf-8' });
259+
fs.writeFileSync('/test-dir/other.codify.json', '{}', { encoding: 'utf-8' });
260+
fs.writeFileSync('/test-dir/ignore-me.txt', '{}', { encoding: 'utf-8' });
261+
262+
const results = await CodifyResolver.resolveAll('test', {
263+
path: '/test-dir'
264+
});
265+
266+
expect(results).toHaveLength(2);
267+
expect(results.map(f => f.path)).toContain('/test-dir/config.codify.jsonc');
268+
expect(results.map(f => f.path)).toContain('/test-dir/other.codify.json');
269+
})
270+
271+
it('Should ignore non-codify files in directory', async () => {
272+
fs.mkdirSync('/test-dir', { recursive: true });
273+
fs.writeFileSync('/test-dir/readme.md', 'content', { encoding: 'utf-8' });
274+
fs.writeFileSync('/test-dir/package.json', '{}', { encoding: 'utf-8' });
275+
276+
const results = await CodifyResolver.resolveAll('test', {
277+
path: '/test-dir',
278+
allowEmpty: true
279+
});
280+
281+
expect(results).toHaveLength(0);
282+
})
283+
})
284+
285+
afterEach(() => {
286+
vi.resetAllMocks();
287+
})
288+
})

src/codify-files/resolver/index.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,7 @@ import { LoginHelper } from '../../connect/login-helper.js';
22
import { Reporter } from '../../ui/reporters/reporter.js';
33
import { InMemoryFile } from './entities.js';
44
import { MultipleFilesError, NoCodifyFileError } from './errors.js';
5-
import { CodifyResolverRunner, ResolverResult } from './runners.js';
6-
7-
export enum ResolverType {
8-
LOCAL = 'LOCAL',
9-
REMOTE_DOCUMENT_ID = 'REMOTE_DOCUMENT_ID',
10-
REMOTE_DOCUMENT = 'REMOTE_DOCUMENT',
11-
TEMPLATE = 'TEMPLATE',
12-
REMOTE_DEFAULT_DOCUMENT = 'REMOTE_DEFAULT_DOCUMENT',
13-
}
5+
import { CodifyResolverRunner, ResolverResult, ResolverType } from './runners.js';
146

157
interface ResolverArgs {
168
resolverType?: ResolverType;
@@ -43,7 +35,12 @@ export class CodifyResolver {
4335
const resolvedFiles = await CodifyResolver.run(location, args)
4436
return this.narrow(resolvedFiles, args);
4537
}
46-
38+
39+
static async resolveAll(location: string, args?: ResolverArgs): Promise<InMemoryFile[]> {
40+
const result = await CodifyResolver.run(location, args)
41+
return result.files;
42+
}
43+
4744
private static async run(location: string, args?: ResolverArgs): Promise<ResolverResult> {
4845
if (args?.resolverType) {
4946
return CodifyResolverRunner.runResolver(location, args.resolverType);

src/codify-files/resolver/runners.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
1-
import fs from 'node:fs/promises';
1+
import * as fs from 'node:fs/promises';
22
import path from 'node:path';
33
import { validate } from 'uuid';
44

55
import { DashboardApiClient } from '../../api/dashboard/index.js';
66
import { config } from '../../config.js';
77
import { InMemoryFile } from './entities.js';
8-
import { ResolverType } from './index.js';
98
import { FileReader } from './readers/file-reader.js';
109
import { RemoteDocumentIdReader } from './readers/remote-document-id-reader.js';
1110
import { RemoteDocumentReader } from './readers/remote-document-reader.js';
1211
import { RemoteTemplateReader } from './readers/remote-template-reader.js';
1312

13+
export enum ResolverType {
14+
LOCAL = 'LOCAL',
15+
REMOTE_DOCUMENT_ID = 'REMOTE_DOCUMENT_ID',
16+
REMOTE_DOCUMENT = 'REMOTE_DOCUMENT',
17+
TEMPLATE = 'TEMPLATE',
18+
REMOTE_DEFAULT_DOCUMENT = 'REMOTE_DEFAULT_DOCUMENT',
19+
}
20+
1421
export interface ResolverResult {
1522
files: InMemoryFile[];
1623
type: ResolverType;
@@ -32,7 +39,7 @@ export class CodifyResolverRunner {
3239
for (const type of resolvers) {
3340
if (!type) continue;
3441

35-
const resolver = this.mapping[type];
42+
const resolver = CodifyResolverRunner.mapping[type];
3643
if (!resolver) continue;
3744

3845
const result = await resolver(location);
@@ -49,7 +56,7 @@ export class CodifyResolverRunner {
4956
}
5057

5158
static async runResolver(location: string, type: ResolverType): Promise<ResolverResult> {
52-
const resolver = this.mapping[type];
59+
const resolver = CodifyResolverRunner.mapping[type];
5360
if (!resolver) {
5461
throw new Error(`Invalid resolver type ${type}`);
5562
}
@@ -58,7 +65,7 @@ export class CodifyResolverRunner {
5865
}
5966

6067
static async resolveLocal(location: string): Promise<ResolverResult> {
61-
const filePaths = await this.getFilePaths(location);
68+
const filePaths = await CodifyResolverRunner.getFilePaths(location);
6269
if (!filePaths) {
6370
return { files: [], type: ResolverType.LOCAL, location };
6471
}

src/codify-files/writer/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export enum SaveType {
2+
EXISTING,
3+
NEW,
4+
NONE
5+
}
6+
7+
8+
export class CodifyWriter {
9+
static async save(location: string, contents: string, type: SaveType): Promise<void> {
10+
}
11+
}

src/orchestrators/destroy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export class DestroyOrchestrator {
2222
ctx.processStarted(ProcessName.DESTROY)
2323

2424
const initializationResult = await PluginInitOrchestrator.run(
25-
{ ...args, allowEmptyProject: true, },
25+
{ ...args, allowEmptyProject: true, allowTemplates: true },
2626
reporter
2727
);
2828
const { pluginManager, project } = initializationResult;

0 commit comments

Comments
 (0)