diff --git a/.changeset/quiet-starter-proof.md b/.changeset/quiet-starter-proof.md new file mode 100644 index 000000000..3773ce808 --- /dev/null +++ b/.changeset/quiet-starter-proof.md @@ -0,0 +1,5 @@ +--- +"@rawsql-ts/ztd-cli": patch +--- + +Tighten the starter first-run experience by keeping the generated smoke QuerySpec typecheckable, wrapping aggregate Postgres connection failures with concise recovery steps, and extending publish artifact verification to run starter typecheck, DB-free smoke tests, and ztd-config before release. diff --git a/packages/ztd-cli/templates/src/features/smoke/queries/smoke/boundary.ts b/packages/ztd-cli/templates/src/features/smoke/queries/smoke/boundary.ts index da6043731..5c37104f8 100644 --- a/packages/ztd-cli/templates/src/features/smoke/queries/smoke/boundary.ts +++ b/packages/ztd-cli/templates/src/features/smoke/queries/smoke/boundary.ts @@ -6,7 +6,7 @@ import { loadSqlResource } from '#features/_shared/loadSqlResource.js'; const smokeSqlResource = loadSqlResource(dirname(fileURLToPath(import.meta.url)), 'smoke.sql'); -export interface SmokeQueryParams { +export interface SmokeQueryParams extends Record { user_id: number; } diff --git a/packages/ztd-cli/templates/tests/support/ztd/verifier.ts b/packages/ztd-cli/templates/tests/support/ztd/verifier.ts index 05e918f6e..c0e3705d7 100644 --- a/packages/ztd-cli/templates/tests/support/ztd/verifier.ts +++ b/packages/ztd-cli/templates/tests/support/ztd/verifier.ts @@ -111,8 +111,8 @@ export async function verifyQuerySpecZtdCase (innerError instanceof Error ? innerError.message : String(innerError))) + .filter((message) => message.trim().length > 0); + + if (messages.length > 0) { + return messages.join('; '); + } + } + + if (error instanceof Error && error.message.trim().length > 0) { + return error.message; + } + + return String(error); +} + function isStarterDbConnectionFailure(error: unknown): boolean { if (typeof error !== 'object' || error === null) { return false; } + if ( + 'errors' in error && + Array.isArray((error as { errors?: unknown }).errors) && + (error as { errors: unknown[] }).errors.some((innerError) => isStarterDbConnectionFailure(innerError)) + ) { + return true; + } + + if ('cause' in error && isStarterDbConnectionFailure((error as { cause?: unknown }).cause)) { + return true; + } + const code = 'code' in error ? String((error as { code?: unknown }).code) : ''; if (['ECONNREFUSED', 'ENOTFOUND', 'ETIMEDOUT', 'ECONNRESET', 'EAI_AGAIN', '28P01', '3D000'].includes(code)) { return true; diff --git a/packages/ztd-cli/tests/init.command.test.ts b/packages/ztd-cli/tests/init.command.test.ts index 0d8a7ee33..cf0941e8a 100644 --- a/packages/ztd-cli/tests/init.command.test.ts +++ b/packages/ztd-cli/tests/init.command.test.ts @@ -264,6 +264,9 @@ test('init starter bootstraps compose, starter DDL, and smoke tests without visi expect(readNormalizedFile(path.join(workspace, 'src', 'features', 'smoke', 'queries', 'smoke', 'smoke.sql'))).toContain('where user_id = :user_id::integer'); expect(readNormalizedFile(path.join(workspace, 'src', 'features', 'smoke', 'boundary.ts'))).toContain('executeSmokeQuerySpec'); expect(readNormalizedFile(path.join(workspace, 'src', 'features', 'smoke', 'queries', 'smoke', 'boundary.ts'))).toContain('loadSqlResource'); + expect(readNormalizedFile(path.join(workspace, 'src', 'features', 'smoke', 'queries', 'smoke', 'boundary.ts'))).toContain( + 'export interface SmokeQueryParams extends Record' + ); expect(readNormalizedFile(path.join(workspace, '.ztd', 'support', 'setup-env.ts'))).toContain('ZTD_DB_PORT'); expect(readNormalizedFile(path.join(workspace, '.ztd', 'support', 'setup-env.ts'))).toContain('ZTD_DB_URL'); expect(readNormalizedFile(path.join(workspace, '.ztd', 'support', 'postgres-testkit.ts'))).toContain('createPostgresTestkitClient'); diff --git a/packages/ztd-cli/tests/publishWorkflow.unit.test.ts b/packages/ztd-cli/tests/publishWorkflow.unit.test.ts index e2ef902de..4c9e8f767 100644 --- a/packages/ztd-cli/tests/publishWorkflow.unit.test.ts +++ b/packages/ztd-cli/tests/publishWorkflow.unit.test.ts @@ -57,7 +57,11 @@ test('standalone pnpm proof apps use the installed ztd bin helper instead of pnp publishedPackageModeScript.indexOf('function verifyPnpmAdapterInstall(packages) {'), ); expect(starterSection).toContain('runInstalledZtdCli(appDir,'); - expect(starterSection).not.toContain('"exec"'); + expect(starterSection).not.toContain('"exec",\n "ztd"'); + expect(starterSection).toContain('"tsc", "--noEmit", "-p", "tsconfig.json"'); + expect(starterSection).toContain('"src/features/smoke/tests/smoke.boundary.test.ts"'); + expect(starterSection).toContain('"src/features/smoke/tests/smoke.validation.test.ts"'); + expect(starterSection).toContain('runInstalledZtdCli(appDir, ["ztd-config"])'); const adapterSection = publishedPackageModeScript.slice( publishedPackageModeScript.indexOf('function verifyPnpmAdapterInstall(packages) {'), diff --git a/packages/ztd-cli/tests/ztdVerifier.unit.test.ts b/packages/ztd-cli/tests/ztdVerifier.unit.test.ts index 6f7b6956a..6a12c5ab8 100644 --- a/packages/ztd-cli/tests/ztdVerifier.unit.test.ts +++ b/packages/ztd-cli/tests/ztdVerifier.unit.test.ts @@ -108,6 +108,43 @@ test('verifyQuerySpecTraditionalCase adds starter recovery steps when Postgres i ).rejects.toThrow(/localhost:55433[\s\S]*docker compose up -d/); }); +test('verifyQuerySpecZtdCase wraps AggregateError connection failures with starter recovery steps', async () => { + process.env.ZTD_DB_URL = 'postgres://ztd:ztd@localhost:15432/ztd'; + const aggregateError = new AggregateError( + [ + Object.assign(new Error('connect ECONNREFUSED ::1:15432'), { + code: 'ECONNREFUSED', + address: '::1', + port: 15432 + }), + Object.assign(new Error('connect ECONNREFUSED 127.0.0.1:15432'), { + code: 'ECONNREFUSED', + address: '127.0.0.1', + port: 15432 + }) + ], + '' + ); + createPostgresTestkitClientMock.mockReturnValue({ + query: vi.fn().mockRejectedValue(aggregateError), + close: closeMock + }); + + const { verifyQuerySpecZtdCase } = await import('../templates/tests/support/ztd/verifier'); + + await expect( + verifyQuerySpecZtdCase( + { + name: 'aggregate-db-down', + beforeDb: {}, + input: {}, + output: { ok: true } + }, + (client) => client.query('select 1', {}) + ) + ).rejects.toThrow(/localhost:15432[\s\S]*docker compose up -d[\s\S]*ZTD_DB_PORT/); +}); + test('verifyQuerySpecTraditionalCase physically prepares fixtures and returns traditional evidence', async () => { const rootDir = mkdtempSync(path.join(tmpdir(), 'ztd-verifier-project-')); tempDirs.push(rootDir); diff --git a/scripts/verify-published-package-mode.mjs b/scripts/verify-published-package-mode.mjs index 26b33d3b1..71fc845d0 100644 --- a/scripts/verify-published-package-mode.mjs +++ b/scripts/verify-published-package-mode.mjs @@ -663,6 +663,15 @@ function verifyPnpmStarterPath(packages) { // Rebind workspace packages to the freshly packed tarballs so the scaffold install exercises the published manifests. fs.writeFileSync(path.join(appDir, "package.json"), `${JSON.stringify(scaffoldPackageJson, null, 2)}\n`, "utf8"); runIn(appDir, PNPM, ["install", "--no-frozen-lockfile"]); + runIn(appDir, PNPM, ["exec", "tsc", "--noEmit", "-p", "tsconfig.json"]); + runIn(appDir, PNPM, [ + "exec", + "vitest", + "run", + "src/features/smoke/tests/smoke.boundary.test.ts", + "src/features/smoke/tests/smoke.validation.test.ts", + ]); + runInstalledZtdCli(appDir, ["ztd-config"]); return appDir; }