Skip to content

Commit 046b971

Browse files
committed
refactor(server-test): extract shared runCli helper, use template literal for runner script
- Moved runCli() from inline per-suite to a shared module-level function that takes (distDir, tmpHome, ...args) — eliminates 60 lines of duplication - Replaced string[] array joined with newlines with a readable template literal (RUNNER_SCRIPT constant) - Fixed duplicate JSDoc comment on setupAppstashContext - Removed stale Suite 3 reference from file header
1 parent a4ea714 commit 046b971

1 file changed

Lines changed: 106 additions & 131 deletions

File tree

graphql/server-test/__tests__/cli-e2e.test.ts

Lines changed: 106 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,8 @@
1818
*
1919
* Suite 2 — Articles (search-seed):
2020
* 5 articles with tsvector, pg_trgm, optional pgvector columns
21-
* Tests: search subcommand, tsvector search, trgm fuzzy matching,
22-
* composite fullTextSearch, search+pagination, pgvector (conditional)
23-
*
24-
* Suite 3 — Blueprint generation:
25-
* Tests: generate-types with live _meta, graceful fallback without --meta
21+
* Tests: tsvector search, trgm fuzzy matching, composite fullTextSearch,
22+
* search+pagination, pgvector error handling, schema introspection
2623
*/
2724

2825
import path from 'path';
@@ -284,79 +281,95 @@ function setupAppstashContext(
284281
}
285282
}
286283

