diff --git a/SKILL.md b/SKILL.md index fb3e9de..e0c371b 100644 --- a/SKILL.md +++ b/SKILL.md @@ -299,6 +299,10 @@ Full flow: `references/error-handling.md`. | HTTP 404 (DDIC download/sync) | NW 7.40 has no `/source/main` endpoint (7.50+) — DDIC objects only create/info/delete | | HTTP 423 (Transport lock) | Object bound to transport — report to user → SE09 | | HTTP 500 (set_source) | Lock 成功但写入 500,传输请求中无对象 → SE09 手动添加对象到请求 → `log/adt-set-source-500.md` | +| DDIC 文本不持久化(NW740) | ADT POST 创建域/数据元素后 DD01T 0 行 / DD04T 占位名 / DD07T 0 行——**ADT create 不写任何文本**。工法:工具报表内 `DDIF_DOMA_GET→改 ddtext/dd07v-ddtext→DDIF_DOMA_PUT→DDIF_DOMA_ACTIVATE`(DTEL 同理 DD04V)。两个坑:①DDIF_DTEL_PUT 签名仅 NAME/DD04V_WA(多传 tpara_wa 运行期转储 500);②GET 返回的 dd07v 行 ddlanguage 为空,PUT 必须补 `ddlanguage='E'` 否则固定值文本静默丢失 | +| oo/classes 激活 200 空响应 | K/S 型传输请求接不了 CLAS 激活(PROG/DOMA 端点不受限)——POST 200 但对象恒 inactive。判据:日志 `Object still inactive after retry`。出路:SE09 建 W 型工作台请求或 SAP GUI 右键 Activate | +| oo/classes 写入 500 "unknown comments" | IMPLEMENTATION 与 METHOD 之间的 `*"` 块注释不能存储——把类级注释移进 METHOD 体内 | +| PUT 源码后 DD02L 恒 N 态 | DD03L 内部类型名是 STRG 不是 SE11 界面显示的 STRING;comptype:数据元素引用='E'(非 'D')、结构引用='S' | | Activate fails, still inactive | double-activate built-in; still fails → report → SE09 | | Open SQL @ 转义不一致 | 语法检查 2 错误——一处用 `@` 则全局必须一致(`WHERE field = @lv_var` → `INTO TABLE @lt_data`) → `log/adt-nw740-open-sql-consistency.md` | | `WITH EMPTY KEY` dump | NW 7.40 unsupported — use `WITH NON-UNIQUE KEY` / `WITH DEFAULT KEY` | @@ -333,7 +337,8 @@ NW 7.40 specifics: - **Activation false-negative** — first activate may report failure but actually succeed; double-activate is built in. - **ABAP syntax limits** — no string templates `\|...\|`, no inline `DATA(...)`, no `WITH EMPTY KEY`. - **Open SQL @ 转义一致性** — NW 7.40 要求一处用 `@` 则全 SQL 必须一致(`WHERE field = @lv_var` → `INTO TABLE @lt_data`),混用导致语法检查 2 错误。详见 `log/adt-nw740-open-sql-consistency.md`。 -- **set_source HTTP 500(传输请求无对象)** — Lock 成功但写入 500,根因:Lock API 创建锁句柄但未绑定对象到传输请求。NW 7.40 需 SE09 手动添加对象。详见 `log/adt-set-source-500.md`。 +- **set_source HTTP 500(传输请求无对象)** — Lock 成功但写入 500,根因:Lock API 创建锁句柄但未绑定对象到传输请求。NW 7.40 需 SE09 手动添加。详见 `log/adt-set-source-500.md`。 +- **DDIC 文本不持久化(S4T 实测 2026-09-14)** — ADT POST 创建域/数据元素不落任何文本(DD01T/DD07T 0 行、DD04T 只剩对象名占位),且建对象时无论 XML 是否带 description。sap-cli 侧已补 JSON description 消费(--description > JSON.description > 对象名),但 NW740 服务端仍不落库——批量落 DDIC 文本用「工具报表 + DDIF_*_GET→PUT→ACTIVATE」补写工法(源码样板见 sap-build 之外的 ABAP_Integrated/src/ZINT.package/program/zint_pilot_ddic_tool.prog.abap),注意 DDIF_DTEL_PUT 仅 NAME/DD04V_WA 两参数、DDIF_DOMA_GET 返回的 dd07v 行 ddlanguage 为空需补 'E'。 - **CSRF token** — all writes require it; sap-cli handles automatically. ### DDIC 结构/表类型落地方案(NW 7.40 无 ADT 端点时) diff --git a/assets/sapcli/cli/parser.py b/assets/sapcli/cli/parser.py index dbff59b..94353be 100644 --- a/assets/sapcli/cli/parser.py +++ b/assets/sapcli/cli/parser.py @@ -192,6 +192,7 @@ CDS View: upload_parser.add_argument("--type", required=True, choices=type_choices, help="对象类型") upload_parser.add_argument("--path", required=True, help="本地源代码文件路径 (.abap)") upload_parser.add_argument("--corr_nr", default=None, help="传输请求编号") + upload_parser.add_argument("--part", default=None, choices=["testclasses"], help="写入局部测试类 include(仅 class 类型,ABAP Unit 局部测试类通道)") # ── syntax-check (远程对象语法检查,不上传不激活) ── sc_parser = subparsers.add_parser("syntax-check", help="语法检查(远程对象,不上传不激活)") diff --git a/assets/sapcli/client/_ddic.py b/assets/sapcli/client/_ddic.py index 55b46d5..486f665 100644 --- a/assets/sapcli/client/_ddic.py +++ b/assets/sapcli/client/_ddic.py @@ -919,6 +919,9 @@ class DdicMixin: logger.info("RUN PROGRAM: POST %s", url) resp = self.session.post(url, headers=hdrs) logger.info("RUN PROGRAM RESPONSE: HTTP %s (%d bytes)", resp.status_code, len(resp.content)) + if resp.status_code != 200: + # Log response body to aid diagnosing server-side dumps + logger.error("RUN PROGRAM ERROR BODY: %s", resp.text[:4000]) resp.raise_for_status() return resp.text @@ -962,6 +965,8 @@ class DdicMixin: logger.info("RUN UNIT TEST: POST %s uri=%s", url, obj_uri) resp = self.session.post(url, headers=hdrs, data=body.encode("utf-8")) logger.info("RUN UNIT TEST RESPONSE: HTTP %s", resp.status_code) + if resp.status_code != 200 or b" bool: params: dict[str, str] = {} if lock_handle: @@ -49,8 +50,9 @@ class SourceMixin: if corr_nr: params["corrNr"] = corr_nr url = f"{self.host}{src_uri}" - hdrs = self._headers("text/plain; charset=utf-8") - logger.info("SET SOURCE: PUT %s (%d chars)", url, len(source)) + ct = content_type or "text/plain; charset=utf-8" + hdrs = self._headers(ct) + logger.info("SET SOURCE: PUT %s (%d chars, ct=%s)", url, len(source), ct) resp = self.session.put( url, headers=hdrs, params=params, data=source.encode("utf-8") ) diff --git a/assets/sapcli/commands/crud.py b/assets/sapcli/commands/crud.py index efdeb28..b611fa3 100644 --- a/assets/sapcli/commands/crud.py +++ b/assets/sapcli/commands/crud.py @@ -859,6 +859,27 @@ def cmd_create(args: argparse.Namespace, client: ADTClient) -> None: source_file: str | None = getattr(args, "source", None) definition_file: str | None = getattr(args, "definition", None) + # JSON definition description: --description wins, then JSON, then name. + # (NW740 ADT create does not persist texts anyway — see SKILL.md known + # limitation; this only affects the XML body sent at creation time.) + if ( + definition_file + and not getattr(args, "description", None) + and obj_type in DDIC_TYPES + ): + import json as _json + import os as _os + + if _os.path.isfile(definition_file): + try: + with open(definition_file, "r", encoding="utf-8") as f: + _data = _json.load(f) + _json_desc = _data.get("description") + if _json_desc: + description = _json_desc + except Exception as _e: + logger.warning("读取定义文件 description 失败: %s", _e) + type_label = get_type_config(obj_type).label if obj_type == "function": diff --git a/assets/sapcli/commands/upload.py b/assets/sapcli/commands/upload.py index 5f20cdc..0b98b17 100644 --- a/assets/sapcli/commands/upload.py +++ b/assets/sapcli/commands/upload.py @@ -33,6 +33,7 @@ def cmd_upload(args: argparse.Namespace, client: ADTClient) -> None: obj_type: str = args.type file_path: str = args.path corr_nr: str | None = getattr(args, "corr_nr", None) + part: str | None = getattr(args, "part", None) parsed = parse_object_name(name, obj_type) type_label = get_type_config(obj_type).label @@ -42,6 +43,16 @@ def cmd_upload(args: argparse.Namespace, client: ADTClient) -> None: print(f" functiongroup 等是容器对象,不包含可编辑的源代码文件") raise InvalidNameError(f"{type_label}没有源代码,不支持 upload 操作") + src_uri = parsed.src_uri + if part == "testclasses": + # Local test classes include (ABAP Unit): classes only. + # ADT unit testruns only discovers LOCAL test classes — global + # test classes are NOT run by the endpoint (S4T proven). + if obj_type != "class": + print(f"\n ✗ --part testclasses 仅支持 class 类型") + raise InvalidNameError("--part testclasses 仅支持 class 类型") + src_uri = f"/sap/bc/adt/oo/classes/{name.lower()}/source/testclasses" + if not os.path.isfile(file_path): print(f"\n ✗ 文件不存在: {file_path}") raise ConfigError(f"文件不存在: {file_path}") @@ -90,7 +101,30 @@ def cmd_upload(args: argparse.Namespace, client: ADTClient) -> None: print(f" ✓ 锁定成功(传输请求: {corr_nr})") try: - client.set_source(parsed.src_uri, source, lock_handle, corr_nr) + if part == "testclasses": + # testclasses endpoint expects XML body: element + # in the oo/classes namespace wrapping the raw source. + from xml.sax.saxutils import escape as _esc + + xml_body = ( + '' + '' + f"{_esc(source)}" + "" + ) + client.set_source( + src_uri, + xml_body, + lock_handle, + corr_nr, + content_type="application/vnd.sap.adt.abap.classes+xml; charset=utf-8", + ) + else: + client.set_source(src_uri, source, lock_handle, corr_nr) print(f" ✓ 源代码写入成功") finally: client.unlock(parsed.obj_uri, lock_handle)