mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-11-05 07:37:30 +00:00
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
* fix(hooks): skip a pnpm shim that cannot run when picking the runner Fixes #3717 * test(hooks): isolate runner liveness checks Inject pnpm liveness and bunx presence in resolver unit tests so they do not depend on installed package managers. Use fixture-only PATHs and POSIX/Windows launchers to cover failed pnpm probes, banner output, fallback execution, argument forwarding, and child exit codes. * fix(cli): prepare native binary before identity Restore skipped LadybugDB install copies before capturing analyzer identity, while keeping native probes and analyzer imports after it. Exercise real npm, pnpm and Bun tarball installs on Linux, macOS and Windows so runner selection is backed by successful indexing. --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
495 lines
20 KiB
TypeScript
495 lines
20 KiB
TypeScript
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { spawnSync } from 'node:child_process';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { load } from 'js-yaml';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { runWebBuild, shouldBuildWeb, shouldPreserveWebOutput } from '../../scripts/build-web.js';
|
|
|
|
/** Default prepare/build stay CLI-only; the web UI ships only via prepack --web. */
|
|
const REPO_ROOT = path.resolve(__dirname, '../../..');
|
|
const CLI_TSC_JS = 'node ../gitnexus/node_modules/typescript/lib/tsc.js';
|
|
const WEB_TSC_JS = 'node ../gitnexus-web/node_modules/typescript/lib/tsc.js';
|
|
const PACKAGE_JSON = JSON.parse(
|
|
readFileSync(path.join(REPO_ROOT, 'gitnexus/package.json'), 'utf8'),
|
|
) as { scripts?: Record<string, string> };
|
|
const tempDirs: string[] = [];
|
|
|
|
interface WorkflowStep {
|
|
name?: string;
|
|
run?: unknown;
|
|
uses?: string;
|
|
with?: Record<string, unknown>;
|
|
env?: Record<string, unknown>;
|
|
if?: string;
|
|
'working-directory'?: string;
|
|
}
|
|
|
|
interface WorkflowJob {
|
|
'timeout-minutes'?: number;
|
|
strategy?: { matrix?: { os?: string[] } };
|
|
steps?: WorkflowStep[];
|
|
}
|
|
|
|
function jobs(workflowPath: string): Record<string, WorkflowJob> {
|
|
const doc = load(readFileSync(path.join(REPO_ROOT, workflowPath), 'utf8')) as {
|
|
jobs?: Record<string, WorkflowJob>;
|
|
};
|
|
return doc.jobs ?? {};
|
|
}
|
|
|
|
function compositeAction(actionPath: string): {
|
|
inputs?: Record<string, { default?: string }>;
|
|
runs?: { steps?: WorkflowStep[] };
|
|
} {
|
|
return load(readFileSync(path.join(REPO_ROOT, actionPath), 'utf8')) as {
|
|
inputs?: Record<string, { default?: string }>;
|
|
runs?: { steps?: WorkflowStep[] };
|
|
};
|
|
}
|
|
|
|
const ciJobs = jobs('.github/workflows/ci-tests.yml');
|
|
const publishJobs = jobs('.github/workflows/publish.yml');
|
|
const qualityJobs = jobs('.github/workflows/ci-quality.yml');
|
|
const setupGitnexus = compositeAction('.github/actions/setup-gitnexus/action.yml');
|
|
const setupGitnexusWeb = compositeAction('.github/actions/setup-gitnexus-web/action.yml');
|
|
|
|
function stepIndex(steps: WorkflowStep[], predicate: (step: WorkflowStep) => boolean): number {
|
|
return steps.findIndex(predicate);
|
|
}
|
|
|
|
const installsWeb = (step: WorkflowStep) =>
|
|
step['working-directory'] === 'gitnexus-web' && String(step.run ?? '').includes('npm ci');
|
|
|
|
function runWeb(
|
|
fixture: ReturnType<typeof buildFixture>,
|
|
overrides: {
|
|
timeoutMs?: number;
|
|
argv?: string[];
|
|
env?: NodeJS.Dict<string>;
|
|
exec?: (...args: unknown[]) => unknown;
|
|
} = {},
|
|
) {
|
|
return runWebBuild({
|
|
root: fixture.root,
|
|
dist: fixture.dist,
|
|
timeoutMs: 600_000,
|
|
argv: ['node', 'build.js'],
|
|
env: {},
|
|
exec: vi.fn(),
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
function buildFixture({ withWeb = true, withNodeModules = true } = {}) {
|
|
const workspace = mkdtempSync(path.join(os.tmpdir(), 'gitnexus-build-web-'));
|
|
tempDirs.push(workspace);
|
|
|
|
const root = path.join(workspace, 'gitnexus');
|
|
const dist = path.join(root, 'dist');
|
|
const webRoot = path.join(workspace, 'gitnexus-web');
|
|
mkdirSync(dist, { recursive: true });
|
|
|
|
if (withWeb) {
|
|
mkdirSync(path.join(webRoot, 'dist', 'assets'), { recursive: true });
|
|
writeFileSync(path.join(webRoot, 'package.json'), '{}');
|
|
writeFileSync(
|
|
path.join(webRoot, 'dist', 'index.html'),
|
|
'<script src="/assets/app.js"></script>',
|
|
);
|
|
writeFileSync(path.join(webRoot, 'dist', 'assets', 'app.js'), 'export {};');
|
|
if (withNodeModules) mkdirSync(path.join(webRoot, 'node_modules'));
|
|
}
|
|
|
|
return { root, dist, webRoot, webDest: path.join(root, 'web') };
|
|
}
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('gitnexus build scripts', () => {
|
|
it('keeps the default build CLI-only', () => {
|
|
expect(PACKAGE_JSON.scripts?.build).toBe('node scripts/build.js');
|
|
expect(PACKAGE_JSON.scripts?.prepare).toBe('node scripts/build.js');
|
|
expect(PACKAGE_JSON.scripts?.prepare).not.toContain('--web');
|
|
});
|
|
|
|
it('compiles gitnexus-shared with gitnexus TypeScript, not a separate TypeScript 7 install', () => {
|
|
const src = readFileSync(path.join(REPO_ROOT, 'gitnexus/scripts/build.js'), 'utf8');
|
|
expect(src).toContain("path.join(ROOT, 'node_modules', 'typescript', 'lib', 'tsc.js')");
|
|
expect(src).toContain('execFileSync(process.execPath, [tscJs]');
|
|
expect(src).not.toMatch(/node_modules['"]?, ['"]\.bin/);
|
|
expect(src).not.toMatch(/execFileSync\([^)]*tsc\.cmd/);
|
|
expect(src).not.toContain("typescript', 'bin', 'tsc'");
|
|
});
|
|
|
|
it.skipIf(!existsSync(path.join(REPO_ROOT, 'gitnexus/node_modules/typescript/lib/tsc.js')))(
|
|
'can launch TypeScript via node + lib/tsc.js on this OS',
|
|
() => {
|
|
const probe = spawnSync(
|
|
process.execPath,
|
|
[path.join(REPO_ROOT, 'gitnexus/node_modules/typescript/lib/tsc.js'), '--version'],
|
|
{ encoding: 'utf8' },
|
|
);
|
|
expect(probe.status).toBe(0);
|
|
expect(probe.stdout).toMatch(/Version \d+/);
|
|
},
|
|
);
|
|
|
|
it('builds the web UI from prepack, which is what ships the tarball', () => {
|
|
expect(PACKAGE_JSON.scripts?.prepack).toContain('scripts/build.js --web');
|
|
expect(PACKAGE_JSON.scripts?.prepack).toContain('scripts/assert-web-assets.mjs web');
|
|
expect(PACKAGE_JSON.scripts?.['build:web']).toBe('node scripts/build.js --web');
|
|
});
|
|
|
|
it('recognizes only explicit CLI or environment opt-ins', () => {
|
|
expect(shouldBuildWeb(['node', 'build.js'], {})).toBe(false);
|
|
expect(shouldBuildWeb(['node', 'build.js', '--web'], {})).toBe(true);
|
|
expect(shouldBuildWeb(['node', 'build.js'], { GITNEXUS_BUILD_WEB: '1' })).toBe(true);
|
|
expect(shouldBuildWeb(['node', 'build.js'], { GITNEXUS_BUILD_WEB: 'true' })).toBe(false);
|
|
});
|
|
|
|
it('removes stale packaged output from a default build', () => {
|
|
const fixture = buildFixture();
|
|
mkdirSync(fixture.webDest, { recursive: true });
|
|
writeFileSync(path.join(fixture.webDest, 'index.html'), 'stale');
|
|
|
|
const exec = vi.fn();
|
|
const result = runWeb(fixture, { exec });
|
|
|
|
expect(result.status).toBe('skipped');
|
|
expect(exec).not.toHaveBeenCalled();
|
|
expect(existsSync(fixture.webDest)).toBe(false);
|
|
});
|
|
|
|
it('preserves prepack output during npm prepare for pack and publish', () => {
|
|
for (const npmCommand of ['pack', 'publish']) {
|
|
const fixture = buildFixture();
|
|
mkdirSync(fixture.webDest, { recursive: true });
|
|
writeFileSync(path.join(fixture.webDest, 'index.html'), npmCommand);
|
|
|
|
expect(
|
|
shouldPreserveWebOutput({
|
|
npm_lifecycle_event: 'prepare',
|
|
npm_command: npmCommand,
|
|
}),
|
|
).toBe(true);
|
|
runWeb(fixture, {
|
|
env: { npm_lifecycle_event: 'prepare', npm_command: npmCommand },
|
|
});
|
|
|
|
expect(readFileSync(path.join(fixture.webDest, 'index.html'), 'utf8')).toBe(npmCommand);
|
|
}
|
|
});
|
|
|
|
it('fails closed when an explicit web build has no web package', () => {
|
|
const fixture = buildFixture({ withWeb: false });
|
|
expect(() => runWeb(fixture, { argv: ['node', 'build.js', '--web'] })).toThrow(
|
|
'web UI requested, but gitnexus-web was not found',
|
|
);
|
|
});
|
|
|
|
it('builds and copies the web UI with an untimed fallback install', () => {
|
|
const fixture = buildFixture({ withNodeModules: false });
|
|
const exec = vi.fn();
|
|
const result = runWeb(fixture, {
|
|
timeoutMs: 123_456,
|
|
argv: ['node', 'build.js', '--web'],
|
|
exec,
|
|
});
|
|
|
|
expect(exec).toHaveBeenNthCalledWith(1, 'npm ci', {
|
|
cwd: fixture.webRoot,
|
|
stdio: 'inherit',
|
|
});
|
|
expect(exec).toHaveBeenNthCalledWith(2, 'npm run build', {
|
|
cwd: fixture.webRoot,
|
|
stdio: 'inherit',
|
|
timeout: 123_456,
|
|
});
|
|
expect(result.status).toBe('built');
|
|
expect(readFileSync(path.join(fixture.webDest, 'index.html'), 'utf8')).toContain('app.js');
|
|
});
|
|
|
|
it('rejects a packaged web UI with missing referenced assets', () => {
|
|
const fixture = buildFixture();
|
|
const checker = path.join(REPO_ROOT, 'gitnexus/scripts/assert-web-assets.mjs');
|
|
|
|
expect(spawnSync(process.execPath, [checker, path.join(fixture.webRoot, 'dist')]).status).toBe(
|
|
0,
|
|
);
|
|
rmSync(path.join(fixture.webRoot, 'dist', 'assets', 'app.js'));
|
|
|
|
const invalid = spawnSync(process.execPath, [checker, path.join(fixture.webRoot, 'dist')], {
|
|
encoding: 'utf8',
|
|
});
|
|
expect(invalid.status).toBe(1);
|
|
expect(invalid.stderr).toContain('references missing assets');
|
|
|
|
const missingIndex = spawnSync(
|
|
process.execPath,
|
|
[checker, path.join(fixture.webRoot, 'none')],
|
|
{
|
|
encoding: 'utf8',
|
|
},
|
|
);
|
|
expect(missingIndex.status).toBe(1);
|
|
expect(missingIndex.stderr).toContain('missing');
|
|
});
|
|
});
|
|
|
|
describe('workflows that need the web UI install it themselves', () => {
|
|
it('packaged install smoke installs gitnexus-web before npm pack', () => {
|
|
const steps = ciJobs['packaged-install-smoke']?.steps ?? [];
|
|
const webIdx = stepIndex(steps, installsWeb);
|
|
const packIdx = stepIndex(steps, (step) => String(step.run ?? '').includes('npm pack'));
|
|
expect(webIdx).toBeGreaterThanOrEqual(0);
|
|
expect(packIdx).toBeGreaterThan(webIdx);
|
|
});
|
|
|
|
it('packaged install smoke validates web assets in the installed tarball', () => {
|
|
const steps = ciJobs['packaged-install-smoke']?.steps ?? [];
|
|
const artifactCheck = steps.find((step) =>
|
|
String(step.run ?? '').includes('scripts/assert-web-assets.mjs'),
|
|
);
|
|
expect(artifactCheck).toBeTruthy();
|
|
expect(String(artifactCheck?.run)).toContain('$INSTALLED/web');
|
|
});
|
|
|
|
it('publish installs gitnexus-web before it packs the tarball', () => {
|
|
const steps = publishJobs['publish']?.steps ?? [];
|
|
const webIdx = stepIndex(steps, installsWeb);
|
|
const publishIdx = stepIndex(steps, (step) =>
|
|
String(step.run ?? '').includes('npm publish --dry-run'),
|
|
);
|
|
expect(webIdx).toBeGreaterThanOrEqual(0);
|
|
expect(publishIdx).toBeGreaterThan(webIdx);
|
|
});
|
|
|
|
it('node floor compat stays CLI-only — it never installs the web tree', () => {
|
|
const steps = ciJobs['node-floor-compat']?.steps ?? [];
|
|
expect(steps.length).toBeGreaterThan(0);
|
|
expect(steps.filter(installsWeb)).toHaveLength(0);
|
|
});
|
|
|
|
it('packaged install smoke skips a pre-pack CLI build and allows the manager smoke budget', () => {
|
|
const job = ciJobs['packaged-install-smoke'];
|
|
const setup = job?.steps?.find((step) => step.uses === './.github/actions/setup-gitnexus');
|
|
expect(job?.['timeout-minutes']).toBe(30);
|
|
expect(setup?.with?.['lifecycle-scripts']).toBe('false');
|
|
expect(setup?.with?.build).toBeUndefined();
|
|
});
|
|
|
|
it('exercises the reported native ARM64 runtime alongside all three desktop OSes', () => {
|
|
const job = ciJobs['packaged-install-smoke'];
|
|
expect(job?.strategy?.matrix?.os).toEqual([
|
|
'windows-latest',
|
|
'macos-latest',
|
|
'ubuntu-latest',
|
|
'ubuntu-24.04-arm',
|
|
]);
|
|
const steps = job?.steps ?? [];
|
|
const setupIdx = stepIndex(steps, (step) => step.uses === './.github/actions/setup-gitnexus');
|
|
const armNodeIdx = stepIndex(steps, (step) => step.with?.['node-version'] === '22.23.1');
|
|
expect(armNodeIdx).toBeGreaterThan(setupIdx);
|
|
expect(steps[armNodeIdx]?.if).toContain("matrix.os == 'ubuntu-24.04-arm'");
|
|
const assertArch = steps.find((step) => step.name === 'Assert native ARM64 runtime');
|
|
expect(assertArch?.if).toContain("matrix.os == 'ubuntu-24.04-arm'");
|
|
expect(String(assertArch?.run)).toContain("process.arch, 'arm64'");
|
|
expect(String(assertArch?.run)).toContain("process.version, 'v22.23.1'");
|
|
});
|
|
|
|
it('indexes the freshly packed tarball with every manager on all three desktop OSes', () => {
|
|
const steps = ciJobs['packaged-install-smoke']?.steps ?? [];
|
|
const packIdx = stepIndex(steps, (step) => step.name === 'Pack gitnexus tarball');
|
|
const smokeIdx = stepIndex(
|
|
steps,
|
|
(step) => step.name === 'Index the PR tarball with npm, pnpm, and Bun',
|
|
);
|
|
expect(smokeIdx).toBeGreaterThan(packIdx);
|
|
expect(steps[smokeIdx]?.if).toBe("matrix.os != 'ubuntu-24.04-arm'");
|
|
expect(String(steps[smokeIdx]?.run)).toContain('node scripts/smoke-package-managers.mjs');
|
|
});
|
|
|
|
it('keeps consumer compilation guards after packing and checks both npm lockfile paths', () => {
|
|
const steps = ciJobs['packaged-install-smoke']?.steps ?? [];
|
|
const packIdx = stepIndex(steps, (step) => step.name === 'Pack gitnexus tarball');
|
|
const guardIdx = stepIndex(steps, (step) => step.name === 'Reject ARM64 consumer compilation');
|
|
const localIdx = stepIndex(
|
|
steps,
|
|
(step) => step.name === 'Lock and install tarball in a fresh consumer project',
|
|
);
|
|
const globalIdx = stepIndex(
|
|
steps,
|
|
(step) => step.name === 'Install gitnexus tarball into isolated prefix',
|
|
);
|
|
const checkIdx = stepIndex(
|
|
steps,
|
|
(step) => step.name === 'Assert no consumer compilation attempts',
|
|
);
|
|
expect(guardIdx).toBeGreaterThan(packIdx);
|
|
expect(localIdx).toBeGreaterThan(guardIdx);
|
|
expect(globalIdx).toBeGreaterThan(guardIdx);
|
|
expect(checkIdx).toBeGreaterThan(globalIdx);
|
|
expect(steps[guardIdx]?.if).toContain("matrix.os == 'ubuntu-24.04-arm'");
|
|
expect(steps[checkIdx]?.if).toContain('always()');
|
|
expect(String(steps[localIdx]?.run)).toContain('for NPM_VERSION in 10.9.4 11.9.0');
|
|
expect(String(steps[localIdx]?.run)).toContain('cmp package-lock.json expected-lock.json');
|
|
});
|
|
|
|
it.skipIf(process.platform === 'win32')('records a rejected compiler invocation', () => {
|
|
const steps = ciJobs['packaged-install-smoke']?.steps ?? [];
|
|
const guard = steps.find((step) => step.name === 'Reject ARM64 consumer compilation');
|
|
const check = steps.find((step) => step.name === 'Assert no consumer compilation attempts');
|
|
expect(guard?.run).toBeTypeOf('string');
|
|
expect(check?.run).toBeTypeOf('string');
|
|
const dir = mkdtempSync(path.join(os.tmpdir(), 'gitnexus-compile-guard-'));
|
|
tempDirs.push(dir);
|
|
const githubPath = path.join(dir, 'github-path');
|
|
const githubEnv = path.join(dir, 'github-env');
|
|
const setup = spawnSync('bash', ['-eu', '-o', 'pipefail', '-c', String(guard?.run)], {
|
|
env: { ...process.env, RUNNER_TEMP: dir, GITHUB_PATH: githubPath, GITHUB_ENV: githubEnv },
|
|
encoding: 'utf8',
|
|
});
|
|
expect(setup.status, setup.stderr).toBe(0);
|
|
const guardEnv = Object.fromEntries(
|
|
readFileSync(githubEnv, 'utf8')
|
|
.trim()
|
|
.split('\n')
|
|
.map((line) => {
|
|
const separator = line.indexOf('=');
|
|
return [line.slice(0, separator), line.slice(separator + 1)];
|
|
}),
|
|
);
|
|
const env = {
|
|
...process.env,
|
|
...guardEnv,
|
|
PATH: `${readFileSync(githubPath, 'utf8').trim()}${path.delimiter}${process.env.PATH}`,
|
|
};
|
|
const clean = spawnSync('bash', ['-eu', '-o', 'pipefail', '-c', String(check?.run)], {
|
|
env,
|
|
encoding: 'utf8',
|
|
});
|
|
expect(clean.status, clean.stderr).toBe(0);
|
|
writeFileSync(path.join(dir, 'probe.c'), 'int main(void) { return 0; }\n');
|
|
const compile = spawnSync('cc', ['-c', 'probe.c', '-o', 'probe.o'], {
|
|
cwd: dir,
|
|
env,
|
|
encoding: 'utf8',
|
|
});
|
|
expect(compile.status).toBe(97);
|
|
expect(compile.stderr).toContain('Unexpected native compilation');
|
|
expect(existsSync(path.join(dir, 'probe.o'))).toBe(false);
|
|
const attempted = spawnSync('bash', ['-eu', '-o', 'pipefail', '-c', String(check?.run)], {
|
|
env,
|
|
encoding: 'utf8',
|
|
});
|
|
expect(attempted.status).toBe(1);
|
|
expect(attempted.stderr).toContain('Unexpected native compilation');
|
|
});
|
|
});
|
|
|
|
describe('setup-gitnexus job budget', () => {
|
|
it('does not npm-ci gitnexus-shared (TypeScript 7 optional-platform install stalls CI)', () => {
|
|
const shared = setupGitnexus.runs?.steps?.find((step) => step.name === 'Build gitnexus-shared');
|
|
expect(String(shared?.run)).toBe(CLI_TSC_JS);
|
|
expect(String(shared?.run)).not.toContain('.bin');
|
|
expect(shared?.if).toContain("lifecycle-scripts == 'false'");
|
|
expect(
|
|
setupGitnexus.runs?.steps?.some(
|
|
(step) =>
|
|
step['working-directory'] === 'gitnexus-shared' &&
|
|
String(step.run ?? '').includes('npm ci'),
|
|
),
|
|
).toBe(false);
|
|
expect(setupGitnexus.inputs?.['lifecycle-scripts']?.default).toBe('true');
|
|
expect(
|
|
setupGitnexus.runs?.steps?.some((step) =>
|
|
String(step.run ?? '').includes('--ignore-scripts'),
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it('setup-gitnexus-web compiles shared with the web TypeScript and skips Playwright browsers', () => {
|
|
const setupNode = setupGitnexusWeb.runs?.steps?.find((step) =>
|
|
String(step.uses ?? '').startsWith('actions/setup-node@'),
|
|
);
|
|
const shared = setupGitnexusWeb.runs?.steps?.find(
|
|
(step) => step.name === 'Build gitnexus-shared',
|
|
);
|
|
const webInstall = setupGitnexusWeb.runs?.steps?.find(
|
|
(step) => step.name === 'Install web dependencies',
|
|
);
|
|
expect(String(setupNode?.with?.['cache-dependency-path'])).toBe(
|
|
'gitnexus-web/package-lock.json',
|
|
);
|
|
expect(String(shared?.run)).toBe(WEB_TSC_JS);
|
|
expect(String(shared?.run)).not.toContain('.bin');
|
|
expect(String(shared?.run)).not.toContain('npm ci');
|
|
expect(webInstall?.env?.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD).toBe('1');
|
|
});
|
|
|
|
it('Vercel installs web first and compiles shared with the web TypeScript', () => {
|
|
const vercel = JSON.parse(
|
|
readFileSync(path.join(REPO_ROOT, 'gitnexus-web/vercel.json'), 'utf8'),
|
|
) as { installCommand?: string };
|
|
const install = String(vercel.installCommand);
|
|
// Vercel runs installCommand with NODE_ENV=production, so npm ci drops
|
|
// typescript unless --include=dev is on that install (not a later step).
|
|
expect(install).toContain('npm ci --include=dev');
|
|
expect(install).toContain('gitnexus-shared');
|
|
expect(install.indexOf('npm ci --include=dev')).toBeLessThan(
|
|
install.indexOf('gitnexus-shared'),
|
|
);
|
|
expect(install).toContain(WEB_TSC_JS);
|
|
expect(install).not.toMatch(/gitnexus-shared[^&]*npm (?:ci|install)/);
|
|
});
|
|
|
|
it('quality typecheck skips prepare/postinstall so tsc --noEmit fits in 10 minutes', () => {
|
|
const job = qualityJobs.typecheck;
|
|
const setup = job?.steps?.find((step) => step.uses === './.github/actions/setup-gitnexus');
|
|
expect(job?.['timeout-minutes']).toBe(10);
|
|
expect(setup?.with?.['lifecycle-scripts']).toBe('false');
|
|
});
|
|
|
|
it('web app tsconfig typechecks React JSX on TypeScript 7 without baseUrl', () => {
|
|
const tsconfig = JSON.parse(
|
|
readFileSync(path.join(REPO_ROOT, 'gitnexus-web/tsconfig.app.json'), 'utf8'),
|
|
) as {
|
|
compilerOptions?: {
|
|
baseUrl?: string;
|
|
jsx?: string;
|
|
jsxImportSource?: string;
|
|
lib?: string[];
|
|
rootDir?: string;
|
|
types?: string[];
|
|
};
|
|
};
|
|
const options = tsconfig.compilerOptions ?? {};
|
|
expect(options.baseUrl).toBeUndefined();
|
|
expect(options.jsx).toBe('react-jsx');
|
|
expect(options.jsxImportSource).toBe('react');
|
|
expect(options.lib).toEqual(expect.arrayContaining(['ESNext', 'DOM', 'DOM.Iterable']));
|
|
expect(options.rootDir).toBe('./src');
|
|
expect(options.types).toEqual(['vite/client']);
|
|
});
|
|
|
|
it('quality typecheck-web can finish a cold web install instead of canceling before cache save', () => {
|
|
expect(qualityJobs['typecheck-web']?.['timeout-minutes']).toBe(15);
|
|
});
|
|
|
|
it('quality format matches lint budget and skips husky during npm ci', () => {
|
|
const formatCi = qualityJobs.format?.steps?.find((step) =>
|
|
String(step.run ?? '').includes('npm ci'),
|
|
);
|
|
const lintCi = qualityJobs.lint?.steps?.find((step) =>
|
|
String(step.run ?? '').includes('npm ci'),
|
|
);
|
|
expect(qualityJobs.format?.['timeout-minutes']).toBe(10);
|
|
expect(qualityJobs.lint?.['timeout-minutes']).toBe(10);
|
|
expect(String(formatCi?.run)).toContain('--ignore-scripts');
|
|
expect(String(lintCi?.run)).toContain('--ignore-scripts');
|
|
});
|
|
});
|