From 8f82430ccb62aa88e054e5e391f32db4be17d60f Mon Sep 17 00:00:00 2001 From: Helen Kwok <107752876+helenkwok@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:06:21 +1030 Subject: [PATCH] fix(cli): add executionReporter.error to the local engine The cloud-component README documents executionReporter.error(msg) and the cloud worker provides it, but the local engine script did not, so calling it threw under thatopen run and local-server. Send it as an ERROR message, print it as [error], and record level "error" in local-server messages as the platform does. --- .changeset/local-reporter-error.md | 5 ++++ docs/cli/run.md | 2 +- src/cli/commands/run.ts | 3 ++ src/cli/lib/engine-script.test.ts | 46 ++++++++++++++++++++++++++++++ src/cli/lib/engine-script.ts | 3 ++ src/cli/lib/execution-manager.ts | 9 ++++-- 6 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 .changeset/local-reporter-error.md create mode 100644 src/cli/lib/engine-script.test.ts diff --git a/.changeset/local-reporter-error.md b/.changeset/local-reporter-error.md new file mode 100644 index 0000000..e48c2df --- /dev/null +++ b/.changeset/local-reporter-error.md @@ -0,0 +1,5 @@ +--- +'@thatopen/services': patch +--- + +`thatopen run` and `thatopen local-server`: `executionReporter.error()` exists locally, as documented and as in the cloud. Error lines print as `[error]` and carry `level: "error"` in local-server messages. diff --git a/docs/cli/run.md b/docs/cli/run.md index 4232a38..bf7c1dc 100644 --- a/docs/cli/run.md +++ b/docs/cli/run.md @@ -6,7 +6,7 @@ description: "Build and run a cloud component locally as a child process, mirror Builds and executes a cloud component locally, replicating what the platform's cloud engine does. Useful for testing component logic and parameters before publishing. -Execution output (`MESSAGE`, `PROGRESS`, `SUCCESS`, `FAIL`) is streamed to the terminal with elapsed time. +Execution output (`MESSAGE`, `ERROR` from `executionReporter.error`, `PROGRESS`, `SUCCESS`, `FAIL`) is streamed to the terminal with elapsed time. **Usage:** ```bash diff --git a/src/cli/commands/run.ts b/src/cli/commands/run.ts index 9176da2..6d36f6c 100644 --- a/src/cli/commands/run.ts +++ b/src/cli/commands/run.ts @@ -117,6 +117,9 @@ export const runCommand = new Command('run') case 'MESSAGE': console.log(`[${elapsed}s] [message] ${msg.message}`); break; + case 'ERROR': + console.error(`[${elapsed}s] [error] ${msg.message}`); + break; case 'PROGRESS': console.log(`[${elapsed}s] [progress] ${msg.message}%`); break; diff --git a/src/cli/lib/engine-script.test.ts b/src/cli/lib/engine-script.test.ts new file mode 100644 index 0000000..c766d6a --- /dev/null +++ b/src/cli/lib/engine-script.test.ts @@ -0,0 +1,46 @@ +import { describe, it, expect } from 'vitest'; +import { fork } from 'node:child_process'; +import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { buildEngineScript } from './engine-script'; + +/** + * The local engine must offer the same executionReporter as the platform's, + * including error(), which the cloud-component README documents. + */ +describe('buildEngineScript', () => { + it('provides executionReporter.error', async () => { + const root = mkdtempSync(join(tmpdir(), 'thatopen-engine-')); + const services = join(root, 'node_modules', '@thatopen', 'services'); + mkdirSync(services, { recursive: true }); + writeFileSync(join(services, 'index.js'), 'exports.EngineServicesClient = class {};'); + const bundle = `var main = async () => { + executionReporter.error('bad row'); + return { type: 'SUCCESS', message: 'done' }; + };`; + const script = join(root, 'engine.js'); + writeFileSync( + script, + buildEngineScript(bundle, 'token', 'http://localhost', {}, { + executionId: 'e', + toolId: 'local', + toolVersion: 'local', + }), + ); + + const messages = await new Promise((resolve, reject) => { + const received: unknown[] = []; + const child = fork(script, [], { stdio: ['ignore', 'ignore', 'ignore', 'ipc'] }); + child.on('message', (m) => received.push(m)); + child.on('error', reject); + child.on('exit', () => resolve(received)); + setTimeout(() => child.kill(), 5000); + }); + + expect(messages).toEqual([ + { type: 'ERROR', message: 'bad row' }, + { type: 'SUCCESS', message: 'done' }, + ]); + }); +}); diff --git a/src/cli/lib/engine-script.ts b/src/cli/lib/engine-script.ts index 1492d77..fa792b9 100644 --- a/src/cli/lib/engine-script.ts +++ b/src/cli/lib/engine-script.ts @@ -43,6 +43,9 @@ const executionReporter = { progress: (message) => { process.send({ type: 'PROGRESS', message }); }, + error: (message) => { + process.send({ type: 'ERROR', message }); + }, }; let thatOpenServices, executionParams; diff --git a/src/cli/lib/execution-manager.ts b/src/cli/lib/execution-manager.ts index 85562b8..6a03ff9 100644 --- a/src/cli/lib/execution-manager.ts +++ b/src/cli/lib/execution-manager.ts @@ -13,6 +13,8 @@ export interface ExecutionMessage { _id: string; executionId: string; content: string; + /** Set to "error" for executionReporter.error lines, as the platform does. */ + level?: 'error'; createdAt: string; } @@ -155,15 +157,18 @@ export class ExecutionManager { state.updatedAt = now; switch (msg.type) { - case 'MESSAGE': { + case 'MESSAGE': + case 'ERROR': { + const isError = msg.type === 'ERROR'; const msgEntity: ExecutionMessage = { _id: randomUUID(), executionId, content: msg.message, + ...(isError && { level: 'error' as const }), createdAt: now, }; state.messages.push(msgEntity); - console.log(`[${executionId.slice(0, 8)}] [message] ${msg.message}`); + console.log(`[${executionId.slice(0, 8)}] [${isError ? 'error' : 'message'}] ${msg.message}`); this.emit(executionId, { messageUpdate: { ...msgEntity,