mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-29 06:32:28 +00:00
* fix(install): validate and package Linux ARM64 parser replacements * fix(install): add rebuilt ARM64 grammar binaries * fix(install): place ARM64 binaries in versioned bundle paths * refactor(install): share ARM64 prebuild validation * fix(review): keep grammar rebuild provenance accurate * test: pin audited ARM64 repair provenance (#3601)
482 lines
19 KiB
TypeScript
482 lines
19 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 keeps a 20-minute budget', () => {
|
|
const job = ciJobs['packaged-install-smoke'];
|
|
const setup = job?.steps?.find((step) => step.uses === './.github/actions/setup-gitnexus');
|
|
expect(job?.['timeout-minutes']).toBe(20);
|
|
expect(setup?.with?.['lifecycle-scripts']).toBe('false');
|
|
expect(setup?.with?.build).toBeUndefined();
|
|
});
|
|
|
|
it('exercises the reported native ARM64 runtime alongside Windows and Linux x64', () => {
|
|
const job = ciJobs['packaged-install-smoke'];
|
|
expect(job?.strategy?.matrix?.os).toEqual([
|
|
'windows-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('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');
|
|
});
|
|
});
|