/** * Publishing a module from an npm-installed @celilo/e2e (celilo#1309). * * Three monorepo assumptions sat in the publish path, and every one passed * forever in-tree and failed for every consumer: * * - the packaging shell-out built its CLI path as `<@celilo/e2e>/apps/celilo/…`, * a directory no published package has ever contained; * - `registerControlPlane` defaulted to `<@celilo/e2e>/modules/celilo-mgmt`, * and the published package ships `modules/` empty, so the staged * celilo-mgmt.netapp sitting beside it was never used; * - publishing a netapp that was ALREADY in the drop zone copied the file onto * itself. In a consumer install the drop zone is the build-infra cache, so * the natural thing to publish was the thing publishing destroyed: `netapps/` * held 38 files afterwards instead of 39. * * Nothing in the monorepo can reach these: there the drop zone is `uploads/`, a * separate directory, and `apps/celilo` is found by walking up. So each helper * takes its roots as arguments and is driven here against a consumer-shaped * tree. */ import { afterEach, describe, expect, test } from 'bun:test'; import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { controlPlaneModuleSource, placeNetapp, resolveCeliloCli } from './container-manager'; const scratch: string[] = []; function tree(): string { const dir = mkdtempSync(join(tmpdir(), 'publish-consumer-')); scratch.push(dir); return dir; } afterEach(() => { for (const dir of scratch.splice(0)) rmSync(dir, { recursive: true, force: true }); }); /** A consumer install: `netapps/` filled by build-infra, no `modules/`, no `apps/`. */ function consumerPackage(staged: string[]): string { const root = tree(); mkdirSync(join(root, 'netapps')); for (const id of staged) writeFileSync(join(root, 'netapps', `${id}.netapp`), `${id} bytes`); return root; } describe('placeNetapp', () => { /** Records every copy and performs none: what matters is whether one was tried. */ function recorder() { const copies: Array<[string, string]> = []; return { copies, copy: (src: string, dest: string) => void copies.push([src, dest]) }; } // Asserted on the ATTEMPT, not on the file afterwards. Copying a file onto // itself destroyed it under bun 1.3.3 and leaves it intact under 1.4.2 on // APFS, so "the file is still there" passes on this host with the guard // deleted. Measured: that is what the first version of this test did. test('a netapp already in the drop zone is not written to', () => { const root = consumerPackage(['celilo-mgmt']); const staged = join(root, 'netapps', 'celilo-mgmt.netapp'); const { copies, copy } = recorder(); const dest = placeNetapp(staged, join(root, 'netapps'), 'celilo-mgmt', copy); expect(dest).toBe(staged); expect(copies).toEqual([]); }); test('nor is the same file reached by a different spelling of its path', () => { const root = consumerPackage(['celilo-mgmt']); const { copies, copy } = recorder(); placeNetapp( `${root}/netapps/../netapps/celilo-mgmt.netapp`, join(root, 'netapps'), 'celilo-mgmt', copy, ); expect(copies).toEqual([]); }); test('a netapp from anywhere else is copied in under the module id', () => { const root = consumerPackage([]); const built = join(tree(), 'mine-1700000000000.netapp'); writeFileSync(built, 'fresh bytes'); const dest = placeNetapp(built, join(root, 'netapps'), 'mine'); expect(dest).toBe(join(root, 'netapps', 'mine.netapp')); expect(readFileSync(dest, 'utf-8')).toBe('fresh bytes'); expect(existsSync(built)).toBe(true); }); test('a missing drop zone is created', () => { const root = tree(); const built = join(tree(), 'mine.netapp'); writeFileSync(built, 'fresh bytes'); placeNetapp(built, join(root, 'uploads'), 'mine'); expect(readFileSync(join(root, 'uploads', 'mine.netapp'), 'utf-8')).toBe('fresh bytes'); }); }); describe('controlPlaneModuleSource', () => { test('a consumer install publishes the staged celilo-mgmt.netapp', () => { const root = consumerPackage(['celilo-mgmt']); expect(controlPlaneModuleSource(undefined, root)).toBe( join(root, 'netapps', 'celilo-mgmt.netapp'), ); }); test('the monorepo publishes from module source, staged netapp or not', () => { const repo = tree(); mkdirSync(join(repo, 'modules', 'celilo-mgmt'), { recursive: true }); const pkg = consumerPackage(['celilo-mgmt']); expect(controlPlaneModuleSource(repo, pkg)).toBe(join(repo, 'modules', 'celilo-mgmt')); }); test('with neither, the error names the command that stages it', () => { const root = consumerPackage([]); expect(() => controlPlaneModuleSource(undefined, root)).toThrow(/cele2e build-infra/); }); }); describe('resolveCeliloCli', () => { test('an explicit celilo root wins: its own source tree', () => { expect(resolveCeliloCli('/repo', () => '/elsewhere/index.ts')).toBe( '/repo/apps/celilo/src/cli/index.ts', ); }); test('with no root it resolves the installed @celilo/cli package', () => { const asked: string[] = []; const cli = resolveCeliloCli(undefined, (specifier) => { asked.push(specifier); return '/consumer/node_modules/@celilo/cli/src/cli/index.ts'; }); expect(asked).toEqual(['@celilo/cli/src/cli/index.ts']); expect(cli).toBe('/consumer/node_modules/@celilo/cli/src/cli/index.ts'); }); test('an install without the CLI says which package is missing', () => { const missing = () => { throw new Error("Cannot find module '@celilo/cli/src/cli/index.ts'"); }; expect(() => resolveCeliloCli(undefined, missing)).toThrow(/@celilo\/cli is not installed/); }); // The two halves the fake resolver cannot see: the dependency is declared, so // a consumer's install actually contains the package, and the entry point the // specifier names really is in it. test('@celilo/e2e declares @celilo/cli, and the default resolver finds its entry point', () => { const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8')); expect(Object.keys(pkg.dependencies)).toContain('@celilo/cli'); const cli = resolveCeliloCli(undefined); expect(cli.endsWith('/src/cli/index.ts')).toBe(true); expect(existsSync(cli)).toBe(true); }); });