284+
/**
285+
* Bootstrap script written to disk for the child process.
286+
* Requires the generated CLI commands and executes them in non-interactive mode.
287+
*/
288+
const RUNNER_SCRIPT = `
289+
const { parseArgv, Inquirerer } = require('inquirerer');
290+
const { commands } = require('./cli/commands');
291+
292+
const argv = parseArgv(process.argv);
293+
const prompter = new Inquirerer({
294+
input: process.stdin,
295+
output: process.stdout,
296+
noTty: true,
297+
});
298+
299+
commands(argv, prompter, { noTty: true })
300+
.then(() => process.exit(0))
301+
.catch((e) => {
302+
console.error(e.message);
303+
process.exit(1);
304+
});
305+
`.trimStart();
306+
307+
/**
308+
* Run the compiled CLI as a child process.
309+
* Uses spawn (not execFileSync) so the Node.js event loop stays unblocked —
310+
* the GraphQL server in this process can respond to requests.
311+
*/
312+
function runCli(
313+
distDir: string,
314+
tmpHome: string,
315+
...args: string[]
316+
): Promise<string> {
317+
const runnerPath = path.join(distDir, '_runner.js');
318+
if (!fs.existsSync(runnerPath)) {
319+
fs.writeFileSync(runnerPath, RUNNER_SCRIPT, 'utf-8');
320+
}
321+
322+
return new Promise<string>((resolve, reject) => {
323+
const child = spawn(
324+
process.execPath,
325+
[runnerPath, ...args],
326+
{
327+
env: {
328+
...process.env,
329+
APPSTASH_BASE_DIR: tmpHome,
330+
NODE_PATH: [
331+
distDir,
332+
...resolveNodePaths(),
333+
].join(path.delimiter),
334+
},
335+
stdio: ['pipe', 'pipe', 'pipe'],
336+
},
337+
);
338+
339+
let stdout = '';
340+
let stderr = '';
341+
child.stdout.on('data', (chunk: Buffer) => {
342+
stdout += chunk.toString();
343+
});
344+
child.stderr.on('data', (chunk: Buffer) => {
345+
stderr += chunk.toString();
346+
});
347+
348+
const timer = setTimeout(() => {
349+
child.kill();
350+
reject(new Error(`CLI timed out after 30s.\nstdout: ${stdout}\nstderr: ${stderr}`));
351+
}, 30000);
352+
353+
child.on('close', (code) => {
354+
clearTimeout(timer);
355+
if (code !== 0) {
356+
reject(new Error(`CLI exited with code ${code}.\nstdout: ${stdout}\nstderr: ${stderr}`));
357+
} else {
358+
resolve(stdout);
359+
}
360+
});
361+
362+
child.stdin.end();
363+
});
364+
}
365+
287366
describe('CLI E2E — generated CLI against real DB', () => {
288367
let server: ServerInfo;
289368
let teardown: () => Promise<void>;
290369
let tmpDir: string;
291370
let tmpHome: string;
292371
let distDir: string;
293372

294-
/**
295-
* Run the compiled CLI as a child process (async).
296-
* Uses spawn instead of execFileSync so the Node.js event loop stays
297-
* unblocked — the GraphQL server in this process can respond to requests.
298-
*/
299-
function runCli(...args: string[]): Promise<string> {
300-
const runnerPath = path.join(distDir, '_runner.js');
301-
if (!fs.existsSync(runnerPath)) {
302-
fs.writeFileSync(
303-
runnerPath,
304-
[
305-
"const { parseArgv, Inquirerer } = require('inquirerer');",
306-
"const { commands } = require('./cli/commands');",
307-
'const argv = parseArgv(process.argv);',
308-
'const prompter = new Inquirerer({ input: process.stdin, output: process.stdout, noTty: true });',
309-
"commands(argv, prompter, { noTty: true }).then(() => process.exit(0)).catch(e => { console.error(e.message); process.exit(1); });",
310-
].join('\n'),
311-
'utf-8',
312-
);
313-
}
314-
315-
return new Promise<string>((resolve, reject) => {
316-
const child = spawn(
317-
process.execPath,
318-
[runnerPath, ...args],
319-
{
320-
env: {
321-
...process.env,
322-
APPSTASH_BASE_DIR: tmpHome,
323-
NODE_PATH: [
324-
distDir,
325-
...resolveNodePaths(),
326-
].join(path.delimiter),
327-
},
328-
stdio: ['pipe', 'pipe', 'pipe'],
329-
},
330-
);
331-
332-
let stdout = '';
333-
let stderr = '';
334-
child.stdout.on('data', (chunk: Buffer) => {
335-
stdout += chunk.toString();
336-
});
337-
child.stderr.on('data', (chunk: Buffer) => {
338-
stderr += chunk.toString();
339-
});
340-
341-
const timer = setTimeout(() => {
342-
child.kill();
343-
reject(new Error(`CLI timed out after 30s.\nstdout: ${stdout}\nstderr: ${stderr}`));
344-
}, 30000);
345-
346-
child.on('close', (code) => {
347-
clearTimeout(timer);
348-
if (code !== 0) {
349-
reject(new Error(`CLI exited with code ${code}.\nstdout: ${stdout}\nstderr: ${stderr}`));
350-
} else {
351-
resolve(stdout);
352-
}
353-
});
354-
355-
// Close stdin immediately — CLI runs in non-interactive mode
356-
child.stdin.end();
357-
});
358-
}
359-
360373
beforeAll(async () => {
361374
// 1. Spin up real DB + GraphQL server with simple-seed fixture
362375
const conn = await getConnections(
@@ -417,6 +430,8 @@ describe('CLI E2E — generated CLI against real DB', () => {
417430

418431
it('should list with --limit, --where (dot-notation), and --fields', async () => {
419432
const output = await runCli(
433+
distDir,
434+
tmpHome,
420435
'animal',
421436
'list',
422437
'--limit',
@@ -451,6 +466,8 @@ describe('CLI E2E — generated CLI against real DB', () => {
451466
it('should support cursor-based forward pagination (--after)', async () => {
452467
// First page: get 2 records
453468
const page1Output = await runCli(
469+
distDir,
470+
tmpHome,
454471
'animal',
455472
'list',
456473
'--limit',
@@ -469,6 +486,8 @@ describe('CLI E2E — generated CLI against real DB', () => {
469486

470487
// Second page: use the endCursor
471488
const page2Output = await runCli(
489+
distDir,
490+
tmpHome,
472491
'animal',
473492
'list',
474493
'--limit',
@@ -497,6 +516,8 @@ describe('CLI E2E — generated CLI against real DB', () => {
497516

498517
it('should find-first with --where.name.equalTo', async () => {
499518
const output = await runCli(
519+
distDir,
520+
tmpHome,
500521
'animal',
501522
'find-first',
502523
'--where.name.equalTo',
@@ -521,6 +542,8 @@ describe('CLI E2E — generated CLI against real DB', () => {
521542

522543
it('should combine --where + --orderBy + --fields for sorted filtered results', async () => {
523544
const output = await runCli(
545+
distDir,
546+
tmpHome,
524547
'animal',
525548
'list',
526549
'--where.species.equalTo',
@@ -550,6 +573,8 @@ describe('CLI E2E — generated CLI against real DB', () => {
550573

551574
it('should handle empty result sets gracefully', async () => {
552575
const output = await runCli(
576+
distDir,
577+
tmpHome,
553578
'animal',
554579
'list',
555580
'--where.species.equalTo',
@@ -734,66 +759,6 @@ describe('CLI E2E — search commands against real DB', () => {
734759
let distDir: string;
735760
let hasVector = false;
736761

737-
function runCli(...args: string[]): Promise<string> {
738-
const runnerPath = path.join(distDir, '_runner.js');
739-
if (!fs.existsSync(runnerPath)) {
740-
fs.writeFileSync(
741-
runnerPath,
742-
[
743-
"const { parseArgv, Inquirerer } = require('inquirerer');",
744-
"const { commands } = require('./cli/commands');",
745-
'const argv = parseArgv(process.argv);',
746-
'const prompter = new Inquirerer({ input: process.stdin, output: process.stdout, noTty: true });',
747-
"commands(argv, prompter, { noTty: true }).then(() => process.exit(0)).catch(e => { console.error(e.message); process.exit(1); });",
748-
].join('\n'),
749-
'utf-8',
750-
);
751-
}
752-
753-
return new Promise<string>((resolve, reject) => {
754-
const child = spawn(
755-
process.execPath,
756-
[runnerPath, ...args],
757-
{
758-
env: {
759-
...process.env,
760-
APPSTASH_BASE_DIR: tmpHome,
761-
NODE_PATH: [
762-
distDir,
763-
...resolveNodePaths(),
764-
].join(path.delimiter),
765-
},
766-
stdio: ['pipe', 'pipe', 'pipe'],
767-
},
768-
);
769-
770-
let stdout = '';
771-
let stderr = '';
772-
child.stdout.on('data', (chunk: Buffer) => {
773-
stdout += chunk.toString();
774-
});
775-
child.stderr.on('data', (chunk: Buffer) => {
776-
stderr += chunk.toString();
777-
});
778-
779-
const timer = setTimeout(() => {
780-
child.kill();
781-
reject(new Error(`CLI timed out after 30s.\nstdout: ${stdout}\nstderr: ${stderr}`));
782-
}, 30000);
783-
784-
child.on('close', (code) => {
785-
clearTimeout(timer);
786-
if (code !== 0) {
787-
reject(new Error(`CLI exited with code ${code}.\nstdout: ${stdout}\nstderr: ${stderr}`));
788-
} else {
789-
resolve(stdout);
790-
}
791-
});
792-
793-
child.stdin.end();
794-
});
795-
}
796-
797762
beforeAll(async () => {
798763
// 1. Spin up real DB + GraphQL server with search-seed fixture
799764
const conn = await getConnections(
@@ -872,6 +837,8 @@ describe('CLI E2E — search commands against real DB', () => {
872837

873838
it('should filter articles by tsvector search via --where.tsvTsv', async () => {
874839
const output = await runCli(
840+
distDir,
841+
tmpHome,
875842
'article',
876843
'list',
877844
'--where.tsvTsv',
@@ -902,6 +869,8 @@ describe('CLI E2E — search commands against real DB', () => {
902869

903870
it('should filter articles by trgm similarity via dot-notation where', async () => {
904871
const output = await runCli(
872+
distDir,
873+
tmpHome,
905874
'article',
906875
'list',
907876
'--where.trgmTitle.value',
@@ -931,6 +900,8 @@ describe('CLI E2E — search commands against real DB', () => {
931900

932901
it('should filter via fullTextSearch composite filter', async () => {
933902
const output = await runCli(
903+
distDir,
904+
tmpHome,
934905
'article',
935906
'list',
936907
'--where.fullTextSearch',
@@ -962,6 +933,8 @@ describe('CLI E2E — search commands against real DB', () => {
962933

963934
it('should combine search filter with --limit for paginated results', async () => {
964935
const output = await runCli(
936+
distDir,
937+
tmpHome,
965938
'article',
966939
'list',
967940
'--where.tsvTsv',
@@ -998,6 +971,8 @@ describe('CLI E2E — search commands against real DB', () => {
998971
// The GraphQL server should reject it with a type error.
999972
// The CLI still exits 0 but returns { ok: false, errors: [...] }.
1000973
const output = await runCli(
974+
distDir,
975+
tmpHome,
1001976
'article',
1002977
'list',
1003978
'--where.vectorEmbedding.vector',

0 commit comments

Comments
 (0)