Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .agents/skills/adt-rununit/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
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")`

## 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.
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -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
95 changes: 95 additions & 0 deletions .opencode/tools/adt_rununit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
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<string, string> {
const env: Record<string, string> = {}
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: tool.schema.string()
.describe(
"ABAP object type: 'CLAS' (class) or 'DEVC' (package). " +
"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."
)
.optional(),
},
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
},
})
8 changes: 8 additions & 0 deletions docs/sessions/2026-05-30_PR-62_adt-rununit-tool.md
Original file line number Diff line number Diff line change
@@ -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.
160 changes: 160 additions & 0 deletions scripts/adt_rununit_core.ts
Original file line number Diff line number Diff line change
@@ -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<AdtRunUnitResult> {
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),
}
}