Skip to content

Commit 9456f88

Browse files
committed
Added example and default config validation
1 parent e3fc9c8 commit 9456f88

5 files changed

Lines changed: 138 additions & 20 deletions

File tree

package-lock.json

Lines changed: 11 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@codifycli/plugin-test",
3-
"version": "1.1.0-beta2",
3+
"version": "1.1.0-beta4",
44
"description": "Testing framework for Codify plugins with complete lifecycle testing, IPC communication, and cross-platform support",
55
"main": "dist/index.js",
66
"typings": "dist/index.d.ts",
@@ -27,7 +27,7 @@
2727
"prepublishOnly": "tsc"
2828
},
2929
"dependencies": {
30-
"@codifycli/schemas": "1.0.0",
30+
"@codifycli/schemas": "1.1.0-beta3",
3131
"ajv": "^8.18.0",
3232
"ajv-formats": "^3.0.1",
3333
"lodash.matches": "^4.6.0",
@@ -39,7 +39,7 @@
3939
"strip-ansi": "^7.1.2"
4040
},
4141
"devDependencies": {
42-
"@codifycli/plugin-core": "1.0.0",
42+
"@codifycli/plugin-core": "1.1.0-beta10",
4343
"@oclif/prettier-config": "^0.2.1",
4444
"@types/debug": "^4.1.12",
4545
"@types/node": "^22",

src/plugin-process.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import {
22
ApplyRequestData,
33
CommandRequestData,
44
CommandRequestDataSchema,
5+
GetResourceInfoRequestData,
6+
GetResourceInfoResponseData,
57
ImportRequestData,
68
ImportResponseData,
79
InitializeResponseData,
@@ -101,6 +103,14 @@ export class PluginProcess {
101103
});
102104
}
103105

106+
async getResourceInfo(data: GetResourceInfoRequestData): Promise<GetResourceInfoResponseData> {
107+
return TestUtils.sendMessageAndAwaitResponse(this.childProcess, {
108+
cmd: 'getResourceInfo',
109+
data,
110+
requestId: nanoid(6),
111+
});
112+
}
113+
104114
kill() {
105115
this.childProcess.kill();
106116
}

src/plugin-tester.ts

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,11 @@ export class PluginTester {
4646
}
4747

