vmware-debug 1.10.0__tar.gz → 1.11.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/.claude-plugin/plugin.json +1 -1
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/.mcp.json +1 -1
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/PKG-INFO +1 -1
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/RELEASE_NOTES.md +42 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/pyproject.toml +1 -1
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/server.json +11 -2
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/skills/vmware-debug/SKILL.md +4 -2
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/skills/vmware-debug/references/capabilities.md +8 -1
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/skills/vmware-debug/references/event-envelope.md +1 -1
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/skills/vmware-debug/references/routing.md +1 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/tests/eval/capability/_scores.json +21 -21
- vmware_debug-1.11.0/tests/eval/regression/test_case_id_non_ascii.py +86 -0
- vmware_debug-1.11.0/tests/eval/regression/test_evidence_payload_contract.py +124 -0
- vmware_debug-1.11.0/tests/eval/regression/test_host_lifecycle_classification.py +244 -0
- vmware_debug-1.11.0/tests/eval/regression/test_skill_name_aliases.py +124 -0
- vmware_debug-1.11.0/tests/eval/regression/test_spike_resolution.py +171 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/tests/test_timeline.py +5 -1
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/uv.lock +1 -1
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/vmware_debug/__init__.py +1 -1
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/vmware_debug/mcp_server/server.py +51 -21
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/api.py +14 -1
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/grading.py +16 -2
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/ids.py +25 -7
- vmware_debug-1.11.0/vmware_debug/ops/cases/payloads.py +110 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/plan.py +48 -13
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/readiness.py +43 -15
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/store.py +8 -4
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/timeline.py +26 -29
- vmware_debug-1.11.0/vmware_debug/ops/skill_names.py +92 -0
- vmware_debug-1.11.0/vmware_debug/ops/timeline.py +568 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/vmware_debug/rules/evidence_sources.yaml +9 -0
- vmware_debug-1.10.0/vmware_debug/ops/timeline.py +0 -326
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/.gitignore +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/README-CN.md +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/README.md +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/SECURITY.md +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/docs/plans/2026-08-28-investigation-layer-design.md +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/skills/vmware-debug/references/agent-guardrails.md +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/skills/vmware-debug/references/cli-reference.md +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/skills/vmware-debug/references/setup-guide.md +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/tests/eval/__init__.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/tests/eval/capability/__init__.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/tests/eval/capability/_family.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/tests/eval/capability/_scoring.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/tests/eval/capability/_skill.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/tests/eval/capability/conftest.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/tests/eval/capability/test_entity_reachability.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/tests/eval/capability/test_error_actionability.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/tests/eval/capability/test_tool_description_quality.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/tests/eval/capability/test_tool_manifest_budget.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/tests/eval/regression/__init__.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/tests/eval/regression/test_capability_grader.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/tests/eval/regression/test_debug_regressions.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/tests/eval/regression/test_investigation_metrics.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/tests/eval/regression/test_result_envelope.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/tests/eval/regression/test_tool_annotations.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/tests/test_case_conclusion.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/tests/test_case_evidence.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/tests/test_case_grading.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/tests/test_case_hypotheses.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/tests/test_case_ids.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/tests/test_case_plan.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/tests/test_case_readiness.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/tests/test_case_store.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/tests/test_case_timeline_close.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/tests/test_case_tools.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/tests/test_knowledge.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/tests/test_safe_error_passthrough.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/vmware_debug/cli.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/vmware_debug/envelope.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/vmware_debug/mcp/__init__.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/vmware_debug/mcp/tools.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/vmware_debug/mcp_server/__init__.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/vmware_debug/ops/__init__.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/__init__.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/conclusion.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/evidence.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/hypotheses.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/knowledge.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/model.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/sources.py +0 -0
- {vmware_debug-1.10.0 → vmware_debug-1.11.0}/vmware_debug/rules/grading_rules.yaml +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "vmware-debug",
|
|
4
4
|
"displayName": "VMware Debug",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.11.0",
|
|
6
6
|
"description": "Read-only VMware incident correlation: timeline, spikes, root-cause routing. 2 MCP tools.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Wei Zhou",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: vmware-debug
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.11.0
|
|
4
4
|
Summary: VMware diagnostic brain — read-only incident triage, log/event correlation, and root-cause routing across the VMware skill family
|
|
5
5
|
Project-URL: Homepage, https://github.com/vmware-skills/VMware-Debug
|
|
6
6
|
Project-URL: Repository, https://github.com/vmware-skills/VMware-Debug
|
|
@@ -1,3 +1,45 @@
|
|
|
1
|
+
## v1.11.0 — the correlation engine, on a real incident
|
|
2
|
+
|
|
3
|
+
A tester fed the investigation layer a genuine failure: on 08-03 at 05:49, four
|
|
4
|
+
ESXi hosts went `EnteringMaintenanceMode` → `EnteredMaintenanceMode` →
|
|
5
|
+
`HostShutdown` → `HostConnectionLost` → `HostSyncFailed` ×24 within 32 seconds,
|
|
6
|
+
and never came back. 3,818 real vCenter events over 40 days, pulled per this
|
|
7
|
+
skill's own envelope documentation. The case-management layer held up. The
|
|
8
|
+
correlation engine — the thing this skill is named for — did not.
|
|
9
|
+
|
|
10
|
+
**80% of events were `uncategorized`, and the headline conclusion was "cannot
|
|
11
|
+
classify, widen your search window".** The classification table's
|
|
12
|
+
`power_lifecycle` keywords were all VM-centric (`power on`/`power off`, `vmx`,
|
|
13
|
+
`ovf`, `clone`, `snapshot`); nothing covered host maintenance mode or host
|
|
14
|
+
shutdown, and `"Shut down of esxi05"` does not match `"power off"`. There is now
|
|
15
|
+
a `host_lifecycle` category — a separate category rather than more keywords,
|
|
16
|
+
because it is a separate investigation, routed to monitor + logs + harden (a
|
|
17
|
+
host that left service on cue was usually told to). `classification_coverage()`
|
|
18
|
+
reports what went unread in every result, and above ~25% it reaches
|
|
19
|
+
`next_checks`. The "widen the window" remedy is gone: the window was already 40
|
|
20
|
+
days, and widening makes the incident *less* findable.
|
|
21
|
+
|
|
22
|
+
**The only spike detected was the day the hosts came back up.**
|
|
23
|
+
`_auto_bin_seconds` was `span/30`, so a 29-day window gave 23.2-hour buckets;
|
|
24
|
+
the incident hour was the second-busiest in the dataset and absent from the
|
|
25
|
+
spike list, while `--bin-seconds 3600` found it at z=8.25. Binning now chooses
|
|
26
|
+
by event *density*, from a floor of 4 events per bin that is derived rather than
|
|
27
|
+
tuned: spikes flag at mean+2σ, and a count only exceeds twice its mean once the
|
|
28
|
+
mean reaches 4. The chosen width, how it was chosen, and the bin count are
|
|
29
|
+
reported.
|
|
30
|
+
|
|
31
|
+
**The same incident described five ways routed five different ways** — decided
|
|
32
|
+
by an incidental noun, with the most accurate description routing nowhere.
|
|
33
|
+
Categories are now ranked by matched-keyword length, so a long phrase beats a
|
|
34
|
+
stray word, and the losing categories are named.
|
|
35
|
+
|
|
36
|
+
Also fixed: `case_timeline`'s payload contract was undocumented and its MCP
|
|
37
|
+
parameter type refused the array shape the docstring implied; pure-Chinese
|
|
38
|
+
summaries collapsed to one `case_id` per second, with a collision message that
|
|
39
|
+
was false; and skill names were compared as raw strings, so `monitor` and
|
|
40
|
+
`vmware-monitor` counted as two independent evidence sources — buying *Probable*
|
|
41
|
+
out of orthography, in the layer built to prevent exactly that.
|
|
42
|
+
|
|
1
43
|
## v1.10.0 — the knowledge layer is implemented, not just described
|
|
2
44
|
|
|
3
45
|
v1.9.0 shipped the investigation layer with `applies_to` documented as the thing
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vmware-debug"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.11.0"
|
|
8
8
|
description = "VMware diagnostic brain — read-only incident triage, log/event correlation, and root-cause routing across the VMware skill family"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -7,12 +7,21 @@
|
|
|
7
7
|
"url": "https://github.com/vmware-skills/VMware-Debug",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "1.
|
|
10
|
+
"version": "1.11.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "pypi",
|
|
14
14
|
"identifier": "vmware-debug",
|
|
15
|
-
"version": "1.
|
|
15
|
+
"version": "1.11.0",
|
|
16
|
+
"runtimeHint": "uvx",
|
|
17
|
+
"packageArguments": [
|
|
18
|
+
{
|
|
19
|
+
"type": "positional",
|
|
20
|
+
"valueHint": "subcommand",
|
|
21
|
+
"value": "mcp",
|
|
22
|
+
"description": "Starts the stdio MCP server. Without it the console script runs the human CLI and exits."
|
|
23
|
+
}
|
|
24
|
+
],
|
|
16
25
|
"transport": {
|
|
17
26
|
"type": "stdio"
|
|
18
27
|
}
|
|
@@ -256,8 +256,10 @@ vmware-debug mcp # start stdio MCP server (proxy-
|
|
|
256
256
|
## Troubleshooting
|
|
257
257
|
|
|
258
258
|
- **`incident_timeline` raises "event[N] could not be normalised"** — event N is missing a timestamp or has an unparseable one. Every event needs `ts` (ISO-8601, epoch seconds, or millis).
|
|
259
|
-
- **
|
|
260
|
-
- **No spikes detected on an obvious burst** — you
|
|
259
|
+
- **Most events come back "uncategorized"** — read `classification` in the result: it says how many, what share, and quotes the texts that matched nothing. Do **not** widen the window; a wider window adds baseline, not signal. The spikes still tell you *when*, which is answerable without a category. If the samples name a subsystem the taxonomy does not know, add a signature (see `references/routing.md`).
|
|
260
|
+
- **No spikes detected on an obvious burst** — check `binning` for the resolution you were given. The width is chosen from event density so that bins average ≥4 events; a burst shorter than one bin can still be flattened by it. Pass `bin_seconds` to narrow. Below three bins there is no baseline at all and nothing is reported.
|
|
261
|
+
- **`case_timeline` says zero events after you submitted results** — check `payload_events` in each `case_submit_evidence` reply. Events are read from a bare list, or from `items`/`events`/`rows`; a *summary* of a tool's result carries none. `note` names which items carried nothing and what keys they held instead.
|
|
262
|
+
- **`case_readiness` says a skill you have is not installed** — both spellings are accepted (`monitor` and `vmware-monitor`). A name it did not recognise comes back in `unrecognised_skills` rather than being read as missing.
|
|
261
263
|
- **It won't execute the fix** — by design. Route to vmware-aiops or vmware-pilot.
|
|
262
264
|
|
|
263
265
|
## Audit & Safety
|
|
@@ -27,9 +27,16 @@ fixed in-process constant, so `total` is a real count and `limit` is `null`.
|
|
|
27
27
|
|
|
28
28
|
## Symptom categories
|
|
29
29
|
|
|
30
|
-
`storage`, `network`, `compute`, `ha_drs`, `
|
|
30
|
+
`storage`, `network`, `compute`, `ha_drs`, `host_lifecycle`, `power_lifecycle`,
|
|
31
|
+
`auth`, `platform`.
|
|
31
32
|
See `references/routing.md` for keyword signatures and the skill each routes to.
|
|
32
33
|
|
|
34
|
+
`host_lifecycle` is a host changing its own availability state — maintenance
|
|
35
|
+
mode, shutdown, reboot, standby, connection loss, sync failure.
|
|
36
|
+
`power_lifecycle` is the VM-level equivalent. They are separate because they are
|
|
37
|
+
separate investigations: the second is a task question for vmware-aiops, the
|
|
38
|
+
first is a cluster, DPM, vLCM or drift question.
|
|
39
|
+
|
|
33
40
|
## Design properties
|
|
34
41
|
|
|
35
42
|
- **Zero cross-skill runtime deps** — correlation is pure functions over plain
|
{vmware_debug-1.10.0 → vmware_debug-1.11.0}/skills/vmware-debug/references/event-envelope.md
RENAMED
|
@@ -21,7 +21,7 @@ runtime dependency** on the other packages (no version lockstep, no heavy instal
|
|
|
21
21
|
| Field | Type | Notes |
|
|
22
22
|
|---|---|---|
|
|
23
23
|
| `ts` | string \| number | ISO-8601, epoch **seconds**, or epoch **millis** (auto-detected). Required. |
|
|
24
|
-
| `source` | string | `monitor` \| `aria` \| `loginsight` \| `nsx` \| `nsx-security` \| `storage` \| ... |
|
|
24
|
+
| `source` | string | `monitor` \| `aria` \| `loginsight` \| `nsx` \| `nsx-security` \| `storage` \| ... The catalogue in `rules/evidence_sources.yaml` spells the same skills `vmware-monitor`, `vmware-aria`, `vmware-log-insight`. Both spellings are understood wherever a skill is named — `case_readiness(available_skills=...)`, and the grader's count of independent sources, which treats two spellings of one skill as one source. |
|
|
25
25
|
| `severity` | string | Free text; normalised to `critical`/`error`/`warning`/`info`/`unknown`. |
|
|
26
26
|
| `entity` | string | The object the event is about (VM/host/datastore). May be empty. |
|
|
27
27
|
| `text` | string | Human-readable message — this is what the symptom classifier matches on. |
|
|
@@ -10,6 +10,7 @@ Keep this in sync with `_CATEGORY_SIGNATURES` in `vmware_debug/ops/timeline.py`
|
|
|
10
10
|
| **network** | vmotion, uplink, link down, mtu, firewall, dfw, segment, tier-0, bgp | vmware-nsx, vmware-nsx-security (traceflow, DFW hits) | pilot |
|
|
11
11
|
| **compute** | cpu ready, memory, balloon, swap, contention, numa | vmware-aria (metrics + anomalies) | pilot (rightsizing) |
|
|
12
12
|
| **ha_drs** | ha, high availability, drs, failover, admission control, isolation | vmware-monitor, vmware-aiops (cluster) | pilot |
|
|
13
|
+
| **host_lifecycle** | maintenance mode, shut down of, host reboot, standby mode, lost connection to, cannot synchronize, not responding | vmware-monitor (host/cluster state), vmware-harden (drift — a host that left service on cue was told to), vmware-log-insight (vpxd/hostd) | pilot |
|
|
13
14
|
| **power_lifecycle** | power on/off, failed to start, boot, vmx, ovf, clone, snapshot | vmware-aiops (task status, snapshot tree), vmware-monitor | aiops |
|
|
14
15
|
| **auth** | login, authentication, denied, 401, 403, token, certificate, tls | config/.env, target cert + time sync | config fix |
|
|
15
16
|
| **platform** | vpxd, hostd, service restart, crash, 503, not responding, disconnected | vmware-monitor (connection/service), vmware-log-insight (vpxd/hostd) | pilot |
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"names_artifact",
|
|
95
95
|
"states_remedy"
|
|
96
96
|
],
|
|
97
|
-
"where": "grading.py:
|
|
97
|
+
"where": "grading.py:57"
|
|
98
98
|
},
|
|
99
99
|
{
|
|
100
100
|
"message": "grading_rules.yaml at {} is not valid YAML: {}. Fix it rather than deleting it \u2014 falling back to the",
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
"names_artifact",
|
|
103
103
|
"states_remedy"
|
|
104
104
|
],
|
|
105
|
-
"where": "grading.py:
|
|
105
|
+
"where": "grading.py:59"
|
|
106
106
|
},
|
|
107
107
|
{
|
|
108
108
|
"message": "A hypothesis needs a statement \u2014 what you think happened, in one line. An unnamed hypothesis cannot ",
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
"names_artifact",
|
|
136
136
|
"states_remedy"
|
|
137
137
|
],
|
|
138
|
-
"where": "plan.py:
|
|
138
|
+
"where": "plan.py:84"
|
|
139
139
|
},
|
|
140
140
|
{
|
|
141
141
|
"message": "evidence_sources.yaml at {} is missing 'classes' or 'routing'. An empty catalogue would make every r",
|
|
@@ -167,7 +167,7 @@
|
|
|
167
167
|
"names_artifact",
|
|
168
168
|
"states_remedy"
|
|
169
169
|
],
|
|
170
|
-
"where": "timeline.py:
|
|
170
|
+
"where": "timeline.py:137"
|
|
171
171
|
},
|
|
172
172
|
{
|
|
173
173
|
"message": "Cannot read case.json for {}: {}",
|
|
@@ -175,7 +175,7 @@
|
|
|
175
175
|
"names_artifact",
|
|
176
176
|
"states_remedy"
|
|
177
177
|
],
|
|
178
|
-
"where": "timeline.py:
|
|
178
|
+
"where": "timeline.py:151"
|
|
179
179
|
}
|
|
180
180
|
],
|
|
181
181
|
"per_dimension_pct": {
|
|
@@ -194,20 +194,20 @@
|
|
|
194
194
|
"detail": {
|
|
195
195
|
"heaviest_tools": {
|
|
196
196
|
"case_open": 354,
|
|
197
|
-
"case_plan":
|
|
197
|
+
"case_plan": 395,
|
|
198
198
|
"case_record_gap": 337,
|
|
199
|
-
"case_submit_evidence":
|
|
200
|
-
"incident_timeline":
|
|
199
|
+
"case_submit_evidence": 693,
|
|
200
|
+
"incident_timeline": 357
|
|
201
201
|
},
|
|
202
|
-
"manifest_tokens":
|
|
203
|
-
"mean_tokens_per_tool":
|
|
202
|
+
"manifest_tokens": 3988,
|
|
203
|
+
"mean_tokens_per_tool": 284.9,
|
|
204
204
|
"reference_context": 16384,
|
|
205
205
|
"tool_count": 14
|
|
206
206
|
},
|
|
207
207
|
"maximum": 16384,
|
|
208
|
-
"pct":
|
|
208
|
+
"pct": 75.7,
|
|
209
209
|
"unit": "tokens",
|
|
210
|
-
"value":
|
|
210
|
+
"value": 12396
|
|
211
211
|
},
|
|
212
212
|
"parameter_documentation_coverage": {
|
|
213
213
|
"detail": {
|
|
@@ -216,8 +216,7 @@
|
|
|
216
216
|
"case_id"
|
|
217
217
|
],
|
|
218
218
|
"case_submit_evidence": [
|
|
219
|
-
"fetched_at"
|
|
220
|
-
"summary"
|
|
219
|
+
"fetched_at"
|
|
221
220
|
],
|
|
222
221
|
"case_timeline": [
|
|
223
222
|
"bin_seconds",
|
|
@@ -228,23 +227,24 @@
|
|
|
228
227
|
}
|
|
229
228
|
},
|
|
230
229
|
"maximum": 45,
|
|
231
|
-
"pct":
|
|
230
|
+
"pct": 86.7,
|
|
232
231
|
"unit": "parameters",
|
|
233
|
-
"value":
|
|
232
|
+
"value": 39
|
|
234
233
|
},
|
|
235
234
|
"per_tool_token_discipline": {
|
|
236
235
|
"detail": {
|
|
237
236
|
"ceiling": 350,
|
|
238
237
|
"over_ceiling": {
|
|
239
238
|
"case_open": 354,
|
|
240
|
-
"case_plan":
|
|
241
|
-
"case_submit_evidence":
|
|
239
|
+
"case_plan": 395,
|
|
240
|
+
"case_submit_evidence": 693,
|
|
241
|
+
"incident_timeline": 357
|
|
242
242
|
}
|
|
243
243
|
},
|
|
244
244
|
"maximum": 14,
|
|
245
|
-
"pct":
|
|
245
|
+
"pct": 71.4,
|
|
246
246
|
"unit": "tools",
|
|
247
|
-
"value":
|
|
247
|
+
"value": 10
|
|
248
248
|
},
|
|
249
249
|
"read_only_manifest_saving": {
|
|
250
250
|
"detail": {
|
|
@@ -281,7 +281,7 @@
|
|
|
281
281
|
"evidence.py:179",
|
|
282
282
|
"evidence.py:211",
|
|
283
283
|
"evidence.py:248",
|
|
284
|
-
"grading.py:
|
|
284
|
+
"grading.py:67",
|
|
285
285
|
"hypotheses.py:47",
|
|
286
286
|
"hypotheses.py:70"
|
|
287
287
|
]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""A case opened in Chinese got no identity of its own.
|
|
2
|
+
|
|
3
|
+
``slugify`` reduces on ``[^a-z0-9]+``, so a summary written entirely in Chinese
|
|
4
|
+
leaves nothing behind and the id falls back to the literal word ``case``:
|
|
5
|
+
``20260830-061014-case``. Two completely different investigations opened in the
|
|
6
|
+
same second then collide — and the collision message said they were "the same
|
|
7
|
+
summary", which was false, and sent the reporter looking for a duplicate that
|
|
8
|
+
did not exist.
|
|
9
|
+
|
|
10
|
+
The id has to stay inside its character set: it is a directory name, and
|
|
11
|
+
``validate_case_id`` is the only thing between a model-chosen string and
|
|
12
|
+
``open(root / case_id)``. So the fix is not to widen the alphabet. It is that an
|
|
13
|
+
id must distinguish summaries that differ, and that the collision message must
|
|
14
|
+
describe what actually collided.
|
|
15
|
+
|
|
16
|
+
The control is readability: an ASCII summary must still produce the readable
|
|
17
|
+
slug the listing was designed around, not a digest for everybody.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import pytest
|
|
23
|
+
|
|
24
|
+
from vmware_debug.ops.cases.ids import new_case_id, validate_case_id
|
|
25
|
+
from vmware_debug.ops.cases.model import Scope
|
|
26
|
+
from vmware_debug.ops.cases.store import CaseExists, create_case
|
|
27
|
+
|
|
28
|
+
AT = "2026-08-30T06:10:14Z"
|
|
29
|
+
|
|
30
|
+
#: Both must be free of Latin characters. An earlier draft wrote the first one
|
|
31
|
+
#: as "四台 ESXi 主机…", and "esxi" survived slugify — so the two ids differed
|
|
32
|
+
#: whatever the fallback did, and the test passed against the defect it was
|
|
33
|
+
#: written to catch.
|
|
34
|
+
ZH_A = "四台主机同时进入维护模式后关机"
|
|
35
|
+
ZH_B = "存储集群延迟升高导致虚拟机无响应"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.fixture(autouse=True)
|
|
39
|
+
def isolated_home(tmp_path, monkeypatch):
|
|
40
|
+
monkeypatch.setenv("OPS_HOME", str(tmp_path / "vmware"))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TestANonAsciiSummaryStillGetsAnIdentity:
|
|
44
|
+
def test_two_different_chinese_summaries_do_not_collide(self):
|
|
45
|
+
assert new_case_id(ZH_A, AT) != new_case_id(ZH_B, AT)
|
|
46
|
+
|
|
47
|
+
def test_the_id_is_still_a_safe_directory_name(self):
|
|
48
|
+
validate_case_id(new_case_id(ZH_A, AT))
|
|
49
|
+
|
|
50
|
+
def test_the_same_summary_still_yields_the_same_id(self):
|
|
51
|
+
"""Ids are replayable: a case folder re-opened from its scope has to
|
|
52
|
+
reconstruct the id it already has."""
|
|
53
|
+
assert new_case_id(ZH_A, AT) == new_case_id(ZH_A, AT)
|
|
54
|
+
|
|
55
|
+
def test_both_cases_can_actually_be_opened_in_the_same_second(self):
|
|
56
|
+
create_case(Scope(summary=ZH_A, determined_by="user report"), at=AT)
|
|
57
|
+
create_case(Scope(summary=ZH_B, determined_by="user report"), at=AT)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TestTheCollisionMessageIsTrue:
|
|
61
|
+
def test_it_does_not_claim_the_summaries_were_identical(self):
|
|
62
|
+
"""Two summaries that differ only outside the id character set still
|
|
63
|
+
collide, and telling the reporter they submitted the same summary sends
|
|
64
|
+
them looking for something that is not there."""
|
|
65
|
+
create_case(Scope(summary="vsan latency!", determined_by="a"), at=AT)
|
|
66
|
+
with pytest.raises(CaseExists) as exc:
|
|
67
|
+
create_case(Scope(summary="vsan latency?", determined_by="a"), at=AT)
|
|
68
|
+
assert "same summary" not in str(exc.value)
|
|
69
|
+
|
|
70
|
+
def test_it_says_what_to_do_about_it(self):
|
|
71
|
+
create_case(Scope(summary="vsan latency", determined_by="a"), at=AT)
|
|
72
|
+
with pytest.raises(CaseExists) as exc:
|
|
73
|
+
create_case(Scope(summary="vsan latency", determined_by="a"), at=AT)
|
|
74
|
+
assert "case_get" in str(exc.value)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class TestControlReadabilityIsNotSacrificed:
|
|
78
|
+
def test_an_ascii_summary_still_gets_its_readable_slug(self):
|
|
79
|
+
"""A digest for every case would pass every test above and destroy the
|
|
80
|
+
one property the slug exists for — a directory listing you can read."""
|
|
81
|
+
assert new_case_id("vSAN datastore latency on cluster-01", AT).endswith(
|
|
82
|
+
"-vsan-datastore-latency-on-cluster-01"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def test_a_mixed_summary_keeps_the_part_that_survives(self):
|
|
86
|
+
assert "esxi05" in new_case_id("主机 esxi05 关机", AT)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""What `payload` has to be for case_timeline to see it, said at the time.
|
|
2
|
+
|
|
3
|
+
The tester submitted the summary dict from a get_events call — ``{"total": 3818,
|
|
4
|
+
"by_type": {...}}`` — as the payload. It was accepted, recorded, and reported
|
|
5
|
+
back a grade, and nothing anywhere said the timeline would never see it.
|
|
6
|
+
case_timeline then answered zero events with the hint "submit the get_events
|
|
7
|
+
result with `payload`", which is what they had just done.
|
|
8
|
+
|
|
9
|
+
Two things were missing and both are pinned here. The contract was undocumented
|
|
10
|
+
— ``payload: Any`` with a docstring that said "the raw result" — and, worse, the
|
|
11
|
+
one moment where the mismatch is knowable is the submission itself, where
|
|
12
|
+
nothing looked.
|
|
13
|
+
|
|
14
|
+
The controls stop the degenerate fix, which is to complain about every payload:
|
|
15
|
+
a real event envelope has to come back clean, evidence that is not an event
|
|
16
|
+
stream at all has to be accepted without being scolded, and the timeline still
|
|
17
|
+
has to build.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
|
|
24
|
+
import pytest
|
|
25
|
+
|
|
26
|
+
from vmware_debug.mcp import tools as t
|
|
27
|
+
from vmware_debug.mcp_server.server import build_server
|
|
28
|
+
|
|
29
|
+
AT = "2026-08-30T09:00:00Z"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.fixture(autouse=True)
|
|
33
|
+
def isolated_home(tmp_path, monkeypatch):
|
|
34
|
+
monkeypatch.setenv("OPS_HOME", str(tmp_path / "vmware"))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.fixture
|
|
38
|
+
def case():
|
|
39
|
+
return t.case_open(summary="four hosts down", determined_by="alarm 42", at=AT)["case_id"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def submit(case, payload, skill="vmware-monitor"):
|
|
43
|
+
return t.case_submit_evidence(
|
|
44
|
+
case_id=case,
|
|
45
|
+
source_skill=skill,
|
|
46
|
+
source_tool="get_events",
|
|
47
|
+
query={"hours": 960},
|
|
48
|
+
summary="3818 events over 40 days",
|
|
49
|
+
fetched_at=AT,
|
|
50
|
+
payload=payload,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
EVENT = {"ts": "2026-08-03T05:49:00Z", "severity": "critical", "text": "Shut down of esxi05"}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TestTheMismatchIsReportedWhereItHappens:
|
|
58
|
+
def test_a_summary_dict_is_not_silently_accepted_as_events(self, case):
|
|
59
|
+
r = submit(case, {"total": 3818, "by_type": {"HostShutdown": 4}})
|
|
60
|
+
assert r["payload_events"] == 0
|
|
61
|
+
assert r["payload_note"]
|
|
62
|
+
|
|
63
|
+
def test_the_note_names_the_keys_that_were_actually_there(self, case):
|
|
64
|
+
""""Submit the get_events result" is unusable advice for someone who
|
|
65
|
+
did. Naming what arrived is what makes the mismatch visible."""
|
|
66
|
+
note = submit(case, {"total": 3818, "by_type": {}})["payload_note"]
|
|
67
|
+
assert "total" in note and "by_type" in note
|
|
68
|
+
|
|
69
|
+
def test_the_note_names_the_keys_it_reads(self, case):
|
|
70
|
+
note = submit(case, {"total": 3818})["payload_note"]
|
|
71
|
+
assert "items" in note and "events" in note and "rows" in note
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class TestTheAcceptedShapes:
|
|
75
|
+
def test_a_bare_list_of_events_is_counted(self, case):
|
|
76
|
+
assert submit(case, [EVENT, EVENT])["payload_events"] == 2
|
|
77
|
+
|
|
78
|
+
def test_the_family_list_envelope_is_counted(self, case):
|
|
79
|
+
assert submit(case, {"items": [EVENT], "total": 1})["payload_events"] == 1
|
|
80
|
+
|
|
81
|
+
def test_a_bare_list_survives_the_mcp_boundary(self):
|
|
82
|
+
"""The documented shape has to be reachable through the tool schema.
|
|
83
|
+
Typed ``Optional[dict]``, an array of events was refused by validation
|
|
84
|
+
before any of this code ran."""
|
|
85
|
+
server = build_server()
|
|
86
|
+
schema = {tool.name: tool.inputSchema for tool in asyncio.run(server.list_tools())}
|
|
87
|
+
payload = schema["case_submit_evidence"]["properties"]["payload"]
|
|
88
|
+
allowed = {
|
|
89
|
+
entry.get("type") for entry in payload.get("anyOf", [payload]) if isinstance(entry, dict)
|
|
90
|
+
}
|
|
91
|
+
assert "array" in allowed, f"payload schema does not accept a list: {payload}"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class TestControls:
|
|
95
|
+
def test_a_real_envelope_is_not_complained_about(self, case):
|
|
96
|
+
"""The fix must not be 'warn on everything'. Banning one phrase is not
|
|
97
|
+
enough — the note has to positively report what it read, or a
|
|
98
|
+
complain-about-everything version passes by rewording."""
|
|
99
|
+
note = submit(case, {"items": [EVENT]})["payload_note"]
|
|
100
|
+
assert "no event rows" not in note
|
|
101
|
+
assert "1 event row" in note and "items" in note
|
|
102
|
+
|
|
103
|
+
def test_evidence_that_is_not_an_event_stream_is_still_accepted(self, case):
|
|
104
|
+
"""A knowledge citation or a config dump carries no events by nature and
|
|
105
|
+
is not a mistake. It is recorded, and it still counts as a source."""
|
|
106
|
+
r = submit(case, None, skill="knowledge-kb")
|
|
107
|
+
assert r["evidence_id"]
|
|
108
|
+
assert r["payload_events"] == 0
|
|
109
|
+
|
|
110
|
+
def test_the_timeline_still_builds_from_a_real_payload(self, case):
|
|
111
|
+
submit(case, {"items": [EVENT]})
|
|
112
|
+
assert t.case_timeline(case_id=case)["event_count"] == 1
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class TestTheTimelineSaysWhichItemsCarriedNothing:
|
|
116
|
+
def test_it_names_the_items_and_what_they_held(self, case):
|
|
117
|
+
submit(case, {"total": 3818, "by_type": {}})
|
|
118
|
+
note = t.case_timeline(case_id=case)["note"]
|
|
119
|
+
assert "E001" in note
|
|
120
|
+
assert "total" in note
|
|
121
|
+
|
|
122
|
+
def test_control_a_populated_timeline_does_not_carry_the_complaint(self, case):
|
|
123
|
+
submit(case, {"items": [EVENT]})
|
|
124
|
+
assert "E001" not in t.case_timeline(case_id=case)["note"]
|