From 98751d360d6e4ee3be50b70096696fc1befbe2f3 Mon Sep 17 00:00:00 2001 From: MagPasulke Date: Sat, 30 May 2026 14:40:00 +0200 Subject: [PATCH 1/6] chore: initialize feature branch From 416d92ff389cf2708ae48425674d572dcb3dacf2 Mon Sep 17 00:00:00 2001 From: MagPasulke Date: Sat, 30 May 2026 14:41:23 +0200 Subject: [PATCH 2/6] feat(tools): add adt_rununit OpenCode tool for remote ABAP unit test execution Adds a new OpenCode tool that triggers ABAP Unit tests on a remote SAP system via abap-adt-api. Follows the same core/wrapper split as adt_gitpull. - scripts/adt_rununit_core.ts: reusable core logic - .opencode/tools/adt_rununit.ts: OpenCode tool wrapper - Supports CLAS and DEVC object types - Defaults to SAP_ROOT_PACKAGE env var when no target specified - Returns formatted text summary with pass/fail counts and failure details --- .opencode/tools/adt_rununit.ts | 97 ++++++++++++++++++++ scripts/adt_rununit_core.ts | 160 +++++++++++++++++++++++++++++++++ 2 files changed, 257 insertions(+) create mode 100644 .opencode/tools/adt_rununit.ts create mode 100644 scripts/adt_rununit_core.ts diff --git a/.opencode/tools/adt_rununit.ts b/.opencode/tools/adt_rununit.ts new file mode 100644 index 0000000..5fe6560 --- /dev/null +++ b/.opencode/tools/adt_rununit.ts @@ -0,0 +1,97 @@ +import { tool } from "@opencode-ai/plugin" +import { readFileSync } from "node:fs" +import { resolve } from "node:path" +import { adtRunUnit } from "../../scripts/adt_rununit_core" + +function loadEnv(dir: string): Record { + const env: Record = {} + try { + const content = readFileSync(resolve(dir, ".env"), "utf-8") + for (const line of content.split("\n")) { + const trimmed = line.trim() + if (!trimmed || trimmed.startsWith("#")) continue + const idx = trimmed.indexOf("=") + if (idx === -1) continue + const key = trimmed.slice(0, idx).trim() + const value = trimmed.slice(idx + 1).trim() + env[key] = value + } + } catch { + // .env not found — fall through to process.env + } + return env +} + +export default tool({ + description: + "Runs ABAP Unit tests on the remote SAP system via ADT. " + + "Optional parameters: objectType (CLAS or DEVC) and objectName. " + + "If omitted, runs tests for the package defined in SAP_ROOT_PACKAGE (.env). " + + "Returns a formatted summary with pass/fail counts and failure details.", + args: { + objectType: { + type: "string", + description: + "ABAP object type: 'CLAS' (class) or 'DEVC' (package). " + + "Defaults to DEVC (package) when objectName is omitted.", + required: false, + }, + objectName: { + type: "string", + description: + "ABAP object name (e.g. 'ZASIS_CL_INTERPRETER' for a class, or 'ZASIS' for a package). " + + "If omitted, uses SAP_ROOT_PACKAGE from .env.", + required: false, + }, + }, + async execute(args, context) { + const rootDir = context.worktree || context.directory + const env = loadEnv(rootDir) + const url = process.env.SAP_ADT_URL || env.SAP_ADT_URL + const user = process.env.SAP_ADT_USER || env.SAP_ADT_USER + const password = process.env.SAP_ADT_PASSWORD || env.SAP_ADT_PASSWORD + + if (!url || !user || !password) { + return ( + "ERROR: Missing SAP credentials. Ensure .env contains SAP_ADT_URL, SAP_ADT_USER, SAP_ADT_PASSWORD.\n" + + ` SAP_ADT_URL: ${url ? "set" : "MISSING"}\n` + + ` SAP_ADT_USER: ${user ? "set" : "MISSING"}\n` + + ` SAP_ADT_PASSWORD: ${password ? "set" : "MISSING"}` + ) + } + + // Resolve object type and name + let objectType: "CLAS" | "DEVC" + let objectName: string + + if (args.objectName) { + objectType = (args.objectType === "CLAS") ? "CLAS" : "DEVC" + objectName = args.objectName + } else { + // No object specified — use root package + const rootPackage = process.env.SAP_ROOT_PACKAGE || env.SAP_ROOT_PACKAGE + if (!rootPackage) { + return ( + "ERROR: No target specified and SAP_ROOT_PACKAGE is not configured.\n" + + "Either pass objectType + objectName, or set SAP_ROOT_PACKAGE in .env." + ) + } + objectType = "DEVC" + objectName = rootPackage + } + + const result = await adtRunUnit({ + url, + user, + password, + objectType, + objectName, + }) + + if (!result.ok) { + return `ERROR: ${result.error}` + } + + return result.summary + }, +}) diff --git a/scripts/adt_rununit_core.ts b/scripts/adt_rununit_core.ts new file mode 100644 index 0000000..a2cdbbc --- /dev/null +++ b/scripts/adt_rununit_core.ts @@ -0,0 +1,160 @@ +import { ADTClient, UnitTestClass, UnitTestAlert, UnitTestAlertKind } from "abap-adt-api" + +export interface AdtRunUnitOptions { + /** SAP system base URL (e.g. https://host:44300) */ + url: string + /** SAP user */ + user: string + /** SAP password */ + password: string + /** Object type: CLAS or DEVC. Defaults to DEVC when objectName is from SAP_ROOT_PACKAGE. */ + objectType: "CLAS" | "DEVC" + /** Object name (e.g. ZASIS_CL_INTERPRETER or ZASIS) */ + objectName: string +} + +export interface AdtRunUnitSuccess { + ok: true + summary: string +} + +export interface AdtRunUnitError { + ok: false + error: string +} + +export type AdtRunUnitResult = AdtRunUnitSuccess | AdtRunUnitError + +/** Build the ADT URI for the given object type and name */ +function buildUri(objectType: "CLAS" | "DEVC", objectName: string): string { + const name = objectName.toLowerCase() + switch (objectType) { + case "CLAS": + return `/sap/bc/adt/oo/classes/${name}` + case "DEVC": + return `/sap/bc/adt/packages/${name}` + } +} + +/** Format a single alert into readable text */ +function formatAlert(alert: UnitTestAlert): string { + const lines: string[] = [] + const kindLabel = alert.kind === UnitTestAlertKind.failedAssertion + ? "Assertion Failed" + : alert.kind === UnitTestAlertKind.exception + ? "Exception" + : "Warning" + lines.push(` [${kindLabel}] ${alert.title}`) + for (const detail of alert.details) { + if (detail.trim()) { + lines.push(` ${detail}`) + } + } + if (alert.stack.length > 0) { + const top = alert.stack[0] + lines.push(` at ${top["adtcore:name"]} (${top["adtcore:uri"]})`) + } + return lines.join("\n") +} + +/** Format full unit test results into a human-readable summary */ +function formatResults(classes: UnitTestClass[], target: string): string { + let totalMethods = 0 + let passed = 0 + let failed = 0 + let totalTime = 0 + const failures: string[] = [] + + for (const cls of classes) { + // Class-level alerts + if (cls.alerts.length > 0) { + for (const alert of cls.alerts) { + if (alert.kind !== UnitTestAlertKind.warning) { + failed++ + totalMethods++ + failures.push(` ${cls["adtcore:name"]}:`) + failures.push(formatAlert(alert)) + } + } + } + + for (const method of cls.testmethods) { + totalMethods++ + totalTime += method.executionTime + if (method.alerts.length > 0) { + const hasFailure = method.alerts.some( + (a) => a.kind !== UnitTestAlertKind.warning + ) + if (hasFailure) { + failed++ + failures.push(` ${cls["adtcore:name"]}->${method["adtcore:name"]}:`) + for (const alert of method.alerts) { + failures.push(formatAlert(alert)) + } + } else { + passed++ + } + } else { + passed++ + } + } + } + + const status = failed > 0 ? "FAILED" : "PASSED" + const lines: string[] = [] + lines.push(`UNIT TESTS ${status}: ${passed}/${totalMethods} passed, ${failed} failed`) + lines.push(` Target: ${target}`) + lines.push(` Classes: ${classes.length}`) + lines.push(` Duration: ${totalTime}ms`) + + if (failures.length > 0) { + lines.push("") + lines.push(" Failures:") + lines.push(...failures) + } + + return lines.join("\n") +} + +/** + * Triggers ABAP Unit tests on a SAP system for the specified object. + */ +export async function adtRunUnit(options: AdtRunUnitOptions): Promise { + const { url, user, password, objectType, objectName } = options + + // 1. Connect to SAP system + let client: ADTClient + try { + client = new ADTClient(url, user, password) + } catch (e: any) { + return { ok: false, error: `Failed to create ADT client: ${e.message || e}` } + } + + // 2. Build target URI + const uri = buildUri(objectType, objectName) + const target = `${objectType} ${objectName.toUpperCase()} (${uri})` + + // 3. Run unit tests + let results: UnitTestClass[] + try { + results = await client.unitTestRun(uri) + } catch (e: any) { + return { + ok: false, + error: `Unit test run failed for ${target}: ${e.message || e}`, + } + } + + // 4. Format and return results + if (results.length === 0) { + return { + ok: true, + summary: `UNIT TESTS PASSED: 0/0 passed, 0 failed\n Target: ${target}\n No test classes found.`, + } + } + + return { + ok: true, + summary: formatResults(results, target), + } +} From 33db5dff8bf33ebecbe3ceb6e03e6658c9356d7a Mon Sep 17 00:00:00 2001 From: MagPasulke Date: Sat, 30 May 2026 14:41:56 +0200 Subject: [PATCH 3/6] docs: add SAP_ROOT_PACKAGE to .env.example and session summary --- .env.example | 5 ++++- docs/sessions/2026-05-30_PR-62_adt-rununit-tool.md | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 docs/sessions/2026-05-30_PR-62_adt-rununit-tool.md diff --git a/.env.example b/.env.example index cde7ecb..59ea59d 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,7 @@ -# SAP ADT Connection (used by .opencode/tools/adt_gitpull.ts) +# SAP ADT Connection (used by .opencode/tools/adt_gitpull.ts and adt_rununit.ts) SAP_ADT_URL=https://your-sap-system:44300 SAP_ADT_USER=DEVELOPER SAP_ADT_PASSWORD=your-password-here + +# Default package for adt_rununit (used when no target object is specified) +SAP_ROOT_PACKAGE=ZASIS diff --git a/docs/sessions/2026-05-30_PR-62_adt-rununit-tool.md b/docs/sessions/2026-05-30_PR-62_adt-rununit-tool.md new file mode 100644 index 0000000..b78280e --- /dev/null +++ b/docs/sessions/2026-05-30_PR-62_adt-rununit-tool.md @@ -0,0 +1,8 @@ +# 2026-05-30 PR-62 adt-rununit-tool + +**Date:** 2026-05-30 +**Title:** Add adt_rununit OpenCode tool for remote ABAP unit test execution + +## Summary + +Added a new OpenCode tool `adt_rununit` that triggers ABAP Unit tests on a remote SAP system via `abap-adt-api`. Follows the established core/wrapper split pattern from `adt_gitpull`: `scripts/adt_rununit_core.ts` contains reusable logic and `.opencode/tools/adt_rununit.ts` provides the OpenCode tool wrapper. Supports targeting specific classes (`CLAS`) or packages (`DEVC`), defaulting to the `SAP_ROOT_PACKAGE` env var when no target is specified. Returns a formatted text summary with pass/fail counts, execution time, and detailed failure information including assertion messages and stack locations. Tool errors (connection, auth) throw while test failures return successfully with the summary. All existing tests pass (44 ABAP unit tests, 0 issues from abaplint). PR #62. From 48365f184c478c6e00ef478f14232f816211c2fc Mon Sep 17 00:00:00 2001 From: MagPasulke Date: Sat, 30 May 2026 14:52:48 +0200 Subject: [PATCH 4/6] fix(tools): use Zod schema for adt_rununit args instead of plain objects The @opencode-ai/plugin tool() function expects Zod schemas for args, not plain {type, description, required} objects. This caused a 'Bad Request' error when OpenCode tried to load the tool. --- .opencode/tools/adt_rununit.ts | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/.opencode/tools/adt_rununit.ts b/.opencode/tools/adt_rununit.ts index 5fe6560..acd409e 100644 --- a/.opencode/tools/adt_rununit.ts +++ b/.opencode/tools/adt_rununit.ts @@ -29,20 +29,18 @@ export default tool({ "If omitted, runs tests for the package defined in SAP_ROOT_PACKAGE (.env). " + "Returns a formatted summary with pass/fail counts and failure details.", args: { - objectType: { - type: "string", - description: + objectType: tool.schema.string() + .describe( "ABAP object type: 'CLAS' (class) or 'DEVC' (package). " + - "Defaults to DEVC (package) when objectName is omitted.", - required: false, - }, - objectName: { - type: "string", - description: + "Defaults to DEVC (package) when objectName is omitted." + ) + .optional(), + objectName: tool.schema.string() + .describe( "ABAP object name (e.g. 'ZASIS_CL_INTERPRETER' for a class, or 'ZASIS' for a package). " + - "If omitted, uses SAP_ROOT_PACKAGE from .env.", - required: false, - }, + "If omitted, uses SAP_ROOT_PACKAGE from .env." + ) + .optional(), }, async execute(args, context) { const rootDir = context.worktree || context.directory From 97564024b09512e195f349163002ac1f205a8284 Mon Sep 17 00:00:00 2001 From: MagPasulke Date: Sat, 30 May 2026 14:55:13 +0200 Subject: [PATCH 5/6] docs(skills): add adt-rununit skill for remote ABAP unit test execution --- .agents/skills/adt-rununit/SKILL.md | 48 +++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .agents/skills/adt-rununit/SKILL.md diff --git a/.agents/skills/adt-rununit/SKILL.md b/.agents/skills/adt-rununit/SKILL.md new file mode 100644 index 0000000..5ea9fd2 --- /dev/null +++ b/.agents/skills/adt-rununit/SKILL.md @@ -0,0 +1,48 @@ +--- +name: adt-rununit +description: Run ABAP Unit tests on the remote SAP system via ADT. +--- + +## Tool + +Use the built-in `adt_rununit` custom tool. + +### Parameters (all optional) + +| Parameter | Description | +|-----------|-------------| +| `objectType` | `CLAS` (class) or `DEVC` (package). Defaults to `DEVC` when omitted. | +| `objectName` | ABAP object name (e.g. `ZASIS_CL_INTERPRETER` or `ZASIS`). If omitted, uses `SAP_ROOT_PACKAGE` from `.env`. | + +### Examples + +- Run all tests in the root package: `adt_rununit()` (no args) +- Run tests for a specific class: `adt_rununit(objectType="CLAS", objectName="ZASIS_CL_INTERPRETER")` +- Run tests for a specific package: `adt_rununit(objectType="DEVC", objectName="ZASIS")` + +## What it does + +1. Reads SAP credentials from `.env` in the project root (`SAP_ADT_URL`, `SAP_ADT_USER`, `SAP_ADT_PASSWORD`) +2. Resolves target object (from parameters or `SAP_ROOT_PACKAGE` env var) +3. Connects to the SAP system via ADT API +4. Triggers ABAP Unit test execution on the target object +5. Returns a formatted summary: pass/fail counts, execution time, and failure details (class, method, assertion, stack) + +## When to use + +Only when the user explicitly requests running unit tests on SAP. Typical scenarios: +- After syncing code to SAP via `adt_gitpull`, user says "run tests" or "run unit tests" +- User wants to verify a specific class passes tests: "run tests for ZASIS_CL_INTERPRETER" +- Final verification before merging a PR + +**Never call autonomously.** Always wait for user trigger. + +## Prerequisites + +- `.env` in project root with valid credentials (see `.env.example`): + ``` + SAP_ADT_URL=https://your-sap-system:44300 + SAP_ADT_USER=DEVELOPER + SAP_ADT_PASSWORD=secret + SAP_ROOT_PACKAGE=ZASIS + ``` From 9a6b2052cf333dd874ba2bdbc33566d314c4e059 Mon Sep 17 00:00:00 2001 From: MagPasulke Date: Sat, 30 May 2026 14:57:55 +0200 Subject: [PATCH 6/6] =?UTF-8?q?docs(skills):=20trim=20adt-rununit=20skill?= =?UTF-8?q?=20=E2=80=94=20remove=20prerequisites=20and=20internals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/skills/adt-rununit/SKILL.md | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/.agents/skills/adt-rununit/SKILL.md b/.agents/skills/adt-rununit/SKILL.md index 5ea9fd2..bbdffd7 100644 --- a/.agents/skills/adt-rununit/SKILL.md +++ b/.agents/skills/adt-rununit/SKILL.md @@ -20,14 +20,6 @@ Use the built-in `adt_rununit` custom tool. - Run tests for a specific class: `adt_rununit(objectType="CLAS", objectName="ZASIS_CL_INTERPRETER")` - Run tests for a specific package: `adt_rununit(objectType="DEVC", objectName="ZASIS")` -## What it does - -1. Reads SAP credentials from `.env` in the project root (`SAP_ADT_URL`, `SAP_ADT_USER`, `SAP_ADT_PASSWORD`) -2. Resolves target object (from parameters or `SAP_ROOT_PACKAGE` env var) -3. Connects to the SAP system via ADT API -4. Triggers ABAP Unit test execution on the target object -5. Returns a formatted summary: pass/fail counts, execution time, and failure details (class, method, assertion, stack) - ## When to use Only when the user explicitly requests running unit tests on SAP. Typical scenarios: @@ -36,13 +28,3 @@ Only when the user explicitly requests running unit tests on SAP. Typical scenar - Final verification before merging a PR **Never call autonomously.** Always wait for user trigger. - -## Prerequisites - -- `.env` in project root with valid credentials (see `.env.example`): - ``` - SAP_ADT_URL=https://your-sap-system:44300 - SAP_ADT_USER=DEVELOPER - SAP_ADT_PASSWORD=secret - SAP_ROOT_PACKAGE=ZASIS - ```