diff --git a/CHANGELOG.md b/CHANGELOG.md index 8998ae4..e6abca2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to this project will be documented in this file. +## 1.9.0 - 2026-09-15 + +Changes since 1.8.0: + +### Added + +- Added `context.assert(fn, description)` for scenario/snippet script steps: runs the given function, requires it to return `true`, logs the outcome (`log` on success, `error` on failure or exception), and throws to stop the step on failure. + +### Changed + +- Documented `context.assert` in `docs/scenario.md`. + ## 1.8.0 - 2026-09-15 Changes since 1.7.1: diff --git a/docs/scenario.md b/docs/scenario.md index b631d01..fa6edd1 100644 --- a/docs/scenario.md +++ b/docs/scenario.md @@ -35,6 +35,7 @@ Available in scope: - `context.getStepOutput(order)`: prior step output by absolute order (`1`, `2`, ...) or relative (`-1` = previous step) - `context.dumpDom(selector?)`: simplified DOM snapshot - `context.log(...args)`, `context.warn(...args)`, `context.error(...args)`: structured step logs +- `context.assert(() => boolean, description)`: runs the arrow function and requires it to return `true`. Logs `description` (as a `log` entry on success, as an `error` entry on failure or exception), then throws to stop the step if the function returned anything other than `true` or threw - `context.runSnippet(name, ...args)`: execute stored snippet code with the same context - `context.getScenarioFiles(opts?)`: list files uploaded to the scenario (`limit`, `offset`) - `context.downloadFile(url, opts?)`: fetch `url` and save the result as a run artifact (requires a real scenario run — throws when invoked ad hoc, e.g. via the `exec_code` MCP tool). `opts` may include `method`, `headers`, `body`, `filename`. `url` also accepts a `data:` URI (`data:;base64,` or `data:,`) to save content generated in-script (e.g. a credential JWT) directly, without an actual network fetch. diff --git a/package.json b/package.json index c6007fe..a0149ba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "liqa", - "version": "1.8.0", + "version": "1.9.0", "private": true, "workspaces": [ "server", diff --git a/server/src/code-executor/code-executor.service.ts b/server/src/code-executor/code-executor.service.ts index 81700e0..05de3b7 100644 --- a/server/src/code-executor/code-executor.service.ts +++ b/server/src/code-executor/code-executor.service.ts @@ -42,6 +42,15 @@ export interface ScriptContext { log: (...args: unknown[]) => void; warn: (...args: unknown[]) => void; error: (...args: unknown[]) => void; + /** + * Runs `fn` and requires it to return `true`. Logs the outcome under + * `description`, then throws if `fn` returned anything other than `true` + * or threw an exception. + */ + assert: ( + fn: () => boolean | Promise, + description: string, + ) => Promise; /** Runs a named snippet with the same context, plus any extra positional args. */ runSnippet: (alias: string, ...args: unknown[]) => Promise; /** Returns metadata + absolute disk path for files attached to the current scenario. */ @@ -141,6 +150,28 @@ export class CodeExecutorService { log: (...args: unknown[]) => scriptLog("log", toStr(args)), warn: (...args: unknown[]) => scriptLog("warn", toStr(args)), error: (...args: unknown[]) => scriptLog("error", toStr(args)), + assert: async ( + fn: () => boolean | Promise, + description: string, + ): Promise => { + let passed: boolean; + let failureReason: string | undefined; + try { + passed = (await fn()) === true; + } catch (err) { + passed = false; + failureReason = (err as Error).message; + } + if (passed) { + scriptLog("log", `Assertion passed: ${description}`); + return; + } + const message = failureReason + ? `Assertion failed: ${description} (${failureReason})` + : `Assertion failed: ${description}`; + scriptLog("error", message); + throw new Error(message); + }, getStepOutput: getStepOutput ?? (() => Promise.resolve(null)), getCredential: (alias: string): unknown => { if (!(alias in credMap)) {