mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-28 06:22:22 +00:00
189 lines
7 KiB
TypeScript
189 lines
7 KiB
TypeScript
import { execFileSync, fork } from 'node:child_process';
|
|
import { createHash } from 'node:crypto';
|
|
import { readFile, realpath } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { describe, expect, it } from 'vitest';
|
|
import type { AnalyzerRunnerIdentity } from '../../src/storage/repo-manager.js';
|
|
import { getStoragePaths, loadMeta } from '../../src/storage/repo-manager.js';
|
|
import { setupMiniRepo } from '../helpers/mini-repo.js';
|
|
import { createTempDir } from '../helpers/test-db.js';
|
|
import { normalizeAnalyzerRunnerIdentityForComparison } from '../../src/core/analyzer-identity.js';
|
|
|
|
function runAnalyzeWorker(
|
|
workerEntry: string,
|
|
repoPath: string,
|
|
env: NodeJS.ProcessEnv,
|
|
force: boolean,
|
|
): Promise<{ alreadyUpToDate?: boolean }> {
|
|
return new Promise((resolve, reject) => {
|
|
const child = fork(workerEntry, [], {
|
|
cwd: repoPath,
|
|
env,
|
|
// Match the CLI subprocess's runtime conditions. Inheriting Vitest's
|
|
// development conditions would intentionally select a different identity.
|
|
execArgv: [],
|
|
stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
|
|
});
|
|
let stderr = '';
|
|
const timer = setTimeout(() => {
|
|
child.kill('SIGKILL');
|
|
reject(new Error(`analyze worker timed out: ${stderr}`));
|
|
}, 240_000);
|
|
child.stderr?.on('data', (chunk: Buffer) => {
|
|
stderr += chunk.toString();
|
|
});
|
|
child.on('message', (message: unknown) => {
|
|
const result = message as {
|
|
type?: string;
|
|
message?: string;
|
|
result?: { alreadyUpToDate?: boolean };
|
|
};
|
|
if (result.type === 'complete') {
|
|
clearTimeout(timer);
|
|
resolve(result.result ?? {});
|
|
} else if (result.type === 'error') {
|
|
clearTimeout(timer);
|
|
reject(new Error(result.message ?? stderr));
|
|
}
|
|
});
|
|
child.on('error', (error) => {
|
|
clearTimeout(timer);
|
|
reject(error);
|
|
});
|
|
child.send({
|
|
type: 'start',
|
|
repoPath,
|
|
options: {
|
|
force,
|
|
skipAgentsMd: true,
|
|
skipSkills: true,
|
|
workerPoolSize: 1,
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
describe('CLI analyzer identity receipt', () => {
|
|
it('keeps CLI and server-worker entrypoints fresh in both directions', async () => {
|
|
const repo = await setupMiniRepo();
|
|
const isolatedHome = await createTempDir();
|
|
const packageRoot = path.resolve(__dirname, '..', '..');
|
|
const cliEntry = await realpath(path.join(packageRoot, 'dist', 'cli', 'index.js'));
|
|
const workerEntry = await realpath(
|
|
path.join(packageRoot, 'dist', 'server', 'analyze-worker.js'),
|
|
);
|
|
const runtimePath = await realpath(process.execPath);
|
|
const packageVersion = (
|
|
JSON.parse(await readFile(path.join(packageRoot, 'package.json'), 'utf8')) as {
|
|
version: string;
|
|
}
|
|
).version;
|
|
const env = {
|
|
...process.env,
|
|
GITNEXUS_HOME: isolatedHome.dbPath,
|
|
GITNEXUS_LANG: 'en',
|
|
GITNEXUS_LBUG_EXTENSION_INSTALL: 'never',
|
|
};
|
|
|
|
try {
|
|
execFileSync(
|
|
process.execPath,
|
|
[cliEntry, 'analyze', repo.dbPath, '--index-only', '--force', '--workers', '1'],
|
|
{ cwd: packageRoot, env, stdio: 'pipe', timeout: 240_000 },
|
|
);
|
|
|
|
const meta = await loadMeta(getStoragePaths(repo.dbPath).storagePath);
|
|
expect(meta?.runnerIdentity).toMatchObject({
|
|
schemaVersion: 4,
|
|
runtime: {
|
|
executablePath: runtimePath,
|
|
version: process.version,
|
|
platform: process.platform,
|
|
architecture: process.arch,
|
|
modulesAbi: process.versions.modules ?? 'unknown',
|
|
libc: expect.any(String),
|
|
},
|
|
cliVersion: packageVersion,
|
|
invokedArtifact: {
|
|
path: cliEntry,
|
|
digest: `sha256:${createHash('sha256')
|
|
.update(await readFile(cliEntry))
|
|
.digest('hex')}`,
|
|
},
|
|
build: {
|
|
kind: 'distribution',
|
|
rootPath: path.join(packageRoot, 'dist'),
|
|
canonicalization: 'gitnexus-analyzer-build-v2',
|
|
digest: expect.stringMatching(/^sha256:[a-f0-9]{64}$/),
|
|
},
|
|
dependencyRuntime: {
|
|
manifestPath: path.join(packageRoot, 'package.json'),
|
|
lockfilePath: path.join(packageRoot, 'package-lock.json'),
|
|
canonicalization: 'gitnexus-analyzer-dependency-runtime-v4',
|
|
packageCount: expect.any(Number),
|
|
artifactCount: expect.any(Number),
|
|
digest: expect.stringMatching(/^sha256:[a-f0-9]{64}$/),
|
|
},
|
|
});
|
|
|
|
expect(meta?.parseCacheIdentity?.base).toMatch(/^sha256:[a-f0-9]{64}$/);
|
|
|
|
// A server worker sees a different process.argv[1], but that entrypoint
|
|
// is diagnostic and already covered by the common build digest. It must
|
|
// take the unchanged fast path after a CLI-authored index.
|
|
const workerFastPath = await runAnalyzeWorker(workerEntry, repo.dbPath, env, false);
|
|
expect(workerFastPath.alreadyUpToDate).toBe(true);
|
|
expect((await loadMeta(getStoragePaths(repo.dbPath).storagePath))?.runnerIdentity).toEqual(
|
|
meta?.runnerIdentity,
|
|
);
|
|
|
|
// Reverse the author: a forced worker run stamps its own entrypoint, and
|
|
// both CLI status and CLI analyze must still regard the semantic runner
|
|
// identity as current rather than rebuilding solely to swap diagnostics.
|
|
await runAnalyzeWorker(workerEntry, repo.dbPath, env, true);
|
|
const workerMeta = await loadMeta(getStoragePaths(repo.dbPath).storagePath);
|
|
expect(workerMeta?.runnerIdentity?.invokedArtifact.path).toBe(workerEntry);
|
|
expect(workerMeta?.parseCacheIdentity).toEqual(meta?.parseCacheIdentity);
|
|
|
|
const status = execFileSync(process.execPath, [cliEntry, 'status', '--json'], {
|
|
cwd: repo.dbPath,
|
|
env,
|
|
encoding: 'utf8',
|
|
timeout: 60_000,
|
|
});
|
|
const parsedStatus = JSON.parse(status) as {
|
|
index: {
|
|
runnerIdentity: AnalyzerRunnerIdentity | null;
|
|
runnerIdentityStatus: string;
|
|
};
|
|
current: { runnerIdentity: AnalyzerRunnerIdentity };
|
|
status: string;
|
|
};
|
|
expect(parsedStatus.index.runnerIdentity).toEqual(workerMeta?.runnerIdentity);
|
|
expect(parsedStatus.current.runnerIdentity.invokedArtifact.path).toBe(cliEntry);
|
|
expect(parsedStatus.current.runnerIdentity).not.toEqual(workerMeta?.runnerIdentity);
|
|
expect(
|
|
normalizeAnalyzerRunnerIdentityForComparison(parsedStatus.current.runnerIdentity),
|
|
).toEqual(normalizeAnalyzerRunnerIdentityForComparison(workerMeta?.runnerIdentity));
|
|
expect(parsedStatus.index.runnerIdentityStatus).toBe('current');
|
|
expect(parsedStatus.status).toBe('up-to-date');
|
|
|
|
execFileSync(
|
|
process.execPath,
|
|
[cliEntry, 'analyze', repo.dbPath, '--index-only', '--workers', '1'],
|
|
{
|
|
cwd: packageRoot,
|
|
env,
|
|
stdio: 'pipe',
|
|
timeout: 240_000,
|
|
},
|
|
);
|
|
expect((await loadMeta(getStoragePaths(repo.dbPath).storagePath))?.runnerIdentity).toEqual(
|
|
workerMeta?.runnerIdentity,
|
|
);
|
|
} finally {
|
|
await repo.cleanup();
|
|
await isolatedHome.cleanup();
|
|
}
|
|
}, 300_000);
|
|
});
|