fix: resolve smoke test failures -- CLI JSON output, port collision, stale DBs

This commit is contained in:
Matt Kane
2026-04-02 15:30:36 +01:00
parent 01af46fb83
commit 8e28cfc5d6
12 changed files with 64 additions and 9 deletions

View File

@@ -0,0 +1,5 @@
---
"emdash": patch
---
Fix CLI `--json` flag so JSON output is clean. Previously, `consola.success()` and other log messages leaked into stdout alongside the JSON data, making it unparseable by scripts. Log messages now go to stderr when `--json` is set.

View File

@@ -149,7 +149,8 @@
"prepublishOnly": "node --run build", "prepublishOnly": "node --run build",
"typecheck": "tsgo --noEmit", "typecheck": "tsgo --noEmit",
"check": "publint && attw --pack --ignore-rules=cjs-resolves-to-esm --ignore-rules=no-resolution --ignore-rules=internal-resolution-error", "check": "publint && attw --pack --ignore-rules=cjs-resolves-to-esm --ignore-rules=no-resolution --ignore-rules=internal-resolution-error",
"test": "vitest" "test": "vitest",
"test:smoke": "vitest run --config vitest.smoke.config.ts"
}, },
"dependencies": { "dependencies": {
"@emdash-cms/admin": "workspace:*", "@emdash-cms/admin": "workspace:*",

View File

@@ -10,7 +10,7 @@ import { defineCommand } from "citty";
import { consola } from "consola"; import { consola } from "consola";
import { connectionArgs, createClientFromArgs } from "../client-factory.js"; import { connectionArgs, createClientFromArgs } from "../client-factory.js";
import { output } from "../output.js"; import { configureOutputMode, output } from "../output.js";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Helpers // Helpers
@@ -77,6 +77,7 @@ const listCommand = defineCommand({
...connectionArgs, ...connectionArgs,
}, },
async run({ args }) { async run({ args }) {
configureOutputMode(args);
try { try {
const client = createClientFromArgs(args); const client = createClientFromArgs(args);
const result = await client.list(args.collection, { const result = await client.list(args.collection, {
@@ -130,6 +131,7 @@ const getCommand = defineCommand({
...connectionArgs, ...connectionArgs,
}, },
async run({ args }) { async run({ args }) {
configureOutputMode(args);
try { try {
const client = createClientFromArgs(args); const client = createClientFromArgs(args);
const item = await client.get(args.collection, args.id, { const item = await client.get(args.collection, args.id, {
@@ -177,6 +179,7 @@ const createCommand = defineCommand({
...connectionArgs, ...connectionArgs,
}, },
async run({ args }) { async run({ args }) {
configureOutputMode(args);
try { try {
const data = await readInputData(args); const data = await readInputData(args);
const client = createClientFromArgs(args); const client = createClientFromArgs(args);
@@ -229,6 +232,7 @@ const updateCommand = defineCommand({
...connectionArgs, ...connectionArgs,
}, },
async run({ args }) { async run({ args }) {
configureOutputMode(args);
try { try {
const data = await readInputData(args); const data = await readInputData(args);
const client = createClientFromArgs(args); const client = createClientFromArgs(args);
@@ -270,6 +274,7 @@ const deleteCommand = defineCommand({
...connectionArgs, ...connectionArgs,
}, },
async run({ args }) { async run({ args }) {
configureOutputMode(args);
try { try {
const client = createClientFromArgs(args); const client = createClientFromArgs(args);
await client.delete(args.collection, args.id); await client.delete(args.collection, args.id);
@@ -297,6 +302,7 @@ const publishCommand = defineCommand({
...connectionArgs, ...connectionArgs,
}, },
async run({ args }) { async run({ args }) {
configureOutputMode(args);
try { try {
const client = createClientFromArgs(args); const client = createClientFromArgs(args);
await client.publish(args.collection, args.id); await client.publish(args.collection, args.id);
@@ -324,6 +330,7 @@ const unpublishCommand = defineCommand({
...connectionArgs, ...connectionArgs,
}, },
async run({ args }) { async run({ args }) {
configureOutputMode(args);
try { try {
const client = createClientFromArgs(args); const client = createClientFromArgs(args);
await client.unpublish(args.collection, args.id); await client.unpublish(args.collection, args.id);
@@ -356,6 +363,7 @@ const scheduleCommand = defineCommand({
...connectionArgs, ...connectionArgs,
}, },
async run({ args }) { async run({ args }) {
configureOutputMode(args);
try { try {
const client = createClientFromArgs(args); const client = createClientFromArgs(args);
await client.schedule(args.collection, args.id, { at: args.at }); await client.schedule(args.collection, args.id, { at: args.at });
@@ -383,6 +391,7 @@ const restoreCommand = defineCommand({
...connectionArgs, ...connectionArgs,
}, },
async run({ args }) { async run({ args }) {
configureOutputMode(args);
try { try {
const client = createClientFromArgs(args); const client = createClientFromArgs(args);
await client.restore(args.collection, args.id); await client.restore(args.collection, args.id);
@@ -410,6 +419,7 @@ const translationsCommand = defineCommand({
...connectionArgs, ...connectionArgs,
}, },
async run({ args }) { async run({ args }) {
configureOutputMode(args);
try { try {
const client = createClientFromArgs(args); const client = createClientFromArgs(args);
const translations = await client.translations(args.collection, args.id); const translations = await client.translations(args.collection, args.id);

View File

@@ -29,6 +29,7 @@ import {
resolveCredentialKey, resolveCredentialKey,
saveCredentials, saveCredentials,
} from "../credentials.js"; } from "../credentials.js";
import { configureOutputMode } from "../output.js";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Types for discovery + device flow responses // Types for discovery + device flow responses
@@ -423,6 +424,7 @@ export const whoamiCommand = defineCommand({
}, },
}, },
async run({ args }) { async run({ args }) {
configureOutputMode(args);
const baseUrl = args.url || "http://localhost:4321"; const baseUrl = args.url || "http://localhost:4321";
// Resolve token: --token flag > EMDASH_TOKEN env > stored credentials // Resolve token: --token flag > EMDASH_TOKEN env > stored credentials

View File

@@ -11,7 +11,7 @@ import { defineCommand } from "citty";
import { consola } from "consola"; import { consola } from "consola";
import { connectionArgs, createClientFromArgs } from "../client-factory.js"; import { connectionArgs, createClientFromArgs } from "../client-factory.js";
import { output } from "../output.js"; import { configureOutputMode, output } from "../output.js";
const listCommand = defineCommand({ const listCommand = defineCommand({
meta: { meta: {
@@ -34,6 +34,7 @@ const listCommand = defineCommand({
}, },
}, },
async run({ args }) { async run({ args }) {
configureOutputMode(args);
const client = createClientFromArgs(args); const client = createClientFromArgs(args);
try { try {
@@ -73,6 +74,7 @@ const uploadCommand = defineCommand({
}, },
}, },
async run({ args }) { async run({ args }) {
configureOutputMode(args);
const client = createClientFromArgs(args); const client = createClientFromArgs(args);
const filename = basename(args.file); const filename = basename(args.file);
@@ -108,6 +110,7 @@ const getCommand = defineCommand({
...connectionArgs, ...connectionArgs,
}, },
async run({ args }) { async run({ args }) {
configureOutputMode(args);
const client = createClientFromArgs(args); const client = createClientFromArgs(args);
try { try {
@@ -134,6 +137,7 @@ const deleteCommand = defineCommand({
...connectionArgs, ...connectionArgs,
}, },
async run({ args }) { async run({ args }) {
configureOutputMode(args);
const client = createClientFromArgs(args); const client = createClientFromArgs(args);
try { try {

View File

@@ -8,7 +8,7 @@ import { defineCommand } from "citty";
import { consola } from "consola"; import { consola } from "consola";
import { connectionArgs, createClientFromArgs } from "../client-factory.js"; import { connectionArgs, createClientFromArgs } from "../client-factory.js";
import { output } from "../output.js"; import { configureOutputMode, output } from "../output.js";
const listCommand = defineCommand({ const listCommand = defineCommand({
meta: { meta: {
@@ -19,6 +19,7 @@ const listCommand = defineCommand({
...connectionArgs, ...connectionArgs,
}, },
async run({ args }) { async run({ args }) {
configureOutputMode(args);
try { try {
const client = createClientFromArgs(args); const client = createClientFromArgs(args);
const menus = await client.menus(); const menus = await client.menus();
@@ -44,6 +45,7 @@ const getCommand = defineCommand({
...connectionArgs, ...connectionArgs,
}, },
async run({ args }) { async run({ args }) {
configureOutputMode(args);
try { try {
const client = createClientFromArgs(args); const client = createClientFromArgs(args);
const menu = await client.menu(args.name); const menu = await client.menu(args.name);

View File

@@ -8,7 +8,7 @@ import { defineCommand } from "citty";
import { consola } from "consola"; import { consola } from "consola";
import { connectionArgs as commonArgs, createClientFromArgs } from "../client-factory.js"; import { connectionArgs as commonArgs, createClientFromArgs } from "../client-factory.js";
import { output } from "../output.js"; import { configureOutputMode, output } from "../output.js";
const listCommand = defineCommand({ const listCommand = defineCommand({
meta: { meta: {
@@ -19,6 +19,7 @@ const listCommand = defineCommand({
...commonArgs, ...commonArgs,
}, },
async run({ args }) { async run({ args }) {
configureOutputMode(args);
try { try {
const client = createClientFromArgs(args); const client = createClientFromArgs(args);
const collections = await client.collections(); const collections = await client.collections();
@@ -44,6 +45,7 @@ const getCommand = defineCommand({
...commonArgs, ...commonArgs,
}, },
async run({ args }) { async run({ args }) {
configureOutputMode(args);
try { try {
const client = createClientFromArgs(args); const client = createClientFromArgs(args);
const collection = await client.collection(args.collection); const collection = await client.collection(args.collection);
@@ -82,6 +84,7 @@ const createCommand = defineCommand({
...commonArgs, ...commonArgs,
}, },
async run({ args }) { async run({ args }) {
configureOutputMode(args);
try { try {
const client = createClientFromArgs(args); const client = createClientFromArgs(args);
const data = await client.createCollection({ const data = await client.createCollection({
@@ -117,6 +120,7 @@ const deleteCommand = defineCommand({
...commonArgs, ...commonArgs,
}, },
async run({ args }) { async run({ args }) {
configureOutputMode(args);
try { try {
if (!args.force) { if (!args.force) {
const confirmed = await consola.prompt(`Delete collection "${args.collection}"?`, { const confirmed = await consola.prompt(`Delete collection "${args.collection}"?`, {
@@ -170,6 +174,7 @@ const addFieldCommand = defineCommand({
...commonArgs, ...commonArgs,
}, },
async run({ args }) { async run({ args }) {
configureOutputMode(args);
try { try {
const client = createClientFromArgs(args); const client = createClientFromArgs(args);
const data = await client.createField(args.collection, { const data = await client.createField(args.collection, {
@@ -206,6 +211,7 @@ const removeFieldCommand = defineCommand({
...commonArgs, ...commonArgs,
}, },
async run({ args }) { async run({ args }) {
configureOutputMode(args);
try { try {
const client = createClientFromArgs(args); const client = createClientFromArgs(args);
await client.deleteField(args.collection, args.field); await client.deleteField(args.collection, args.field);

View File

@@ -8,7 +8,7 @@ import { defineCommand } from "citty";
import { consola } from "consola"; import { consola } from "consola";
import { connectionArgs, createClientFromArgs } from "../client-factory.js"; import { connectionArgs, createClientFromArgs } from "../client-factory.js";
import { output } from "../output.js"; import { configureOutputMode, output } from "../output.js";
export const searchCommand = defineCommand({ export const searchCommand = defineCommand({
meta: { meta: {
@@ -38,6 +38,7 @@ export const searchCommand = defineCommand({
...connectionArgs, ...connectionArgs,
}, },
async run({ args }) { async run({ args }) {
configureOutputMode(args);
try { try {
const client = createClientFromArgs(args); const client = createClientFromArgs(args);
const results = await client.search(args.query, { const results = await client.search(args.query, {

View File

@@ -8,7 +8,7 @@ import { defineCommand } from "citty";
import { consola } from "consola"; import { consola } from "consola";
import { connectionArgs, createClientFromArgs } from "../client-factory.js"; import { connectionArgs, createClientFromArgs } from "../client-factory.js";
import { output } from "../output.js"; import { configureOutputMode, output } from "../output.js";
/** Pattern to replace whitespace with hyphens for slug generation */ /** Pattern to replace whitespace with hyphens for slug generation */
const WHITESPACE_PATTERN = /\s+/g; const WHITESPACE_PATTERN = /\s+/g;
@@ -22,6 +22,7 @@ const listCommand = defineCommand({
...connectionArgs, ...connectionArgs,
}, },
async run({ args }) { async run({ args }) {
configureOutputMode(args);
try { try {
const client = createClientFromArgs(args); const client = createClientFromArgs(args);
const taxonomies = await client.taxonomies(); const taxonomies = await client.taxonomies();
@@ -56,6 +57,7 @@ const termsCommand = defineCommand({
...connectionArgs, ...connectionArgs,
}, },
async run({ args }) { async run({ args }) {
configureOutputMode(args);
try { try {
const client = createClientFromArgs(args); const client = createClientFromArgs(args);
const result = await client.terms(args.name, { const result = await client.terms(args.name, {
@@ -97,6 +99,7 @@ const addTermCommand = defineCommand({
...connectionArgs, ...connectionArgs,
}, },
async run({ args }) { async run({ args }) {
configureOutputMode(args);
try { try {
const client = createClientFromArgs(args); const client = createClientFromArgs(args);
const label = args.name; const label = args.name;

View File

@@ -4,6 +4,20 @@ interface OutputArgs {
json?: boolean; json?: boolean;
} }
/**
* Redirect consola output to stderr so it doesn't pollute JSON on stdout.
*
* Call this early in any command that uses `output()` with `--json`.
* Safe to call multiple times — only applies the redirect once.
*/
export function configureOutputMode(args: OutputArgs): void {
if (args.json || !process.stdout.isTTY) {
// Send all consola output to stderr so stdout is clean JSON
consola.options.stdout = process.stderr;
consola.options.stderr = process.stderr;
}
}
/** /**
* Output data as JSON or pretty-printed. * Output data as JSON or pretty-printed.
* *

View File

@@ -14,7 +14,7 @@ import { describe, it, expect, beforeAll, afterAll } from "vitest";
import type { TestServerContext } from "../server.js"; import type { TestServerContext } from "../server.js";
import { assertNodeVersion, createTestServer } from "../server.js"; import { assertNodeVersion, createTestServer } from "../server.js";
const PORT = 4398; const PORT = 4396;
const TIMEOUT = 60_000; const TIMEOUT = 60_000;
/** Helper: raw fetch with auth headers */ /** Helper: raw fetch with auth headers */

View File

@@ -1,5 +1,6 @@
import { execFile, spawn } from "node:child_process"; import { execFile, spawn } from "node:child_process";
import { resolve } from "node:path"; import { rmSync } from "node:fs";
import { join, resolve } from "node:path";
import { promisify } from "node:util"; import { promisify } from "node:util";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
@@ -203,6 +204,12 @@ describe.sequential("Site smoke matrix", () => {
async () => { async () => {
await ensureBuilt(); await ensureBuilt();
// Remove stale database files so each run starts fresh.
// SQLite demos use data.db; WAL/SHM sidecars may also exist.
for (const file of ["data.db", "data.db-wal", "data.db-shm"]) {
rmSync(join(site.dir, file), { force: true });
}
const baseUrl = `http://localhost:${site.port}`; const baseUrl = `http://localhost:${site.port}`;
const serverProcess = spawn("pnpm", ["exec", "astro", "dev", "--port", String(site.port)], { const serverProcess = spawn("pnpm", ["exec", "astro", "dev", "--port", String(site.port)], {
cwd: site.dir, cwd: site.dir,