Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions examples/llm-memory-db-mssql/.noorm/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ paths:

build:
include:
- sql/00_types
- sql/01_reference
- sql/02_tables
- sql/03_validators
- sql/04_subtypes
- sql/05_binary
- sql/06_seeds
- sql/07_functions
- sql/08_views
- sql/09_procedures
- 00_types
- 01_reference
- 02_tables
- 03_validators
- 04_subtypes
- 05_binary
- 06_seeds
- 07_functions
- 08_views
- 09_procedures
exclude: []

stages:
Expand Down Expand Up @@ -46,7 +46,7 @@ rules:
match:
isTest: true
include:
- sql/06_seeds
- 06_seeds

strict:
enabled: false
Expand Down
65 changes: 64 additions & 1 deletion packages/cli/scripts/postinstall.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
*/

import { createHash } from 'crypto';
import { createReadStream, createWriteStream, existsSync, chmodSync, mkdirSync, renameSync, unlinkSync } from 'fs';
import { createReadStream, createWriteStream, existsSync, chmodSync, mkdirSync, readFileSync, renameSync, unlinkSync } from 'fs';
import { get as httpsGet } from 'https';
import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
Expand All @@ -19,6 +19,8 @@ const PACKAGE_ROOT = resolve(__dirname, '..');
const BIN_DIR = resolve(PACKAGE_ROOT, 'bin');
const REPO = 'noormdev/noorm';

const ROOT_PACKAGE_NAME = '@noormdev/main';

const BINARY_NAME = process.platform === 'win32' ? 'noorm.exe' : 'noorm';
const DEST = resolve(BIN_DIR, BINARY_NAME);
const DOWNLOAD_DEST = `${DEST}.download`;
Expand All @@ -40,6 +42,62 @@ class ChecksumFailure extends Error {

}

/**
* Local mirror of `attemptSync` from `@logosdx/utils`, which this script
* cannot import: postinstall runs before the workspace is built, and in a
* consumer's tree the dependency need not be installed at all.
*
* Exists so the callers below keep the repo's error-tuple convention instead
* of ad-hoc try/catch control flow -- the one unavoidable try/catch is
* encapsulated here, exactly as the real utility encapsulates it.
*/
function attemptSync(fn) {

try {
return [fn(), null];
}
catch (err) {
return [null, err];
}

}

/**
* Detects the noorm monorepo source checkout, where this script must not run.
*
* The download is pinned to the release tag matching this package's version,
* which in a source checkout is routinely not a published release -- every
* commit after a version bump points at a tag that does not exist yet, and
* tags cut before the release workflow started emitting checksums.txt have
* nothing to verify against. Either way the download fails, and a failed
* verification is a hard failure by design (see main().catch below), so it
* takes `bun install` down with it.
*
* Nothing in the repo needs it: the CLI is built from source here
* (`bun run build:packages`, or `bun run build:binary` for a real binary) and
* the downloaded asset is never executed. Skipping is the whole fix.
*
* Keyed on the private workspace root two levels up, which the npm tarball
* never ships -- an installed copy lives at node_modules/@noormdev/cli, whose
* grandparent is node_modules itself and has no package.json.
*/
function isSourceCheckout() {

const rootPackage = resolve(PACKAGE_ROOT, '..', '..', 'package.json');

if (!existsSync(rootPackage)) {
return false;
}

// An unreadable or malformed package.json two levels up says nothing about
// being in the source repo -- fall through to the normal download path
// rather than silently skipping a consumer's install.
const [pkg] = attemptSync(() => JSON.parse(readFileSync(rootPackage, 'utf8')));

return pkg?.name === ROOT_PACKAGE_NAME;

}

