﻿---
name: hmos-integration-test
description: "Run on-device self-test for a HarmonyOS app. Parse test_case.md, install the HAP, execute AutoTest, and produce a verification report. Optionally enters a test-and-fix loop. Triggers on phrases like '跑自测', '运行自测', '执行自测', '自动测试', '设备测试', '跑用例', '跑自动化测试', 'self test', 'run autotest'."
allowed-tools:
  - Read
  - Bash
  - Agent
  - Glob
  - AskUserQuestion
---

# Self-Test Skill

You are a **Self-Test Runner**. Your job is to orchestrate test execution directly — parse test_case.md, install the HAP, run batch tests, generate reports — and optionally drive a test-and-fix loop (test → fix → build → retest, default up to 3 rounds, configurable via `max-rounds`). You invoke the skill's `scripts/` tools directly via `node`; `self-test-runner.ts` handles HAP install/uninstall, batch execution, polling, and timeout auto-kill in a single call.

---

## Prerequisites

The skill requires two user inputs: `test-case-path` and `hap-path`. `project-dir` (the HarmonyOS project root containing `AppScope/app.json5` — the skill resolves `bundle_name`/`app_name` from it via the resolve-metadata tool during round 1 setup) is derived automatically when omitted: walk up from each `hap-path` entry (directory entries first, then file entries' parent dirs), then from `test-case-path`'s directory, looking for a directory containing `AppScope/app.json5`. Ask the user only if derivation finds nothing. `output-path` is optional — if omitted, it defaults to the directory containing `test-case-path`. After the initial test the skill can optionally enter a test-and-fix loop. Each round's outputs are snapshotted into `<output-path>/round-{n}/` (the skill always writes to `<output-path>/` root; round dirs are populated by the snapshot step); the canonical "latest" copies live at `<output-path>/` root.

A package is required (this test installs the app on a connected real HarmonyOS device or emulator). `hap-path` accepts **one or more** comma-separated paths; **each entry may be a single `.hap`/`.hsp` file or a directory**. The union of all entries must form the full package set — exactly one entry HAP plus any feature HAPs and **in-app HSPs** (Harmony Shared Packages built together with the app). In-app HSPs cannot install standalone — they depend on the main HAP — so every listed path is gathered and installed together in one `hdc install -r` transaction; the entry HAP and the HSPs **need not live in the same directory**. Confirm the package source(s) with the user before proceeding.

---

## Step 0 — Environment Variables Check (run first)

Verify (read env vars with `$env:VAR` in PowerShell on Windows; `echo "$VAR"` on macOS/Linux):

| Variable | Purpose | Valid when |
|---|---|---|
| `HOMETRANS_MODEL_API_KEY` | LLM api key (shared with UI alignment, set by `ht init`) | non-empty |
| `HOMETRANS_MODEL_NAME` | Model name (e.g. `qwen3.7-plus`) | non-empty |
| `HOMETRANS_MODEL_BASE_URL` | Model API base URL | non-empty |
| `HOMETRANS_TOOL_PATH` | HomeTrans tools dir (default `~/.hometrans/tools`) | path exists |

If `HOMETRANS_MODEL_API_KEY` is missing or empty, **stop and ask the user** to run `ht init` first.

### Step 0a — Generate autotest.yaml (if not exists)

If `~/.hometrans/autotest.yaml` does **not** exist, generate it from the env vars above:

```yaml
model:
  unified:
    name: "<HOMETRANS_MODEL_NAME>"
    base_url: "<HOMETRANS_MODEL_BASE_URL>"
    api_key: "<HOMETRANS_MODEL_API_KEY>"
    provider: "openai"

agent:
  mode: "single"
```

Write it to `~/.hometrans/autotest.yaml`. Then tell the user:

> autotest.yaml 已生成到 `~/.hometrans/autotest.yaml`。当前为 single 模式。如需启用 layered（Planner+Executor 双 Agent）模式，请编辑此文件添加 `execute`/`decision` 模型配置并将 `mode` 改为 `"layered"`。

If `autotest.yaml` already exists, skip generation — the user may have manually configured layered mode. Do **not** overwrite their settings.

This yaml is passed to AutoTestAgent's `batch_runner.js` via `--config` flag, read directly by `ConfigManager` — no translation layer needed.

### Step 0b — @autotest/agent 检查（自动安装）

`self-test-runner.ts` 在启动 batch_runner 前会先定位 `@autotest/agent` 的 `batch_runner.js`（从脚本自身位置向上查找 node_modules，再回退到全局 npm root）。**若找不到，自动安装**：

- 先从 `@buaa_smat/hometrans` 的 `package.json` 读取它依赖的 `@autotest/agent` 版本（避免装错成 `latest` 而漏掉所需修复）。
- 执行 `npm install -g @autotest/agent@<版本>`，然后重试定位。
- 仍找不到则报错并提示手动安装：`npm install -g <autotest-agent-tarball.tgz>` 或重装 `npm install -g @buaa_smat/hometrans`。

> 因此本 skill **不需要**在 Step 0 手动校验 `@autotest/agent`——runner 自愈。仅当 runner 报"auto-install failed"时才需人工介入（典型原因：该版本未发布到 npm，需装 tarball）。

## Step 1 — Parse Inputs

Extract the following from the user's message. If any required input is missing, use `AskUserQuestion` with the template below — do not guess and do not invent paths.

| Variable | Required | Meaning | Typical Phrasing |
|----------|----------|---------|------------------|
| `test-case-path` | yes | Path to test_case.md | "测试用例在...", "用例文件...", "test case path" |
| `hap-path` | yes | **One or more** comma-separated `.hap`/`.hsp` files or dirs forming the full package set (see **Prerequisites**): one entry HAP + any feature HAPs / in-app HSPs, possibly in different dirs, installed in one transaction. | "HAP路径...", "安装包在...", "包目录...", "hap和hsp在...", "hap/hsp...", "hap file..." |
| `project-dir` | no (derived from `hap-path` / `test-case-path`; ask only if derivation fails) | Path to the HarmonyOS project root (the directory containing `AppScope/app.json5`). The skill resolves `bundle_name`/`app_name` from it via the resolve-metadata tool during round 1 setup. | "工程目录...", "HarmonyOS工程...", "项目根...", "project dir..." |
| `output-path` | no (default: directory of `test-case-path`) | Directory for all output artifacts | "输出到...", "产物目录...", "output to..." |
| `pre-test-case-path` | no | Path to pre_test_case.md | "前置用例...", "pre test case..." |
| `android-project-path` | no (for fix loop) | Path to Android source project for reference-based fixing | "Android路径...", "android project..." |
| `max-rounds` | no (default `3`) | Max iterations for the test-and-fix loop. Must be a positive integer (`>= 1`). Only meaningful when the fix loop is enabled. | "最多 X 轮", "max rounds X", "迭代 X 次" |

**AskUserQuestion template (use this exact shape — do NOT ad-lib free-form questions):**

```
question: "缺少必填参数 <param>。请提供具体路径。"
choices:
  - { value: "<typed_path>", label: "提供具体路径（在补充消息里给出绝对路径）" }
```

Map the user's reply back to the variable.

**Default `output-path`**: If the user did not provide `output-path`, set it to the parent directory of `test-case-path` (the directory portion, without the filename). Use this resolved value everywhere downstream.

**Validation command** (platform-agnostic — adapt to OS):

Verify that `test-case-path` points to a readable file; that **every** comma-separated entry in `hap-path` points to a readable file **or directory**; and that, across all entries, the aggregated package set contains **at least one `.hap`** (the entry package). `output-path` is an existing directory (create if missing).

| OS | Example check |
|----|---------------|
| macOS / Linux / bash on Windows | `test -f "<path>" && echo OK` |
| Native PowerShell on Windows | `Test-Path -LiteralPath "<path>" -PathType Leaf` |

Pick whichever shell is available — the operation, not the syntax, is what matters.

**HSP auto-discovery guard**: After validating `hap-path`, scan the **parent directory of the entry HAP** and any listed **directory entries** for `*.hsp` files and an `hsp/` subdirectory — this catches the common failure where only the entry HAP is passed but the app needs its in-app HSPs at runtime. For any `*.hsp` **not already covered** by `hap-path`, append its containing directory (or the individual files) to `hap-path` as extra comma-separated entries, and warn the user: "Auto-discovered N in-app HSP(s) in `<path>` and appended to hap-path — omitting them makes the app malfunction on the device." If none are found → proceed normally (single-module project).

---

## Step 2 — Test-and-Fix Loop

### Entry — Ask User (Opt-In for Fix Loop)

Before entering the loop, if the user did NOT already say "自动修复" / "fix and retest" / "自动修", ask:

> 检测到将进入测试修复循环。是否启用自动修复和重测？（最多 `<MAX_ROUNDS>` 轮，每轮测试失败后自动修代码、重建 HAP、重新测试）

Substitute `<MAX_ROUNDS>` in the question text with the parsed value (default `3`).

- User says yes / "是" / "好" / "修" → proceed with the full loop below (2.0 → 2A → 2B → 2C → 2D).
- User says no / "不用" / "skip" → run **2A only** (single test round), skip 2B–2D, snapshot the round directory (2.5), then go to Step 3.

> If the user explicitly said "自动修复" / "fix and retest" in the original message, skip the question and enter the full loop directly.

---

### 2.0 — Loop Setup

1. Record `CURRENT_HAP = <hap-path>` (the full comma-separated list as the user gave it) and keep an immutable copy `ORIGINAL_HAP_PATH = <hap-path>`. `CURRENT_HAP` is passed verbatim as `hap-path` to the skill (package-set format: see **Prerequisites**).
2. Initialize `round = 1`, `MAX_ROUNDS = <max-rounds from Step 1, default 3>`. If the parsed value is missing or not a positive integer, use `3`.
3. Set `ROUND_DIR = <output-path>/round-1`. Create the directory.
4. **Resolve `project-dir`** (if the user did not provide it): walk up from each `hap-path` entry — for directory entries, check the entry itself then each parent; for file entries, start from the file's parent — looking for a directory containing `AppScope/app.json5`. If no `hap-path` entry yields it, repeat the walk from `test-case-path`'s directory. If still nothing, use `AskUserQuestion` to get it. Store as `<project-dir>`.
5. `PROJECT_ROOT` is **unset** at this point — it will be read after round 1's agent invocation produces `<output-path>/app-metadata.json`.
6. Mark step description with round info (e.g., "**Round 1/<MAX_ROUNDS>**").

---

### 2A — Test (per round)

The skill orchestrates everything via the tool scripts bundled in its own `scripts/` directory (NOT the legacy `$HOMETRANS_TOOL_PATH` copy). Before running any `node` command below, resolve `SKILL_SCRIPTS` to the absolute path of this skill's `scripts/` directory — for opencode that is `~/.config/opencode/skills/hmos-integration-test/scripts`. The scripts locate `@autotest/agent`'s `batch_runner.js` by walking up from their own location and falling back to the global npm root, so they run correctly whether invoked from the repo, the skill dir, or a project.

The skill calls `self-test-runner.ts` to orchestrate: HAP install/uninstall + batch execution + polling. `self-test-runner.ts` automatically reads `agent.mode` from `autotest.yaml` and forwards it to `batch_runner` via `--mode` (you do not pass `--mode` yourself).

**Round 1 — Setup phase (parse + metadata)**:

1. **Resolve app metadata**:
   ```bash
   node "$SKILL_SCRIPTS/resolve-metadata-tool.ts" --project-dir "<project-dir>" --output "<output-path>/app-metadata.json"
   ```
   Read the JSON from stdout → store `bundle_name`, `app_name`, and `project_root` (→ `PROJECT_ROOT`).

2. **Parse test_case.md** → `<output-path>/_extracted.json` → **Generate testcases.json**:
   ```bash
   node "$SKILL_SCRIPTS/testcases-tool.ts" "<output-path>/_extracted.json" "<output-path>/testcases.json" --validate
   ```

   **Pre-cases** (run first in the batch): if `pre-test-case-path` was provided (or `pre_test_case.md` was auto-discovered in the same directory as `test-case-path`), parse its `### Scenario` blocks too — same `{case_name, actions, expected_results}` shape — and **prepend each pre-case to the `cases` array of `_extracted.json` with its `case_name` prefixed by `[PRE] `** (so `testcases-tool.ts` composes them into `testcases.json` and `report-tool.ts` flags them as pre-cases via the `[PRE] ` prefix). Pre-cases thus run first in the same batch; their failures are treated as environment/setup issues, not application defects.

**Round 2+ — skip setup phase**, reuse `<output-path>/testcases.json` and `<output-path>/app-metadata.json`.

**All rounds — Run self-test-runner (install + execute + poll)**:

```bash
node "$SKILL_SCRIPTS/self-test-runner.ts" \
  --testcases "<output-path>/testcases.json" \
  --hap "<CURRENT_HAP>" \
  --bundle-name "<bundle_name>" \
  --task-dir "<output-path>/task" \
  --output-dir "<output-path>" \
  --category "<app_name>" \
  --config ~/.hometrans/autotest.yaml \
  --timeout auto
```

> `--mode` is read automatically from `autotest.yaml`'s `agent.mode`, so do not pass it manually.

This single command handles:
- `hdc uninstall` + `hdc install -r` (HAP installation)
- Spawn `batch_runner.js` (resolved from `@autotest/agent`) as a detached background process, forwarding `--mode` read from `autotest.yaml`
- Poll every 60s for `summary.json` (COMPLETED) or process death (CRASHED)
- Auto-kill on timeout

Parse the last line of stdout as terminal status JSON. Branch on `status`:

| `status` | Action |
|-----------|--------|
| `COMPLETED` | Read `<output-path>/task/task_*/task_results.jsonl` for per-case results; proceed to generate report |
| `CRASHED` | Write error to user; stop |
| `TIMEOUT` | Write timeout to user; stop |
| `FAILED` | Write error to user; stop |

5. **Generate report** — after COMPLETED:
   ```bash
   node "$SKILL_SCRIPTS/report-tool.ts" \
     --task-subdir "<latest task_* subdir>" \
     --app-metadata "<output-path>/app-metadata.json" \
     --hap "<entry-hap-basename>" \
     --device "<device-serial>" \
     --suite "<suite-name>" \
     --out "<output-path>/self-test-report.md" \
     --validate
   ```

---

### 2A.1 — Detect early-exit (sentinel) reports first

The agent writes a degraded sentinel report when T1 / T3 / T4 / T6 fail before the case table is rendered. First, guard the empty-suite case — the batch launcher returns early without writing `summary.json` when there are 0 cases, which the sentinel check below would otherwise misreport as a batch crash:

- If `<output-path>/testcases.json` exists and contains 0 cases → set `stop_reason = no_testcases`, surface a clear message to the user (`<output-path>/testcases.json contains 0 cases — nothing to run`), snapshot the round (2.5), and exit the loop. Do NOT enter 2B. (The 2A.2 `total == 0` guard remains as a secondary safety net.)

Then check that the report exists:

- If `<output-path>/self-test-report.md` does **not** exist (e.g., T8 failure or a hard error not covered by the sentinel set) → set `stop_reason = agent_early_exit`, `early_exit_reason = "no self-test-report.md written — agent failed before producing a report"`, snapshot the round (2.5), and exit the loop. Do NOT enter 2B.

Then grep the report for the first `status:` line and the first `reason:` line:

| Shell | Example |
|-------|---------|
| bash / sh | `grep -m1 -E '^status:' "<output-path>/self-test-report.md"` |
| PowerShell | `Select-String -Path "<output-path>/self-test-report.md" -Pattern '^status:' | Select-Object -First 1` |

Run both `status:` and `reason:` checks.

- If the first line matches `status: FAIL` → this is a sentinel report. Set `stop_reason = agent_early_exit`, capture the `reason:` line for the user (`early_exit_reason = <reason>`), snapshot the round (2.5), and exit the loop. Do NOT enter 2B (the fixer cannot fix an environment / connectivity / config issue).
- Otherwise → proceed to 2A.2 below.

---

### 2A.2 — Parse the normal case table

Read `<output-path>/self-test-report.md` (first 80 lines). Extract:
- `total` — total test cases
- `passed` — passed count
- `failed` — failed count

**Empty-suite guard**: If `total == 0` (e.g., `testcases.json` happened to be empty), set `stop_reason = no_testcases`, surface a clear message to the user (`<output-path>/testcases.json contains 0 cases — nothing to run`), snapshot the round (2.5), and exit the loop. Do NOT enter the fix loop.

Otherwise: Set `round_all_passed = true` if `failed == 0` and `passed == total` (and `total > 0`, which the guard above ensures).

---

### 2B — Fix (if failed > 0)

If `round_all_passed`:
- Skip 2B and 2C.
- Set `stop_reason = all_passed`.
- Snapshot the round (2.5) and exit the loop.

If `failed > 0`:

Launch `self-test-fixer`. Use `PROJECT_ROOT`:

```
Agent(
  subagent_type="self-test-fixer",
  description="Fix Self-Test Failures (Round {round})",
  prompt="self_test_report_path: <output-path>/self-test-report.md\nharmony_project_dir: <PROJECT_ROOT>\noutput_path: <ROUND_DIR>"
)
```

> `android_project_dir` is optional — if the user mentioned an Android source path, add the line `android_project_dir: <android-project-path>` to the prompt. Omit otherwise.

After completion, read `<ROUND_DIR>/self-test-fix-report.md` (first 60 lines). Extract from its 概览 section (Chinese field names per self-test-fixer.md, English semantics in parentheses):
- `白盒确认问题存在` (confirmed — white-box confirmed defect count)
- `白盒判定为误报` (false positives count)
- `修复成功` (successfully fixed count)
- `修复失败（2次尝试后）` (failed to fix count)

Set `round_no_confirmed_defects = true` if `confirmed == 0` (all failures are false positives).

Surface a one-line summary:

> Round N: test=<passed>/<total>, confirmed=X, fixed=Y, false_positives=Z

---

### 2C — Build (if defects were confirmed)

If `round_no_confirmed_defects`:
- Skip 2C.
- Set `stop_reason = no_confirmed_defects`.
- Snapshot the round (2.5) and exit the loop.

If `confirmed > 0`:

**Capture `BUILD_START`** immediately before invoking `hmos-fix-build-errors` — e.g. `touch "<ROUND_DIR>/.build_start_marker"` (or record a timestamp). This marks the moment the rebuild begins, so the re-collection step below can tell freshly-rebuilt module outputs from stale ones left by a prior build.

**Determine signing mode**: Check `ORIGINAL_HAP_PATH` (the user's original input). If it contains `*-signed.hap` or `*-signed.hsp` entries → the original packages were signed (real device path) → pass `--signed` to assert signed output. If not (emulator path, unsigned packages) → omit `--signed` so the skill produces unsigned output. This prevents the rebuilt HAP from failing `hdc install -r` on a real device while allowing unsigned testing on emulators.

Then invoke the `hmos-fix-build-errors` skill with `<PROJECT_ROOT>` (and `--signed` if determined above) for a rebuild.

After completion, locate the authoritative entry directly from `<PROJECT_ROOT>/entry/build/default/outputs/default/`. Identify `ENTRY_HAP` in this priority order:

1. `<PROJECT_ROOT>/entry/build/default/outputs/default/entry-default-*.hap`
2. First `*.hap` under `<PROJECT_ROOT>/entry/build/default/outputs/default/`

If no `.hap` is found → report "Build did not produce a HAP — cannot re-test." Set `stop_reason = no_hap`. Snapshot the round (2.5) and exit the loop.

**Assemble the next round's install set into `<ROUND_DIR>/package-set/`** — the rebuilt entry + freshly-rebuilt in-app HSPs + any original packages the rebuild did not reproduce, all in ONE directory. Use `<ROUND_DIR>/package-set/` (a dedicated subdir), **not** `<ROUND_DIR>` itself, because step 1 clears the destination and `<ROUND_DIR>` holds the round's reports and `.build_start_marker`.

1. **Clear/create** `<ROUND_DIR>/package-set/`, then copy `ENTRY_HAP` into it. The entry is placed **unconditionally** (it is authoritative; the freshness gate below does NOT apply to it).
2. **Re-collect freshly-rebuilt in-app HSPs** from the build tree `<PROJECT_ROOT>/*/build/default/outputs/default/` into `<ROUND_DIR>/package-set/`, admitting **only `*.hsp` whose mtime ≥ `BUILD_START`** (cross-platform: `find "<dir>" -newer "<ROUND_DIR>/.build_start_marker"`, or compare `stat` mtimes). The mtime gate is essential: incremental rebuilds run no clean, so non-entry module dirs still hold *stale* `.hsp` from prior builds — only the ones rebuilt *this* round (mtime ≥ `BUILD_START`) are admitted. **Do NOT re-collect feature HAPs here** (leave them entirely to the carry-forward in step 3). Skip `*ohosTest*` / `*-test-*`.
3. **Carry-forward gap-fill** from the **original** package set (`ORIGINAL_HAP_PATH`, expanded: every `.hap`/`.hsp` across all its files/dirs), copying into `<ROUND_DIR>/package-set/` only basenames **still absent** after steps 1–2. **The rebuilt entry is always authoritative — never let an original entry HAP survive next to it:**
   - **In-app HSPs** (`.hsp`): copy forward every one whose basename is not already in `package-set/`. (A freshly-rebuilt HSP from step 2 already occupies its basename → its stale original is correctly skipped.)
   - **The original entry HAP: never carry it forward.** Identify it deterministically: a `.hap` named `entry-*`; else, if there is **exactly one** `.hap` among the original packages, that one. Copying it would leave **two entry HAPs** and install stale code.
   - **Feature HAPs** (any other, clearly non-entry, module `.hap`): copy forward those whose basename is not already in `package-set/`. **If there are multiple non-`entry-*` `.hap` files and none is the sole `.hap`** — you cannot confidently tell feature from entry — **do NOT copy any of them and log that they were dropped.**

   Use the cross-platform copy syntax from 2.5.
4. **Entry-uniqueness fallback:** if `package-set/` does not end with exactly one `entry-*.hap` (or assembly otherwise failed), discard `package-set/` and fall back to **today's** behavior — copy `ENTRY_HAP` from the build tree into `<ROUND_DIR>`, carry the originals forward directly into `<ROUND_DIR>`, and set `CURRENT_HAP = <ROUND_DIR>`. This keeps the loop no-worse-than-before on a malformed build.
5. Otherwise set `CURRENT_HAP = <ROUND_DIR>/package-set/`.

This yields one directory holding exactly one entry HAP (the rebuilt one) + freshly-rebuilt in-app HSPs + carried-forward originals for anything not rebuilt — collision-free, installed together by the next round's `hdc install -r`. If the original input was a lone `.hap` with no extras and no HSP was rebuilt, `package-set/` simply holds the rebuilt entry.

---

### 2D — Loop Control

```
# stop_reason may already be set by 2A.1 (agent_early_exit), 2A.2 (no_testcases),
# 2B (all_passed / no_confirmed_defects), or 2C (no_hap).
if stop_reason is set:        snapshot (2.5) and exit loop
elif round >= MAX_ROUNDS:     stop_reason = max_rounds_reached; snapshot (2.5); exit loop
else:
    snapshot (2.5)            # snapshot the just-completed round
    round += 1
    ROUND_DIR = <output-path>/round-{round}
    create ROUND_DIR
    go to 2A
```

---

### 2.5 — Per-round Snapshot

After every round's 2A returns (round 1 and rounds 2+), snapshot these files from `<output-path>/` to `<ROUND_DIR>`. Each copy follows the rule **"skip if source missing"**.

| Source | Destination | Notes |
|--------|-------------|-------|
| `<output-path>/self-test-report.md` | `<ROUND_DIR>/self-test-report.md` | Always expected after a successful T8. If missing, sentinel-FAIL detection in 2A.1 already routed via `agent_early_exit`. |
| `<output-path>/task/` | `<ROUND_DIR>/task/` | Per-round runner artifacts. Skip if missing (e.g., agent FAILed before T6 created the dir). |
| `<output-path>/_extracted.json` | `<ROUND_DIR>/_extracted.json` | Only present after round 1 (round 1 setup phase step 2 wrote it). Skip in rounds 2+. |

**NOT snapshotted** (these stay at root only as the canonical, cross-round artifacts):
- `<output-path>/testcases.json` — written once by round 1; rounds 2+ read it in place.
- `<output-path>/app-metadata.json` — written once by round 1; rounds 2+ read it in place.

**`task/` cleanup ordering**: T5 (inside the agent) cleans `<output-path>/task/` at the start of each test invocation. The invariant: by the time T5 wipes the root `task/` at the start of round N, the previous round's `task/` has already been snapshotted into `<output-path>/round-{N-1}/task/`.

**Cross-platform copy syntax**:

| Shell | Example |
|-------|---------|
| bash / sh | `cp -fr "<source>" "<destination>"` |
| PowerShell | `Copy-Item -LiteralPath "<source>" -Destination "<destination>" -Recurse -Force` |

Use forward slashes for cross-platform safety; both shells accept them.

---

### 2.6 — Loop Finalization (package mirror only)

When the loop exits, mirror the final package(s) from `CURRENT_HAP` (which may live under the last `<ROUND_DIR>`) to `<output-path>/`. This is the only "latest" artifact whose primary location is a round dir; `self-test-report.md`, `testcases.json`, `app-metadata.json`, and `task/` already live at `<output-path>/` as canonical copies.

`CURRENT_HAP` may be a single file, a directory, or a comma-separated list of files/dirs. Walk **every** entry:
- For each entry that is a **directory** → copy every `.hap`/`.hsp` in it to `<output-path>/` (keep filenames). Do not copy non-package files.
- For each entry that is a **`.hap`/`.hsp` file** → copy it to `<output-path>/` (a lone single-`.hap` entry is copied to `<output-path>/entry-default.hap`; any other entry keeps its filename).

The goal: after finalization, `<output-path>/` holds the **complete installable set** (entry HAP + all HSPs / feature HAPs) so a later `hdc install -r` of every `.hap`/`.hsp` under `<output-path>/` (the engine expands a directory into its `.hap`/`.hsp` file list) reproduces the tested install.

Use the same cross-platform copy syntax as above. Skip any copy whose source already resides at the destination.

---

## Step 3 — Surface Results

Read `<output-path>/self-test-report.md` (first 80 lines) and extract the overview section:

```
## 测试概览

- **测试套件**: <suite-name>
- **测试时间**: <time-range>
- **设备**: <device-serial>
- **应用**: <app-name> (<bundle-name>)
- **HAP**: <entry-hap-basename>
- **总用例数**: <total>（前置 <pre-total> + 常规 <regular-total>）
- **通过**: <passed>（前置 <pre-pass> / 常规 <regular-pass>）
- **失败**: <failed>（FAIL <fail-count> + UNKNOWN <unknown-count>）
- **常规通过率**: <regular-pass-rate>（仅功能场景，反映本次需求质量）
- **含前置通过率**: <pass-rate>（仅供整体参考；前置用例属于数据/环境准备，与本次需求功能无关）
```

> If the report file is missing or malformed, report the error to the user with the agent's output path — do NOT fabricate results. If the report exists but is truncated or unexpectedly formatted, read up to the first 80 lines for the overview section, and point the user to the full file at `<output-path>/self-test-report.md`.

**Pre-case reminder**: If pre-cases failed, note that these are environment/setup issues (permissions, media imports, tutorial skipping), NOT application defects. The regular pass rate (常规通过率) is the primary quality metric.

**Point the user to the full report**: `<output-path>/self-test-report.md` and per-round artifacts at `<output-path>/round-{N}/`.

### Loop Summary (if loop ran)

After the report, present:

```
## 自测修复循环完成

| 指标 | 值 |
|------|-----|
| 迭代次数 | <round>/<MAX_ROUNDS> |
| 最终状态 | <passed>/<total> 通过（<pass-rate>%） |
| 停止原因 | <stop_reason> |
```

`<stop_reason>` is one of:
- `all_passed` — all cases passed, loop exited successfully.
- `no_confirmed_defects` — failures exist but fixer judged them all as false positives.
- `no_hap` — the rebuild did not produce an entry HAP in the build output, so the loop cannot continue.
- `max_rounds_reached` — hit the configured `MAX_ROUNDS` cap (default 3) with failures remaining.
- `no_testcases` — `<output-path>/testcases.json` contained 0 cases; nothing to run. Surface the path so the user can inspect.
- `agent_early_exit` — the skill wrote a sentinel FAIL report (config / device / autotest-dir / batch crash / precondition failure), or no `self-test-report.md` was written (skill failed before producing a report). Surface the captured `early_exit_reason` so the user knows what to fix manually.

If failures remain (for `max_rounds_reached`), list them briefly (scenario names from the report overview). Also surface `<ROUND_DIR>/self-test-fix-report.md` when it exists, and describe the final rebuild outcome from the loop summary instead of relying on a removed build-stage report artifact.

---

## Error Handling

| Scenario | Action |
|----------|--------|
| `test-case-path` does not exist | Ask user for correct path, do not proceed |
| any `hap-path` entry does not exist | Name the missing entry, ask user for the correct path, do not proceed |
| `hap-path` entries all exist but none contributes a `.hap` (no entry package) | Report: "No entry HAP found across the given paths — at least one `.hap` is required." Do not proceed |
| `output-path` parent does not exist | Create it, proceed |
| Agent returns error / times out | Report the error to user. Suggest: check device connection (`npx --yes devecocli device list`), verify the HAP is valid, check the `HOMETRANS_MODEL_API_KEY` env var, and confirm `~/.hometrans/autotest.yaml` has a real `api_key` |
| `<output-path>/self-test-report.md` missing after agent completes | Report: "Agent completed but no report was generated. Check agent output at <output-path>." |
| `self-test-report.md` exists but format is unrecognizable | Show the file path, report the first 80 lines as context, let user investigate |
| Device not found (agent reports "No device") | Remind user: connect a HarmonyOS device or emulator, verify `npx --yes devecocli device list` shows it |
| Pre-case failures in report | Note: these are environment issues, not app defects. Highlight regular pass rate |
| `<output-path>/testcases.json` or `app-metadata.json` missing after round 1 | Round 1 setup may have failed; inspect agent output at `<output-path>/` |
| `<output-path>/testcases.json` is empty (`total == 0` in 2A) | Report: "testcases.json contains 0 cases — nothing to run." Exit loop with `stop_reason = no_testcases`. Do NOT enter the fix loop. |
| The skill writes a sentinel FAIL report (first line `status: FAIL`), or no `self-test-report.md` was written (skill failed before producing a report) | Capture the `reason:` line. Exit loop with `stop_reason = agent_early_exit` and surface the captured reason. Do NOT enter the fix loop — these failures (config / device / autotest-dir / crash / timeout / precondition failures / report not produced) are not application defects. |
| Fixer returns `confirmed == 0` (all false positives) | Exit loop with `stop_reason = no_confirmed_defects`. Surface the fix report summary. |
| hmos-fix-build-errors produces no HAP in build output | Report: "Build did not produce a HAP — cannot re-test." Exit loop with `stop_reason = no_hap`. |
| Loop reaches `MAX_ROUNDS` (default 3) with failures remaining | Report `stop_reason = max_rounds_reached`. Surface final pass rate and remaining failures. |
| `app-metadata.json` missing after round 1 (PROJECT_ROOT unknown) | Surface agent error; PROJECT_ROOT cannot be resolved. Do NOT enter 2B. |

---

## Key Constraints

- **Direct orchestration**: The skill runs test tools directly via `node "$SKILL_SCRIPTS/..."` (scripts bundled in the skill's own `scripts/` directory) and resolves `batch_runner.js` from `@autotest/agent` internally (walk-up + global npm root fallback + auto-install). This SKILL.md is the **single source of truth** for the procedure. The `self-tester` **subagent** (`agents/self-tester.md`) is a thin wrapper that loads this skill — callers (e.g. `hmos-convert-pipeline` Stage 4) may launch it for context isolation on long runs; it forwards the same kebab params and reports the result back, without duplicating any procedure. The legacy `$HOMETRANS_TOOL_PATH` scripts (tools/test-tools/autotest) are no longer used.
- **Parameter purity**: Pass only parameter values to the agent's prompt in `key: value` format. Do NOT add extra instructions, format descriptions, schema hints, or implementation suggestions. The agent has its own built-in workflow.
- **Do NOT read full reports**: Self-test reports can be very large (10KB–200KB). Only read the overview (first 80 lines for test report, first 60 lines for fix report). Point the user to the full file; never fabricate results when a report is missing or unreadable.
- **Quote all paths**: Paths may contain spaces. Always wrap paths in double quotes in commands.
- **Pre-cases are environment scripts**: Their failures indicate testing-environment issues, not app bugs. The regular pass rate is the primary quality metric.
- **Fix-and-retest loop is opt-in**: Follow Step 2 Entry — ask the user unless they already opted in with "自动修复".
- **Test → Fix → Build is the mandatory order**: Never re-test without rebuilding after a fix (would test old code). Never skip the build step in the loop.
- **`CURRENT_HAP` must track the latest build**: After each 2C rebuild, update `CURRENT_HAP` to the new HAP from `ROUND_DIR`. Never re-test with the old HAP.
- **Per-round snapshots**: Each round writes its latest artifacts to `<output-path>/` (root), then the SKILL snapshots per-round outputs to `<output-path>/round-{N}/`. The persistent JSONs (`testcases.json`, `app-metadata.json`) are written exactly once by round 1 and consumed unchanged by rounds 2+. Never pass `output_path: <output-path>/round-N` to the agent — it always writes to `<output-path>/` root; round dirs are populated only by this snapshot step.
- **Loop iterations are strictly sequential**: Never launch more than one fixer or tester at a time.
