147 lines
6.1 KiB
TypeScript
147 lines
6.1 KiB
TypeScript
/**
|
|
* E-4 — Regression guard for scripts/oss-subtree-split.sh.
|
|
*
|
|
* This is a STATIC analysis of the script + the package structure it
|
|
* operates on. It does NOT actually run `git subtree split` (that's a
|
|
* minutes-long operation per package, gated behind an explicit local
|
|
* invocation). Instead it locks down:
|
|
*
|
|
* 1. The script exists and is executable.
|
|
* 2. The list of forbidden top-level entries the script checks for
|
|
* is the actual list of monorepo-only directories. If we ever
|
|
* add a new monorepo-level dir (e.g. `tools/`) without updating
|
|
* the script's forbidden list, a real subtree-split could leak
|
|
* it; this test surfaces that gap.
|
|
* 3. Every `packages/hive-mind-*` directory contains the package-
|
|
* level files an OSS-mirror expects (package.json + a real src/
|
|
* or index file at minimum). If a package is gutted to `export {}`
|
|
* we don't want the OSS mirror to ship an empty shell.
|
|
* 4. The forbidden list does NOT include legitimate package-internal
|
|
* directories (docs/, assets/, src/, tests/, dist/) — those exist
|
|
* inside packages and must be allowed.
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import { readFileSync, existsSync, statSync, readdirSync } from 'node:fs';
|
|
import { join, resolve } from 'node:path';
|
|
|
|
const REPO_ROOT = resolve(__dirname, '..');
|
|
const SCRIPT_PATH = join(REPO_ROOT, 'scripts', 'oss-subtree-split.sh');
|
|
const PACKAGES_DIR = join(REPO_ROOT, 'packages');
|
|
|
|
describe('oss-subtree-split.sh — static guards', () => {
|
|
it('script exists at the documented path', () => {
|
|
expect(existsSync(SCRIPT_PATH)).toBe(true);
|
|
expect(statSync(SCRIPT_PATH).isFile()).toBe(true);
|
|
});
|
|
|
|
it('script has a shebang for bash', () => {
|
|
const content = readFileSync(SCRIPT_PATH, 'utf-8');
|
|
expect(content.startsWith('#!/usr/bin/env bash')).toBe(true);
|
|
});
|
|
|
|
it('script uses set -euo pipefail (fail-fast)', () => {
|
|
const content = readFileSync(SCRIPT_PATH, 'utf-8');
|
|
expect(content).toMatch(/set -euo pipefail/);
|
|
});
|
|
|
|
it('discovers packages/hive-mind-* directories dynamically (Wave 2/3 auto-included)', () => {
|
|
const content = readFileSync(SCRIPT_PATH, 'utf-8');
|
|
// The default-PACKAGES discovery loop is the seam that picks up
|
|
// newly-added hive-mind-* packages without script edits.
|
|
expect(content).toMatch(/packages\/hive-mind-\*/);
|
|
expect(content).toMatch(/mapfile.*PACKAGES.*ls -1d/);
|
|
});
|
|
});
|
|
|
|
describe('oss-subtree-split.sh — forbidden monorepo-level entries', () => {
|
|
/**
|
|
* Mirror of the script's forbidden-list. If you add a new monorepo-
|
|
* level top-level dir, update BOTH the script and this test (and
|
|
* verify no package legitimately uses that name internally).
|
|
*/
|
|
const FORBIDDEN: readonly string[] = [
|
|
'apps',
|
|
'packages',
|
|
'sidecar',
|
|
'.planning',
|
|
'.scratch',
|
|
'.mind',
|
|
'benchmarks',
|
|
];
|
|
|
|
// .planning / .scratch / .mind are gitignored working dirs — forbidden from
|
|
// the OSS export if present, but legitimately ABSENT on a clean checkout (CI).
|
|
// The "dead guard" existence check therefore applies only to the tracked dirs;
|
|
// a gitignored working dir that's simply not present is fine.
|
|
const GITIGNORED_WORKING_DIRS = new Set(['.planning', '.scratch', '.mind']);
|
|
it('every tracked forbidden entry exists as a monorepo-level dir (otherwise the guard is dead)', () => {
|
|
for (const f of FORBIDDEN) {
|
|
const monorepoLevel = join(REPO_ROOT, f);
|
|
if (GITIGNORED_WORKING_DIRS.has(f) && !existsSync(monorepoLevel)) continue;
|
|
expect(
|
|
existsSync(monorepoLevel),
|
|
`Forbidden entry '${f}' is not present at monorepo root — the script's negative-assertion guard is dead.`,
|
|
).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('script lists every forbidden entry in its loop', () => {
|
|
const content = readFileSync(SCRIPT_PATH, 'utf-8');
|
|
for (const f of FORBIDDEN) {
|
|
// Each forbidden token appears in the `for forbidden in …` loop.
|
|
expect(
|
|
content,
|
|
`Script does not list '${f}' as a forbidden top-level entry.`,
|
|
).toContain(f);
|
|
}
|
|
});
|
|
|
|
it('forbidden list does not include legitimate package-internal dirs', () => {
|
|
const PACKAGE_INTERNAL_DIRS: readonly string[] = ['src', 'tests', 'dist', 'docs', 'assets'];
|
|
const content = readFileSync(SCRIPT_PATH, 'utf-8');
|
|
// Locate the `for forbidden in` line specifically — we want to
|
|
// ensure the FORBIDDEN tokens don't accidentally include things
|
|
// every package has internally.
|
|
const match = content.match(/for forbidden in\s+([^\n;]+)/);
|
|
expect(match).toBeTruthy();
|
|
const tokens = (match![1] ?? '').trim().split(/\s+/);
|
|
for (const internal of PACKAGE_INTERNAL_DIRS) {
|
|
expect(
|
|
tokens.includes(internal),
|
|
`Script forbids '${internal}' but that's a legitimate package-internal dir — splits would always fail.`,
|
|
).toBe(false);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('oss-subtree-split.sh — package-level shape', () => {
|
|
function listHiveMindPackages(): string[] {
|
|
return readdirSync(PACKAGES_DIR)
|
|
.filter((name) => name.startsWith('hive-mind-'))
|
|
.filter((name) => statSync(join(PACKAGES_DIR, name)).isDirectory());
|
|
}
|
|
|
|
it('at least one hive-mind-* package exists (so the script has something to split)', () => {
|
|
expect(listHiveMindPackages().length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it.each(listHiveMindPackages())('package %s has package.json', (name) => {
|
|
const pkgJson = join(PACKAGES_DIR, name, 'package.json');
|
|
expect(existsSync(pkgJson), `${name} missing package.json`).toBe(true);
|
|
});
|
|
|
|
it.each(listHiveMindPackages())('package %s has src/ (real code, not a placeholder)', (name) => {
|
|
const src = join(PACKAGES_DIR, name, 'src');
|
|
expect(existsSync(src), `${name} missing src/`).toBe(true);
|
|
});
|
|
|
|
it.each(listHiveMindPackages())('package %s package.json declares Apache-2.0 license', (name) => {
|
|
const pkg = JSON.parse(readFileSync(join(PACKAGES_DIR, name, 'package.json'), 'utf-8'));
|
|
expect(
|
|
pkg.license,
|
|
`${name} must be Apache-2.0 to ship via the OSS subtree-split (matches the OSS repo's license).`,
|
|
).toBe('Apache-2.0');
|
|
});
|
|
});
|