719032b19f
Update Schema / Update configuration json schema (push) Has been cancelled
Memory Benchmark / Memory Test (Full Analysis) (push) Has been cancelled
CI Quality / Lint GitHub Actions (actionlint) (push) Has been cancelled
CI Quality / Lint GitHub Actions (zizmor) (push) Has been cancelled
CI Quality / Check typos (push) Has been cancelled
CI / Test coverage (push) Has been cancelled
CI / Test (22.x, windows-latest) (push) Has been cancelled
CI / Test (24.x, macos-latest) (push) Has been cancelled
CI / Test (24.x, ubuntu-latest) (push) Has been cancelled
CI / Test (24.x, windows-latest) (push) Has been cancelled
CI / Test (26.x, macos-latest) (push) Has been cancelled
CI / Test (26.x, ubuntu-latest) (push) Has been cancelled
CI / Test (26.x, windows-latest) (push) Has been cancelled
CI / Test with Bun (latest, macos-latest) (push) Has been cancelled
CI / Test with Bun (latest, ubuntu-latest) (push) Has been cancelled
CI / Test with Bun (latest, windows-latest) (push) Has been cancelled
CI / Build and run (22.x, macos-latest) (push) Has been cancelled
CI / Build and run (22.x, ubuntu-latest) (push) Has been cancelled
CI / Build and run (22.x, windows-latest) (push) Has been cancelled
autofix.ci / autofix (push) Has been cancelled
CI Browser Extension / Lint Browser Extension (push) Has been cancelled
CI Browser Extension / Test Browser Extension (push) Has been cancelled
Memory Benchmark / Memory Test (push) Has been cancelled
CI Website / Lint Website Client (push) Has been cancelled
CI Website / Lint Website Server (push) Has been cancelled
CI Website / Test Website Server (push) Has been cancelled
CI Website / Bundle Website Server (push) Has been cancelled
CI / Lint Biome (push) Has been cancelled
CI / Lint oxlint (push) Has been cancelled
CI / Lint TypeScript (push) Has been cancelled
CI / Lint Secretlint (push) Has been cancelled
CI / Test (22.x, macos-latest) (push) Has been cancelled
CI / Test (22.x, ubuntu-latest) (push) Has been cancelled
CI / Build and run (24.x, macos-latest) (push) Has been cancelled
CI / Build and run (24.x, ubuntu-latest) (push) Has been cancelled
CI / Build and run (24.x, windows-latest) (push) Has been cancelled
CI / Build and run (26.x, macos-latest) (push) Has been cancelled
CI / Build and run (26.x, ubuntu-latest) (push) Has been cancelled
CI / Build and run (26.x, windows-latest) (push) Has been cancelled
CI / Build and run with Bun (latest, macos-latest) (push) Has been cancelled
CI / Build and run with Bun (latest, ubuntu-latest) (push) Has been cancelled
CI / Build and run with Bun (latest, windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Docker / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Docker / build (linux/arm/v7, ubuntu-24.04-arm) (push) Has been cancelled
Docker / build (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Docker / merge (push) Has been cancelled
Pack repository with Repomix / pack-repo (push) Has been cancelled
Performance Benchmark History / Benchmark (macos-latest) (push) Has been cancelled
Performance Benchmark History / Benchmark (ubuntu-latest) (push) Has been cancelled
Performance Benchmark History / Benchmark (windows-latest) (push) Has been cancelled
Performance Benchmark History / Store Results (push) Has been cancelled
Test Repomix Action / Test Node.js 22 (push) Has been cancelled
Test Repomix Action / Test Node.js 24 (push) Has been cancelled
Test Repomix Action / Test Node.js 26 (push) Has been cancelled
312 lines
12 KiB
TypeScript
312 lines
12 KiB
TypeScript
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|
import { pack } from '../../src/core/packager.js';
|
|
import { createMockConfig } from '../testing/testUtils.js';
|
|
|
|
vi.mock('node:fs/promises');
|
|
vi.mock('fs/promises');
|
|
vi.mock('../../src/core/metrics/TokenCounter.js', () => {
|
|
return {
|
|
TOKEN_ENCODINGS: ['o200k_base', 'cl100k_base', 'p50k_base', 'p50k_edit', 'r50k_base'],
|
|
TokenCounter: vi.fn().mockImplementation(() => ({
|
|
countTokens: vi.fn().mockReturnValue(10),
|
|
free: vi.fn(),
|
|
})),
|
|
};
|
|
});
|
|
|
|
describe('packager', () => {
|
|
beforeEach(() => {
|
|
vi.resetAllMocks();
|
|
});
|
|
|
|
test('pack should orchestrate packing files and generating output', async () => {
|
|
// Use a POSIX separator: globby yields forward-slash paths on every platform
|
|
// and pack() normalizes display paths to forward slashes, so the mock must match
|
|
// that (path.join would emit "dir1\\file2.txt" on Windows and fail the assertion).
|
|
const file2Path = 'dir1/file2.txt';
|
|
const mockRawFiles = [
|
|
{ path: 'file1.txt', content: 'raw content 1' },
|
|
{ path: file2Path, content: 'raw content 2' },
|
|
];
|
|
const mockProcessedFiles = [
|
|
{ path: 'file1.txt', content: 'processed content 1' },
|
|
{ path: file2Path, content: 'processed content 2' },
|
|
];
|
|
const mockOutput = 'mock output';
|
|
const mockFilePaths = ['file1.txt', file2Path];
|
|
|
|
const mockDeps = {
|
|
searchFiles: vi.fn().mockResolvedValue({
|
|
filePaths: mockFilePaths,
|
|
emptyDirPaths: [],
|
|
}),
|
|
sortPaths: vi.fn().mockImplementation((paths) => paths),
|
|
collectFiles: vi.fn().mockResolvedValue({ rawFiles: mockRawFiles, skippedFiles: [] }),
|
|
processFiles: vi.fn().mockReturnValue(mockProcessedFiles),
|
|
validateFileSafety: vi.fn().mockResolvedValue({
|
|
safeFilePaths: mockFilePaths,
|
|
safeRawFiles: mockRawFiles,
|
|
suspiciousFilesResults: [],
|
|
suspiciousGitDiffResults: [],
|
|
suspiciousGitLogResults: [],
|
|
}),
|
|
produceOutput: vi.fn().mockResolvedValue({
|
|
outputForMetrics: mockOutput,
|
|
}),
|
|
createMetricsTaskRunner: vi.fn().mockReturnValue({
|
|
taskRunner: {
|
|
run: vi.fn().mockResolvedValue(0),
|
|
cleanup: vi.fn().mockResolvedValue(undefined),
|
|
},
|
|
warmupPromise: Promise.resolve(),
|
|
}),
|
|
calculateMetrics: vi.fn().mockResolvedValue({
|
|
totalFiles: 2,
|
|
totalCharacters: 11,
|
|
totalTokens: 10,
|
|
fileCharCounts: {
|
|
'file1.txt': 19,
|
|
[file2Path]: 19,
|
|
},
|
|
fileTokenCounts: {
|
|
'file1.txt': 10,
|
|
[file2Path]: 10,
|
|
},
|
|
gitDiffTokenCount: 0,
|
|
gitLogTokenCount: 0,
|
|
}),
|
|
};
|
|
|
|
const mockConfig = createMockConfig();
|
|
const progressCallback = vi.fn();
|
|
const result = await pack(['root'], mockConfig, progressCallback, mockDeps);
|
|
|
|
expect(mockDeps.searchFiles).toHaveBeenCalledWith('root', mockConfig, undefined);
|
|
expect(mockDeps.collectFiles).toHaveBeenCalledWith(mockFilePaths, 'root', mockConfig, progressCallback);
|
|
expect(mockDeps.validateFileSafety).toHaveBeenCalled();
|
|
expect(mockDeps.processFiles).toHaveBeenCalled();
|
|
expect(mockDeps.produceOutput).toHaveBeenCalled();
|
|
expect(mockDeps.calculateMetrics).toHaveBeenCalled();
|
|
|
|
// pack() resolves each file's inclusion level (against its per-root-relative
|
|
// path) and carries it on the raw file before handing them to the security
|
|
// check and file processing. With the default config every file resolves to
|
|
// 'full'.
|
|
const expectedLeveledRawFiles = mockRawFiles.map((file) => ({ ...file, level: 'full' as const }));
|
|
expect(mockDeps.validateFileSafety).toHaveBeenCalledWith(
|
|
expectedLeveledRawFiles,
|
|
progressCallback,
|
|
mockConfig,
|
|
undefined,
|
|
undefined,
|
|
);
|
|
expect(mockDeps.processFiles).toHaveBeenCalledWith(expectedLeveledRawFiles, mockConfig, progressCallback);
|
|
expect(mockDeps.produceOutput).toHaveBeenCalledWith(
|
|
['root'],
|
|
mockConfig,
|
|
mockProcessedFiles,
|
|
mockFilePaths,
|
|
undefined,
|
|
undefined,
|
|
progressCallback,
|
|
[{ rootLabel: 'root', files: mockFilePaths }],
|
|
undefined,
|
|
);
|
|
expect(mockDeps.calculateMetrics).toHaveBeenCalledWith(
|
|
mockProcessedFiles,
|
|
expect.anything(),
|
|
progressCallback,
|
|
mockConfig,
|
|
undefined,
|
|
undefined,
|
|
expect.objectContaining({ taskRunner: expect.anything() }),
|
|
);
|
|
|
|
// Verify that calculateMetrics received a promise that resolves to the expected output
|
|
const outputArg = mockDeps.calculateMetrics.mock.calls[0][1];
|
|
await expect(outputArg).resolves.toBe(mockOutput);
|
|
|
|
// Check the result of pack function
|
|
expect(result.totalFiles).toBe(2);
|
|
expect(result.totalCharacters).toBe(11);
|
|
expect(result.totalTokens).toBe(10);
|
|
expect(result.fileCharCounts).toEqual({
|
|
'file1.txt': 19,
|
|
[file2Path]: 19,
|
|
});
|
|
expect(result.fileTokenCounts).toEqual({
|
|
'file1.txt': 10,
|
|
[file2Path]: 10,
|
|
});
|
|
expect(result.skippedFiles).toEqual([]);
|
|
});
|
|
|
|
test('applies file processors per root and passes the transformed content to the security check and processing', async () => {
|
|
const collectedRawFiles = [{ path: 'a.json', content: 'raw content' }];
|
|
const transformedRawFiles = [{ path: 'a.json', content: 'transformed content' }];
|
|
const mockFilePaths = ['a.json'];
|
|
|
|
const applyFileProcessors = vi.fn().mockResolvedValue(transformedRawFiles);
|
|
|
|
const mockDeps = {
|
|
searchFiles: vi.fn().mockResolvedValue({ filePaths: mockFilePaths, emptyDirPaths: [] }),
|
|
sortPaths: vi.fn().mockImplementation((paths) => paths),
|
|
collectFiles: vi.fn().mockResolvedValue({ rawFiles: collectedRawFiles, skippedFiles: [] }),
|
|
applyFileProcessors,
|
|
processFiles: vi.fn().mockReturnValue([{ path: 'a.json', content: 'transformed content' }]),
|
|
validateFileSafety: vi.fn().mockResolvedValue({
|
|
safeFilePaths: mockFilePaths,
|
|
safeRawFiles: transformedRawFiles,
|
|
suspiciousFilesResults: [],
|
|
suspiciousGitDiffResults: [],
|
|
suspiciousGitLogResults: [],
|
|
}),
|
|
produceOutput: vi.fn().mockResolvedValue({ outputForMetrics: 'out' }),
|
|
createMetricsTaskRunner: vi.fn().mockReturnValue({
|
|
taskRunner: { run: vi.fn().mockResolvedValue(0), cleanup: vi.fn().mockResolvedValue(undefined) },
|
|
warmupPromise: Promise.resolve(),
|
|
}),
|
|
calculateMetrics: vi.fn().mockResolvedValue({
|
|
totalFiles: 1,
|
|
totalCharacters: 0,
|
|
totalTokens: 0,
|
|
fileCharCounts: {},
|
|
fileTokenCounts: {},
|
|
gitDiffTokenCount: 0,
|
|
gitLogTokenCount: 0,
|
|
}),
|
|
};
|
|
|
|
const config = createMockConfig();
|
|
const progressCallback = vi.fn();
|
|
await pack(['root'], config, progressCallback, mockDeps);
|
|
|
|
// Processors run per root, on the collected per-root-relative files (before path rewrite).
|
|
expect(applyFileProcessors).toHaveBeenCalledWith(collectedRawFiles, 'root', config, progressCallback);
|
|
|
|
// The security scan and file processing must operate on the TRANSFORMED content
|
|
// (what actually ships), not the original collected content.
|
|
expect(mockDeps.validateFileSafety).toHaveBeenCalledWith(
|
|
[expect.objectContaining({ path: 'a.json', content: 'transformed content' })],
|
|
progressCallback,
|
|
config,
|
|
undefined,
|
|
undefined,
|
|
);
|
|
expect(mockDeps.processFiles).toHaveBeenCalledWith(
|
|
[expect.objectContaining({ path: 'a.json', content: 'transformed content' })],
|
|
config,
|
|
progressCallback,
|
|
);
|
|
});
|
|
|
|
describe('parallel error handling', () => {
|
|
// The pipeline runs several stages in parallel (security check + file processing,
|
|
// output generation + metrics). Regressions in error propagation or worker cleanup
|
|
// are easy to introduce when adding parallel branches.
|
|
const mockFilePaths = ['file1.txt'];
|
|
const mockRawFiles = [{ path: 'file1.txt', content: 'raw' }];
|
|
const mockProcessedFiles = [{ path: 'file1.txt', content: 'processed' }];
|
|
|
|
const baseDeps = () => {
|
|
const cleanup = vi.fn().mockResolvedValue(undefined);
|
|
return {
|
|
cleanup,
|
|
deps: {
|
|
searchFiles: vi.fn().mockResolvedValue({ filePaths: mockFilePaths, emptyDirPaths: [] }),
|
|
sortPaths: vi.fn().mockImplementation((p) => p),
|
|
collectFiles: vi.fn().mockResolvedValue({ rawFiles: mockRawFiles, skippedFiles: [] }),
|
|
processFiles: vi.fn().mockResolvedValue(mockProcessedFiles),
|
|
validateFileSafety: vi.fn().mockResolvedValue({
|
|
safeFilePaths: mockFilePaths,
|
|
safeRawFiles: mockRawFiles,
|
|
suspiciousFilesResults: [],
|
|
suspiciousGitDiffResults: [],
|
|
suspiciousGitLogResults: [],
|
|
}),
|
|
produceOutput: vi.fn().mockResolvedValue({ outputForMetrics: 'output' }),
|
|
// Mirror real calculateMetrics behavior: await the outputForMetrics promise so a
|
|
// produceOutput rejection propagates here instead of becoming unhandled.
|
|
calculateMetrics: vi.fn().mockImplementation(async (_files, outputPromise) => {
|
|
await outputPromise;
|
|
return {
|
|
totalFiles: 1,
|
|
totalCharacters: 9,
|
|
totalTokens: 1,
|
|
fileCharCounts: { 'file1.txt': 9 },
|
|
fileTokenCounts: { 'file1.txt': 1 },
|
|
gitDiffTokenCount: 0,
|
|
gitLogTokenCount: 0,
|
|
};
|
|
}),
|
|
createMetricsTaskRunner: vi.fn().mockReturnValue({
|
|
taskRunner: { run: vi.fn().mockResolvedValue(0), cleanup },
|
|
warmupPromise: Promise.resolve(),
|
|
}),
|
|
getGitDiffs: vi.fn().mockResolvedValue(undefined),
|
|
getGitLogs: vi.fn().mockResolvedValue(undefined),
|
|
prefetchSortData: vi.fn().mockResolvedValue(undefined),
|
|
sortOutputFiles: vi.fn().mockImplementation((files) => files),
|
|
},
|
|
};
|
|
};
|
|
|
|
test('cleans up the metrics worker pool when validateFileSafety rejects', async () => {
|
|
const { cleanup, deps } = baseDeps();
|
|
deps.validateFileSafety = vi.fn().mockRejectedValue(new Error('security check failed'));
|
|
|
|
await expect(pack(['root'], createMockConfig(), vi.fn(), deps)).rejects.toThrow('security check failed');
|
|
|
|
expect(cleanup).toHaveBeenCalled();
|
|
});
|
|
|
|
test('cleans up the metrics worker pool when produceOutput rejects', async () => {
|
|
const { cleanup, deps } = baseDeps();
|
|
deps.produceOutput = vi.fn().mockRejectedValue(new Error('output failed'));
|
|
|
|
await expect(pack(['root'], createMockConfig(), vi.fn(), deps)).rejects.toThrow('output failed');
|
|
|
|
expect(cleanup).toHaveBeenCalled();
|
|
});
|
|
|
|
test('cleans up the metrics worker pool when calculateMetrics rejects', async () => {
|
|
const { cleanup, deps } = baseDeps();
|
|
deps.calculateMetrics = vi.fn().mockRejectedValue(new Error('metrics failed'));
|
|
|
|
await expect(pack(['root'], createMockConfig(), vi.fn(), deps)).rejects.toThrow('metrics failed');
|
|
|
|
expect(cleanup).toHaveBeenCalled();
|
|
});
|
|
|
|
test('a prefetchSortData failure does not block the pipeline', async () => {
|
|
const { cleanup, deps } = baseDeps();
|
|
deps.prefetchSortData = vi.fn().mockRejectedValue(new Error('git failed'));
|
|
|
|
const result = await pack(['root'], createMockConfig(), vi.fn(), deps);
|
|
|
|
// Pack should complete successfully even though the prefetch failed.
|
|
expect(result.totalFiles).toBe(1);
|
|
expect(deps.sortOutputFiles).toHaveBeenCalled();
|
|
expect(cleanup).toHaveBeenCalled();
|
|
});
|
|
|
|
test('cleans up the metrics worker pool even when the warmup promise rejects', async () => {
|
|
const { cleanup, deps } = baseDeps();
|
|
// Pre-attach a no-op handler so the rejection is observed at construction time,
|
|
// before pack() reaches `await metricsWarmupPromise`. Production code mirrors this
|
|
// with `.catch(() => {})` in packager.ts:262, so the warmup rejection is fully
|
|
// contained — but vitest's unhandled-rejection detector can flag it eagerly here.
|
|
const warmupPromise = Promise.reject(new Error('warmup failed'));
|
|
warmupPromise.catch(() => {});
|
|
deps.createMetricsTaskRunner = vi.fn().mockReturnValue({
|
|
taskRunner: { run: vi.fn().mockResolvedValue(0), cleanup },
|
|
warmupPromise,
|
|
});
|
|
|
|
await expect(pack(['root'], createMockConfig(), vi.fn(), deps)).rejects.toThrow('warmup failed');
|
|
|
|
expect(cleanup).toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|