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
9 changes: 7 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,19 @@ jobs:
test:
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
node-version: ['22.x', '24.x']

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: ${{ matrix.node-version }}
cache: 'npm'
cache-dependency-path: packages/package-lock.json

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '22.x'
cache: 'npm'
cache-dependency-path: packages/package-lock.json
registry-url: 'https://registry.npmjs.org'
Expand Down
5 changes: 2 additions & 3 deletions packages/ohno-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@
"chokidar": "^4.0.0",
"commander": "^12.1.0",
"ink": "^4.4.1",
"react": "^18.3.1",
"sql.js": "^1.11.0"
"react": "^18.3.1"
},
"devDependencies": {
"@types/node": "^22.10.0",
Expand All @@ -52,7 +51,7 @@
"typescript": "^5.7.0"
},
"engines": {
"node": ">=18.0.0"
"node": ">=22.16.0"
},
"license": "MIT"
}
87 changes: 86 additions & 1 deletion packages/ohno-cli/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { mkdtempSync, rmSync, mkdirSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { createRequire } from "module";
import { TaskDatabase } from "@stevestomp/ohno-core";
import { TaskDatabase, OhnoDatabaseLockedError } from "@stevestomp/ohno-core";
import { createCli } from "./cli.js";

const require = createRequire(import.meta.url);
Expand Down Expand Up @@ -1347,4 +1347,89 @@ describe("CLI Commands", () => {
expect(watchOption).toBeDefined();
});
});

describe("OhnoDatabaseLockedError - CLI entry point error handler", () => {
it("MUST: OhnoDatabaseLockedError is a real error class importable from ohno-core", () => {
const err = new OhnoDatabaseLockedError("SQLITE_BUSY", "test");
expect(err).toBeInstanceOf(OhnoDatabaseLockedError);
expect(err.sqliteCode).toBe("SQLITE_BUSY");
});

it("MUST: CLI error handler emits stderr message for OhnoDatabaseLockedError without --json", () => {
// Test the logic of the main().catch() handler by simulating it directly.
const stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
const stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => {}) as any);

const err = new OhnoDatabaseLockedError(
"SQLITE_BUSY",
"Database is locked by another ohno process; retry timed out after 5s. Try again, or check for stale ohno-mcp processes with 'ps aux | grep ohno-mcp'."
);

// Simulate the catch handler from index.ts (without --json in argv)
const originalArgv = process.argv;
process.argv = ["node", "ohno", "tasks"]; // no --json
if (err instanceof OhnoDatabaseLockedError) {
if (process.argv.includes('--json')) {
process.stdout.write(JSON.stringify({
success: false,
error: err.message,
errorCode: err.sqliteCode,
}) + '\n');
} else {
process.stderr.write(err.message + '\n');
}
process.exit(1);
}
process.argv = originalArgv;

expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining("retry timed out after 5s"));
expect(stdoutSpy).not.toHaveBeenCalled();
expect(exitSpy).toHaveBeenCalledWith(1);

stderrSpy.mockRestore();
stdoutSpy.mockRestore();
exitSpy.mockRestore();
});

it("MUST: CLI error handler emits JSON to stdout for OhnoDatabaseLockedError with --json", () => {
const stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
const stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => {}) as any);

const err = new OhnoDatabaseLockedError(
"SQLITE_BUSY",
"Database is locked by another ohno process; retry timed out after 5s. Try again, or check for stale ohno-mcp processes with 'ps aux | grep ohno-mcp'."
);

const originalArgv = process.argv;
process.argv = ["node", "ohno", "tasks", "--json"];
if (err instanceof OhnoDatabaseLockedError) {
if (process.argv.includes('--json')) {
process.stdout.write(JSON.stringify({
success: false,
error: err.message,
errorCode: err.sqliteCode,
}) + '\n');
} else {
process.stderr.write(err.message + '\n');
}
process.exit(1);
}
process.argv = originalArgv;

const jsonOutput = JSON.parse(stdoutSpy.mock.calls[0]?.[0] as string);
expect(jsonOutput.success).toBe(false);
expect(jsonOutput.error).toContain("retry timed out after 5s");
expect(jsonOutput.errorCode).toBe("SQLITE_BUSY");
expect(stderrSpy).not.toHaveBeenCalled();
expect(exitSpy).toHaveBeenCalledWith(1);

stderrSpy.mockRestore();
stdoutSpy.mockRestore();
exitSpy.mockRestore();
});
});
});
26 changes: 24 additions & 2 deletions packages/ohno-cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,29 @@
* ohno-cli entry point
*/

import "./node-guard.js";
import { createCli } from "./cli.js";
import { OhnoDatabaseLockedError } from "@stevestomp/ohno-core";

const program = createCli();
program.parse();
async function main() {
const program = createCli();
await program.parseAsync();
}

main().catch((err) => {
if (err instanceof OhnoDatabaseLockedError) {
if (process.argv.includes('--json')) {
process.stdout.write(JSON.stringify({
success: false,
error: err.message,
errorCode: err.sqliteCode,
}) + '\n');
} else {
process.stderr.write(err.message + '\n');
}
process.exit(1);
}
// Re-throw or print other errors using existing semantics
process.stderr.write(String(err?.stack ?? err) + '\n');
process.exit(1);
});
Loading
Loading