mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
ci(config): add jest config and PR workflow for migration tests
config/__tests__/ specs depend on api/jest.config.js module mappings but had no dedicated runner. Adds config/jest.config.js extending api config with absolutized paths, npm test:config script, and a GitHub Actions workflow triggered by changes to config/, api/models/, api/db/, or packages/ ACL code.
This commit is contained in:
parent
3cc7a8e79e
commit
ac4aa80f10
4 changed files with 256 additions and 0 deletions
88
.github/workflows/config-review.yml
vendored
Normal file
88
.github/workflows/config-review.yml
vendored
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
name: Config Migration Tests
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'config/**'
|
||||
- 'api/models/**'
|
||||
- 'api/db/**'
|
||||
- 'packages/data-schemas/src/**'
|
||||
- 'packages/data-provider/src/**'
|
||||
- 'packages/api/src/acl/**'
|
||||
- 'packages/api/src/shared-links/**'
|
||||
|
||||
env:
|
||||
NODE_ENV: CI
|
||||
NODE_OPTIONS: '--max-old-space-size=${{ secrets.NODE_MAX_OLD_SPACE_SIZE || 6144 }}'
|
||||
|
||||
jobs:
|
||||
test-config:
|
||||
name: 'Tests: config migrations'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js 20.19
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.19'
|
||||
|
||||
- name: Restore node_modules cache
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
api/node_modules
|
||||
packages/api/node_modules
|
||||
packages/data-provider/node_modules
|
||||
packages/data-schemas/node_modules
|
||||
key: node-modules-backend-${{ runner.os }}-20.19-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
||||
run: npm ci
|
||||
|
||||
- name: Restore data-provider build cache
|
||||
id: cache-data-provider
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: packages/data-provider/dist
|
||||
key: build-data-provider-${{ runner.os }}-${{ hashFiles('packages/data-provider/src/**', 'packages/data-provider/tsconfig*.json', 'packages/data-provider/rollup.config.js', 'packages/data-provider/package.json') }}
|
||||
|
||||
- name: Build data-provider
|
||||
if: steps.cache-data-provider.outputs.cache-hit != 'true'
|
||||
run: npm run build:data-provider
|
||||
|
||||
- name: Restore data-schemas build cache
|
||||
id: cache-data-schemas
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: packages/data-schemas/dist
|
||||
key: build-data-schemas-${{ runner.os }}-${{ hashFiles('packages/data-schemas/src/**', 'packages/data-schemas/tsconfig*.json', 'packages/data-schemas/rollup.config.js', 'packages/data-schemas/package.json', 'packages/data-provider/src/**', 'packages/data-provider/tsconfig*.json', 'packages/data-provider/rollup.config.js', 'packages/data-provider/package.json') }}
|
||||
|
||||
- name: Build data-schemas
|
||||
if: steps.cache-data-schemas.outputs.cache-hit != 'true'
|
||||
run: npm run build:data-schemas
|
||||
|
||||
- name: Restore api build cache
|
||||
id: cache-api
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: packages/api/dist
|
||||
key: build-api-${{ runner.os }}-${{ hashFiles('packages/api/src/**', 'packages/api/tsconfig*.json', 'packages/api/server-rollup.config.js', 'packages/api/package.json', 'packages/data-provider/src/**', 'packages/data-provider/tsconfig*.json', 'packages/data-provider/rollup.config.js', 'packages/data-provider/package.json', 'packages/data-schemas/src/**', 'packages/data-schemas/tsconfig*.json', 'packages/data-schemas/rollup.config.js', 'packages/data-schemas/package.json') }}
|
||||
|
||||
- name: Build api
|
||||
if: steps.cache-api.outputs.cache-hit != 'true'
|
||||
run: npm run build:api
|
||||
|
||||
- name: Create empty auth.json file
|
||||
run: |
|
||||
mkdir -p api/data
|
||||
echo '{}' > api/data/auth.json
|
||||
|
||||
- name: Prepare .env.test file
|
||||
run: cp api/test/.env.test.example api/test/.env.test
|
||||
|
||||
- name: Run config migration tests
|
||||
run: npm run test:config
|
||||
149
config/__tests__/migrate-shared-link-permissions.spec.js
Normal file
149
config/__tests__/migrate-shared-link-permissions.spec.js
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
jest.mock('../connect', () => jest.fn().mockResolvedValue(true));
|
||||
jest.mock('@librechat/api', () => ({
|
||||
ensureRequiredCollectionsExist: jest.fn().mockResolvedValue(undefined),
|
||||
matchModelName: jest.fn(),
|
||||
findMatchingPattern: jest.fn(),
|
||||
}));
|
||||
jest.mock('@librechat/data-schemas', () => ({
|
||||
...jest.requireActual('@librechat/data-schemas'),
|
||||
logger: { error: jest.fn(), info: jest.fn(), warn: jest.fn(), debug: jest.fn() },
|
||||
}));
|
||||
jest.mock('~/cache/getLogStores', () => jest.fn());
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
const { MongoMemoryServer } = require('mongodb-memory-server');
|
||||
const { createMethods } = require('@librechat/data-schemas');
|
||||
|
||||
describe('migrate-shared-link-permissions', () => {
|
||||
let mongoServer;
|
||||
let SharedLink, AclEntry;
|
||||
let migrateSharedLinkPermissions;
|
||||
const testUserId = new mongoose.Types.ObjectId();
|
||||
|
||||
beforeAll(async () => {
|
||||
mongoServer = await MongoMemoryServer.create();
|
||||
await mongoose.connect(mongoServer.getUri());
|
||||
|
||||
const migration = require('../migrate-shared-link-permissions');
|
||||
migrateSharedLinkPermissions = migration.migrateSharedLinkPermissions;
|
||||
|
||||
SharedLink = mongoose.models.SharedLink;
|
||||
AclEntry = mongoose.models.AclEntry;
|
||||
|
||||
await createMethods(mongoose).seedDefaultRoles();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await mongoose.disconnect();
|
||||
await mongoServer.stop();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await AclEntry.deleteMany({});
|
||||
await SharedLink.deleteMany({});
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
async function createLegacyLink(isPublic = true) {
|
||||
const link = await SharedLink.create({
|
||||
shareId: `share-${Date.now()}-${Math.random()}`,
|
||||
conversationId: 'convo1',
|
||||
user: testUserId,
|
||||
messages: [],
|
||||
});
|
||||
await mongoose.connection.db
|
||||
.collection('sharedlinks')
|
||||
.updateOne({ _id: link._id }, { $set: { isPublic } });
|
||||
return link;
|
||||
}
|
||||
|
||||
test('removes isPublic from all links on success', async () => {
|
||||
const link1 = await createLegacyLink(true);
|
||||
const link2 = await createLegacyLink(true);
|
||||
|
||||
const result = await migrateSharedLinkPermissions({ dryRun: false, batchSize: 100 });
|
||||
|
||||
expect(result.errors).toBe(0);
|
||||
expect(result.failedLinkCount).toBe(0);
|
||||
|
||||
const raw1 = await mongoose.connection.db
|
||||
.collection('sharedlinks')
|
||||
.findOne({ _id: link1._id });
|
||||
const raw2 = await mongoose.connection.db
|
||||
.collection('sharedlinks')
|
||||
.findOne({ _id: link2._id });
|
||||
expect(raw1).not.toHaveProperty('isPublic');
|
||||
expect(raw2).not.toHaveProperty('isPublic');
|
||||
});
|
||||
|
||||
test('preserves isPublic on links with partial write errors', async () => {
|
||||
const link1 = await createLegacyLink(true);
|
||||
const link2 = await createLegacyLink(true);
|
||||
const link3 = await createLegacyLink(true);
|
||||
|
||||
const originalBulkWrite = AclEntry.bulkWrite.bind(AclEntry);
|
||||
jest.spyOn(AclEntry, 'bulkWrite').mockImplementationOnce(async (ops, options) => {
|
||||
const failedIndices = [];
|
||||
const successOps = [];
|
||||
|
||||
for (let i = 0; i < ops.length; i++) {
|
||||
if (ops[i].updateOne.filter.resourceId.equals(link2._id)) {
|
||||
failedIndices.push(i);
|
||||
} else {
|
||||
successOps.push(ops[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (successOps.length > 0) {
|
||||
await originalBulkWrite(successOps, options);
|
||||
}
|
||||
|
||||
const error = new Error('Partial bulk write failure');
|
||||
error.writeErrors = failedIndices.map((idx) => ({
|
||||
index: idx,
|
||||
errmsg: 'Simulated write error',
|
||||
}));
|
||||
throw error;
|
||||
});
|
||||
|
||||
const result = await migrateSharedLinkPermissions({ dryRun: false, batchSize: 100 });
|
||||
|
||||
expect(result.failedLinkCount).toBe(1);
|
||||
expect(result.errors).toBeGreaterThan(0);
|
||||
|
||||
const raw1 = await mongoose.connection.db
|
||||
.collection('sharedlinks')
|
||||
.findOne({ _id: link1._id });
|
||||
const raw2 = await mongoose.connection.db
|
||||
.collection('sharedlinks')
|
||||
.findOne({ _id: link2._id });
|
||||
const raw3 = await mongoose.connection.db
|
||||
.collection('sharedlinks')
|
||||
.findOne({ _id: link3._id });
|
||||
|
||||
expect(raw2).toHaveProperty('isPublic', true);
|
||||
expect(raw1).not.toHaveProperty('isPublic');
|
||||
expect(raw3).not.toHaveProperty('isPublic');
|
||||
});
|
||||
|
||||
test('preserves isPublic on all batch links when bulk write fails entirely', async () => {
|
||||
const link1 = await createLegacyLink(true);
|
||||
const link2 = await createLegacyLink(true);
|
||||
|
||||
jest.spyOn(AclEntry, 'bulkWrite').mockRejectedValueOnce(new Error('Connection lost'));
|
||||
|
||||
const result = await migrateSharedLinkPermissions({ dryRun: false, batchSize: 100 });
|
||||
|
||||
expect(result.failedLinkCount).toBe(2);
|
||||
|
||||
const raw1 = await mongoose.connection.db
|
||||
.collection('sharedlinks')
|
||||
.findOne({ _id: link1._id });
|
||||
const raw2 = await mongoose.connection.db
|
||||
.collection('sharedlinks')
|
||||
.findOne({ _id: link2._id });
|
||||
|
||||
expect(raw1).toHaveProperty('isPublic', true);
|
||||
expect(raw2).toHaveProperty('isPublic', true);
|
||||
});
|
||||
});
|
||||
18
config/jest.config.js
Normal file
18
config/jest.config.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
const path = require('path');
|
||||
const apiConfig = require('../api/jest.config');
|
||||
|
||||
const apiDir = path.resolve(__dirname, '..', 'api');
|
||||
|
||||
const resolvedMapper = Object.fromEntries(
|
||||
Object.entries(apiConfig.moduleNameMapper).map(([key, value]) => [
|
||||
key,
|
||||
value.replace('<rootDir>', apiDir),
|
||||
]),
|
||||
);
|
||||
|
||||
module.exports = {
|
||||
...apiConfig,
|
||||
roots: ['<rootDir>'],
|
||||
setupFiles: apiConfig.setupFiles.map((f) => path.resolve(apiDir, f)),
|
||||
moduleNameMapper: resolvedMapper,
|
||||
};
|
||||
|
|
@ -67,6 +67,7 @@
|
|||
"test:packages:api": "cd packages/api && npm run test:ci",
|
||||
"test:packages:data-provider": "cd packages/data-provider && npm run test:ci",
|
||||
"test:packages:data-schemas": "cd packages/data-schemas && npm run test:ci",
|
||||
"test:config": "jest --config config/jest.config.js",
|
||||
"test:all": "npm run test:client && npm run test:api && npm run test:packages:api && npm run test:packages:data-provider && npm run test:packages:data-schemas",
|
||||
"e2e:update": "npm run e2e:prepare && playwright test --config=e2e/playwright.config.local.ts --update-snapshots",
|
||||
"e2e:report": "npx playwright show-report e2e/playwright-report",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue