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
38 changes: 19 additions & 19 deletions packages/sdk/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@
"license": "Apache-2.0",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.30.0",
"@relayfile/adapter-core": "0.8.5",
"@relayfile/relay-helpers": "0.6.1",
"@relayfile/adapter-core": "0.8.7",
"@relayfile/relay-helpers": "0.6.2",
"@relayflows/surface": "2.0.66",
"@types/js-yaml": "^4.0.9",
"acorn": "^8.18.0",
Expand Down
81 changes: 80 additions & 1 deletion packages/sdk/tests/bin.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import {
mkdtempSync,
cpSync,
existsSync,
lstatSync,
mkdirSync,
readFileSync,
realpathSync,
rmSync,
statSync,
symlinkSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { dirname, join, relative, isAbsolute, sep } from 'node:path';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { afterEach, describe, expect, it } from 'vitest';
Expand All @@ -32,6 +37,49 @@ function temporaryDirectory(): string {
return directory;
}

/** Stage owned package payloads so npm 10's prepare hook cannot rebuild shared dist. */
function installSurfacePackage(directory: string): void {
const source = realpathSync(join(SDK, 'node_modules/@relayflows/surface'));
const location = relative(realpathSync(ROOT), source);
expect(isAbsolute(location) || location === '..' || location.startsWith(`..${sep}`),
'Surface must come from this owned checkout, never a foreign worktree').toBe(false);
const manifest = JSON.parse(readFileSync(join(source, 'package.json'), 'utf8'));
expect(manifest.name).toBe('@relayflows/surface');
const staging = join(directory, 'surface-package');
mkdirSync(staging);
for (const file of manifest.files as string[]) {
expect(isAbsolute(file) || file.split(/[\\/]/).includes('..')).toBe(false);
const target = join(staging, file);
mkdirSync(dirname(target), { recursive: true });
cpSync(join(source, file), target, { recursive: true, dereference: true,
filter: path => !relative(source, path).split(sep).includes('node_modules') });
}
// Fixture-only lifecycle normalization follows scripts/publish.test.mjs.
// Runtime files, exports, versions and peer contracts still come from Surface.
const stagingManifest = structuredClone(manifest);
for (const hook of ['prepare', 'prepack', 'postpack']) delete stagingManifest.scripts?.[hook];
writeFileSync(join(staging, 'package.json'), JSON.stringify(stagingManifest));
const packed = spawnSync('npm', ['pack', '--ignore-scripts', '--json', '--pack-destination', directory], {
cwd: staging, encoding: 'utf8', timeout: 60_000,
});
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
expect(packed.status, packed.stderr + packed.stdout).toBe(0);
const [{ filename }] = JSON.parse(packed.stdout) as Array<{ filename: string }>;
writeFileSync(join(directory, 'package.json'), '{"private":true,"type":"module"}');
const installed = spawnSync('npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund',
'--omit=dev', '--registry=https://registry.npmjs.org', '--save-exact', join(directory, filename)], {
cwd: directory, encoding: 'utf8', timeout: 90_000,
});
expect(installed.status, installed.stderr + installed.stdout).toBe(0);
const target = join(directory, 'node_modules/@relayflows/surface');
expect(lstatSync(target).isSymbolicLink()).toBe(false);
expect(realpathSync(target)).toBe(join(realpathSync(directory), 'node_modules/@relayflows/surface'));
const installedManifest = JSON.parse(readFileSync(join(target, 'package.json'), 'utf8'));
expect(installedManifest).toEqual(stagingManifest);
expect({ ...installedManifest, scripts: manifest.scripts }).toEqual(manifest);
const helpers = JSON.parse(readFileSync(join(directory, 'node_modules/@relayfile/relay-helpers/package.json'), 'utf8'));
expect(helpers.version).toBe(manifest.peerDependencies['@relayfile/relay-helpers']);
}