4848
const plugin = new PluginProcess(pluginPath);
49+
4950
try {
5051
await this.initializeAndValidate(plugin, configs);
52+
53+
await this.validateExampleAndDefaultConfigs(plugin, configs);
5154

5255
console.info(chalk.cyan('Testing plan...'))
5356
const plans = await this.planConfigs(plugin, configs);
@@ -215,7 +218,79 @@ export class PluginTester {
215218

216219
const invalidConfigs = validate.resourceValidations.filter((v) => !v.isValid)
217220
if (invalidConfigs.length > 0) {
218-
throw new Error(`The following configs did not validate:\n ${JSON.stringify(invalidConfigs, null, 2)}`)
221+
const invalidWithConfigs = invalidConfigs.map((v) => ({
222+
config: configs.find((c) => c.type === v.resourceType && (!v.resourceName || c.name === v.resourceName)),
223+
validation: v,
224+
}));
225+
throw new Error(`The following configs did not validate:\n ${JSON.stringify(invalidWithConfigs, null, 2)}`)
226+
}
227+
}
228+
229+
private static async validateExampleAndDefaultConfigs(
230+
plugin: PluginProcess,
231+
configs: ResourceConfig[],
232+
): Promise<void> {
233+
console.info(chalk.cyan('Validating examples and default config...'))
234+
const uniqueTypes = [...new Set(configs.map((c) => c.type))];
235+
236+
const errors: string[] = [];
237+
238+
for (const type of uniqueTypes) {
239+
const { defaultConfig, exampleConfigs } = await plugin.getResourceInfo({ type });
240+
241+
// Validate exampleConfigs — these should be fully valid configs
242+
const exampleEntries: Array<[string, Array<Record<string, unknown>>]> = [];
243+
if (exampleConfigs?.example1?.configs) exampleEntries.push(['exampleConfigs.example1', exampleConfigs.example1.configs as Array<Record<string, unknown>>]);
244+
if (exampleConfigs?.example2?.configs) exampleEntries.push(['exampleConfigs.example2', exampleConfigs.example2.configs as Array<Record<string, unknown>>]);
245+
246+
for (const [label, exampleConfigList] of exampleEntries) {
247+
for (const [idx, exampleConfig] of exampleConfigList.entries()) {
248+
const configLabel = `resource '${type}' ${label}[${idx}]`;
249+
await this.validateSingleExampleConfig(plugin, configLabel, exampleConfig, errors);
250+
}
251+
}
252+
253+
// Validate defaultConfig — partial config, so only warn on failure
254+
if (defaultConfig) {
255+
try {
256+
const validate = await plugin.validate({
257+
configs: [{ core: { type }, parameters: defaultConfig }]
258+
});
259+
const invalid = validate.resourceValidations.filter((v) => !v.isValid);
260+
if (invalid.length > 0) {
261+
console.warn(chalk.yellow(`Resource '${type}' defaultConfig failed validation (this may be expected for partial configs):\n${JSON.stringify(invalid, null, 2)}`));
262+
}
263+
} catch (error) {
264+
console.warn(chalk.yellow(`Resource '${type}' defaultConfig could not be validated: ${error}`));
265+
}
266+
}
267+
}
268+
269+
if (errors.length > 0) {
270+
const message = errors.length === 1
271+
? errors[0]
272+
: `Multiple example config validation failures:\n\n${errors.map((e, i) => `[${i + 1}] ${e}`).join('\n\n')}`;
273+
throw new Error(message);
274+
}
275+
}
276+
277+
private static async validateSingleExampleConfig(
278+
plugin: PluginProcess,
279+
label: string,
280+
exampleConfig: Record<string, unknown>,
281+
errors: string[],
282+
): Promise<void> {
283+
try {
284+
const { coreParameters, parameters } = splitUserConfig(exampleConfig as ResourceConfig);
285+
const validate = await plugin.validate({
286+
configs: [{ core: coreParameters, parameters }]
287+
});
288+
const invalid = validate.resourceValidations.filter((v) => !v.isValid);
289+
if (invalid.length > 0) {
290+
errors.push(`${label} failed validation:\nConfig: ${JSON.stringify(exampleConfig, null, 2)}\nErrors: ${JSON.stringify(invalid, null, 2)}`);
291+
}
292+
} catch (error) {
293+
errors.push(`${label} could not be validated:\nConfig: ${JSON.stringify(exampleConfig, null, 2)}\nError: ${error}`);
219294
}
220295
}
221296

test/test-plugin.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {
22
CreatePlan,
3-
DestroyPlan,
3+
DestroyPlan, ExampleConfig,
44
ModifyPlan,
55
ParameterChange,
66
Plugin,
@@ -22,13 +22,37 @@ export interface TestConfig2 extends StringIndexedObject {
2222
propB: string[];
2323
}
2424

25+
const defaultConfig: Partial<TestConfig> = {
26+
propA: ''
27+
}
28+
29+
const example1: ExampleConfig = {
30+
configs: [
31+
{ type: 'test', propA: 'abc', propB: 6, propC: 'def' }
32+
],
33+
title: 'Example 1'
34+
}
35+
36+
const example2: ExampleConfig = {
37+
configs: [
38+
{ type: 'test', propA: 'abc', propB: 6, propC: 'def', propD: 'asda' },
39+
{ type: 'test2', propA: 'ghi', propB: ['a', 'b', 'c'] }
40+
],
41+
title: 'Example 2',
42+
description: 'ABCDEF'
43+
}
2544

2645
export class TestResource extends Resource<TestConfig> {
2746
getSettings(): ResourceSettings<TestConfig> {
2847
return {
2948
id: 'test',
3049
operatingSystems: [OS.Linux, OS.Darwin],
3150
allowMultiple: true,
51+
defaultConfig,
52+
exampleConfigs: {
53+
example1,
54+
example2
55+
}
3256
};
3357
}
3458

@@ -58,6 +82,10 @@ export class TestResource2 extends Resource<TestConfig2> {
5882
operatingSystems: [OS.Linux, OS.Darwin],
5983
parameterSettings: {
6084
propB: { type: 'array' }
85+
},
86+
exampleConfigs: {
87+
example1,
88+
example2
6189
}
6290
};
6391
}
@@ -82,6 +110,7 @@ export class TestResource2 extends Resource<TestConfig2> {
82110

83111
export class TestUninstallResource extends Resource<TestConfig> {
84112
first = true;
113+
85114
getSettings(): ResourceSettings<TestConfig> {
86115
return {
87116
id: 'test-uninstall',
@@ -134,9 +163,11 @@ export class TestModifyResource extends Resource<TestConfig> {
134163
return super.modify(pc, plan);
135164
}
136165

137-
async create(plan: CreatePlan<TestConfig>): Promise<void> {}
166+
async create(plan: CreatePlan<TestConfig>): Promise<void> {
167+
}
138168

139-
async destroy(plan: DestroyPlan<TestConfig>): Promise<void> {}
169+
async destroy(plan: DestroyPlan<TestConfig>): Promise<void> {
170+
}
140171
}
141172

142173
export class TestDestroyResource extends Resource<TestConfig> {
@@ -188,9 +219,11 @@ export class WindowsOnlyResource extends Resource<TestConfig> {
188219
return super.modify(pc, plan);
189220
}
190221

191-
async create(plan: CreatePlan<TestConfig>): Promise<void> {}
222+
async create(plan: CreatePlan<TestConfig>): Promise<void> {
223+
}
192224

193-
async destroy(plan: DestroyPlan<TestConfig>): Promise<void> {}
225+
async destroy(plan: DestroyPlan<TestConfig>): Promise<void> {
226+
}
194227
}
195228

196229
export class TestDestroyResource2 extends TestDestroyResource {

0 commit comments

Comments
 (0)