Skip to content

Commit 32291c4

Browse files
feat: update ESLint rules for CLI files and test fixtures
- Added ESLint overrides for CLI command files to allow console output and disable quotes/semicolon rules - Updated timestamps in test fixture data to maintain consistent time offsets across sample documents - Preserved existing document content and structure while adjusting temporal data The changes focus on improving development experience by relaxing style rules for CLI output while maintaining test data integrity with updated timestamps.
1 parent 35ca770 commit 32291c4

File tree

15 files changed

+3287
-2442
lines changed

15 files changed

+3287
-2442
lines changed

.eslintrc.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,5 +213,19 @@ module.exports = {
213213
"no-console": "off", // Allow console in DX tools
214214
},
215215
},
216+
217+
// === CLI COMMAND FILES - Override production rules for user-facing output ===
218+
{
219+
files: [
220+
"src/cli/commands/**/*.{js,jsx}",
221+
"src/cli/interactive-wizard.{js,jsx}",
222+
"src/cli/doctor-command.{js,jsx}",
223+
],
224+
rules: {
225+
"no-console": "off", // CLI commands are supposed to print to console
226+
quotes: "off",
227+
semi: "off",
228+
},
229+
},
216230
],
217231
};
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
/**
2+
* CI Contract Compliance Test
3+
*
4+
* Validates that all mock plugins comply with their respective contracts.
5+
* This ensures contract integrity and catches drift between contracts and implementations.
6+
*
7+
* Fails fast on any contract violation to prevent broken plugins from being released.
8+
*/
9+
10+
const fs = require("fs");
11+
const path = require("path");
12+
13+
// Mock plugin imports
14+
const MockLoader = require("../fixtures/mock-plugins/mock-loader");
15+
const MockEmbedder = require("../fixtures/mock-plugins/mock-embedder");
16+
const MockRetriever = require("../fixtures/mock-plugins/mock-retriever");
17+
const MockLLM = require("../fixtures/mock-plugins/mock-llm");
18+
const MockReranker = require("../fixtures/mock-plugins/mock-reranker");
19+
20+
// Contract paths
21+
const CONTRACTS_DIR = path.join(__dirname, "..", "..", "contracts");
22+
23+
/**
24+
* Load contract JSON for a plugin type
25+
* @param {string} type - Plugin type (loader, embedder, retriever, llm, reranker)
26+
* @returns {object} Contract JSON object
27+
*/
28+
function loadContract(type) {
29+
const contractPath = path.join(CONTRACTS_DIR, `${type}-contract.json`);
30+
31+
if (!fs.existsSync(contractPath)) {
32+
throw new Error(
33+
`Contract file not found for type '${type}': ${contractPath}`,
34+
);
35+
}
36+
37+
const contractData = fs.readFileSync(contractPath, "utf8");
38+
return JSON.parse(contractData);
39+
}
40+
41+
/**
42+
* Validate that a plugin instance implements all required methods from its contract
43+
* @param {object} plugin - Plugin instance to validate
44+
* @param {object} contract - Contract JSON object
45+
* @returns {object} Validation result with { valid: boolean, errors: string[] }
46+
*/
47+
function validatePluginContract(plugin, contract) {
48+
const errors = [];
49+
50+
// Validate plugin has metadata
51+
if (!plugin.metadata) {
52+
errors.push("Plugin must have metadata property");
53+
} else {
54+
// Validate metadata fields
55+
if (!plugin.metadata.name) {
56+
errors.push("Plugin metadata must include name");
57+
}
58+
if (!plugin.metadata.version) {
59+
errors.push("Plugin metadata must include version");
60+
}
61+
if (!plugin.metadata.type) {
62+
errors.push("Plugin metadata must include type");
63+
}
64+
if (plugin.metadata.type !== contract.type) {
65+
errors.push(
66+
`Plugin type '${plugin.metadata.type}' does not match contract type '${contract.type}'`,
67+
);
68+
}
69+
}
70+
71+
// Validate required methods exist
72+
if (contract.required && Array.isArray(contract.required)) {
73+
contract.required.forEach((methodName) => {
74+
if (typeof plugin[methodName] !== "function") {
75+
errors.push(`Plugin must implement required method '${methodName}'`);
76+
}
77+
});
78+
}
79+
80+
// Validate method signatures from contract.methods
81+
if (contract.methods && Array.isArray(contract.methods)) {
82+
contract.methods.forEach((methodSpec) => {
83+
const methodName = methodSpec.name;
84+
85+
if (typeof plugin[methodName] !== "function") {
86+
errors.push(
87+
`Plugin must implement method '${methodName}' as defined in contract`,
88+
);
89+
} else {
90+
// Validate function accepts the expected number of parameters
91+
const expectedParamCount = methodSpec.parameters
92+
? methodSpec.parameters.length
93+
: 0;
94+
const actualParamCount = plugin[methodName].length;
95+
96+
// Allow functions to accept more parameters (optional params) but not fewer
97+
if (actualParamCount < expectedParamCount) {
98+
errors.push(
99+
`Method '${methodName}' accepts ${actualParamCount} parameters but contract requires at least ${expectedParamCount}`,
100+
);
101+
}
102+
}
103+
});
104+
}
105+
106+
return {
107+
valid: errors.length === 0,
108+
errors,
109+
};
110+
}
111+
112+
/**
113+
* Test helper to validate a plugin instance against its contract
114+
* @param {string} type - Plugin type
115+
* @param {object} PluginClass - Plugin class constructor
116+
*/
117+
function testPluginCompliance(type, PluginClass) {
118+
describe(`${type} plugin contract compliance`, () => {
119+
let plugin;
120+
let contract;
121+
122+
beforeAll(() => {
123+
// Load contract
124+
try {
125+
contract = loadContract(type);
126+
} catch (error) {
127+
throw new Error(
128+
`Failed to load contract for ${type}: ${error.message}`,
129+
);
130+
}
131+
132+
// Instantiate plugin
133+
plugin = new PluginClass();
134+
});
135+
136+
it("should load contract successfully", () => {
137+
expect(contract).toBeDefined();
138+
expect(contract.type).toBe(type);
139+
});
140+
141+
it("should have valid metadata", () => {
142+
expect(plugin.metadata).toBeDefined();
143+
expect(plugin.metadata.name).toBeTruthy();
144+
expect(plugin.metadata.version).toBeTruthy();
145+
expect(plugin.metadata.type).toBe(type);
146+
});
147+
148+
it("should implement all required methods", () => {
149+
const validation = validatePluginContract(plugin, contract);
150+
151+
if (!validation.valid) {
152+
// Fail fast: log all errors and fail the test
153+
console.error(`\n❌ Contract compliance failed for ${type} plugin:`);
154+
validation.errors.forEach((error) => {
155+
console.error(` - ${error}`);
156+
});
157+
}
158+
159+
expect(validation.valid).toBe(true);
160+
expect(validation.errors).toEqual([]);
161+
});
162+
163+
it("should have correctly typed methods", () => {
164+
if (contract.methods) {
165+
contract.methods.forEach((methodSpec) => {
166+
const method = plugin[methodSpec.name];
167+
expect(typeof method).toBe("function");
168+
});
169+
}
170+
});
171+
172+
it("should match contract version requirements", () => {
173+
expect(contract.version).toBeDefined();
174+
expect(typeof contract.version).toBe("string");
175+
176+
// Validate semver format
177+
const semverPattern = /^\d+\.\d+\.\d+$/;
178+
expect(contract.version).toMatch(semverPattern);
179+
});
180+
});
181+
}
182+
183+
// Run compliance tests for all plugin types
184+
describe("CI Contract Compliance Suite", () => {
185+
describe("Mock Plugin Contract Validation", () => {
186+
// Test each plugin type
187+
testPluginCompliance("loader", MockLoader);
188+
testPluginCompliance("embedder", MockEmbedder);
189+
testPluginCompliance("retriever", MockRetriever);
190+
testPluginCompliance("llm", MockLLM);
191+
testPluginCompliance("reranker", MockReranker);
192+
});
193+
194+
describe("Contract File Integrity", () => {
195+
const requiredContracts = [
196+
"loader",
197+
"embedder",
198+
"retriever",
199+
"llm",
200+
"reranker",
201+
];
202+
203+
it("should have all required contract files present", () => {
204+
const missingContracts = [];
205+
206+
requiredContracts.forEach((type) => {
207+
const contractPath = path.join(CONTRACTS_DIR, `${type}-contract.json`);
208+
if (!fs.existsSync(contractPath)) {
209+
missingContracts.push(type);
210+
}
211+
});
212+
213+
if (missingContracts.length > 0) {
214+
console.error(
215+
`\n❌ Missing contract files for: ${missingContracts.join(", ")}`,
216+
);
217+
}
218+
219+
expect(missingContracts).toEqual([]);
220+
});
221+
222+
it("should have valid JSON in all contract files", () => {
223+
const invalidContracts = [];
224+
225+
requiredContracts.forEach((type) => {
226+
try {
227+
loadContract(type);
228+
} catch (error) {
229+
invalidContracts.push({ type, error: error.message });
230+
}
231+
});
232+
233+
if (invalidContracts.length > 0) {
234+
console.error("\n❌ Invalid contract files:");
235+
invalidContracts.forEach(({ type, error }) => {
236+
console.error(` - ${type}: ${error}`);
237+
});
238+
}
239+
240+
expect(invalidContracts).toEqual([]);
241+
});
242+
});
243+
244+
describe("Fail-Fast Validation", () => {
245+
it("should detect missing methods immediately", () => {
246+
// Create a mock plugin with missing methods
247+
class BrokenPlugin {
248+
constructor() {
249+
this.metadata = {
250+
name: "broken-plugin",
251+
version: "1.0.0",
252+
type: "loader",
253+
};
254+
}
255+
// Missing load() method
256+
}
257+
258+
const brokenPlugin = new BrokenPlugin();
259+
const contract = loadContract("loader");
260+
const validation = validatePluginContract(brokenPlugin, contract);
261+
262+
expect(validation.valid).toBe(false);
263+
expect(validation.errors.length).toBeGreaterThan(0);
264+
expect(validation.errors.some((e) => e.includes("load"))).toBe(true);
265+
});
266+
267+
it("should detect type mismatches immediately", () => {
268+
// Create a mock plugin with wrong type
269+
class WrongTypePlugin {
270+
constructor() {
271+
this.metadata = {
272+
name: "wrong-type",
273+
version: "1.0.0",
274+
type: "embedder", // Wrong type
275+
};
276+
}
277+
278+
async load() {
279+
return [];
280+
}
281+
}
282+
283+
const wrongPlugin = new WrongTypePlugin();
284+
const contract = loadContract("loader");
285+
const validation = validatePluginContract(wrongPlugin, contract);
286+
287+
expect(validation.valid).toBe(false);
288+
expect(validation.errors.some((e) => e.includes("type"))).toBe(true);
289+
});
290+
291+
it("should detect missing metadata immediately", () => {
292+
// Create a mock plugin without metadata
293+
class NoMetadataPlugin {
294+
async load() {
295+
return [];
296+
}
297+
}
298+
299+
const noMetadataPlugin = new NoMetadataPlugin();
300+
const contract = loadContract("loader");
301+
const validation = validatePluginContract(noMetadataPlugin, contract);
302+
303+
expect(validation.valid).toBe(false);
304+
expect(validation.errors.some((e) => e.includes("metadata"))).toBe(true);
305+
});
306+
});
307+
});

0 commit comments

Comments
 (0)