function invoke(entry: string, flow: string, env: NodeJS.ProcessEnv = process.env) {
return spawnSync(entry, ['check', flow], {
cwd: ROOT,
Expand All @@ -55,6 +103,37 @@ function expectMissingCliRefusal(result: ReturnType<typeof invoke>): void {
}

describe('built flows binary', () => {
it('loads authored bare package imports in the standalone without enabling dotenv or bunfig', () => {
const directory = temporaryDirectory();
const standalone = join(directory, 'flows');
const built = spawnSync(process.execPath, [STANDALONE_BUILDER,
process.platform === 'darwin' ? 'bun-darwin-arm64' : 'bun-linux-x64', standalone], {
cwd: ROOT, encoding: 'utf8', timeout: 120_000,
env: { ...process.env, FLOWS_BUILD_BUN: process.env['FLOWS_BUILD_BUN'] ?? 'bun' },
});
expect(built.status, built.stderr + built.stdout).toBe(0);
installSurfacePackage(directory);
writeFileSync(join(directory, '.env'), 'FLOWS_STANDALONE_DOTENV_PROBE=unexpected\n');
writeFileSync(join(directory, 'bunfig.toml'), 'preload = ["./must-not-preload.mjs"]\n');
writeFileSync(join(directory, 'must-not-preload.mjs'), "throw new Error('bunfig unexpectedly loaded');\n");
const flow = join(directory, 'physical-surface.flow.ts');
writeFileSync(flow, `import { flow } from '@relayflows/surface';
import { writeFileSync } from 'node:fs';
if (process.env.FLOWS_STANDALONE_DOTENV_PROBE !== undefined) throw new Error('dotenv unexpectedly loaded');
export default flow('physical-surface', async f => {
writeFileSync(${JSON.stringify(join(directory, 'body-executed'))}, 'unexpected');
f.done('success');
});\n`);
const checked = spawnSync(standalone, ['check', '--json', '--no-daemon-check', flow], {
cwd: directory, encoding: 'utf8', timeout: 30_000,
// No NODE_PATH/NODE_OPTIONS or workspace package-resolution fallback.
env: { PATH: process.env.PATH, HOME: directory },
});
expect(checked.status, checked.stderr + checked.stdout).toBe(0);
expect(JSON.parse(checked.stdout)).toMatchObject({ ok: true, path: flow });
expect(existsSync(join(directory, 'body-executed'))).toBe(false);
}, 150_000);

it('build produces an executable CLI artifact', () => {
expect(statSync(BUILT_CLI).mode & 0o111).not.toBe(0);
});
Expand Down
60 changes: 58 additions & 2 deletions packages/sdk/tests/gitlab-helper-path.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,62 @@
import { PreviewTransport, type gitlabClient } from '@relayfile/relay-helpers';
import { helperClients } from '@relayflows/surface/runtime';
import { expect, it } from 'vitest';
import { helperClients, type HelperCall } from '@relayflows/surface/runtime';
import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { afterEach, expect, it, vi } from 'vitest';
import { helperTransport } from '../src/helper-writeback.js';

const directories: string[] = [];
afterEach(() => {
for (const directory of directories.splice(0)) rmSync(directory, { recursive: true, force: true });
vi.unstubAllEnvs();
});

const projectPath = 'acme/platform/web';
const prefix = `/gitlab/projects/${projectPath}`;
const issues = Array.from({ length: 8 }, (_, index) => ({ iid: index + 1, title: `issue ${index + 1}`, state: index === 7 ? 'closed' : 'opened' }));
const requests = [{ iid: 9, title: 'merged request', state: 'merged' }];

function collectionFixtures(): Record<string, unknown> {
return {
...Object.fromEntries(issues.map(issue => [`${prefix}/issues/${issue.iid}__issue/meta.json`, issue])),
[`${prefix}/merge_requests/9__request/meta.json`]: requests[0],
[`${prefix}/issues/_index.json`]: issues.map(({ iid }) => ({ iid })),
[`${prefix}/merge_requests/_index.json`]: [{ iid: 9 }],
[`${prefix}/issues/1__issue/comments/1.json`]: { iid: 'child' },
[`${prefix}/issues/1__issue/children/meta.json`]: { iid: 'grandchild' },
[`${prefix}/issues/by-id/1.json`]: { iid: 'alias' },
[`${prefix}/issues/by-id/1/meta.json`]: { iid: 'directory-alias' },
'/gitlab/projects/acme/other/issues/1__foreign/meta.json': { iid: 'foreign' },
};
}

for (const mode of ['filesystem', 'preview'] as const) {
for (const resource of ['issues', 'merge-requests'] as const) {
it(`${mode} lists exact ${resource} records through the shipped Surface factory`, async () => {
const fixtures = collectionFixtures();
let transport;
if (mode === 'preview') {
transport = new PreviewTransport({ fixtures });
} else {
const directory = mkdtempSync(join(tmpdir(), 'gitlab-helper-collections-'));
directories.push(directory);
for (const [path, value] of Object.entries(fixtures)) {
const target = join(directory, path);
mkdirSync(dirname(target), { recursive: true });
writeFileSync(target, JSON.stringify(value));
}
symlinkSync('1__issue', join(directory, prefix, 'issues', 'linked-record'));
vi.stubEnv('RELAYFILE_MOUNT_PATH', directory);
vi.stubEnv('RELAYFLOWS_GITLAB_MOCK', '0');
const call: HelperCall = { type: 'effect', provider: 'gitlab', verb: `${resource}.list`, args: [{ projectPath }] };
transport = helperTransport(call, directory, 'collection-proof', resource, new AbortController().signal).transport;
}
const client = helperClients.gitlab!({ transport }) as ReturnType<typeof gitlabClient>;
expect(await client[resource].list({ projectPath })).toEqual(resource === 'issues' ? issues : requests);
});
}
}

it('reads and writes the mounted GitLab project through the shipped helper factory', async () => {
const projectPath = 'acme/platform/web';
Expand Down
Loading
Loading