mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 14:01:24 +08:00
Compare commits
10 Commits
v2026.5.7
...
codex/open
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d8750cdab | ||
|
|
a18f9bc29c | ||
|
|
5d0dc24085 | ||
|
|
d4056692d6 | ||
|
|
deaab200f6 | ||
|
|
71cfa13192 | ||
|
|
d98f4f9df3 | ||
|
|
17affb4a61 | ||
|
|
cc32d032b5 | ||
|
|
fa20679dab |
@@ -30,6 +30,7 @@ This page describes the current CLI behavior. If commands change, update this do
|
||||
- [`status`](/cli/status)
|
||||
- [`health`](/cli/health)
|
||||
- [`sessions`](/cli/sessions)
|
||||
- [`report`](/cli/report)
|
||||
- [`gateway`](/cli/gateway)
|
||||
- [`logs`](/cli/logs)
|
||||
- [`system`](/cli/system)
|
||||
@@ -151,6 +152,10 @@ openclaw [--dev] [--profile <name>] <command>
|
||||
status
|
||||
health
|
||||
sessions
|
||||
report
|
||||
bug
|
||||
feature
|
||||
security
|
||||
gateway
|
||||
call
|
||||
health
|
||||
@@ -712,6 +717,27 @@ Options:
|
||||
- `--store <path>`
|
||||
- `--active <minutes>`
|
||||
|
||||
### `report`
|
||||
|
||||
Prepare sanitized bug reports, feature requests, and private security report packets for `openclaw/openclaw`.
|
||||
|
||||
Subcommands:
|
||||
|
||||
- `bug`
|
||||
- `feature`
|
||||
- `security`
|
||||
|
||||
Shared options:
|
||||
|
||||
- `--title <text>`
|
||||
- `--summary <text>`
|
||||
- `--json`
|
||||
- `--markdown`
|
||||
- `--output <file>`
|
||||
- `--submit`
|
||||
- `--yes`
|
||||
- `--non-interactive`
|
||||
|
||||
## Reset / Uninstall
|
||||
|
||||
### `reset`
|
||||
|
||||
204
docs/cli/report.md
Normal file
204
docs/cli/report.md
Normal file
@@ -0,0 +1,204 @@
|
||||
---
|
||||
summary: "CLI reference for `openclaw report` (bug reports, feature requests, and private security packets)"
|
||||
read_when:
|
||||
- You want to prepare a sanitized GitHub issue draft from local OpenClaw state
|
||||
- You want to submit a public bug or feature issue with `gh`
|
||||
- You need a private security report packet instead of a public issue
|
||||
title: "report"
|
||||
---
|
||||
|
||||
# `openclaw report`
|
||||
|
||||
Prepare sanitized reports for `openclaw/openclaw`.
|
||||
|
||||
`openclaw report` turns a small amount of user input plus local runtime/config context into:
|
||||
|
||||
- public bug report drafts
|
||||
- public feature request drafts
|
||||
- private security report packets
|
||||
|
||||
Public bug and feature reports can optionally be submitted with `gh`. Security reports never create a public GitHub issue.
|
||||
|
||||
## Subcommands
|
||||
|
||||
- `openclaw report bug`
|
||||
- `openclaw report feature`
|
||||
- `openclaw report security`
|
||||
|
||||
## Shared flags
|
||||
|
||||
- `--title <text>`: explicit report title
|
||||
- `--summary <text>`: short summary (used in public reports and as a fallback title)
|
||||
- `--json`: emit the structured sanitized payload
|
||||
- `--markdown`: emit only the rendered report body
|
||||
- `--output <file>`: write the sanitized body to a file
|
||||
- `--submit`: submit a public bug/feature issue when the report is ready
|
||||
- `--yes`: skip interactive confirmation for submission
|
||||
- `--non-interactive`: disable prompts; public submission requires `--yes`
|
||||
|
||||
If neither `--json` nor `--markdown` is passed, the default output is a human-readable sanitized preview.
|
||||
|
||||
`--yes` only skips the final interactive confirmation. It does not skip draft generation, diagnostics, or probe execution.
|
||||
|
||||
## Bug reports
|
||||
|
||||
Use `openclaw report bug` for broken behavior, regressions, or operational failures.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
openclaw report bug \
|
||||
--summary "Gateway times out behind mitmproxy" \
|
||||
--repro "1. Start gateway behind proxy\n2. Send any LLM request" \
|
||||
--expected "Model responds successfully" \
|
||||
--actual "Requests fail with timeout" \
|
||||
--impact "Blocks all LLM traffic"
|
||||
```
|
||||
|
||||
```bash
|
||||
openclaw report bug \
|
||||
--summary "Gateway times out behind mitmproxy" \
|
||||
--repro "1. Start gateway behind proxy\n2. Send any LLM request" \
|
||||
--expected "Model responds successfully" \
|
||||
--actual "Requests fail with timeout" \
|
||||
--impact "Blocks all LLM traffic" \
|
||||
--probe gateway \
|
||||
--submit
|
||||
```
|
||||
|
||||
Bug-specific flags:
|
||||
|
||||
- `--repro <text>`: steps to reproduce
|
||||
- `--expected <text>`: expected behavior
|
||||
- `--actual <text>`: observed behavior
|
||||
- `--impact <text>`: severity or workflow impact
|
||||
- `--previous-version <text>`: optional regression context
|
||||
- `--evidence <text>`: extra evidence to append
|
||||
- `--additional-information <text>`: broad extra details, clues, timelines, or hypotheses
|
||||
- `--context <text>`: compatibility alias for `--additional-information`
|
||||
- `--probe <general|model|channel|gateway|none>`: bounded evidence collection mode
|
||||
|
||||
Required fields for a submission-eligible bug report:
|
||||
|
||||
- summary
|
||||
- repro
|
||||
- expected
|
||||
- actual
|
||||
- impact
|
||||
|
||||
Auto-collected where available:
|
||||
|
||||
- OpenClaw version
|
||||
- OS/runtime summary
|
||||
- configured model/provider hints
|
||||
- a short bounded probe summary when `--probe` is enabled
|
||||
|
||||
Probe guidance:
|
||||
|
||||
- `general`: runtime summary, proxy env context, gateway/model/channel signals, and one recent sanitized runtime error when available
|
||||
- `gateway`: gateway reachability, health, and proxy context
|
||||
- `model`: provider auth overview plus a bounded live model-path check with combined proxy-status output
|
||||
- `channel`: configured-channel summary plus recent channel/runtime issue hints
|
||||
|
||||
## Feature requests
|
||||
|
||||
Use `openclaw report feature` for improvements or new capabilities.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
openclaw report feature \
|
||||
--summary "Add a report dry-run flag" \
|
||||
--problem "Operators want draft output without touching GitHub" \
|
||||
--solution "Support report --submit only when explicitly requested" \
|
||||
--impact "Safer issue authoring from scripts"
|
||||
```
|
||||
|
||||
Feature-specific flags:
|
||||
|
||||
- `--problem <text>`: problem to solve
|
||||
- `--solution <text>`: proposed solution
|
||||
- `--impact <text>`: expected impact
|
||||
- `--alternatives <text>`: alternatives considered
|
||||
- `--evidence <text>`: examples or supporting evidence
|
||||
- `--additional-information <text>`: broad extra details, clues, timelines, or hypotheses
|
||||
- `--context <text>`: compatibility alias for `--additional-information`
|
||||
- `--probe <general|model|channel|gateway|none>`: optional bounded evidence collection
|
||||
|
||||
Required fields for a submission-eligible feature request:
|
||||
|
||||
- summary
|
||||
- problem
|
||||
- solution
|
||||
- impact
|
||||
|
||||
## Security reports
|
||||
|
||||
Use `openclaw report security` for private vulnerability reports or sensitive disclosures.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
openclaw report security \
|
||||
--title "Gateway token exposed in logs" \
|
||||
--severity high \
|
||||
--impact "Operator credential disclosure" \
|
||||
--component "gateway auth logging" \
|
||||
--reproduction "Run startup flow with verbose logging enabled" \
|
||||
--demonstrated-impact "Token appears in terminal output" \
|
||||
--environment "macOS 15.4, OpenClaw 2026.3.x" \
|
||||
--remediation "Mask auth values before logging"
|
||||
```
|
||||
|
||||
Security-specific flags:
|
||||
|
||||
- `--severity <text>`
|
||||
- `--impact <text>`
|
||||
- `--component <text>`
|
||||
- `--reproduction <text>`
|
||||
- `--demonstrated-impact <text>`
|
||||
- `--environment <text>`
|
||||
- `--remediation <text>`
|
||||
|
||||
Rules:
|
||||
|
||||
- `report security` never calls `gh issue create`
|
||||
- `--submit` is ignored as a public-issue path and returns a blocked submission status
|
||||
- terminal output stays private-report-oriented
|
||||
- use `--output` or `--markdown` to save a private report packet for manual sending
|
||||
|
||||
Private route: send completed security reports to `security@openclaw.ai`.
|
||||
|
||||
## Redaction and submission behavior
|
||||
|
||||
The command sanitizes common sensitive values before rendering output or submitting:
|
||||
|
||||
- tokens / bearer values / API keys
|
||||
- email addresses
|
||||
- phone numbers
|
||||
- private user handles
|
||||
- local user path prefixes such as `/Users/<name>` or `/home/<name>`
|
||||
|
||||
For public bug and feature reports:
|
||||
|
||||
- `--submit` is required before any GitHub issue is created
|
||||
- interactive runs ask for confirmation before `gh issue create`
|
||||
- non-interactive submission requires both `--submit` and `--yes`
|
||||
- if required fields are missing, the command returns a structured blocked state instead of guessing
|
||||
- generated report bodies include a short provenance footer noting they were generated via `openclaw report`
|
||||
|
||||
## JSON output
|
||||
|
||||
`--json` emits a stable sanitized payload with fields such as:
|
||||
|
||||
- `kind`
|
||||
- `title`
|
||||
- `body`
|
||||
- `labels`
|
||||
- `evidence`
|
||||
- `redactionsApplied`
|
||||
- `missingFields`
|
||||
- `submissionEligible`
|
||||
- `submission`
|
||||
|
||||
This is intended for scripting and higher-level automation.
|
||||
@@ -37,6 +37,7 @@ x-i18n:
|
||||
- [`status`](/cli/status)
|
||||
- [`health`](/cli/health)
|
||||
- [`sessions`](/cli/sessions)
|
||||
- [`report`](/cli/report)
|
||||
- [`gateway`](/cli/gateway)
|
||||
- [`logs`](/cli/logs)
|
||||
- [`system`](/cli/system)
|
||||
@@ -156,6 +157,10 @@ openclaw [--dev] [--profile <name>] <command>
|
||||
status
|
||||
health
|
||||
sessions
|
||||
report
|
||||
bug
|
||||
feature
|
||||
security
|
||||
gateway
|
||||
call
|
||||
health
|
||||
|
||||
210
docs/zh-CN/cli/report.md
Normal file
210
docs/zh-CN/cli/report.md
Normal file
@@ -0,0 +1,210 @@
|
||||
---
|
||||
read_when:
|
||||
- 你想基于本地 OpenClaw 状态生成脱敏后的 GitHub issue 草稿
|
||||
- 你想用 `gh` 提交公开的 bug 或功能请求
|
||||
- 你需要私下发送安全报告而不是创建公开 issue
|
||||
summary: "`openclaw report` 的 CLI 参考(bug 报告、功能请求和私有安全报告包)"
|
||||
title: report
|
||||
x-i18n:
|
||||
generated_at: "2026-03-21T03:20:00Z"
|
||||
model: gpt-5.4
|
||||
provider: openai
|
||||
source_path: cli/report.md
|
||||
workflow: 15
|
||||
---
|
||||
|
||||
# `openclaw report`
|
||||
|
||||
为 `openclaw/openclaw` 准备脱敏后的报告。
|
||||
|
||||
`openclaw report` 会把少量用户输入与本地运行时/配置上下文组合起来,生成:
|
||||
|
||||
- 公开 bug 报告草稿
|
||||
- 公开功能请求草稿
|
||||
- 私有安全报告包
|
||||
|
||||
公开的 bug 和功能请求可以选择用 `gh` 提交。安全报告永远不会创建公开 GitHub issue。
|
||||
|
||||
## 子命令
|
||||
|
||||
- `openclaw report bug`
|
||||
- `openclaw report feature`
|
||||
- `openclaw report security`
|
||||
|
||||
## 共享标志
|
||||
|
||||
- `--title <text>`:显式指定报告标题
|
||||
- `--summary <text>`:简短摘要(公开报告中使用,也可作为回退标题)
|
||||
- `--json`:输出结构化的脱敏 payload
|
||||
- `--markdown`:仅输出渲染后的报告正文
|
||||
- `--output <file>`:将脱敏后的正文写入文件
|
||||
- `--submit`:在报告完整时提交公开 bug/feature issue
|
||||
- `--yes`:跳过提交前的交互确认
|
||||
- `--non-interactive`:禁用提示;公开提交时需要同时传 `--yes`
|
||||
|
||||
如果既没有传 `--json` 也没有传 `--markdown`,默认输出为人类可读的脱敏预览。
|
||||
|
||||
`--yes` 只会跳过最后的交互确认,不会跳过草稿生成、诊断收集或 probe 执行。
|
||||
|
||||
## Bug 报告
|
||||
|
||||
`openclaw report bug` 用于故障行为、回归问题或运行失败。
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
openclaw report bug \
|
||||
--summary "Gateway 在 mitmproxy 后超时" \
|
||||
--repro "1. 在代理后启动 gateway\n2. 发送任意 LLM 请求" \
|
||||
--expected "模型成功响应" \
|
||||
--actual "请求因超时失败" \
|
||||
--impact "阻塞所有 LLM 流量"
|
||||
```
|
||||
|
||||
```bash
|
||||
openclaw report bug \
|
||||
--summary "Gateway 在 mitmproxy 后超时" \
|
||||
--repro "1. 在代理后启动 gateway\n2. 发送任意 LLM 请求" \
|
||||
--expected "模型成功响应" \
|
||||
--actual "请求因超时失败" \
|
||||
--impact "阻塞所有 LLM 流量" \
|
||||
--probe gateway \
|
||||
--submit
|
||||
```
|
||||
|
||||
Bug 专用标志:
|
||||
|
||||
- `--repro <text>`:复现步骤
|
||||
- `--expected <text>`:期望行为
|
||||
- `--actual <text>`:实际行为
|
||||
- `--impact <text>`:对用户或运维的影响
|
||||
- `--previous-version <text>`:可选的回归版本上下文
|
||||
- `--evidence <text>`:额外证据
|
||||
- `--additional-information <text>`:更宽泛的附加细节、线索、时间线或假设
|
||||
- `--context <text>`:`--additional-information` 的兼容别名
|
||||
- `--probe <general|model|channel|gateway|none>`:有限的证据采集模式
|
||||
|
||||
要达到可提交的 bug 报告状态,至少需要:
|
||||
|
||||
- `summary`
|
||||
- `repro`
|
||||
- `expected`
|
||||
- `actual`
|
||||
- `impact`
|
||||
|
||||
如果可用,还会自动收集:
|
||||
|
||||
- OpenClaw 版本
|
||||
- OS / 运行时摘要
|
||||
- 已配置的 model / provider 线索
|
||||
- 在启用 `--probe` 时生成的简短探测摘要
|
||||
|
||||
Probe 说明:
|
||||
|
||||
- `general`:运行时摘要、代理环境上下文、gateway/model/channel 信号,以及可用时的一条近期脱敏错误摘要
|
||||
- `gateway`:gateway 可达性、health 和代理上下文
|
||||
- `model`:provider 认证概览,以及带有代理状态组合输出的有限 live model-path 检查
|
||||
- `channel`:已配置 channel 摘要,以及近期 channel / runtime 问题线索
|
||||
|
||||
## 功能请求
|
||||
|
||||
`openclaw report feature` 用于产品改进或新能力需求。
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
openclaw report feature \
|
||||
--summary "添加 report dry-run 标志" \
|
||||
--problem "运维希望生成草稿而不直接触发 GitHub" \
|
||||
--solution "只有显式传 --submit 时才允许真正提交" \
|
||||
--impact "让脚本化 issue 编写更安全"
|
||||
```
|
||||
|
||||
Feature 专用标志:
|
||||
|
||||
- `--problem <text>`:要解决的问题
|
||||
- `--solution <text>`:提议的解决方案
|
||||
- `--impact <text>`:预期影响
|
||||
- `--alternatives <text>`:考虑过的替代方案
|
||||
- `--evidence <text>`:支持证据或示例
|
||||
- `--additional-information <text>`:更宽泛的附加细节、线索、时间线或假设
|
||||
- `--context <text>`:`--additional-information` 的兼容别名
|
||||
- `--probe <general|model|channel|gateway|none>`:可选的有限证据采集
|
||||
|
||||
要达到可提交的功能请求状态,至少需要:
|
||||
|
||||
- `summary`
|
||||
- `problem`
|
||||
- `solution`
|
||||
- `impact`
|
||||
|
||||
## 安全报告
|
||||
|
||||
`openclaw report security` 用于私下提交漏洞或敏感披露。
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
openclaw report security \
|
||||
--title "Gateway token 出现在日志中" \
|
||||
--severity high \
|
||||
--impact "运维凭证泄露" \
|
||||
--component "gateway auth logging" \
|
||||
--reproduction "启用 verbose logging 并运行启动流程" \
|
||||
--demonstrated-impact "token 出现在终端输出中" \
|
||||
--environment "macOS 15.4, OpenClaw 2026.3.x" \
|
||||
--remediation "在日志输出前先 mask 授权值"
|
||||
```
|
||||
|
||||
Security 专用标志:
|
||||
|
||||
- `--severity <text>`
|
||||
- `--impact <text>`
|
||||
- `--component <text>`
|
||||
- `--reproduction <text>`
|
||||
- `--demonstrated-impact <text>`
|
||||
- `--environment <text>`
|
||||
- `--remediation <text>`
|
||||
|
||||
规则:
|
||||
|
||||
- `report security` 永远不会调用 `gh issue create`
|
||||
- `--submit` 不会触发公开 issue 创建,而是返回被阻止的提交状态
|
||||
- 终端输出会保持为适合私有报告的内容
|
||||
- 可使用 `--output` 或 `--markdown` 保存私有报告包,后续手动发送
|
||||
|
||||
私有提交路径:将完整安全报告发送到 `security@openclaw.ai`。
|
||||
|
||||
## 脱敏与提交流程
|
||||
|
||||
该命令会在渲染输出或提交前自动脱敏常见敏感值:
|
||||
|
||||
- token / bearer 值 / API key
|
||||
- 邮箱地址
|
||||
- 电话号码
|
||||
- 私人用户句柄
|
||||
- 本地用户路径前缀,例如 `/Users/<name>` 或 `/home/<name>`
|
||||
|
||||
对于公开的 bug 和 feature 报告:
|
||||
|
||||
- 只有显式传 `--submit` 才会创建 GitHub issue
|
||||
- 交互式运行时会在 `gh issue create` 前要求确认
|
||||
- 非交互式提交需要同时传 `--submit` 和 `--yes`
|
||||
- 如果缺少必填字段,命令会返回结构化的阻止状态,而不是猜测内容
|
||||
- 生成后的报告正文会附带一条简短来源说明,表明该草稿由 `openclaw report` 生成
|
||||
|
||||
## JSON 输出
|
||||
|
||||
`--json` 会输出稳定的脱敏 payload,字段包括:
|
||||
|
||||
- `kind`
|
||||
- `title`
|
||||
- `body`
|
||||
- `labels`
|
||||
- `evidence`
|
||||
- `redactionsApplied`
|
||||
- `missingFields`
|
||||
- `submissionEligible`
|
||||
- `submission`
|
||||
|
||||
该输出适合脚本和更高层的自动化流程。
|
||||
261
skills/openclaw-feedback/SKILL.md
Normal file
261
skills/openclaw-feedback/SKILL.md
Normal file
@@ -0,0 +1,261 @@
|
||||
---
|
||||
name: openclaw-feedback
|
||||
description: Invoke when the user starts complaining about a bug, broken behavior, regression, product issue, feature request, or private security problem in `openclaw/openclaw`. Route them into the right `openclaw report` flow.
|
||||
user-invocable: true
|
||||
metadata:
|
||||
{
|
||||
"openclaw":
|
||||
{
|
||||
"emoji": "🐙",
|
||||
"requires": { "bins": ["gh"] },
|
||||
"install":
|
||||
[
|
||||
{
|
||||
"id": "brew",
|
||||
"kind": "brew",
|
||||
"formula": "gh",
|
||||
"bins": ["gh"],
|
||||
"label": "Install GitHub CLI (brew)",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
---
|
||||
|
||||
# OpenClaw Feedback
|
||||
|
||||
Use this skill only for `openclaw/openclaw`.
|
||||
|
||||
## Goal
|
||||
|
||||
- Route the user into the correct `openclaw report` flow with minimal extra questions.
|
||||
- Treat `openclaw report` as the only authoritative path for drafting, previewing, diagnostics, redaction, and submission results.
|
||||
|
||||
## Do
|
||||
|
||||
- Tell the user you are using the `openclaw-feedback` skill.
|
||||
- In that opening, explain that getting enough context matters because stronger context produces a more accurate, actionable GitHub issue and avoids weak or misleading filings.
|
||||
- In that opening, give the user a clear way to decline or cancel issue filing if they do not want to proceed.
|
||||
- Decide `bug`, `feature`, or `private security report`.
|
||||
- Ask only for missing required fields, with at most 1-3 short questions.
|
||||
- Derive as much as possible from the conversation, the active diagnosis session, and the generated report draft before asking the user anything.
|
||||
- Treat `openclaw report bug|feature|security` as the source of truth for drafting, redaction, diagnostics, previewing, and submission behavior.
|
||||
- If you are unsure about flags or subcommand shape, run `openclaw report --help` before invoking the report flow.
|
||||
- Tell the user report generation can take a moment when diagnostics or probes are included.
|
||||
- Relay the generated draft or blocked result from `openclaw report` directly.
|
||||
- Show the full sanitized draft before asking to submit.
|
||||
- Ask permission in plain English before adding `--submit`.
|
||||
- Only after approval, use `--submit` for public bug or feature issues.
|
||||
- If submission succeeds, include the created GitHub issue URL in the final reply to the user.
|
||||
|
||||
## Do Not
|
||||
|
||||
- Never create the issue before user approval.
|
||||
- Never file against any repo other than `openclaw/openclaw`.
|
||||
- Never maintain a separate manual issue-writing path when `openclaw report` is available.
|
||||
- Never publish a security report as a public issue.
|
||||
- Never fall back to manual filing if `openclaw report` or `gh` is unavailable.
|
||||
- Never ask the user for fields the conversation, diagnostics, or report output already make clear.
|
||||
- Never read report metadata such as labels, submission eligibility, or redactions back to the user unless it is directly useful to the decision.
|
||||
|
||||
## If X Then Y
|
||||
|
||||
- If the request is a vulnerability, leaked credential, or private security report: use `openclaw report security`; do not create a public issue.
|
||||
- If the request is clearly a broken behavior or regression: use `openclaw report bug`.
|
||||
- If the request is clearly asking for a new capability or improvement: use `openclaw report feature`.
|
||||
- If the type is unclear: ask one short question to decide bug vs feature.
|
||||
- If the user already gave enough detail: skip extra questions.
|
||||
- If summary, likely title, environment, diagnosis clues, repro outline, or impact can be derived from the conversation or active diagnosis work: do not ask for them again.
|
||||
- If diagnostics would materially improve the report: use `--probe general|gateway|model|channel` on the `openclaw report` command instead of assembling standalone diagnostics yourself.
|
||||
- If the issue is still too weak after a short recovery attempt: return `NOT_ENOUGH_INFO`.
|
||||
- If unsafe content cannot be safely redacted without losing the technical meaning: return `BLOCKED_UNSAFE_CONTENT`.
|
||||
- If `openclaw report` or `gh` is unavailable: return `BLOCKED_MISSING_TOOL`.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Say: `I’m using the openclaw-feedback skill to prepare an OpenClaw GitHub issue. I want to gather enough context to make the issue accurate and useful for maintainers without over-questioning you. Report generation can take a moment if I include diagnostics or probes. If you do not want to file an issue, just tell me and I’ll stop.`
|
||||
2. Decide `bug`, `feature`, or `private security report`.
|
||||
3. Derive as much as possible from the conversation and current diagnosis context before asking anything.
|
||||
4. Ask only for missing required user facts:
|
||||
- bug: summary, steps to reproduce, expected behavior, actual behavior, impact
|
||||
optional regression context: previous version -> `--previous-version`
|
||||
- feature: summary, problem to solve, proposed solution, impact
|
||||
- security: title, severity, impact, affected component, technical reproduction, demonstrated impact, environment, remediation advice
|
||||
If a field can be inferred with high confidence from the conversation or diagnosis session, infer it instead of asking.
|
||||
5. Choose the matching command:
|
||||
- `openclaw report bug`
|
||||
- `openclaw report feature`
|
||||
- `openclaw report security`
|
||||
6. If targeted diagnostics are useful, add one probe mode:
|
||||
- `--probe general`
|
||||
- `--probe gateway`
|
||||
- `--probe model`
|
||||
- `--probe channel`
|
||||
Choose `--probe gateway` for proxy, gateway, or timeout/network failures.
|
||||
Choose `--probe model` for provider auth, model-call, or dispatcher/proxy-path issues.
|
||||
Choose `--probe channel` for channel integrations or account-specific failures.
|
||||
7. Run `openclaw report <kind> ...` and trust its output as authoritative.
|
||||
8. Show the full sanitized draft or blocked result without reformatting it into a separate skill-owned state machine.
|
||||
9. When showing the draft, emphasize the user-visible problem and the proposed report body, not internal metadata like labels or submission headers.
|
||||
10. After showing the draft, ask in plain English: `If this draft looks right, I can submit it to GitHub now.`
|
||||
11. Do not mention CLI flags like `--submit` in the user-facing approval question.
|
||||
12. Only if the user clearly approves, rerun or continue with `--submit` for public bug or feature issues.
|
||||
13. If the issue is created successfully, include the created GitHub URL in the final reply.
|
||||
14. For security, keep the report private and route the user to `security@openclaw.ai`.
|
||||
|
||||
## Common Commands
|
||||
|
||||
- Help: `openclaw report --help`
|
||||
- Bug draft: `openclaw report bug`
|
||||
- Feature draft: `openclaw report feature`
|
||||
- Security private report draft: `openclaw report security`
|
||||
- Public issue submission after approval: add `--submit`
|
||||
|
||||
## Flag Mapping
|
||||
|
||||
- summary -> `--summary`
|
||||
- repro -> `--repro`
|
||||
- expected -> `--expected`
|
||||
- actual -> `--actual`
|
||||
- impact -> `--impact`
|
||||
- previous version -> `--previous-version`
|
||||
- additional information -> `--additional-information`
|
||||
- feature problem -> `--problem`
|
||||
- feature solution -> `--solution`
|
||||
|
||||
Use `--additional-information` for details that do not fit neatly into `--repro`, `--expected`, `--actual`, or `--evidence`, including:
|
||||
|
||||
- useful loose context
|
||||
- regression clues
|
||||
- timelines
|
||||
- unusual observations
|
||||
- operator hypotheses worth preserving
|
||||
|
||||
Prefer deriving these from the conversation when they are already clear instead of asking the user to restate them.
|
||||
|
||||
When passing multiline text into the CLI as a single quoted argument, encode line breaks as literal `\n` so `openclaw report` can render them back as real line breaks in the final issue body.
|
||||
|
||||
## Private Security Reports
|
||||
|
||||
If the request is a security issue:
|
||||
|
||||
- use `openclaw report security` as the source of truth for the private report draft
|
||||
- do not create a public GitHub issue
|
||||
- do not include exploit details in chat unless needed to route the report
|
||||
- ask only for missing required private-report fields
|
||||
- briefly summarize the category, affected area, and impact in private-report-safe terms only
|
||||
- tell the user to report it privately to `security@openclaw.ai`
|
||||
|
||||
## PII And Secret Redaction
|
||||
|
||||
- Let `openclaw report` handle redaction by default.
|
||||
- If you must quote or summarize content before invoking it, redact tokens, passwords, emails, phone numbers, private-person handles, home-directory names, and unnecessary local file paths.
|
||||
|
||||
## Permission Preview
|
||||
|
||||
Let `openclaw report` define the preview, readiness, blocked-submission, and created-output wording.
|
||||
|
||||
Do not restate or simulate `READY_TO_CREATE`, `SUBMISSION_BLOCKED`, or other CLI output formats inside this skill.
|
||||
|
||||
If the user has not clearly approved filing after seeing the full draft, stop before adding `--submit`.
|
||||
|
||||
## Notes
|
||||
|
||||
- `openclaw report` is the authoritative path for title/body formatting, bounded diagnostics, redaction, degraded diagnostics handling, submission gating, and `gh` failure behavior.
|
||||
- Keep this skill focused on orchestration, not manual issue authoring.
|
||||
- Do not invent extra workflow, output schema, or issue-body rules beyond what `openclaw report` already implements.
|
||||
|
||||
## Examples
|
||||
|
||||
### Example: bug with gateway diagnostics
|
||||
|
||||
User says: `OpenClaw times out behind mitmproxy.`
|
||||
|
||||
Assistant says:
|
||||
|
||||
```text
|
||||
I’m using the openclaw-feedback skill to prepare an OpenClaw GitHub issue. I want to gather enough context to make the issue accurate and useful for maintainers without over-questioning you. Report generation can take a moment if I include diagnostics or probes.
|
||||
If you do not want to file an issue, just tell me and I’ll stop.
|
||||
|
||||
I need a few bug-report details: what steps reproduce it, what you expected, what actually happened, and the impact.
|
||||
```
|
||||
|
||||
If those fields are not already clear from the diagnosis session, ask for them. Otherwise infer them and run:
|
||||
|
||||
```bash
|
||||
openclaw report bug --summary "Gateway times out behind mitmproxy" --repro "1. Start gateway\n2. Send request\n3. Observe timeout" --expected "..." --actual "..." --impact "..." --probe gateway
|
||||
```
|
||||
|
||||
Then show the draft itself, not internal report metadata. Only add `--submit` after explicit approval.
|
||||
|
||||
After showing the draft, ask:
|
||||
|
||||
```text
|
||||
If this draft looks right, I can submit it to GitHub now.
|
||||
```
|
||||
|
||||
If submission succeeds, include the created issue URL in the final reply.
|
||||
|
||||
### Example: regression bug with loose extra context
|
||||
|
||||
User says: `This worked last week, but after updating it started timing out.`
|
||||
|
||||
Assistant says:
|
||||
|
||||
```text
|
||||
I’m using the openclaw-feedback skill to prepare an OpenClaw GitHub issue. I want to gather enough context to make the issue accurate and useful for maintainers without over-questioning you. Report generation can take a moment if I include diagnostics or probes.
|
||||
If you do not want to file an issue, just tell me and I’ll stop.
|
||||
|
||||
I need the repro steps, expected behavior, actual behavior, and impact. I can also include the previous version and any extra observations that do not fit neatly into the main bug fields.
|
||||
```
|
||||
|
||||
If the missing facts are not already clear from the conversation or diagnosis session, ask for them. Otherwise infer them and run:
|
||||
|
||||
```bash
|
||||
openclaw report bug --summary "Regression after update" --repro "1. Start gateway\n2. Send request\n3. Observe failure" --expected "..." --actual "..." --impact "..." --previous-version "2026.3.14" --additional-information "Worked last week; now every call times out behind the same proxy setup." --probe model
|
||||
```
|
||||
|
||||
Then show the draft and ask:
|
||||
|
||||
```text
|
||||
If this draft looks right, I can submit it to GitHub now.
|
||||
```
|
||||
|
||||
If submission succeeds, include the created issue URL in the final reply.
|
||||
|
||||
### Example: feature request
|
||||
|
||||
User says: `Please add a way to export a report draft without submitting it.`
|
||||
|
||||
If the user already gave enough detail, derive the report directly and run:
|
||||
|
||||
```bash
|
||||
openclaw report feature --summary "Export report drafts without submission" --problem "Operators want a clean draft artifact without creating a GitHub issue" --solution "Allow report generation and file output without submit" --impact "Safer scripted issue authoring"
|
||||
```
|
||||
|
||||
Then show the draft and ask:
|
||||
|
||||
```text
|
||||
If this draft looks right, I can submit it to GitHub now.
|
||||
```
|
||||
|
||||
If submission succeeds, include the created issue URL in the final reply.
|
||||
|
||||
### Example: private security report
|
||||
|
||||
User says: `I found a token leak in logs.`
|
||||
|
||||
Assistant says:
|
||||
|
||||
```text
|
||||
I’m using the openclaw-feedback skill to route this into the private OpenClaw security-report flow. I want to gather enough context to make the report accurate and actionable while keeping it private.
|
||||
If you do not want to file a report, just tell me and I’ll stop.
|
||||
```
|
||||
|
||||
Then gather only missing private-report fields and run:
|
||||
|
||||
```bash
|
||||
openclaw report security --title "Token leak in logs" --severity high --impact "..." --component "..." --reproduction "..." --demonstrated-impact "..." --environment "..." --remediation "..."
|
||||
```
|
||||
|
||||
Do not create a public issue.
|
||||
@@ -216,6 +216,19 @@ const coreEntries: CoreCliEntry[] = [
|
||||
mod.registerStatusHealthSessionsCommands(program);
|
||||
},
|
||||
},
|
||||
{
|
||||
commands: [
|
||||
{
|
||||
name: "report",
|
||||
description: "Prepare sanitized bug, feature, and security reports",
|
||||
hasSubcommands: true,
|
||||
},
|
||||
],
|
||||
register: async ({ program }) => {
|
||||
const mod = await import("./register.report.js");
|
||||
mod.registerReportCommand(program);
|
||||
},
|
||||
},
|
||||
{
|
||||
commands: [
|
||||
{
|
||||
|
||||
@@ -80,6 +80,11 @@ describe("registerPreActionHooks", () => {
|
||||
function buildProgram() {
|
||||
const program = new Command().name("openclaw");
|
||||
program.command("status").action(() => {});
|
||||
const report = program.command("report");
|
||||
report
|
||||
.command("bug")
|
||||
.option("--json")
|
||||
.action(() => {});
|
||||
program
|
||||
.command("backup")
|
||||
.command("create")
|
||||
@@ -254,6 +259,19 @@ describe("registerPreActionHooks", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("suppresses doctor stdout for report commands even without --json", async () => {
|
||||
await runPreAction({
|
||||
parseArgv: ["report", "bug"],
|
||||
processArgv: ["node", "openclaw", "report", "bug"],
|
||||
});
|
||||
|
||||
expect(ensureConfigReadyMock).toHaveBeenCalledWith({
|
||||
runtime: runtimeMock,
|
||||
commandPath: ["report", "bug"],
|
||||
suppressDoctorStdout: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("bypasses config guard for config validate", async () => {
|
||||
await runPreAction({
|
||||
parseArgv: ["config", "validate"],
|
||||
|
||||
@@ -37,6 +37,7 @@ const PLUGIN_REQUIRED_COMMANDS = new Set([
|
||||
]);
|
||||
const CONFIG_GUARD_BYPASS_COMMANDS = new Set(["backup", "doctor", "completion", "secrets"]);
|
||||
const JSON_PARSE_ONLY_COMMANDS = new Set(["config set"]);
|
||||
const QUIET_STRUCTURED_COMMANDS = new Set(["report"]);
|
||||
let configGuardModulePromise: Promise<typeof import("./config-guard.js")> | undefined;
|
||||
let pluginRegistryModulePromise: Promise<typeof import("../plugin-registry.js")> | undefined;
|
||||
|
||||
@@ -115,6 +116,13 @@ function isJsonOutputMode(commandPath: string[], argv: string[]): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
function shouldSuppressDoctorStdout(commandPath: string[], argv: string[]): boolean {
|
||||
if (QUIET_STRUCTURED_COMMANDS.has(commandPath[0] ?? "")) {
|
||||
return true;
|
||||
}
|
||||
return isJsonOutputMode(commandPath, argv);
|
||||
}
|
||||
|
||||
export function registerPreActionHooks(program: Command, programVersion: string) {
|
||||
program.hook("preAction", async (_thisCommand, actionCommand) => {
|
||||
setProcessTitleForCommand(actionCommand);
|
||||
@@ -143,7 +151,7 @@ export function registerPreActionHooks(program: Command, programVersion: string)
|
||||
if (shouldBypassConfigGuard(commandPath)) {
|
||||
return;
|
||||
}
|
||||
const suppressDoctorStdout = isJsonOutputMode(commandPath, argv);
|
||||
const suppressDoctorStdout = shouldSuppressDoctorStdout(commandPath, argv);
|
||||
const { ensureConfigReady } = await loadConfigGuardModule();
|
||||
await ensureConfigReady({
|
||||
runtime: defaultRuntime,
|
||||
|
||||
151
src/cli/program/register.report.test.ts
Normal file
151
src/cli/program/register.report.test.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { Command } from "commander";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const reportCommand = vi.fn();
|
||||
const runtime = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock("../../commands/report.js", () => ({
|
||||
reportCommand,
|
||||
}));
|
||||
|
||||
vi.mock("../../runtime.js", () => ({
|
||||
defaultRuntime: runtime,
|
||||
}));
|
||||
|
||||
let registerReportCommand: typeof import("./register.report.js").registerReportCommand;
|
||||
|
||||
beforeAll(async () => {
|
||||
({ registerReportCommand } = await import("./register.report.js"));
|
||||
});
|
||||
|
||||
describe("registerReportCommand", () => {
|
||||
async function runCli(args: string[]) {
|
||||
const program = new Command();
|
||||
registerReportCommand(program);
|
||||
await program.parseAsync(args, { from: "user" });
|
||||
return program;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
reportCommand.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it("registers bug reports with probe mode", async () => {
|
||||
await runCli([
|
||||
"report",
|
||||
"bug",
|
||||
"--summary",
|
||||
"Gateway timeout",
|
||||
"--repro",
|
||||
"1. Start gateway",
|
||||
"--expected",
|
||||
"Model responds",
|
||||
"--actual",
|
||||
"Timeout",
|
||||
"--impact",
|
||||
"Blocks requests",
|
||||
"--previous-version",
|
||||
"2026.3.14",
|
||||
"--additional-information",
|
||||
"Worked last week behind mitmproxy.",
|
||||
"--probe",
|
||||
"gateway",
|
||||
]);
|
||||
|
||||
expect(reportCommand).toHaveBeenCalledWith({
|
||||
kind: "bug",
|
||||
options: expect.objectContaining({
|
||||
summary: "Gateway timeout",
|
||||
repro: "1. Start gateway",
|
||||
expected: "Model responds",
|
||||
actual: "Timeout",
|
||||
impact: "Blocks requests",
|
||||
previousVersion: "2026.3.14",
|
||||
additionalInformation: "Worked last week behind mitmproxy.",
|
||||
probe: "gateway",
|
||||
}),
|
||||
runtime,
|
||||
});
|
||||
});
|
||||
|
||||
it("passes both additional-information and context through for merge handling", async () => {
|
||||
await runCli([
|
||||
"report",
|
||||
"feature",
|
||||
"--summary",
|
||||
"Need better retry visibility",
|
||||
"--problem",
|
||||
"Retries are opaque",
|
||||
"--solution",
|
||||
"Show retry state in UI",
|
||||
"--impact",
|
||||
"Reduces debugging time",
|
||||
"--additional-information",
|
||||
"Users noticed this after rollout.",
|
||||
"--context",
|
||||
"Might be related to proxy retries.",
|
||||
]);
|
||||
|
||||
expect(reportCommand).toHaveBeenCalledWith({
|
||||
kind: "feature",
|
||||
options: expect.objectContaining({
|
||||
additionalInformation: "Users noticed this after rollout.",
|
||||
context: "Might be related to proxy retries.",
|
||||
}),
|
||||
runtime,
|
||||
});
|
||||
});
|
||||
|
||||
it("documents new report options and probe descriptions in help text", async () => {
|
||||
const program = new Command();
|
||||
registerReportCommand(program);
|
||||
const report = program.commands.find((command) => command.name() === "report");
|
||||
const help =
|
||||
report?.commands.find((command) => command.name() === "bug")?.helpInformation() ?? "";
|
||||
|
||||
expect(help).toContain("--non-interactive");
|
||||
expect(help).toContain("--previous-version");
|
||||
expect(help).toContain("--additional-information");
|
||||
expect(help).toContain("general=runtime+proxy");
|
||||
expect(help).toContain("model=auth/live check");
|
||||
});
|
||||
|
||||
it("registers security reports without public probe options", async () => {
|
||||
await runCli([
|
||||
"report",
|
||||
"security",
|
||||
"--title",
|
||||
"Token leak",
|
||||
"--severity",
|
||||
"high",
|
||||
"--impact",
|
||||
"Credential exposure",
|
||||
"--component",
|
||||
"Gateway auth",
|
||||
"--reproduction",
|
||||
"Run startup",
|
||||
"--demonstrated-impact",
|
||||
"Token printed",
|
||||
"--environment",
|
||||
"macOS",
|
||||
"--remediation",
|
||||
"Mask logs",
|
||||
]);
|
||||
|
||||
expect(reportCommand).toHaveBeenCalledWith({
|
||||
kind: "security",
|
||||
options: expect.objectContaining({
|
||||
title: "Token leak",
|
||||
severity: "high",
|
||||
component: "Gateway auth",
|
||||
remediation: "Mask logs",
|
||||
}),
|
||||
runtime,
|
||||
});
|
||||
});
|
||||
});
|
||||
167
src/cli/program/register.report.ts
Normal file
167
src/cli/program/register.report.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import type { Command } from "commander";
|
||||
import {
|
||||
reportCommand,
|
||||
type BugProbeMode,
|
||||
type BugReportOptions,
|
||||
type FeatureReportOptions,
|
||||
type SecurityReportOptions,
|
||||
} from "../../commands/report.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { formatDocsLink } from "../../terminal/links.js";
|
||||
import { theme } from "../../terminal/theme.js";
|
||||
import { runCommandWithRuntime } from "../cli-utils.js";
|
||||
import { formatHelpExamples } from "../help-format.js";
|
||||
|
||||
function addSharedOptions(command: Command) {
|
||||
return command
|
||||
.option("--title <text>", "Report title")
|
||||
.option("--summary <text>", "Short report summary")
|
||||
.option("--json", "Output JSON instead of text", false)
|
||||
.option("--markdown", "Output rendered Markdown body", false)
|
||||
.option("--output <file>", "Write the sanitized body to a file")
|
||||
.option("--submit", "Create the GitHub issue when the report is ready", false)
|
||||
.option("--yes", "Skip interactive confirmation for submission", false)
|
||||
.option("--non-interactive", "Disable prompts; requires --yes for submission", false);
|
||||
}
|
||||
|
||||
function resolveProbe(value: unknown): BugProbeMode | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim().toLowerCase();
|
||||
if (
|
||||
trimmed === "general" ||
|
||||
trimmed === "model" ||
|
||||
trimmed === "channel" ||
|
||||
trimmed === "gateway" ||
|
||||
trimmed === "none"
|
||||
) {
|
||||
return trimmed;
|
||||
}
|
||||
throw new Error("--probe must be one of: general, model, channel, gateway, none");
|
||||
}
|
||||
|
||||
export function registerReportCommand(program: Command) {
|
||||
const report = program
|
||||
.command("report")
|
||||
.description("Prepare sanitized bug, feature, and security reports for openclaw/openclaw")
|
||||
.addHelpText(
|
||||
"after",
|
||||
() =>
|
||||
`\n${theme.heading("Behavior:")}\n- Running ${theme.command("openclaw report ...")} without ${theme.command("--submit")} generates a draft/preview.\n- Add ${theme.command("--submit")} to create the issue after approval.\n- Non-interactive submission requires ${theme.command("--submit --yes")}.\n\n${theme.heading("Examples:")}\n${formatHelpExamples(
|
||||
[
|
||||
[
|
||||
'openclaw report bug --summary "Gateway timeouts" --repro "..."',
|
||||
"Draft and preview a bug report.",
|
||||
],
|
||||
[
|
||||
'openclaw report bug --summary "Gateway timeouts" --repro "..." --expected "..." --actual "..." --impact "..." --probe gateway',
|
||||
"Draft a bug report with gateway and proxy diagnostics.",
|
||||
],
|
||||
[
|
||||
'openclaw report bug --summary "Regression after update" --repro "..." --expected "..." --actual "..." --impact "..." --previous-version "2026.3.14"',
|
||||
"Draft a regression report with previous-version context.",
|
||||
],
|
||||
[
|
||||
'openclaw report bug --summary "Gateway timeouts" --repro "..." --expected "..." --actual "..." --impact "..." --additional-information "Worked last week behind mitmproxy; now every call times out."',
|
||||
"Draft a bug report with broad additional information.",
|
||||
],
|
||||
[
|
||||
'openclaw report bug --summary "Gateway timeouts" --repro "..." --expected "..." --actual "..." --impact "..." --submit --yes',
|
||||
"Submit in non-interactive mode when all required fields are present.",
|
||||
],
|
||||
[
|
||||
'openclaw report feature --summary "Add foo" --problem "..." --solution "..." --impact "..."',
|
||||
"Draft a feature request.",
|
||||
],
|
||||
[
|
||||
'openclaw report security --title "Token leak" --severity high --impact "..."',
|
||||
"Prepare a private security report packet.",
|
||||
],
|
||||
],
|
||||
)}`,
|
||||
)
|
||||
.addHelpText(
|
||||
"after",
|
||||
() =>
|
||||
`\n${theme.muted("Docs:")} ${formatDocsLink("/cli/report", "docs.openclaw.ai/cli/report")}\n`,
|
||||
);
|
||||
|
||||
addSharedOptions(
|
||||
report
|
||||
.command("bug")
|
||||
.description("Prepare a public bug report for openclaw/openclaw")
|
||||
.option("--repro <text>", "Steps to reproduce")
|
||||
.option("--expected <text>", "Expected behavior")
|
||||
.option("--actual <text>", "Actual behavior")
|
||||
.option("--impact <text>", "User or operator impact")
|
||||
.option("--previous-version <text>", "Optional previous version for regression context")
|
||||
.option("--evidence <text>", "Extra evidence to include")
|
||||
.option(
|
||||
"--additional-information <text>",
|
||||
"Broad extra details, clues, timelines, hypotheses, or other useful context",
|
||||
)
|
||||
.option("--context <text>", "Compatibility alias for additional information")
|
||||
.option(
|
||||
"--probe <mode>",
|
||||
"Targeted diagnostics: general=runtime+proxy, gateway=reachability, model=auth/live check, channel=channel status, none=skip",
|
||||
)
|
||||
.action(async (opts) => {
|
||||
const options: BugReportOptions = {
|
||||
...opts,
|
||||
probe: resolveProbe(opts.probe),
|
||||
};
|
||||
await runCommandWithRuntime(defaultRuntime, async () => {
|
||||
await reportCommand({ kind: "bug", options, runtime: defaultRuntime });
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
addSharedOptions(
|
||||
report
|
||||
.command("feature")
|
||||
.description("Prepare a public feature request for openclaw/openclaw")
|
||||
.option("--problem <text>", "Problem to solve")
|
||||
.option("--solution <text>", "Proposed solution")
|
||||
.option("--impact <text>", "Expected impact")
|
||||
.option("--alternatives <text>", "Alternatives considered")
|
||||
.option("--evidence <text>", "Supporting evidence or examples")
|
||||
.option(
|
||||
"--additional-information <text>",
|
||||
"Broad extra details, clues, timelines, hypotheses, or other useful context",
|
||||
)
|
||||
.option("--context <text>", "Compatibility alias for additional information")
|
||||
.option(
|
||||
"--probe <mode>",
|
||||
"Optional diagnostics context: general=runtime+proxy, gateway=reachability, model=auth/live check, channel=channel status, none=skip",
|
||||
)
|
||||
.action(async (opts) => {
|
||||
const options: FeatureReportOptions = {
|
||||
...opts,
|
||||
probe: resolveProbe(opts.probe),
|
||||
};
|
||||
await runCommandWithRuntime(defaultRuntime, async () => {
|
||||
await reportCommand({ kind: "feature", options, runtime: defaultRuntime });
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
addSharedOptions(
|
||||
report
|
||||
.command("security")
|
||||
.description("Prepare a private security report packet")
|
||||
.option("--severity <text>", "Severity assessment")
|
||||
.option("--impact <text>", "Demonstrated impact")
|
||||
.option("--component <text>", "Affected component")
|
||||
.option("--reproduction <text>", "Technical reproduction")
|
||||
.option("--demonstrated-impact <text>", "Observed exploit or impact")
|
||||
.option("--environment <text>", "Environment details")
|
||||
.option("--remediation <text>", "Suggested remediation advice")
|
||||
.action(async (opts) => {
|
||||
const options: SecurityReportOptions = { ...opts };
|
||||
await runCommandWithRuntime(defaultRuntime, async () => {
|
||||
await reportCommand({ kind: "security", options, runtime: defaultRuntime });
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
583
src/commands/report.test.ts
Normal file
583
src/commands/report.test.ts
Normal file
@@ -0,0 +1,583 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const readBestEffortConfig = vi.fn();
|
||||
const resolveCommandSecretRefsViaGateway = vi.fn();
|
||||
const probeGateway = vi.fn();
|
||||
const buildGatewayConnectionDetails = vi.fn();
|
||||
const callGateway = vi.fn();
|
||||
const resolveGatewayProbeAuthSafe = vi.fn();
|
||||
const promptYesNo = vi.fn();
|
||||
const runExec = vi.fn();
|
||||
const getStatusSummary = vi.fn();
|
||||
const buildChannelsTable = vi.fn();
|
||||
const buildChannelSummary = vi.fn();
|
||||
const collectChannelStatusIssues = vi.fn();
|
||||
const resolveOpenClawAgentDir = vi.fn();
|
||||
const ensureAuthProfileStore = vi.fn();
|
||||
const resolveProviderAuthOverview = vi.fn();
|
||||
const runAuthProbes = vi.fn();
|
||||
|
||||
vi.mock("../config/config.js", () => ({
|
||||
readBestEffortConfig,
|
||||
}));
|
||||
|
||||
vi.mock("../cli/command-secret-gateway.js", () => ({
|
||||
resolveCommandSecretRefsViaGateway,
|
||||
}));
|
||||
|
||||
vi.mock("../cli/command-secret-targets.js", () => ({
|
||||
getStatusCommandSecretTargetIds: () => ["gateway.auth.token"],
|
||||
}));
|
||||
|
||||
vi.mock("../gateway/probe.js", () => ({
|
||||
probeGateway,
|
||||
}));
|
||||
|
||||
vi.mock("../gateway/probe-auth.js", () => ({
|
||||
resolveGatewayProbeAuthSafe,
|
||||
}));
|
||||
|
||||
vi.mock("../gateway/call.js", () => ({
|
||||
buildGatewayConnectionDetails,
|
||||
callGateway,
|
||||
}));
|
||||
|
||||
vi.mock("../cli/prompt.js", () => ({
|
||||
promptYesNo,
|
||||
}));
|
||||
|
||||
vi.mock("../process/exec.js", () => ({
|
||||
runExec,
|
||||
}));
|
||||
|
||||
vi.mock("./status.js", () => ({
|
||||
getStatusSummary,
|
||||
}));
|
||||
|
||||
vi.mock("./status-all/channels.js", () => ({
|
||||
buildChannelsTable,
|
||||
}));
|
||||
|
||||
vi.mock("../infra/channel-summary.js", () => ({
|
||||
buildChannelSummary,
|
||||
}));
|
||||
|
||||
vi.mock("../infra/channels-status-issues.js", () => ({
|
||||
collectChannelStatusIssues,
|
||||
}));
|
||||
|
||||
vi.mock("../agents/agent-paths.js", () => ({
|
||||
resolveOpenClawAgentDir,
|
||||
}));
|
||||
|
||||
vi.mock("../agents/auth-profiles.js", () => ({
|
||||
ensureAuthProfileStore,
|
||||
}));
|
||||
|
||||
vi.mock("./models/list.auth-overview.js", () => ({
|
||||
resolveProviderAuthOverview,
|
||||
}));
|
||||
|
||||
vi.mock("./models/list.probe.js", () => ({
|
||||
runAuthProbes,
|
||||
}));
|
||||
|
||||
const runtime = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
};
|
||||
|
||||
describe("reportCommand", () => {
|
||||
let buildReportPayload: typeof import("./report.js").buildReportPayload;
|
||||
let reportCommand: typeof import("./report.js").reportCommand;
|
||||
let originalIsTTY: boolean | undefined;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
({ buildReportPayload, reportCommand } = await import("./report.js"));
|
||||
runtime.log.mockReset();
|
||||
runtime.error.mockReset();
|
||||
runtime.exit.mockReset();
|
||||
const cfg = {
|
||||
gateway: { mode: "local" },
|
||||
channels: { telegram: { enabled: true } },
|
||||
models: { providers: { anthropic: { models: [{ id: "claude-sonnet-4.5" }] } } },
|
||||
agents: { defaults: { model: "anthropic/claude-sonnet-4.5" } },
|
||||
};
|
||||
readBestEffortConfig.mockResolvedValue(cfg);
|
||||
resolveCommandSecretRefsViaGateway.mockResolvedValue({
|
||||
resolvedConfig: cfg,
|
||||
diagnostics: ["token=abc123", "path=/Users/private-user/project"],
|
||||
});
|
||||
buildGatewayConnectionDetails.mockReturnValue({
|
||||
url: "ws://127.0.0.1:9090",
|
||||
message: "gateway local",
|
||||
});
|
||||
callGateway.mockResolvedValue({ ok: true, channelAccounts: {} });
|
||||
resolveGatewayProbeAuthSafe.mockReturnValue({ auth: {} });
|
||||
probeGateway.mockResolvedValue({
|
||||
ok: true,
|
||||
connectLatencyMs: 42,
|
||||
});
|
||||
getStatusSummary.mockResolvedValue({
|
||||
channelSummary: ["telegram ok", "discord off"],
|
||||
queuedSystemEvents: [],
|
||||
runtimeVersion: "2026.3.20",
|
||||
heartbeat: { defaultAgentId: "main", agents: [] },
|
||||
sessions: {
|
||||
paths: [],
|
||||
count: 0,
|
||||
defaults: { model: "anthropic/claude-sonnet-4.5", contextTokens: 200000 },
|
||||
recent: [],
|
||||
byAgent: [],
|
||||
},
|
||||
});
|
||||
buildChannelsTable.mockResolvedValue({
|
||||
rows: [{ id: "telegram", label: "Telegram", enabled: true, state: "ok", detail: "linked" }],
|
||||
details: [],
|
||||
});
|
||||
buildChannelSummary.mockResolvedValue(["telegram linked", "discord disabled"]);
|
||||
collectChannelStatusIssues.mockReturnValue([]);
|
||||
resolveOpenClawAgentDir.mockReturnValue("/tmp/openclaw-agent");
|
||||
ensureAuthProfileStore.mockReturnValue({ profiles: {} });
|
||||
resolveProviderAuthOverview.mockReturnValue({
|
||||
effective: { kind: "env", detail: "sk-***" },
|
||||
profiles: { count: 1 },
|
||||
});
|
||||
runAuthProbes.mockResolvedValue({
|
||||
totalTargets: 1,
|
||||
durationMs: 120,
|
||||
results: [
|
||||
{
|
||||
provider: "anthropic",
|
||||
model: "claude-sonnet-4.5",
|
||||
label: "default",
|
||||
source: "env",
|
||||
status: "timeout",
|
||||
error: "proxy timeout via mitmproxy",
|
||||
latencyMs: 1200,
|
||||
},
|
||||
],
|
||||
});
|
||||
promptYesNo.mockResolvedValue(true);
|
||||
runExec.mockResolvedValue({
|
||||
stdout: "https://github.com/openclaw/openclaw/issues/999\n",
|
||||
stderr: "",
|
||||
});
|
||||
originalIsTTY = process.stdin.isTTY;
|
||||
Object.defineProperty(process.stdin, "isTTY", {
|
||||
configurable: true,
|
||||
value: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllEnvs();
|
||||
Object.defineProperty(process.stdin, "isTTY", {
|
||||
configurable: true,
|
||||
value: originalIsTTY,
|
||||
});
|
||||
});
|
||||
|
||||
it("reports missing bug fields and sanitizes sensitive content", async () => {
|
||||
const payload = await buildReportPayload({
|
||||
kind: "bug",
|
||||
options: {
|
||||
summary: "Gateway timeout for user@example.com",
|
||||
repro: "Run from /Users/private-user/project",
|
||||
},
|
||||
});
|
||||
|
||||
expect(payload.missingFields).toEqual(["Expected behavior", "Actual behavior", "Impact"]);
|
||||
expect(payload.title).toBe("[Bug]: Gateway timeout for [email-redacted]");
|
||||
expect(payload.body).toContain("[email-redacted]");
|
||||
expect(payload.body).toContain("/Users/user/project");
|
||||
expect(payload.body).not.toContain("abc123");
|
||||
expect(payload.body).toContain("_Generated via `openclaw report`._");
|
||||
expect(payload.submissionEligible).toBe(false);
|
||||
});
|
||||
|
||||
it("renders literal escaped newline sequences as real line breaks in report sections", async () => {
|
||||
const payload = await buildReportPayload({
|
||||
kind: "bug",
|
||||
options: {
|
||||
summary: "Gateway timeout",
|
||||
repro: "1. Start gateway\\n2. Send request\\n3. Observe failure",
|
||||
expected: "Model responds",
|
||||
actual: "Timeout",
|
||||
impact: "Blocks requests",
|
||||
},
|
||||
});
|
||||
|
||||
expect(payload.body).toContain("1. Start gateway\n2. Send request\n3. Observe failure");
|
||||
expect(payload.body).not.toContain("\\n2. Send request");
|
||||
});
|
||||
|
||||
it("writes markdown output that matches the generated body", async () => {
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-report-test-"));
|
||||
const outputPath = path.join(tmpDir, "bug.md");
|
||||
|
||||
const payload = await reportCommand({
|
||||
kind: "bug",
|
||||
options: {
|
||||
summary: "Gateway timeout",
|
||||
repro: "1. Start gateway\n2. Send request",
|
||||
expected: "Model responds",
|
||||
actual: "Timeout",
|
||||
impact: "Blocks requests",
|
||||
markdown: true,
|
||||
output: outputPath,
|
||||
},
|
||||
runtime,
|
||||
});
|
||||
|
||||
const written = await fs.readFile(outputPath, "utf8");
|
||||
expect(runtime.log).toHaveBeenCalledWith(payload.body);
|
||||
expect(written).toBe(payload.body);
|
||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("suppresses config warning logging while building a report and restores the env", async () => {
|
||||
process.env.OPENCLAW_SUPPRESS_CONFIG_WARNINGS = "0";
|
||||
readBestEffortConfig.mockImplementation(async () => {
|
||||
expect(process.env.OPENCLAW_SUPPRESS_CONFIG_WARNINGS).toBe("1");
|
||||
return {
|
||||
gateway: { mode: "local" },
|
||||
models: { providers: { anthropic: { models: [{ id: "claude-sonnet-4.5" }] } } },
|
||||
agents: { defaults: { model: "anthropic/claude-sonnet-4.5" } },
|
||||
};
|
||||
});
|
||||
|
||||
await reportCommand({
|
||||
kind: "bug",
|
||||
options: {
|
||||
summary: "Gateway timeout",
|
||||
repro: "1. Start gateway\n2. Send request",
|
||||
expected: "Model responds",
|
||||
actual: "Timeout",
|
||||
impact: "Blocks requests",
|
||||
},
|
||||
runtime,
|
||||
});
|
||||
|
||||
expect(process.env.OPENCLAW_SUPPRESS_CONFIG_WARNINGS).toBe("0");
|
||||
});
|
||||
|
||||
it("collects gateway evidence for gateway probe mode", async () => {
|
||||
vi.stubEnv("HTTPS_PROXY", "http://proxy.local:8080");
|
||||
|
||||
const payload = await buildReportPayload({
|
||||
kind: "bug",
|
||||
options: {
|
||||
summary: "Gateway timeout",
|
||||
repro: "1. Start gateway",
|
||||
expected: "Model responds",
|
||||
actual: "Timeout",
|
||||
impact: "Blocks requests",
|
||||
probe: "gateway",
|
||||
},
|
||||
});
|
||||
|
||||
expect(payload.evidenceDetails.some((detail) => detail.source === "gateway")).toBe(true);
|
||||
expect(payload.evidence).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining("Gateway target"),
|
||||
expect.stringContaining("Gateway probe"),
|
||||
expect.stringContaining("Effective HTTPS proxy"),
|
||||
]),
|
||||
);
|
||||
expect(payload.body).toContain("## Evidence");
|
||||
expect(payload.body).toContain("Gateway probe");
|
||||
});
|
||||
|
||||
it("collects model evidence for model probe mode", async () => {
|
||||
vi.stubEnv("HTTPS_PROXY", "http://proxy.local:8080");
|
||||
|
||||
const payload = await buildReportPayload({
|
||||
kind: "bug",
|
||||
options: {
|
||||
summary: "Provider auth mismatch",
|
||||
repro: "1. Run model list",
|
||||
expected: "Provider is ready",
|
||||
actual: "Auth mismatch",
|
||||
impact: "Blocks completions",
|
||||
probe: "model",
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolveProviderAuthOverview).toHaveBeenCalled();
|
||||
expect(runAuthProbes).toHaveBeenCalled();
|
||||
expect(payload.evidenceDetails).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ source: "model", label: "Model path" }),
|
||||
expect.objectContaining({ source: "model", label: "Provider auth" }),
|
||||
expect.objectContaining({ source: "model", label: "Model probe" }),
|
||||
expect.objectContaining({ source: "recentErrors", label: "Recent model-call error" }),
|
||||
]),
|
||||
);
|
||||
expect(payload.body).toContain("Provider auth");
|
||||
expect(payload.body).toContain("Recent model-call error");
|
||||
expect(payload.body).toContain("Model probe: timeout via env proxy http://proxy.local:8080");
|
||||
});
|
||||
|
||||
it("collects channel evidence for channel probe mode", async () => {
|
||||
collectChannelStatusIssues.mockReturnValue([{ message: "Telegram account needs relink" }]);
|
||||
|
||||
const payload = await buildReportPayload({
|
||||
kind: "bug",
|
||||
options: {
|
||||
summary: "Telegram send failed",
|
||||
repro: "1. Send a message",
|
||||
expected: "Message sends",
|
||||
actual: "Delivery fails",
|
||||
impact: "Breaks channel delivery",
|
||||
probe: "channel",
|
||||
},
|
||||
});
|
||||
|
||||
expect(payload.evidenceDetails).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ source: "channel", label: "Configured channels" }),
|
||||
expect.objectContaining({
|
||||
source: "channel",
|
||||
label: "Gateway-reported channel issue",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(payload.body).toContain("Telegram account needs relink");
|
||||
});
|
||||
|
||||
it("bounds public evidence while preserving richer details for general feature probes", async () => {
|
||||
vi.stubEnv("HTTPS_PROXY", "http://proxy.local:8080");
|
||||
vi.stubEnv("NO_PROXY", "localhost,127.0.0.1");
|
||||
const payload = await buildReportPayload({
|
||||
kind: "feature",
|
||||
options: {
|
||||
summary: "Need better retry visibility",
|
||||
problem: "Retries are opaque",
|
||||
solution: "Show retry state in UI",
|
||||
impact: "Reduces debugging time",
|
||||
evidence: "User report from user@example.com",
|
||||
probe: "general",
|
||||
},
|
||||
});
|
||||
|
||||
expect(payload.evidence.length).toBeLessThanOrEqual(5);
|
||||
expect(payload.evidenceDetails.length).toBeGreaterThan(payload.evidence.length);
|
||||
expect(payload.body).toContain("## Evidence");
|
||||
expect(payload.body).not.toContain("Secret diagnostics");
|
||||
expect(payload.body).not.toContain("Degraded diagnostics");
|
||||
expect(payload.body).not.toContain("token=abc123");
|
||||
expect(payload.evidenceDetails).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ source: "proxy", label: "Proxy env" }),
|
||||
expect.objectContaining({ source: "proxy", label: "Effective HTTPS proxy" }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("renders previous version when provided", async () => {
|
||||
const payload = await buildReportPayload({
|
||||
kind: "bug",
|
||||
options: {
|
||||
summary: "Regression after update",
|
||||
repro: "1. Start gateway",
|
||||
expected: "Model responds",
|
||||
actual: "Timeout",
|
||||
impact: "Blocks requests",
|
||||
previousVersion: "2026.3.14",
|
||||
},
|
||||
});
|
||||
|
||||
expect(payload.body).toContain("## Previous version");
|
||||
expect(payload.body).toContain("2026.3.14");
|
||||
});
|
||||
|
||||
it("renders additional information with the new heading for bug reports", async () => {
|
||||
const payload = await buildReportPayload({
|
||||
kind: "bug",
|
||||
options: {
|
||||
summary: "Gateway timeout",
|
||||
repro: "1. Start gateway",
|
||||
expected: "Model responds",
|
||||
actual: "Timeout",
|
||||
impact: "Blocks requests",
|
||||
additionalInformation:
|
||||
"Worked last week for user@example.com from /Users/private-user/project",
|
||||
},
|
||||
});
|
||||
|
||||
expect(payload.body).toContain("## Additional information");
|
||||
expect(payload.body).not.toContain("## Additional context");
|
||||
expect(payload.body).toContain("[email-redacted]");
|
||||
expect(payload.body).toContain("/Users/user/project");
|
||||
});
|
||||
|
||||
it("merges additional information and context for feature reports", async () => {
|
||||
const payload = await buildReportPayload({
|
||||
kind: "feature",
|
||||
options: {
|
||||
summary: "Need better retry visibility",
|
||||
problem: "Retries are opaque",
|
||||
solution: "Show retry state in UI",
|
||||
impact: "Reduces debugging time",
|
||||
additionalInformation: "Users noticed this after the last rollout.",
|
||||
context: "Might be related to proxy retries.",
|
||||
},
|
||||
});
|
||||
|
||||
expect(payload.body).toContain("## Additional information");
|
||||
expect(payload.body).toContain("Users noticed this after the last rollout.");
|
||||
expect(payload.body).toContain("Might be related to proxy retries.");
|
||||
});
|
||||
|
||||
it("submits a public bug report in non-interactive mode with --yes", async () => {
|
||||
const payload = await reportCommand({
|
||||
kind: "bug",
|
||||
options: {
|
||||
summary: "Gateway timeout",
|
||||
repro: "1. Start gateway",
|
||||
expected: "Model responds",
|
||||
actual: "Timeout",
|
||||
impact: "Blocks requests",
|
||||
submit: true,
|
||||
yes: true,
|
||||
nonInteractive: true,
|
||||
json: true,
|
||||
},
|
||||
runtime,
|
||||
});
|
||||
|
||||
expect(runExec).toHaveBeenCalledWith(
|
||||
"gh",
|
||||
expect.arrayContaining(["issue", "create", "--repo", "openclaw/openclaw"]),
|
||||
expect.any(Object),
|
||||
);
|
||||
expect(payload.submission.created).toBe(true);
|
||||
expect(payload.submission.url).toBe("https://github.com/openclaw/openclaw/issues/999");
|
||||
});
|
||||
|
||||
it("prints the created issue url in human output after successful submission", async () => {
|
||||
await reportCommand({
|
||||
kind: "bug",
|
||||
options: {
|
||||
summary: "Gateway timeout",
|
||||
repro: "1. Start gateway",
|
||||
expected: "Model responds",
|
||||
actual: "Timeout",
|
||||
impact: "Blocks requests",
|
||||
submit: true,
|
||||
yes: true,
|
||||
nonInteractive: true,
|
||||
},
|
||||
runtime,
|
||||
});
|
||||
|
||||
expect(runtime.log).toHaveBeenCalledWith("Created: https://github.com/openclaw/openclaw/issues/999");
|
||||
});
|
||||
|
||||
it("keeps draft generation working when config and secret resolution degrade", async () => {
|
||||
readBestEffortConfig.mockRejectedValueOnce(new Error("config missing"));
|
||||
resolveCommandSecretRefsViaGateway.mockRejectedValueOnce(new Error("secret refs unavailable"));
|
||||
|
||||
const payload = await buildReportPayload({
|
||||
kind: "bug",
|
||||
options: {
|
||||
summary: "Gateway timeout",
|
||||
repro: "1. Start gateway",
|
||||
expected: "Model responds",
|
||||
actual: "Timeout",
|
||||
impact: "Blocks requests",
|
||||
probe: "general",
|
||||
},
|
||||
});
|
||||
|
||||
expect(payload.submissionEligible).toBe(true);
|
||||
expect(payload.evidenceDetails).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
source: "secretDiagnostics",
|
||||
label: "Degraded diagnostics",
|
||||
includedInPublicBody: false,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(payload.body).not.toContain("config load degraded");
|
||||
});
|
||||
|
||||
it("blocks public submission for security reports", async () => {
|
||||
const payload = await reportCommand({
|
||||
kind: "security",
|
||||
options: {
|
||||
title: "Token leak",
|
||||
severity: "high",
|
||||
impact: "Credential reuse risk",
|
||||
component: "Gateway auth",
|
||||
reproduction: "Trigger auth flow",
|
||||
demonstratedImpact: "Token exposed in logs",
|
||||
environment: "macOS",
|
||||
remediation: "Mask token before logging",
|
||||
submit: true,
|
||||
json: true,
|
||||
},
|
||||
runtime,
|
||||
});
|
||||
|
||||
expect(runExec).not.toHaveBeenCalled();
|
||||
expect(payload.submission.created).toBe(false);
|
||||
expect(payload.submission.blockedReason).toContain("security reports");
|
||||
});
|
||||
|
||||
it("requires confirmation for interactive bug submission", async () => {
|
||||
promptYesNo.mockResolvedValue(false);
|
||||
|
||||
const payload = await reportCommand({
|
||||
kind: "bug",
|
||||
options: {
|
||||
summary: "Gateway timeout",
|
||||
repro: "1. Start gateway",
|
||||
expected: "Model responds",
|
||||
actual: "Timeout",
|
||||
impact: "Blocks requests",
|
||||
submit: true,
|
||||
},
|
||||
runtime,
|
||||
});
|
||||
|
||||
expect(promptYesNo).toHaveBeenCalled();
|
||||
expect(runExec).not.toHaveBeenCalled();
|
||||
expect(payload.submission.blockedReason).toBe("submission cancelled");
|
||||
});
|
||||
|
||||
it("returns a structured blocked submission when gh create fails", async () => {
|
||||
runExec.mockRejectedValue(new Error("gh auth login required"));
|
||||
|
||||
const payload = await reportCommand({
|
||||
kind: "bug",
|
||||
options: {
|
||||
summary: "Gateway timeout",
|
||||
repro: "1. Start gateway",
|
||||
expected: "Model responds",
|
||||
actual: "Timeout",
|
||||
impact: "Blocks requests",
|
||||
submit: true,
|
||||
yes: true,
|
||||
nonInteractive: true,
|
||||
},
|
||||
runtime,
|
||||
});
|
||||
|
||||
expect(payload.submission.created).toBe(false);
|
||||
expect(payload.submission.blockedReason).toContain("gh issue create failed");
|
||||
expect(payload.submission.blockedReason).toContain("gh auth login required");
|
||||
expect(runtime.log).toHaveBeenCalledWith(expect.stringContaining("Report status: Ready"));
|
||||
expect(runtime.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Submission blocked: gh issue create failed"),
|
||||
);
|
||||
});
|
||||
});
|
||||
1264
src/commands/report.ts
Normal file
1264
src/commands/report.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -86,6 +86,14 @@ const OPEN_DM_POLICY_ALLOW_FROM_RE =
|
||||
const CONFIG_AUDIT_LOG_FILENAME = "config-audit.jsonl";
|
||||
const loggedInvalidConfigs = new Set<string>();
|
||||
|
||||
function shouldSuppressConfigWarningsFromEnv(value: string | undefined): boolean {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return normalized !== "" && normalized !== "0" && normalized !== "false" && normalized !== "off";
|
||||
}
|
||||
|
||||
type ConfigWriteAuditResult = "rename" | "copy-fallback" | "failed";
|
||||
|
||||
type ConfigWriteAuditRecord = {
|
||||
@@ -786,7 +794,10 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
(error as { code?: string; details?: string }).details = details;
|
||||
throw error;
|
||||
}
|
||||
if (validated.warnings.length > 0) {
|
||||
if (
|
||||
validated.warnings.length > 0 &&
|
||||
!shouldSuppressConfigWarningsFromEnv(process.env.OPENCLAW_SUPPRESS_CONFIG_WARNINGS)
|
||||
) {
|
||||
const details = validated.warnings
|
||||
.map(
|
||||
(iss) =>
|
||||
@@ -1123,7 +1134,10 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
const issueMessage = issue?.message ?? "invalid";
|
||||
throw new Error(formatConfigValidationFailure(pathLabel, issueMessage));
|
||||
}
|
||||
if (validated.warnings.length > 0) {
|
||||
if (
|
||||
validated.warnings.length > 0 &&
|
||||
!shouldSuppressConfigWarningsFromEnv(process.env.OPENCLAW_SUPPRESS_CONFIG_WARNINGS)
|
||||
) {
|
||||
const details = validated.warnings
|
||||
.map((warning) => `- ${warning.path}: ${warning.message}`)
|
||||
.join("\n");
|
||||
|
||||
@@ -339,7 +339,6 @@ describe("chat view", () => {
|
||||
|
||||
expect(container.textContent).not.toContain("context used");
|
||||
});
|
||||
|
||||
it("uses the assistant avatar URL for the welcome state when the identity avatar is only initials", () => {
|
||||
const container = document.createElement("div");
|
||||
render(
|
||||
|
||||
Reference in New Issue
Block a user