@devilsdev/rag-pipeline-utils 1.0.1 โ 2.0.0
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/.codecov.yml +21 -0
- package/.github/workflows/ci.yml +18 -0
- package/.husky/pre-commit +5 -0
- package/CHANGELOG.md +33 -0
- package/__tests__/fixtures/.ragrc.json +8 -0
- package/__tests__/fixtures/invalid-ragrc.json +4 -0
- package/__tests__/fixtures/src/mocks/openai-embedder.js +16 -0
- package/__tests__/fixtures/src/mocks/openai-llm.js +9 -0
- package/__tests__/fixtures/src/mocks/pdf-loader.js +17 -0
- package/__tests__/fixtures/src/mocks/pinecone-retriever.js +26 -0
- package/__tests__/fixtures/valid-ragrc.json +16 -0
- package/__tests__/integration/cli/config-flow.test.js +84 -15
- package/__tests__/integration/config/load-config.test.js +45 -30
- package/__tests__/unit/config/validate-schema.test.js +121 -0
- package/__tests__/unit/reranker/llm-reranker.test.js +9 -33
- package/__tests__/unit/reranker/reranker.enriched.test.js +19 -0
- package/__tests__/unit/reranker/reranker.fallback.test.js +15 -0
- package/__tests__/unit/reranker/reranker.snapshot.test.js +9 -11
- package/__tests__/unit/reranker/reranker.structured-output.test.js +17 -0
- package/bin/cli.js +49 -155
- package/package.json +17 -2
- package/plugin.config.json +14 -0
- package/scripts/banner-injector.js +89 -0
- package/scripts/ci-runner.js +51 -0
- package/scripts/repair-fixtures.js +30 -0
- package/scripts/setup.js +26 -2
- package/scripts/verify-fixture-mocks.js +45 -0
- package/scripts/verify-fixtures.js +36 -0
- package/src/config/load-config.js +19 -21
- package/src/config/load-plugin-config.js +56 -0
- package/src/config/validate-plugin-schema.js +28 -0
- package/src/config/validate-schema.js +88 -30
- package/src/core/create-pipeline.js +40 -26
- package/src/core/plugin-contracts.js +21 -0
- package/src/core/plugin-registry.js +4 -17
- package/src/mocks/openai-embedder.js +30 -0
- package/src/mocks/openai-llm.js +18 -0
- package/src/mocks/pdf-loader.js +17 -0
- package/src/mocks/pinecone-retriever.js +25 -0
- package/src/reranker/llm-reranker.js +28 -43
- package/src/utils/ci/diagnostic-reporter.js +36 -0
- package/src/utils/validate-plugin-contract.js +36 -0
- package/.github/workflows/release.yml +0 -20
package/.codecov.yml
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# .codecov.yml
|
|
2
|
+
|
|
3
|
+
codecov:
|
|
4
|
+
notify:
|
|
5
|
+
after_n_builds: 1
|
|
6
|
+
|
|
7
|
+
coverage:
|
|
8
|
+
status:
|
|
9
|
+
project:
|
|
10
|
+
default:
|
|
11
|
+
target: 90%
|
|
12
|
+
threshold: 5%
|
|
13
|
+
patch:
|
|
14
|
+
default:
|
|
15
|
+
target: 90%
|
|
16
|
+
threshold: 5%
|
|
17
|
+
|
|
18
|
+
comment:
|
|
19
|
+
layout: "reach, diff, flags, files"
|
|
20
|
+
behavior: default
|
|
21
|
+
require_changes: false
|
package/.github/workflows/ci.yml
CHANGED
|
@@ -44,3 +44,21 @@ jobs:
|
|
|
44
44
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
45
45
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # ๐ critical for npm publi
|
|
46
46
|
run: npx semantic-release
|
|
47
|
+
|
|
48
|
+
- name: ๐งช Run Tests with Coverage
|
|
49
|
+
run: npm test
|
|
50
|
+
|
|
51
|
+
- name: Validate Fixtures
|
|
52
|
+
run: npm run validate-fixtures
|
|
53
|
+
|
|
54
|
+
- name: Lint code
|
|
55
|
+
run: npm run lint
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
- name: ๐ Upload coverage to Codecov
|
|
59
|
+
uses: codecov/codecov-action@v5
|
|
60
|
+
with:
|
|
61
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
62
|
+
slug: DevilsDev/rag-pipeline-utils
|
|
63
|
+
|
|
64
|
+
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
# [2.0.0](https://github.com/DevilsDev/rag-pipeline-utils/compare/v1.0.1...v2.0.0) (2025-04-17)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **ci:** correct .ragrc.json plugin paths and CLI test cwd to resolve plugin loading in CI ([911c23b](https://github.com/DevilsDev/rag-pipeline-utils/commit/911c23b1d3ddb9e2d938b37f4422a5d81995e97f))
|
|
9
|
+
* **ci:** resolve plugin path error in .ragrc.json for GitHub Actions compatibility ([affc739](https://github.com/DevilsDev/rag-pipeline-utils/commit/affc739d5c4ff2070b1a1ca0ccf3f9137d6cee5a))
|
|
10
|
+
* **ci:** resolve plugin path failures in .ragrc.json during CLI integration tests ([04b977b](https://github.com/DevilsDev/rag-pipeline-utils/commit/04b977b3b745fa3d3ab47c84b2aeee3285896068))
|
|
11
|
+
* **cli:** correct path to load-plugin-config.js after moving to /src/config ([f743498](https://github.com/DevilsDev/rag-pipeline-utils/commit/f743498ec32e03151eda9ba7fe5886bda8da792f))
|
|
12
|
+
* **config:** use default export when importing plugin registry ([ef8dce1](https://github.com/DevilsDev/rag-pipeline-utils/commit/ef8dce12bfab62bc9faec764198b98c0c356472e))
|
|
13
|
+
* **test:** ensure .ragrc.json copied to project root for CLI fallback in CI ([31bd2b8](https://github.com/DevilsDev/rag-pipeline-utils/commit/31bd2b844279fb0404056d9929e5f28f9127eb12))
|
|
14
|
+
* **tests:** define __dirname manually for ESM compatibility in CLI integration test ([18b8dad](https://github.com/DevilsDev/rag-pipeline-utils/commit/18b8dadabb852243dda2b78de048c71106a759fc))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* **ci:** integrate full fixture validation, schema contracts, and CLI test flow ([2b57fe9](https://github.com/DevilsDev/rag-pipeline-utils/commit/2b57fe90e64a806815c1f66f5e2a745780270ce6))
|
|
20
|
+
* **cli:** dynamic plugin registration with config fallback and middleware injection ([cc24b7e](https://github.com/DevilsDev/rag-pipeline-utils/commit/cc24b7e74d0a2630eb1c6d2def69b370329dcc2b))
|
|
21
|
+
* **plugins:** enable dynamic plugin registration from JSON config ([d81d6d7](https://github.com/DevilsDev/rag-pipeline-utils/commit/d81d6d7d5f2d21b5a46b379b530c162ba9c30398))
|
|
22
|
+
* **testing:** integrate full fixture and validation setup for CLI + reranker ([ab96037](https://github.com/DevilsDev/rag-pipeline-utils/commit/ab9603765594e7718e2688b80797e1f5e8afe86f))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### BREAKING CHANGES
|
|
26
|
+
|
|
27
|
+
* **cli:** now requires named options for middleware injection; existing usage must be updated.
|
|
28
|
+
|
|
29
|
+
## [1.0.2] - 2025-04-15
|
|
30
|
+
### Fixed
|
|
31
|
+
- Corrected plugin path resolution in `.ragrc.json` fixture for CI compatibility.
|
|
32
|
+
- Resolved `ERR_MODULE_NOT_FOUND` during CLI integration tests in GitHub Actions.
|
|
33
|
+
- Ensured consistent module resolution across environments (Windows, Linux, CI).
|
|
34
|
+
|
|
35
|
+
|
|
3
36
|
## [1.0.1](https://github.com/DevilsDev/rag-pipeline-utils/compare/v1.0.0...v1.0.1) (2025-04-14)
|
|
4
37
|
|
|
5
38
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"loader": { "pdf": "./src/mocks/pdf-loader.js" },
|
|
3
|
+
"embedder": { "openai": "./src/mocks/openai-embedder.js" },
|
|
4
|
+
"retriever": { "pinecone": "./src/mocks/pinecone-retriever.js" },
|
|
5
|
+
"llm": { "openai": "./src/mocks/openai-llm.js" },
|
|
6
|
+
"namespace": "cli-config-test",
|
|
7
|
+
"pipeline": ["loader", "embedder", "retriever"]
|
|
8
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock OpenAI Embedder Plugin
|
|
3
|
+
* Implements: embedder.embed(docs), embedder.embedQuery(query)
|
|
4
|
+
*/
|
|
5
|
+
export default class OpenAIEmbedder {
|
|
6
|
+
embed(documents) {
|
|
7
|
+
return documents.map(doc => ({
|
|
8
|
+
id: doc.id,
|
|
9
|
+
values: [0.1, 0.2, 0.3],
|
|
10
|
+
}));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
embedQuery(query) {
|
|
14
|
+
return [0.1, 0.2, 0.3];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock PDF Loader Plugin
|
|
3
|
+
* Implements: loader.load(filePath)
|
|
4
|
+
*/
|
|
5
|
+
export default class PdfLoader {
|
|
6
|
+
/**
|
|
7
|
+
* Loads a file and returns mock document chunks.
|
|
8
|
+
* @param {string} filePath
|
|
9
|
+
* @returns {Array<{ id: string, content: string }>}
|
|
10
|
+
*/
|
|
11
|
+
load(filePath) {
|
|
12
|
+
return [
|
|
13
|
+
{ id: 'doc1', content: `Loaded content from ${filePath}` },
|
|
14
|
+
{ id: 'doc2', content: 'Second document content' },
|
|
15
|
+
];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock Pinecone Retriever Plugin
|
|
3
|
+
* Implements: retriever.store(vectors), retriever.search(queryVector)
|
|
4
|
+
*/
|
|
5
|
+
export default class PineconeRetriever {
|
|
6
|
+
constructor() {
|
|
7
|
+
this._store = [];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Stores embedded vectors.
|
|
12
|
+
* @param {Array<{ id: string, values: number[] }>} vectors
|
|
13
|
+
*/
|
|
14
|
+
store(vectors) {
|
|
15
|
+
this._store = vectors;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Searches mock memory and returns matching docs.
|
|
20
|
+
* @param {number[]} queryVector
|
|
21
|
+
* @returns {Array<{ id: string, score: number }>}
|
|
22
|
+
*/
|
|
23
|
+
search(queryVector) {
|
|
24
|
+
return this._store.map(v => ({ id: v.id, score: 0.99 }));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"loader": {
|
|
3
|
+
"pdf": "./src/mocks/pdf-loader.js"
|
|
4
|
+
},
|
|
5
|
+
"embedder": {
|
|
6
|
+
"openai": "./src/mocks/openai-embedder.js"
|
|
7
|
+
},
|
|
8
|
+
"retriever": {
|
|
9
|
+
"pinecone": "./src/mocks/pinecone-retriever.js"
|
|
10
|
+
},
|
|
11
|
+
"llm": {
|
|
12
|
+
"openai": "./src/mocks/openai-llm.js"
|
|
13
|
+
},
|
|
14
|
+
"namespace": "cli-config-test",
|
|
15
|
+
"pipeline": ["loader", "embedder", "retriever"]
|
|
16
|
+
}
|
|
@@ -1,29 +1,98 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Version:
|
|
3
|
-
* Description:
|
|
2
|
+
* Version: 1.1.0
|
|
3
|
+
* Description: Final CLI config fallback integration test with fixture mock copying and validation
|
|
4
4
|
* Author: Ali Kahwaji
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { execSync } from 'child_process';
|
|
8
|
-
import
|
|
8
|
+
import {
|
|
9
|
+
mkdirSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
copyFileSync,
|
|
13
|
+
existsSync,
|
|
14
|
+
readdirSync,
|
|
15
|
+
readFileSync
|
|
16
|
+
} from 'fs';
|
|
17
|
+
import { join, resolve } from 'path';
|
|
18
|
+
import { fileURLToPath } from 'url';
|
|
9
19
|
|
|
10
|
-
const
|
|
20
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
21
|
+
const __dirname = resolve(__filename, '..');
|
|
22
|
+
|
|
23
|
+
const CLI_PATH = resolve('bin/cli.js');
|
|
24
|
+
const TEMP_TEST_DIR = resolve('__tests__/__temp__/cli-config-fallback');
|
|
25
|
+
const FIXTURE_CONFIG = join(TEMP_TEST_DIR, '.ragrc.json');
|
|
26
|
+
const FIXTURE_PDF = resolve('__tests__/fixtures/sample.pdf');
|
|
27
|
+
|
|
28
|
+
// Fixture sources
|
|
29
|
+
const FIXTURE_SRC = resolve('__tests__/fixtures/src/mocks');
|
|
30
|
+
const PDF_LOADER_SRC = join(FIXTURE_SRC, 'pdf-loader.js');
|
|
31
|
+
const EMBEDDER_SRC = join(FIXTURE_SRC, 'openai-embedder.js');
|
|
32
|
+
const RETRIEVER_SRC = join(FIXTURE_SRC, 'pinecone-retriever.js');
|
|
33
|
+
const LLM_SRC = join(FIXTURE_SRC, 'openai-llm.js');
|
|
34
|
+
|
|
35
|
+
const FIXTURE_CONFIG_OBJECT = {
|
|
36
|
+
loader: { pdf: './src/mocks/pdf-loader.js' },
|
|
37
|
+
embedder: { openai: './src/mocks/openai-embedder.js' },
|
|
38
|
+
retriever: { pinecone: './src/mocks/pinecone-retriever.js' },
|
|
39
|
+
llm: { openai: './src/mocks/openai-llm.js' },
|
|
40
|
+
namespace: 'cli-config-test',
|
|
41
|
+
pipeline: ['loader', 'embedder', 'retriever']
|
|
42
|
+
};
|
|
11
43
|
|
|
12
44
|
describe('CLI integration with .ragrc.json config fallback', () => {
|
|
45
|
+
beforeAll(() => {
|
|
46
|
+
rmSync(TEMP_TEST_DIR, { recursive: true, force: true });
|
|
47
|
+
mkdirSync(join(TEMP_TEST_DIR, 'src/mocks'), { recursive: true });
|
|
48
|
+
|
|
49
|
+
// Assert existence
|
|
50
|
+
[FIXTURE_PDF, PDF_LOADER_SRC, EMBEDDER_SRC, RETRIEVER_SRC, LLM_SRC].forEach(file => {
|
|
51
|
+
if (!existsSync(file)) throw new Error(`[FATAL] Missing required fixture: ${file}`);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Copy fixture PDF
|
|
55
|
+
copyFileSync(FIXTURE_PDF, join(TEMP_TEST_DIR, 'sample.pdf'));
|
|
56
|
+
|
|
57
|
+
// Copy mock plugins into temp dir
|
|
58
|
+
copyFileSync(PDF_LOADER_SRC, join(TEMP_TEST_DIR, 'src/mocks/pdf-loader.js'));
|
|
59
|
+
copyFileSync(EMBEDDER_SRC, join(TEMP_TEST_DIR, 'src/mocks/openai-embedder.js'));
|
|
60
|
+
copyFileSync(RETRIEVER_SRC, join(TEMP_TEST_DIR, 'src/mocks/pinecone-retriever.js'));
|
|
61
|
+
copyFileSync(LLM_SRC, join(TEMP_TEST_DIR, 'src/mocks/openai-llm.js'));
|
|
62
|
+
|
|
63
|
+
// Write valid .ragrc.json
|
|
64
|
+
writeFileSync(FIXTURE_CONFIG, JSON.stringify(FIXTURE_CONFIG_OBJECT, null, 2));
|
|
65
|
+
|
|
66
|
+
console.debug('[DEBUG] .ragrc.json contents:\n', JSON.stringify(FIXTURE_CONFIG_OBJECT, null, 2));
|
|
67
|
+
console.debug('[SETUP] Temp dir:', TEMP_TEST_DIR);
|
|
68
|
+
console.debug('[SETUP] Contents:', readdirSync(TEMP_TEST_DIR));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterAll(() => {
|
|
72
|
+
rmSync(TEMP_TEST_DIR, { recursive: true, force: true });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('validates plugin sections contain resolvable names', () => {
|
|
76
|
+
const config = JSON.parse(readFileSync(FIXTURE_CONFIG, 'utf-8'));
|
|
77
|
+
['loader', 'embedder', 'retriever', 'llm'].forEach(type => {
|
|
78
|
+
expect(typeof config[type]).toBe('object');
|
|
79
|
+
expect(Object.keys(config[type]).length).toBeGreaterThan(0);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
13
83
|
test('executes CLI ingest using config fallback', () => {
|
|
14
|
-
const result = execSync(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
);
|
|
18
|
-
expect(result).
|
|
84
|
+
const result = execSync(`node ${CLI_PATH} ingest sample.pdf`, {
|
|
85
|
+
cwd: TEMP_TEST_DIR,
|
|
86
|
+
encoding: 'utf-8'
|
|
87
|
+
});
|
|
88
|
+
expect(result).toMatch(/Ingestion complete/);
|
|
19
89
|
});
|
|
20
90
|
|
|
21
91
|
test('executes CLI query using config fallback', () => {
|
|
22
|
-
const result = execSync(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
expect(result).toContain('Generated answer using: Chunk about pine trees');
|
|
92
|
+
const result = execSync(`node ${CLI_PATH} query "What is this test about?"`, {
|
|
93
|
+
cwd: TEMP_TEST_DIR,
|
|
94
|
+
encoding: 'utf-8'
|
|
95
|
+
});
|
|
96
|
+
expect(result).toMatch(/Answer:/);
|
|
28
97
|
});
|
|
29
98
|
});
|
|
@@ -1,52 +1,67 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Version:
|
|
3
|
-
*
|
|
4
|
-
* Description: Integration tests for loading and validating .ragrc.json config
|
|
2
|
+
* Version: 1.1.1
|
|
3
|
+
* Description: Integration tests for loading and validating .ragrc.json via loadRagConfig()
|
|
5
4
|
* Author: Ali Kahwaji
|
|
5
|
+
* File: __tests__/integration/config/load-config.test.js
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import fs from 'fs';
|
|
9
|
-
import
|
|
9
|
+
import { copyFileSync, mkdirSync, rmSync, existsSync } from 'fs';
|
|
10
|
+
import { join, resolve } from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
10
12
|
import { loadRagConfig } from '../../../src/config/load-config.js';
|
|
11
13
|
|
|
12
|
-
const
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = resolve(__filename, '..');
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
llm: 'openai-gpt-4',
|
|
19
|
-
chunk: {
|
|
20
|
-
strategy: 'semantic',
|
|
21
|
-
size: 500
|
|
22
|
-
}
|
|
23
|
-
};
|
|
17
|
+
// Fixture paths
|
|
18
|
+
const FIXTURE_DIR = resolve(__dirname, '../../fixtures');
|
|
19
|
+
const VALID_FIXTURE = join(FIXTURE_DIR, 'valid-ragrc.json');
|
|
20
|
+
const INVALID_FIXTURE = join(FIXTURE_DIR, 'invalid-ragrc.json');
|
|
24
21
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
};
|
|
22
|
+
// Temp directory used during tests
|
|
23
|
+
const TEMP_DIR = resolve('__tests__', '__temp__', 'load-config');
|
|
24
|
+
const TEMP_CONFIG = join(TEMP_DIR, '.ragrc.json');
|
|
29
25
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
// Ensure fixtures are available before copying
|
|
27
|
+
function assertFixturesExist() {
|
|
28
|
+
const required = [VALID_FIXTURE, INVALID_FIXTURE];
|
|
29
|
+
required.forEach(file => {
|
|
30
|
+
if (!existsSync(file)) {
|
|
31
|
+
throw new Error(`โ Required test fixture missing: ${file}`);
|
|
34
32
|
}
|
|
35
33
|
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('loadRagConfig()', () => {
|
|
37
|
+
beforeAll(() => {
|
|
38
|
+
rmSync(TEMP_DIR, { recursive: true, force: true });
|
|
39
|
+
mkdirSync(TEMP_DIR, { recursive: true });
|
|
40
|
+
|
|
41
|
+
assertFixturesExist();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterAll(() => {
|
|
45
|
+
rmSync(TEMP_DIR, { recursive: true, force: true });
|
|
46
|
+
});
|
|
36
47
|
|
|
37
48
|
test('loads a valid .ragrc.json config file successfully', () => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
copyFileSync(VALID_FIXTURE, TEMP_CONFIG);
|
|
50
|
+
|
|
51
|
+
const config = loadRagConfig(TEMP_CONFIG);
|
|
52
|
+
expect(config).toBeDefined();
|
|
53
|
+
expect(config.namespace).toBe('cli-config-test');
|
|
54
|
+
expect(config.loader?.pdf).toBe('./src/mocks/pdf-loader.js');
|
|
55
|
+
expect(config.pipeline).toEqual(expect.arrayContaining(['loader', 'embedder', 'retriever']));
|
|
41
56
|
});
|
|
42
57
|
|
|
43
58
|
test('throws validation error for invalid .ragrc.json config', () => {
|
|
44
|
-
|
|
45
|
-
expect(() => loadRagConfig()).toThrow(/
|
|
59
|
+
copyFileSync(INVALID_FIXTURE, TEMP_CONFIG);
|
|
60
|
+
expect(() => loadRagConfig(TEMP_CONFIG)).toThrow(/Config validation failed/);
|
|
46
61
|
});
|
|
47
62
|
|
|
48
63
|
test('throws error if config file does not exist', () => {
|
|
49
|
-
|
|
64
|
+
const NON_EXISTENT = join(TEMP_DIR, 'missing.json');
|
|
65
|
+
expect(() => loadRagConfig(NON_EXISTENT)).toThrow(/Config file not found/);
|
|
50
66
|
});
|
|
51
67
|
});
|
|
52
|
-
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 1.1.0
|
|
3
|
+
* Description: Schema validation tests for .ragrc.json using AJV and external fixtures
|
|
4
|
+
* Author: Ali Kahwaji
|
|
5
|
+
* File: __tests__/unit/config/validate-schema.test.js
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync } from 'fs';
|
|
9
|
+
import { resolve } from 'path';
|
|
10
|
+
import { validateRagrcSchema, validatePluginSchema } from '../../../src/config/validate-schema.js';
|
|
11
|
+
|
|
12
|
+
const readFixture = (name) => {
|
|
13
|
+
const filePath = resolve('__tests__/fixtures',name.replace('.ragrc', '-ragrc'));
|
|
14
|
+
return JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
describe('validateRagrcSchema', () => {
|
|
18
|
+
test('passes with valid structured plugin schema', () => {
|
|
19
|
+
const config = readFixture('valid-ragrc.json');
|
|
20
|
+
const result = validateRagrcSchema(config);
|
|
21
|
+
expect(result.valid).toBe(true);
|
|
22
|
+
expect(result.errors).toBeUndefined(); // updated: null => undefined
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('fails with missing pipeline', () => {
|
|
26
|
+
const config = {
|
|
27
|
+
loader: { pdf: './src/mocks/pdf-loader.js' },
|
|
28
|
+
namespace: 'no-pipeline'
|
|
29
|
+
};
|
|
30
|
+
const result = validateRagrcSchema(config);
|
|
31
|
+
expect(result.valid).toBe(false);
|
|
32
|
+
expect(result.errors).toEqual(
|
|
33
|
+
expect.arrayContaining([
|
|
34
|
+
expect.objectContaining({
|
|
35
|
+
keyword: 'required',
|
|
36
|
+
params: expect.objectContaining({ missingProperty: 'pipeline' })
|
|
37
|
+
})
|
|
38
|
+
])
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('fails with non-object plugin sections', () => {
|
|
43
|
+
const config = {
|
|
44
|
+
loader: './src/invalid.js', // should be object
|
|
45
|
+
embedder: { openai: './src/mocks/openai-embedder.js' },
|
|
46
|
+
retriever: { pinecone: './src/mocks/pinecone-retriever.js' },
|
|
47
|
+
llm: { openai: './src/mocks/openai-llm.js' },
|
|
48
|
+
pipeline: ['loader'],
|
|
49
|
+
namespace: 'invalid-section'
|
|
50
|
+
};
|
|
51
|
+
const result = validateRagrcSchema(config);
|
|
52
|
+
expect(result.valid).toBe(false);
|
|
53
|
+
expect(result.errors).toEqual(
|
|
54
|
+
expect.arrayContaining([
|
|
55
|
+
expect.objectContaining({ instancePath: '/loader' })
|
|
56
|
+
])
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('fails with non-string namespace', () => {
|
|
61
|
+
const config = {
|
|
62
|
+
loader: { pdf: './src/mocks/pdf-loader.js' },
|
|
63
|
+
embedder: { openai: './src/mocks/openai-embedder.js' },
|
|
64
|
+
retriever: { pinecone: './src/mocks/pinecone-retriever.js' },
|
|
65
|
+
llm: { openai: './src/mocks/openai-llm.js' },
|
|
66
|
+
pipeline: ['loader'],
|
|
67
|
+
namespace: 12345
|
|
68
|
+
};
|
|
69
|
+
const result = validateRagrcSchema(config);
|
|
70
|
+
expect(result.valid).toBe(false);
|
|
71
|
+
expect(result.errors).toEqual(
|
|
72
|
+
expect.arrayContaining([
|
|
73
|
+
expect.objectContaining({ instancePath: '/namespace' })
|
|
74
|
+
])
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('validatePluginSchema (subset)', () => {
|
|
80
|
+
test('passes with plugin-only valid structure', () => {
|
|
81
|
+
const config = {
|
|
82
|
+
loader: { pdf: './src/mocks/pdf-loader.js' },
|
|
83
|
+
embedder: { openai: './src/mocks/openai-embedder.js' },
|
|
84
|
+
retriever: { pinecone: './src/mocks/pinecone-retriever.js' },
|
|
85
|
+
llm: { openai: './src/mocks/openai-llm.js' }
|
|
86
|
+
};
|
|
87
|
+
const result = validatePluginSchema(config);
|
|
88
|
+
expect(result.valid).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('fails when plugin section is missing', () => {
|
|
92
|
+
const config = {
|
|
93
|
+
embedder: { openai: './mock.js' },
|
|
94
|
+
retriever: { pinecone: './mock.js' },
|
|
95
|
+
llm: { openai: './mock.js' }
|
|
96
|
+
};
|
|
97
|
+
const result = validatePluginSchema(config);
|
|
98
|
+
expect(result.valid).toBe(false);
|
|
99
|
+
expect(result.errors).toEqual(
|
|
100
|
+
expect.arrayContaining([
|
|
101
|
+
expect.objectContaining({ instancePath: '', message: expect.stringContaining('must have required property') })
|
|
102
|
+
])
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('fails when plugin paths are not strings', () => {
|
|
107
|
+
const config = {
|
|
108
|
+
loader: { pdf: 12345 },
|
|
109
|
+
embedder: { openai: './mock.js' },
|
|
110
|
+
retriever: { pinecone: './mock.js' },
|
|
111
|
+
llm: { openai: './mock.js' }
|
|
112
|
+
};
|
|
113
|
+
const result = validatePluginSchema(config);
|
|
114
|
+
expect(result.valid).toBe(false);
|
|
115
|
+
expect(result.errors).toEqual(
|
|
116
|
+
expect.arrayContaining([
|
|
117
|
+
expect.objectContaining({ instancePath: '/loader/pdf' })
|
|
118
|
+
])
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -1,42 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Description:
|
|
5
|
-
* Author:
|
|
2
|
+
* File: __tests__/unit/reranker/llm-reranker.test.js
|
|
3
|
+
* Version: 1.0.0
|
|
4
|
+
* Description: Validates core behavior of the LLMReranker module
|
|
5
|
+
* Author: @devilsdev
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { LLMReranker } from '../../../src/reranker/llm-reranker.js';
|
|
9
9
|
|
|
10
10
|
describe('LLMReranker', () => {
|
|
11
|
-
const mockDocs = [
|
|
12
|
-
{ text: 'context about math' },
|
|
13
|
-
{ text: 'context about vectors' },
|
|
14
|
-
{ text: 'context about nothing' }
|
|
15
|
-
];
|
|
16
|
-
|
|
17
11
|
test('returns reranked documents based on mock LLM JSON output', async () => {
|
|
18
|
-
const mockLLM = {
|
|
19
|
-
|
|
20
|
-
return '[1, 0, 2]';
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
const reranker = new LLMReranker({ llm: mockLLM });
|
|
24
|
-
const result = await reranker.rerank('What are vectors?', mockDocs);
|
|
25
|
-
expect(result.map(d => d.text)).toEqual([
|
|
26
|
-
'context about vectors',
|
|
27
|
-
'context about math',
|
|
28
|
-
'context about nothing'
|
|
29
|
-
]);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test('gracefully falls back if LLM returns invalid JSON', async () => {
|
|
33
|
-
const mockLLM = {
|
|
34
|
-
async generate() {
|
|
35
|
-
return 'this is not JSON';
|
|
36
|
-
}
|
|
37
|
-
};
|
|
12
|
+
const mockLLM = { async generate() { return '[1, 0, 2]'; } };
|
|
13
|
+
const docs = [{ text: 'A' }, { text: 'B' }, { text: 'C' }];
|
|
38
14
|
const reranker = new LLMReranker({ llm: mockLLM });
|
|
39
|
-
const result = await reranker.rerank('
|
|
40
|
-
expect(result.
|
|
15
|
+
const result = await reranker.rerank('What is A?', docs);
|
|
16
|
+
expect(result.map(d => d.text)).toEqual(['B', 'A', 'C']);
|
|
41
17
|
});
|
|
42
|
-
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: __tests__/unit/reranker/reranker.enriched.test.js
|
|
3
|
+
* Version: 1.0.0
|
|
4
|
+
* Description: Tests support for extended document structures
|
|
5
|
+
* Author: @devilsdev
|
|
6
|
+
*/
|
|
7
|
+
import { LLMReranker } from '../../../src/reranker/llm-reranker.js';
|
|
8
|
+
|
|
9
|
+
test('supports documents with metadata', async () => {
|
|
10
|
+
const mockLLM = { async generate() { return '[2, 0, 1]'; } };
|
|
11
|
+
const docs = [
|
|
12
|
+
{ text: 'One', source: 'alpha' },
|
|
13
|
+
{ text: 'Two', source: 'beta' },
|
|
14
|
+
{ text: 'Three', source: 'gamma' }
|
|
15
|
+
];
|
|
16
|
+
const reranker = new LLMReranker({ llm: mockLLM });
|
|
17
|
+
const result = await reranker.rerank('Choose the most complete', docs);
|
|
18
|
+
expect(result[0]).toHaveProperty('source', 'gamma');
|
|
19
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: __tests__/unit/reranker/reranker.fallback.test.js
|
|
3
|
+
* Version: 1.0.0
|
|
4
|
+
* Description: Validates fallback handling for invalid LLM output
|
|
5
|
+
* Author: @devilsdev
|
|
6
|
+
*/
|
|
7
|
+
import { LLMReranker } from '../../../src/reranker/llm-reranker.js';
|
|
8
|
+
|
|
9
|
+
test('gracefully handles invalid LLM output', async () => {
|
|
10
|
+
const mockLLM = { async generate() { return 'INVALID_JSON'; } };
|
|
11
|
+
const docs = [{ text: 'X' }, { text: 'Y' }, { text: 'Z' }];
|
|
12
|
+
const reranker = new LLMReranker({ llm: mockLLM });
|
|
13
|
+
const result = await reranker.rerank('Prompt?', docs);
|
|
14
|
+
expect(result.map(d => d.text)).toEqual(['X', 'Y', 'Z']);
|
|
15
|
+
});
|
|
@@ -1,17 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: __tests__/unit/reranker/reranker.snapshot.test.js
|
|
3
|
+
* Version: 1.0.0
|
|
4
|
+
* Description: Captures a snapshot baseline of reranked results
|
|
5
|
+
* Author: @devilsdev
|
|
6
|
+
*/
|
|
1
7
|
import { LLMReranker } from '../../../src/reranker/llm-reranker.js';
|
|
2
8
|
|
|
3
9
|
test('LLMReranker output snapshot', async () => {
|
|
4
|
-
const mockLLM = {
|
|
5
|
-
|
|
6
|
-
return '[1, 0, 2]';
|
|
7
|
-
}
|
|
8
|
-
};
|
|
9
|
-
const mockDocs = [
|
|
10
|
-
{ text: 'A' },
|
|
11
|
-
{ text: 'B' },
|
|
12
|
-
{ text: 'C' }
|
|
13
|
-
];
|
|
10
|
+
const mockLLM = { async generate() { return '[1, 0, 2]'; } };
|
|
11
|
+
const mockDocs = [{ text: 'A' }, { text: 'B' }, { text: 'C' }];
|
|
14
12
|
const reranker = new LLMReranker({ llm: mockLLM });
|
|
15
13
|
const result = await reranker.rerank('What is A?', mockDocs);
|
|
16
14
|
expect(result).toMatchSnapshot();
|
|
17
|
-
});
|
|
15
|
+
});
|