From 8a0e040ccdc7d9746efe19ff2460cfbc1ad0d157 Mon Sep 17 00:00:00 2001 From: Galmanus Date: Tue, 25 Aug 2026 08:11:15 -0300 Subject: [PATCH] fix(core): wrap raw errors as CaatingaError in escaping catch blocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit toCaatingaError() was exported but never called inside packages/core, so raw NodeJS.ErrnoException / SyntaxError / etc. escaped core catch blocks untyped — library consumers got errors with no CaatingaError code or hint. Audited all 23 `throw error;` sites. Converted the 15 that let a raw, non-CaatingaError escape to callers, to `throw toCaatingaError(error)` (a no-op pass-through for anything already a CaatingaError): read-artifacts, write-artifacts, load-config, sync-frontend-env, create-project-from-template (x2), estimate-deploy-cost, invoke-contract, build-contract, build-workspace, resolve-method-args, resolve-source-address, verify-dependency-contract, check-stellar-cli-version, artifacts-lock. Left 8 sites unchanged, each deliberately: - 4 are `if (error instanceof CaatingaError) throw error;` pass-throughs where the raw case is already wrapped adjacently (run-command, check-binary, check-stellar-sdk-version, create-project ln 101). - 4 are retry-loop internal rethrows (deploy x2, upgrade, run-post-deploy) that are control flow, not caller escapes, and depend on the original error for the transient/last-attempt decision. Adds to-caatinga-error-wrapping.test.ts (real fs EISDIR/rename failures now surface as CaatingaError / UNEXPECTED_ERROR). error-surface unaffected (no new codes). Full core suite: 501 pass. Closes #88 --- packages/core/src/artifacts/artifacts-lock.ts | 4 +- packages/core/src/artifacts/read-artifacts.ts | 4 +- .../core/src/artifacts/write-artifacts.ts | 3 +- packages/core/src/config/load-config.ts | 4 +- packages/core/src/contracts/build-contract.ts | 4 +- .../core/src/contracts/build-workspace.ts | 4 +- .../src/contracts/estimate-deploy-cost.ts | 4 +- .../core/src/contracts/invoke-contract.ts | 4 +- .../core/src/contracts/resolve-method-args.ts | 4 +- .../src/contracts/resolve-source-address.ts | 4 +- .../contracts/verify-dependency-contract.ts | 4 +- .../errors/to-caatinga-error-wrapping.test.ts | 42 +++++++++++++++++++ .../core/src/frontend/sync-frontend-env.ts | 4 +- .../stellar-cli/check-stellar-cli-version.ts | 4 +- .../templates/create-project-from-template.ts | 6 +-- 15 files changed, 71 insertions(+), 28 deletions(-) create mode 100644 packages/core/src/errors/to-caatinga-error-wrapping.test.ts diff --git a/packages/core/src/artifacts/artifacts-lock.ts b/packages/core/src/artifacts/artifacts-lock.ts index d9dba3e0..726ca9d4 100644 --- a/packages/core/src/artifacts/artifacts-lock.ts +++ b/packages/core/src/artifacts/artifacts-lock.ts @@ -1,6 +1,6 @@ import { open, readFile, rename, unlink } from "node:fs/promises"; import path from "node:path"; -import { CaatingaError, CaatingaErrorCode } from "../errors/CaatingaError.js"; +import { CaatingaError, CaatingaErrorCode, toCaatingaError } from "../errors/CaatingaError.js"; const LOCK_RETRY_DELAY_MS = 50; const LOCK_TIMEOUT_MS = 15_000; @@ -113,7 +113,7 @@ async function acquireLock(lockPath: string, deadline: number): Promise { return; } catch (error) { if ((error as NodeJS.ErrnoException).code !== "EEXIST") { - throw error; + throw toCaatingaError(error); } owner = await readLockOwner(lockPath); diff --git a/packages/core/src/artifacts/read-artifacts.ts b/packages/core/src/artifacts/read-artifacts.ts index eb893fe1..a4fc2cec 100644 --- a/packages/core/src/artifacts/read-artifacts.ts +++ b/packages/core/src/artifacts/read-artifacts.ts @@ -1,7 +1,7 @@ import { readFile } from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; -import { CaatingaError, CaatingaErrorCode } from "../errors/CaatingaError.js"; +import { CaatingaError, CaatingaErrorCode, toCaatingaError } from "../errors/CaatingaError.js"; import { CaatingaArtifactsSchema, CURRENT_ARTIFACTS_SCHEMA_VERSION, @@ -44,6 +44,6 @@ export async function readArtifacts(cwd = process.cwd()): Promise undefined); - throw error; + throw toCaatingaError(error); } return artifactsPath; diff --git a/packages/core/src/config/load-config.ts b/packages/core/src/config/load-config.ts index d4f713ed..45870f61 100644 --- a/packages/core/src/config/load-config.ts +++ b/packages/core/src/config/load-config.ts @@ -2,7 +2,7 @@ import { access } from "node:fs/promises"; import path from "node:path"; import { createJiti } from "jiti"; import { z } from "zod"; -import { CaatingaError, CaatingaErrorCode } from "../errors/CaatingaError.js"; +import { CaatingaError, CaatingaErrorCode, toCaatingaError } from "../errors/CaatingaError.js"; import { isDependenciesNotInstalledError } from "./is-dependencies-not-installed-error.js"; import { CaatingaConfigSchema, type CaatingaConfig } from "./config.schema.js"; @@ -47,6 +47,6 @@ export async function loadConfig(options: LoadConfigOptions = {}): Promise { + let tmpDir: string; + + afterEach(async () => { + if (tmpDir) { + await rm(tmpDir, { recursive: true, force: true }); + } + }); + + it("writeArtifacts wraps a raw rename failure", async () => { + tmpDir = await mkdtemp(path.join(os.tmpdir(), "caatinga-wrap-")); + // A directory at the artifacts path makes the atomic rename fail with a + // raw fs error (EISDIR/ENOTEMPTY), not an ENOENT the code handles. + await mkdir(path.join(tmpDir, "caatinga.artifacts.json")); + + await expect(writeArtifacts(createInitialArtifacts("app"), tmpDir)).rejects.toBeInstanceOf( + CaatingaError + ); + }); + + it("readArtifacts wraps a raw read failure", async () => { + tmpDir = await mkdtemp(path.join(os.tmpdir(), "caatinga-wrap-")); + // Reading the artifacts path when it is a directory throws EISDIR — a raw + // error distinct from the ENOENT (missing file) the code returns empty for. + await mkdir(path.join(tmpDir, "caatinga.artifacts.json")); + + await expect(readArtifacts(tmpDir)).rejects.toMatchObject({ + code: CaatingaErrorCode.UNEXPECTED_ERROR, + }); + }); +}); diff --git a/packages/core/src/frontend/sync-frontend-env.ts b/packages/core/src/frontend/sync-frontend-env.ts index d4a78610..6e5950d8 100644 --- a/packages/core/src/frontend/sync-frontend-env.ts +++ b/packages/core/src/frontend/sync-frontend-env.ts @@ -2,7 +2,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import { readArtifacts } from "../artifacts/read-artifacts.js"; import type { CaatingaConfig } from "../config/config.schema.js"; -import { CaatingaError, CaatingaErrorCode } from "../errors/CaatingaError.js"; +import { CaatingaError, CaatingaErrorCode, toCaatingaError } from "../errors/CaatingaError.js"; import { resolveNetwork } from "../networks/resolve-network.js"; export type SyncFrontendEnvOptions = { @@ -35,7 +35,7 @@ async function readExistingEnv(envFile: string): Promise { if ((error as NodeJS.ErrnoException).code === "ENOENT") { return undefined; } - throw error; + throw toCaatingaError(error); } } diff --git a/packages/core/src/stellar-cli/check-stellar-cli-version.ts b/packages/core/src/stellar-cli/check-stellar-cli-version.ts index 9f3ae244..29c90bca 100644 --- a/packages/core/src/stellar-cli/check-stellar-cli-version.ts +++ b/packages/core/src/stellar-cli/check-stellar-cli-version.ts @@ -1,4 +1,4 @@ -import { CaatingaError, CaatingaErrorCode } from "../errors/CaatingaError.js"; +import { CaatingaError, CaatingaErrorCode, toCaatingaError } from "../errors/CaatingaError.js"; import { runCommand } from "../shell/run-command.js"; import { evaluateStellarCliCompatibility, @@ -36,7 +36,7 @@ export async function checkStellarCliVersion( ); } - throw error; + throw toCaatingaError(error); } const version = parseStellarCliVersion(rawOutput); diff --git a/packages/core/src/templates/create-project-from-template.ts b/packages/core/src/templates/create-project-from-template.ts index 9143254c..941ec8e0 100644 --- a/packages/core/src/templates/create-project-from-template.ts +++ b/packages/core/src/templates/create-project-from-template.ts @@ -1,7 +1,7 @@ import { cp, lstat, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; -import { CaatingaError, CaatingaErrorCode } from "../errors/CaatingaError.js"; +import { CaatingaError, CaatingaErrorCode, toCaatingaError } from "../errors/CaatingaError.js"; import { readArtifacts } from "../artifacts/read-artifacts.js"; import { createInitialArtifacts, writeArtifacts } from "../artifacts/write-artifacts.js"; import { @@ -67,7 +67,7 @@ async function ensureArtifacts(targetDir: string, projectName: string): Promise< return; } - throw error; + throw toCaatingaError(error); } } @@ -109,7 +109,7 @@ async function readTemplateManifest(templateDir: string): Promise