From 8abc83b42cf74725fc3472a26232c79d609f246b Mon Sep 17 00:00:00 2001 From: Kasun Jayarathna Date: Wed, 22 Jul 2026 12:10:38 +0530 Subject: [PATCH] Enhance error handling in fetch-assets command and update JSON file writing logic --- commands/fetch-assets.js | 7 +++- tasks/fetch-assets.js | 4 +- test/fetch-assets.test.js | 82 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 test/fetch-assets.test.js diff --git a/commands/fetch-assets.js b/commands/fetch-assets.js index 7f79ecaf..d47dee94 100644 --- a/commands/fetch-assets.js +++ b/commands/fetch-assets.js @@ -51,10 +51,13 @@ const run = async (argv) => { ui.log.info('Done'); } } catch (error) { - ui.log.error('Done with errors', context.errors); + // `context.errors` is often empty here, so fall back to the thrown error + // to avoid reporting a failure with no detail at all + ui.log.error('Done with errors', context.errors.length ? context.errors : error); + return; } - if (argv.zip) { + if (argv.zip && context.outputFile) { ui.log.ok(`Zip file (${(context.outputFile.size / (1000 * 1000)).toFixed(2)}MB) saved at: ${context.outputFile.path}`); } diff --git a/tasks/fetch-assets.js b/tasks/fetch-assets.js index ad271c43..870a49ab 100644 --- a/tasks/fetch-assets.js +++ b/tasks/fetch-assets.js @@ -1,3 +1,4 @@ +import {join} from 'node:path'; import MgAssetScraperDb from '@tryghost/mg-assetscraper-db'; import fsUtils from '@tryghost/mg-fs-utils'; import {makeTaskRunner} from '@tryghost/listr-smart-renderer'; @@ -36,7 +37,8 @@ const getTaskRunner = (options, logger) => { { title: 'Create JSON file', task: async (ctx) => { - await ctx.assetScraper.writeUpdatedJson(ctx.fileCache.jsonDir); + // `jsonDir` is a directory, so the scraper needs a file path within it to write to + await ctx.assetScraper.writeUpdatedJson(join(ctx.fileCache.jsonDir, 'ghost-import.json')); } }, { diff --git a/test/fetch-assets.test.js b/test/fetch-assets.test.js new file mode 100644 index 00000000..c47e5876 --- /dev/null +++ b/test/fetch-assets.test.js @@ -0,0 +1,82 @@ +import {describe, test, mock, beforeEach} from 'node:test'; +import assert from 'node:assert/strict'; +import {join} from 'node:path'; +import errors from '@tryghost/errors'; + +const jsonDir = '/tmp/mg/abc123/zip'; + +const mockWriteUpdatedJson = mock.fn(() => Promise.resolve()); +const mockInit = mock.fn(() => Promise.resolve()); +const mockGetTasks = mock.fn(() => []); + +mock.module('@tryghost/mg-assetscraper-db', { + defaultExport: function MgAssetScraperDb() { + return { + init: mockInit, + getTasks: mockGetTasks, + writeUpdatedJson: mockWriteUpdatedJson + }; + } +}); + +const mockZipWrite = mock.fn(() => Promise.resolve({path: '/tmp/out.zip', size: 1024})); + +mock.module('@tryghost/mg-fs-utils', { + defaultExport: { + FileCache: function FileCache() { + return { + jsonDir: jsonDir, + zipDir: jsonDir, + defaultZipFileName: 'gh-example-123.zip' + }; + }, + zip: { + write: mockZipWrite + } + } +}); + +describe('Fetch assets', function () { + beforeEach(() => { + mockWriteUpdatedJson.mock.resetCalls(); + mockZipWrite.mock.resetCalls(); + }); + + test('writes the updated JSON to a file path, not to the JSON directory', async function () { + const fetchAssets = await import('../tasks/fetch-assets.js'); + const runner = fetchAssets.default.getTaskRunner({ + jsonFile: '/tmp/example.ghost.json', + zip: false, + verbose: false + }); + + await runner.run({errors: []}); + + assert.strictEqual(mockWriteUpdatedJson.mock.callCount(), 1); + + const [outputPath] = mockWriteUpdatedJson.mock.calls[0].arguments; + assert.notStrictEqual(outputPath, jsonDir, 'must not pass the directory itself — writeFile would throw EISDIR'); + assert.strictEqual(outputPath, join(jsonDir, 'ghost-import.json')); + }); +}); + +describe('Fetch assets command', function () { + beforeEach(() => { + mockWriteUpdatedJson.mock.resetCalls(); + mockZipWrite.mock.resetCalls(); + }); + + test('does not throw when an earlier task failed and no zip was produced', async function () { + mockWriteUpdatedJson.mock.mockImplementationOnce(() => { + return Promise.reject(new errors.InternalServerError({message: 'EISDIR: illegal operation on a directory'})); + }); + + const command = await import('../commands/fetch-assets.js'); + + await assert.doesNotReject(async () => { + await command.default.run({jsonFile: '/tmp/example.ghost.json', zip: true, verbose: false}); + }); + + assert.strictEqual(mockZipWrite.mock.callCount(), 0, 'zip must not be reported when it never ran'); + }); +});