/**
* Resolves the platform suffix used in binary asset names.
*
Expand Down Expand Up @@ -290,6 +348,11 @@ async function verifyChecksum(checksumsUrl, assetName) {
*/
async function main() {

if (isSourceCheckout()) {
console.log('noorm source checkout detected - skipping release binary download (build one with `bun run build:binary`).');
return;
}

const suffix = getPlatformSuffix();
const version = await getVersion();
const assetName = `noorm-${suffix}`;
Expand Down
195 changes: 195 additions & 0 deletions tests/cli/postinstall-source-checkout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/**
* Tests for the npm postinstall binary download (`packages/cli/scripts/postinstall.js`).
*
* The script is spawned as a real `node` process rather than imported: it is
* plain ESM shipped in the npm tarball (the repo does not set `allowJs`), and
* spawning exercises the exact invocation a package manager performs.
*
* The invariant under test is a split one, and both halves matter:
*
* - Inside this monorepo the release binary must NEVER be downloaded. The
* script pins its download to the release tag matching packages/cli's
* version, which in a source checkout is routinely unreleased (any commit
* after a version bump, and every commit before that version is cut). CI
* builds the CLI from source and never executes the downloaded binary, so
* the download is dead weight that can only ever break `bun install`.
*
* - For an npm consumer the download must still happen. A skip check that is
* too broad would silently install the wrapper with no binary behind it,
* which is a worse failure than the one it fixes -- it surfaces at first
* use rather than at install time.
*
* Both cases run against a fabricated directory tree rather than the real
* repo, so neither depends on whether packages/cli/bin happens to be populated
* on the machine running the tests.
*/
import { describe, it, expect, beforeAll, afterAll } from 'bun:test';
import { mkdtemp, mkdir, rm, writeFile, copyFile } from 'fs/promises';
import { existsSync } from 'fs';
import { tmpdir } from 'os';
import { join, resolve } from 'path';

const REAL_SCRIPT = resolve(import.meta.dir, '../../packages/cli/scripts/postinstall.js');

// Matched loosely (case-insensitive substring) so the test pins the behavior,
// not the exact prose of the console message.
const SKIP_MARKER = 'source checkout';
const DOWNLOAD_MARKER = 'Downloading noorm';

let workDir: string;

/**
* Writes a package tree containing a copy of the real postinstall script and
* returns the directory the script lands in.
*
* @param layout directory path, relative to workDir, that plays the role of
* packages/cli -- i.e. where package.json and scripts/postinstall.js go.
* @param rootPkg package.json to write two levels above `layout`, or null to
* leave that level empty (the npm-consumer case).
*/
const fabricate = async (layout: string, rootPkg: Record<string, unknown> | null) => {

const pkgDir = join(workDir, layout);

await mkdir(join(pkgDir, 'scripts'), { recursive: true });
await copyFile(REAL_SCRIPT, join(pkgDir, 'scripts', 'postinstall.js'));

await writeFile(
join(pkgDir, 'package.json'),
JSON.stringify({ name: '@noormdev/cli', version: '1.0.0-alpha.39' }),
);

if (rootPkg) {

await writeFile(
join(pkgDir, '..', '..', 'package.json'),
JSON.stringify(rootPkg),
);

}

return pkgDir;

};

/**
* Runs the fabricated script to completion and returns its output.
*
* Only safe for cases that terminate without network access -- see
* `runUntilDownload` for the consumer case, which does reach the network.
*/
const run = async (pkgDir: string) => {

const proc = Bun.spawn(['node', join(pkgDir, 'scripts', 'postinstall.js')], {
stdout: 'pipe',
stderr: 'pipe',
env: { ...process.env, NOORM_INSECURE: '' },
});

const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);

return { stdout, stderr, exitCode, output: stdout + stderr };

};

/**
* Runs the script only until it announces the download, then kills it.
*
* The consumer path deliberately hits GitHub Releases, which a test must not
* wait on. Reading up to the announcement is enough to prove the skip check
* did not swallow this case, and killing early keeps the test hermetic: the
* assertion holds whether or not the machine has network.
*/
const runUntilDownload = async (pkgDir: string) => {

const proc = Bun.spawn(['node', join(pkgDir, 'scripts', 'postinstall.js')], {
stdout: 'pipe',
stderr: 'pipe',
env: { ...process.env, NOORM_INSECURE: '' },
});

let seen = '';

const reader = proc.stdout.getReader();
const decoder = new TextDecoder();

while (!seen.includes(DOWNLOAD_MARKER)) {

const { done, value } = await reader.read();

if (done) break;

seen += decoder.decode(value, { stream: true });

}

proc.kill();
await proc.exited;

return seen;

};

beforeAll(async () => {

workDir = await mkdtemp(join(tmpdir(), 'noorm-postinstall-'));

});

afterAll(async () => {

await rm(workDir, { recursive: true, force: true });

});

describe('postinstall: binary download', () => {

it('skips the download inside the noorm monorepo source checkout', async () => {

const pkgDir = await fabricate('repo/packages/cli', {
name: '@noormdev/main',
workspaces: ['packages/*'],
});

const { output, exitCode } = await run(pkgDir);

expect(output.toLowerCase()).toContain(SKIP_MARKER);
expect(output).not.toContain(DOWNLOAD_MARKER);

// The whole point: a source checkout must not be able to fail an install.
expect(exitCode).toBe(0);

expect(existsSync(join(pkgDir, 'bin', 'noorm'))).toBe(false);

});

it('still downloads for an npm consumer outside the monorepo', async () => {

const pkgDir = await fabricate('consumer/node_modules/@noormdev/cli', null);

const seen = await runUntilDownload(pkgDir);

expect(seen).toContain(DOWNLOAD_MARKER);
expect(seen.toLowerCase()).not.toContain(SKIP_MARKER);

});

it('does not mistake an unrelated parent package for the monorepo root', async () => {

const pkgDir = await fabricate('other/packages/cli', {
name: '@someoneelse/monorepo',
workspaces: ['packages/*'],
});

const seen = await runUntilDownload(pkgDir);

expect(seen).toContain(DOWNLOAD_MARKER);
expect(seen.toLowerCase()).not.toContain(SKIP_MARKER);

});

});
6 changes: 5 additions & 1 deletion tests/sdk/destructive-ops.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ function makeState(access: ConfigAccess, options: ContextState['options'] = {}):

}

/** Fake Change fixture for delete() gate tests. rm({ force: true }) on a nonexistent path silently no-ops, so a placeholder path is safe for every test except the dedicated disk-mutation block below. */
/**
* Fake Change fixture for delete() gate tests. rm({ force: true }) on a
* nonexistent path silently no-ops, so a placeholder path is safe for every
* test except the dedicated disk-mutation block below.
*/
function makeFakeChange(changePath: string = join(tmpdir(), 'sample-change')): Change {

return {
Expand Down
Loading