myxce/.kiro/specs/resource-value-verification/design.md
Rongsong Shen 1bc65d2c47
docs(resource-value-verification): close design-review doc/comment gaps
Two retrospective design-review findings:

- design.md's System Flows sequence diagram showed the controller-layer
  resval check as an independently-launched client session, implying no
  dependency on the injecting worker staying connected. As-built (bug-1579),
  host-attrs resources reset to baseline the instant the worker disconnects,
  so the check actually runs nested inside the still-open worker session via
  xce_test's `!` shell-escape. Updated the diagram and prose to show this,
  and documented the still-open residual risk for custom host-attrs
  resource names in Error Handling.
- Added explicit "do not change this format" warning comments at both
  trace-log call sites (core_events.c's RESVALS_DELTA case, apiserver's
  OnResourceLocationUpdated) that xce-test-resources' log-grep depends on --
  an unrelated logging cleanup at either site would otherwise silently turn
  every future verification run into a false UNREACHABLE/timeout.

No behavior change in either source file -- comments only.
2026-08-19 11:00:57 +00:00

360 lines
46 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Design Document
## Overview
This feature closes a coverage gap in `xce_test`'s worker-context `resupdate` command (its resource-name table is missing 7 real dynamic resources) and adds a repeatable, self-contained skill that injects a synthetic resource value through `resupdate` and confirms it is correctly received and stored at the controller, the live scheduler, and the Go apiserver — the exact three-layer propagation path that took a multi-hour live-deployment investigation to verify manually during bug-1569.
**Purpose**: Turn a class of bug (resource-value routing/propagation defects) that previously required deploying real worker/controller/scheduler/apiserver daemons and waiting on periodic reporting cycles into a command that reproduces and confirms the fix in seconds.
**Users**: XCE engineers debugging or regression-testing resource-value propagation across the controller/scheduler/apiserver boundary.
**Impact**: Extends `xce_test` (test.c) with a small, backward-compatible change; extends `xce-deploy-ha`'s `deploy-ha.sh`/`SKILL.md` with an opt-in, conditionally-generated apiserver deployment step (see Design Decisions below — no change to existing callers that don't need an apiserver); adds two single-line trace-log additions (scheduler, apiserver); adds one new skill. No production runtime *behavior* changes — the trace-log additions are inert log-only output at debug/trace level, and the `xce-deploy-ha` extension only activates when the apiserver binary is present, mirroring its existing worker/scheduler conditional-generation pattern.
### Goals
- `resupdate` accepts any resource name the cluster has registered (Requirement 1).
- Confirm the existing `join` + `resupdate` worker-context flow needs no real worker daemon (Requirement 2).
- A single command deploys controller + scheduler + apiserver, injects one or more resource values, and reports PASS/FAIL per layer (Requirements 3–6).
### Non-Goals
- Renaming `xce_test`, per-command help, reference documentation, JSON output mode, non-interactive batch mode, argument-convention unification — explicitly deferred to a future "enhance xce_test" project (see requirements.md Future Considerations).
- A new live scheduler admin query RPC — rejected in favor of trace-log observation, which needs no new RPC at all (see research.md Design Decisions).
- The record+replay+REPL flow and the apiserver REST/`xcecli` query path — both considered and superseded during design validation in favor of live trace-log observation at each layer's point of application; see research.md Design Decisions for why (this feature's own originating bugs, bug-1568 and bug-1569A, were both storage-layer defects found via exactly this technique, and the REPL's step/continue requirement had already produced one false negative live).
- General-purpose apiserver production deployment/HA — the new `xce-deploy-ha` apiserver step is a minimal single-node instance for local verification only, and no longer needs functional auth/RBAC since no REST query is issued against it.
- A new, standalone apiserver deploy script — considered and superseded in favor of extending `xce-deploy-ha` itself, reusing its already-precedented conditional (binary-present-only) generation pattern instead of re-implementing the same conventions in parallel (see research.md Design Decisions).
- CI/automated-pipeline integration.
- Fixing unrelated bugs the verification run happens to surface — those are reported, not silently patched, by this feature.
## Boundary Commitments
### This Spec Owns
- `set_resval()`/`cmd_resupdate` in `src/controller/testsuite/test.c`: accepting any registered resource name, with an explicit type-override escape hatch.
- A single trace/debug log line in `xce_core_push_events()`'s `XCE_CORE_EVENT_RESVALS_DELTA` case (`src/scheduler/src/core_events.c`), emitted at the point the live scheduler daemon applies a resource update — the only scheduler-side change this spec makes.
- A single trace/debug log line in `OnResourceLocationUpdated` (`src/frontend/apiserver/backend/handler.go:92-95`), emitted at the point the apiserver stores a received resource update — the only apiserver-side change this spec makes.
- An opt-in, conditionally-generated apiserver deployment step added to `xce-deploy-ha`'s `src/tools/deploy-ha.sh` and documented in its `SKILL.md` (Step 4d, alongside the existing Step 4b worker/scheduler steps) — generates `start-apiserver.sh` only when a pre-built apiserver binary already exists at its own Go-build output path (same "caller opts in by having already built it" spirit as the worker/scheduler conditional, though checked at a different path since apiserver is a separate build system — see Components for the precise distinction). Existing `xce-deploy-ha` callers that never build an apiserver (e.g. `xce-test-ha`) see zero behavior change.
- A new skill (`.claude/skills/xce-test-resources/`) that orchestrates deploy + inject + three-layer verify + PASS/FAIL report.
### Out of Boundary
- `resource_sync.c`'s real, production periodic load-index collection — unchanged.
- The controller's resval-forwarding hooks (`do_update_resvals`, `sched_resource_hook_fun`, `apiserver_resource_hook_fun`) — unchanged; this design only consumes them.
- The scheduler's REPL (`repl.c`/`repl_info.c`), recording admin RPCs, and the apiserver's REST API/`xcecli` query path — none of these are touched or driven by this design (superseded by the trace-log decision; see research.md Design Decisions). They remain available as a future follow-up if a query-layer-specific check is ever needed.
- All other scheduler/apiserver logic beyond the two single-line trace additions listed under "This Spec Owns" above — no behavior change, no new code paths, purely additive log output.
- `xce-deploy-ha`'s existing controller/worker/scheduler steps (1–4c) — unmodified; only a new, conditionally-triggered step is added alongside them.
- The broader "enhance xce_test" ideas listed in requirements.md's Future Considerations.
### Allowed Dependencies
- `xce-deploy-ha`'s controller deployment conventions and its existing worker/scheduler conditional-generation pattern (directory layout, TLS cert generation, `cluster.bin` build via `xceadmin`, Step 3's "generate the extra start script only if the binary exists" rule) — reused directly, by extending the same script, not duplicated in a parallel one (see research.md Design Decisions).
- `xce_test`'s existing worker context (`join`, `resupdate`) and client context (`resval`) — unmodified except for Requirement 1's table extension.
- `make build-server` (`src/frontend/Makefile`) to build the apiserver binary; `make test-integration`'s `XCE_BIN`/`SRV_URL` convention as the closest existing precedent for pointing it at a controller.
- Each daemon's own existing logging infrastructure (`tinylog` in C for the scheduler, zap `logger.Debug` in Go for the apiserver — per `error-handling.md`) as the channel the new trace lines are written through; no new logging mechanism is introduced.
### Revalidation Triggers
- The wire shape of `xce_resval_t` / `resupdate`'s CLI flags changes.
- `xce_core_push_events()`'s `XCE_CORE_EVENT_RESVALS_DELTA` handling, or `OnResourceLocationUpdated`'s call site, is refactored such that the trace log's format/location moves.
- `xce-deploy-ha`'s Step 3 conditional-generation pattern (or its directory layout) changes shape — this feature's apiserver step is now part of that same script, so a shape change there is a direct, first-party revalidation trigger rather than a drift risk between two separate scripts.
- If a future bug turns out to live specifically in the query/REST/serialization path rather than storage, revisit adding a real query-based check for that layer (not precluded by this design, just not built here).
## Architecture
### Existing Architecture Analysis
A resource value reaches all three layers through **one** controller-side write, not three independent paths (see research.md): `do_update_resvals()` applies it to the controller's own `w->host.resvals` (queried via `resval`) and fires `HOOK_DETAIL_RESOURCE_UPDATE`, which fans out to both `sched_resource_hook_fun` (scheduler) and `apiserver_resource_hook_fun` (apiserver). This design's job is purely to exercise this existing path with a synthetic worker and observe the result at each of the three existing points of application — it introduces no new production data path, and (per the trace-log decision) reads state at the storage point rather than through a query surface.
### Architecture Pattern & Boundary Map
```mermaid
graph TB
Skill[xce-test-resources skill]
WorkerCtx[xce_test worker context]
ClientCtx[xce_test client context]
Controller[xce_controller]
Scheduler[xce_scheduler daemon]
SchedLog[scheduler log]
Apiserver[apiserver]
ApiLog[apiserver log]
Skill --> WorkerCtx
WorkerCtx -->|join and resupdate| Controller
Skill --> ClientCtx
ClientCtx -->|resval query| Controller
Controller -->|resource update notify| Scheduler
Controller -->|resource update push| Apiserver
Scheduler -->|trace log on apply| SchedLog
Apiserver -->|trace log on apply| ApiLog
Skill -->|tail and grep| SchedLog
Skill -->|tail and grep| ApiLog
```
**Architecture Integration**:
- Selected pattern: black-box orchestration over existing interfaces plus two minimal, inert trace-log additions at each layer's point of application — this feature adds a coordinating layer (the skill), one small compatibility fix (`resupdate`'s resource table), and the two log lines; it does not add a new data path or query surface.
- Domain boundaries: `resupdate` table fix (C, test.c) is independent of the skill (bash orchestration); the `xce-deploy-ha` apiserver extension is independent of both and reusable by any `xce-deploy-ha` caller, not just this skill; the two trace-log additions are independent single-line changes in their respective daemons.
- Existing patterns preserved: `xce-deploy-ha`'s scratch-directory deploy convention and worker/scheduler conditional-generation pattern (extended, not duplicated, for apiserver), `xce-test-ha`'s VERDICT-line reporting convention, each daemon's own existing logging infrastructure (no new logging mechanism).
- New components rationale: the `xce-test-resources` skill is new because no existing tool exercises all three layers together; the `resupdate` fix is new because the current table is demonstrably incomplete; the `xce-deploy-ha` apiserver step and `start-all.sh` extend an existing tool rather than introduce a new one, following its own established conditional-generation precedent; the two trace-log lines are new because neither layer currently logs the value it applies (confirmed by direct inspection — see research.md).
- Steering compliance: production code changes are two single-line, debug/trace-level log additions with no behavior change, plus `test.c`'s test-tooling scope; TDD applies to the `set_resval()` change (see Testing Strategy).
### Technology Stack
| Layer | Choice / Version | Role in Feature | Notes |
|-------|------------------|------------------|-------|
| Test client (C, gnu17) | `xce_test` (existing binary, `src/controller/testsuite/test.c`) | Inject via worker context; query controller via client context | Requirements 1, 2, 3 |
| Scheduler (C, gnu17) | Live `xce_scheduler` daemon (existing, one new trace-log line) | Applies the resource update and logs it at the point of application | Requirement 4; see research.md Design Decisions |
| API server (Go) | `apiserver` binary via `make build-server` (one new trace-log line in `backend/handler.go`) | Receives and stores the resource update, logs it at the point of application | Requirement 5; see research.md Design Decisions |
| Orchestration | Bash skill script, `xce-deploy-ha`/`xce-test-ha` conventions | Deploy, inject, tail/grep each layer's log, verify, report, clean up | Requirement 6 |
## File Structure Plan
### Directory Structure
```
src/
└── controller/testsuite/
├── resval_typeresolve.c # NEW: set_resval()'s name→Value_u_which resolution, extracted for isolated unit testing
├── resval_typeresolve.h # NEW: its declaration
├── test_resval_typeresolve.c # NEW: Unity unit tests for resval_typeresolve.c (4 cases, see Testing Strategy)
└── test.c # MODIFIED: set_resval() delegates to resval_typeresolve.c; cmd_resupdate() gains -t flag + post-update accept/reject verification
src/controller/meson.build # MODIFIED: add resval_typeresolve.c to xce_test_src (both xce_test and test_resval_typeresolve.c link against it); add test_resval_typeresolve.c + a new controller_unit_test-style test() target — these are hand-maintained explicit file lists, not globs
src/scheduler/src/core_events.c # MODIFIED: one new trace/debug log line in xce_core_push_events()'s XCE_CORE_EVENT_RESVALS_DELTA case
src/frontend/apiserver/backend/handler.go # MODIFIED: one new trace/debug log line in OnResourceLocationUpdated
src/tools/
└── deploy-ha.sh # MODIFIED: Step 3 gains (a) a conditional apiserver-config-generation branch mirroring the existing worker/scheduler conditional, generating start-apiserver.sh (minimal boot-only config + start command — no functional auth/RBAC required, see research.md Design Decisions), and (b) an always-generated start-all.sh that sequences start.sh + health-check + whichever of start-worker.sh/start-scheduler.sh/start-apiserver.sh exist for this deploy — automates the manual recipe SKILL.md's Steps 4/4b/4c already document
.claude/skills/
├── xce-deploy-ha/
│ └── SKILL.md # MODIFIED: new Step 4d documenting the opt-in apiserver step, alongside existing Step 4b (worker/scheduler)
└── xce-test-resources/
├── SKILL.md # NEW: skill description, usage, VERDICT format (xce-test-ha convention)
└── run-resource-verify.sh # NEW: orchestrates deploy + inject + 3-layer verify (log-tail/grep for scheduler+apiserver) + verdict + cleanup
```
### Modified Files
- `src/controller/testsuite/test.c` — `set_resval()` becomes a thin wrapper around the new `resval_typeresolve.c` module; `cmd_resupdate()` gains an optional `-t <numeric|string>` flag per `-n`/`-v` pair, and, after sending the update, a follow-up per-name verification query (see Components below — Requirement 1.2/1.3's mechanism).
- `src/controller/meson.build` — `xce_test_src` (line 57) gains `testsuite/resval_typeresolve.c` so both `xce_test` and the new unit test binary can link it; a new Unity test source list/target (following the `controller_unit_test`/`sched_push_unit_test` pattern already in this file, e.g. lines 86, 215, 222) gains `testsuite/test_resval_typeresolve.c`. Both lists are hand-maintained and explicit, not glob-based — this file must be edited or the new module/test never builds.
- `src/scheduler/src/core_events.c` — `xce_core_push_events()`'s `XCE_CORE_EVENT_RESVALS_DELTA` case gains one trace/debug-level log line (via this component's existing `tinylog`-based logging, e.g. `TRACE`) emitting the resolved worker host, resource name, and applied value, immediately after the value is written into the worker's live state. Debug/trace level only — never fires at WARN/ERROR, never changes control flow. This is the exact site bug-1568 was root-caused at via ad hoc instrumentation; this spec makes that instrumentation permanent and machine-greppable.
- `src/frontend/apiserver/backend/handler.go` — `OnResourceLocationUpdated` (line 92) gains one `logger.Debug(...)` call (zap, per this component's existing convention) logging the resource name, host/location, and stored value, immediately after `store.ResourceLocations.Put()`.
- `src/tools/deploy-ha.sh` — Step 3 gains two additions:
1. A new conditional branch, same *opt-in-by-presence* spirit as the existing worker/scheduler branch (but checking a pre-built binary at the Go build's own output path, e.g. `/tmp/apiserver`, rather than alongside `$BINARY` — a different build system, see Components below): if present, generate `$TOP/scripts/start-apiserver.sh` (minimal boot-only config) and a `logger.yaml` with `logging.level: debug`; absent, generates nothing, so existing callers are unaffected.
2. An always-generated `$TOP/scripts/start-all.sh`: runs `start.sh` for the controller node(s), waits on Step 5's existing health-check, then runs whichever of `start-worker.sh`/`start-scheduler.sh`/`start-apiserver.sh` were actually generated for this deploy (skips any that weren't) — the same backgrounding pattern (`&`, or `setsid nohup … & disown` per SKILL.md's own noted improvement) Steps 4/4b/4c already document doing by hand.
- `.claude/skills/xce-deploy-ha/SKILL.md` — documents the new opt-in apiserver step as "Step 4d" (following the existing Step 4b worker/scheduler pattern) and `start-all.sh` as a documented shortcut for Steps 4/4b/4c combined.
### New Files
- `src/controller/testsuite/resval_typeresolve.c`/`.h` — `set_resval()`'s existing `resinfo[]` table lookup plus the new numeric-by-default fallback, moved out of `test.c` into a small, dependency-light module (no networking, no REPL state) so it is independently unit-testable. `test.c` (4298 lines, its own `main()`, `libedit`/network/REPL dependencies) cannot itself be white-box `#include`d into a Unity test the way smaller single-purpose files in this codebase are — extraction is required, not optional, to satisfy Requirement 1.1/1.4's testability.
- `src/controller/testsuite/test_resval_typeresolve.c` — Unity test program for `resval_typeresolve.c`, registered as its own `meson.build` `test()` target (see Modified Files above); this is the file task 1.1's RED tests and task 2.1's GREEN tests live in.
## System Flows
```mermaid
sequenceDiagram
participant Skill as xce-test-resources
participant Worker as xce_test (worker ctx, ONE session)
participant Nested as xce_test (nested ! client ctx)
participant Ctrl as controller
participant Sched as scheduler daemon
participant Api as apiserver
Skill->>Worker: join synthetic-worker
Skill->>Worker: resupdate -n <res> -v <value>
Worker->>Ctrl: xce_update_resvals (wire)
Ctrl->>Ctrl: apply to w.host.resvals
Ctrl->>Sched: notify schedUpdateResvals
Ctrl->>Api: notify schedUpdateResvals
Sched->>Sched: apply + trace-log resource, host, value
Api->>Api: store.ResourceLocations.Put + debug-log resource, host, value
Worker->>Nested: !<shell-cmd> --internal-controller-check (worker STILL connected)
Nested->>Ctrl: resval -n <res> -m synthetic-worker (query)
Ctrl-->>Nested: controller-observed value
Nested-->>Worker: result written to a marker file (nested exit code ignored)
Worker->>Ctrl: quit (worker disconnects HERE -- not before)
Skill->>Skill: parse marker file for controller-observed value
Skill->>Sched: tail/grep scheduler log for expected line (bounded wait)
Sched-->>Skill: scheduler-observed value (or timeout)
Skill->>Api: tail/grep apiserver log for expected line (bounded wait)
Api-->>Skill: apiserver-observed value (or timeout)
Skill->>Skill: compare all three vs injected value, emit verdict
```
**Load-bearing constraint the diagram makes explicit (as-built, bug-1579)**: the controller-layer check is NOT an independently-launched client session run after the worker disconnects. Any host-attrs-derived resource (`type`/`model`/`cpuf`/`maxcpus`/`maxmem`/`maxswap`/`maxtmp`/`ndisks` — `worker.c`'s `N_HOST_ATTRS`) is reset to baseline by the controller the instant the injecting worker disconnects — so a controller-layer check run as a separate, later session against any host-attrs name would ALWAYS false-MISMATCH, independent of whether the underlying propagation is actually correct (discovered as bug-1579, task 6.2, round-1 REJECTED). The fix nests the controller-layer `resval` query INSIDE the injecting worker's own still-open session, via `xce_test`'s `!<shell-command>` escape (`preprocess_and_run()` — works in any REPL context, including `worker`, with no `leave_ctx()`/disconnect) — the query genuinely runs before `quit`, not merely before some unrelated timeout. The scheduler and apiserver legs have no such constraint: both are pure log-tail/grep against a file, entirely independent of whether the worker connection is still open, so they run after the worker session ends like any other step.
Key simplification from the superseded design: no `record`/`replay`/REPL round-trip and no REST/`xcecli` query are needed — the scheduler and apiserver legs both poll their own live daemon's log file for the trace line the injection is expected to produce, within a bounded wait, exactly like the controller leg's own bounded-wait posture. This removes the REPL's `step`/`continue` pitfall entirely (there is no replay step in this flow) and the apiserver's auth/RBAC config from the critical path (see research.md Design Decisions).
## Requirements Traceability
| Requirement | Summary | Components | Interfaces | Flows |
|-------------|---------|------------|------------|-------|
| 1.1–1.4 | `resupdate` accepts any registered resource name | `set_resval()`/`cmd_resupdate` (test.c) | CLI flags `-n`/`-v`/`-t` | — |
| 2.1–2.2 | Inject without a real worker daemon | `xce_test` worker context (`join`, `resupdate`, existing) | CLI | Sequence diagram: join → resupdate → wire update |
| 3.1–3.2 | Controller-side visibility check | `xce-test-resources` skill, `xce_test` client context (`resval`, existing) | CLI | Sequence diagram: `resval` query → controller-observed value |
| 4.1–4.3 | Scheduler-side visibility check | `xce-test-resources` skill, scheduler trace log (`core_events.c`, new) | Log line, bounded-wait grep | Sequence diagram: scheduler applies + trace-logs → skill tails/greps scheduler log |
| 5.1, 5.2, 5.4 | Apiserver-side visibility check | `xce-test-resources` skill, `xce-deploy-ha` apiserver step, apiserver trace log (`handler.go`, new) | Log line, bounded-wait grep | Sequence diagram: apiserver stores + debug-logs → skill tails/greps apiserver log |
| 5.3 | Apiserver local deployment capability | `xce-deploy-ha` apiserver step + `start-all.sh` | Bash script | — |
| 6.1–6.5 | Repeatable run, PASS/FAIL verdict, cleanup | `xce-test-resources` skill (`run-resource-verify.sh`) | CLI, VERDICT line | Sequence diagram: final compare + verdict step |
## Components and Interfaces
| Component | Domain/Layer | Intent | Req Coverage | Key Dependencies (P0/P1) | Contracts |
|-----------|--------------|--------|---------------|---------------------------|-----------|
| `set_resval()` / `cmd_resupdate` | Test Tooling / C | Accept any registered resource name for injection | 1.1–1.4 | `xcecli_update_resvals` (P0), `xcecli_resval` (P0) | Batch |
| Scheduler resval trace log | Scheduler Core / C | Log the resolved host/resource/value at the point the live daemon applies an update | 4.1–4.3 | `xce_core_push_events()` (P0), `tinylog` (P0) | Event |
| Apiserver resval trace log | API Server / Go | Log the resource/host/value at the point the apiserver stores a received update | 5.1, 5.2, 5.4 | `OnResourceLocationUpdated` (P0), zap `logger` (P0) | Event |
| `xce-deploy-ha` apiserver step | Deployment Tooling / Bash | Extend `deploy-ha.sh` to conditionally stand up a minimal, boot-only single-node apiserver for local verification | 5.3 | `make build-server` (P0), a running controller (P0), `deploy-ha.sh`'s existing Step 3 (P0) | Batch |
| `xce-test-resources` skill | Test Tooling / Bash | Orchestrate deploy, inject, three-layer verify (log-tail/grep for scheduler+apiserver), report, clean up | 3.1–3.2, 4.1–4.3, 5.1, 5.2, 5.4, 6.1–6.5 | `xce-deploy-ha` (P0, extended), `xce_test` (P0), scheduler/apiserver trace logs (P0) | Batch |
### Test Tooling / C
#### `set_resval()` / `cmd_resupdate`
| Field | Detail |
|-------|--------|
| Intent | Resolve a `-n <name> -v <value>` pair into a wire `xce_resval_t`, for any resource the cluster has registered |
| Requirements | 1.1, 1.2, 1.3, 1.4 |
**Responsibilities & Constraints**
- Resolve a resource name to a `Value_u_which` tag (`resval_typeresolve.c`): known non-numeric names (`type`, `model`) resolve to `Value_u_svalue`; every other name — including ones not seen before — defaults to `Value_u_fvalue`, overridable via a new optional `-t <numeric|string>` flag.
- Preserve existing behavior byte-for-byte for every currently-supported resource name (Requirement 1.4) — this is a strictly additive change to the fallback path, not a rewrite of the existing table lookup.
- Report per-name accept/reject outcome when a single invocation carries multiple `-n`/`-v` pairs (Requirement 1.3). **Confirmed via direct source inspection**: the controller's synchronous RPC reply cannot carry this signal — `update_resval_list()` (`src/controller/worker.c:185-198`) unconditionally `return 0` regardless of any per-item failure, so `do_update_resvals()`'s `reply.status` is always success even when every submitted name is unregistered. `cmd_resupdate()` therefore verifies acceptance itself: after the `xcecli_update_resvals()` call, it issues one follow-up `xcecli_resval()` query per submitted `(resource, worker)` pair — the same client-library call `cmd_resval`/`resval` already uses — and compares the returned value against what was just sent. A match is "accepted"; a not-found result or a mismatched value is "rejected", satisfying Requirement 1.2's clear per-name error without any controller-side change.
**Dependencies**
- Outbound: `xcecli_update_resvals` (`src/lib/client.c`) — unchanged, existing wire call (P0). `xcecli_resval` (`src/lib/client.c`) — unchanged, existing wire call, reused here as the accept/reject verification signal (P0).
**Contracts**: Service [ ] / API [ ] / Event [ ] / Batch [x] / State [ ]
##### Batch / Job Contract
- Trigger: operator (or the `xce-test-resources` skill) runs `resupdate -n <name> -v <value> [-t <numeric|string>] [-n <name2> -v <value2> ...]` in the `xce_test` worker context.
- Input / validation: resource name (string, required per pair), value (string, parsed per resolved type), optional type override.
- Output / destination: one `xce_update_resvals` RPC to the controller carrying every resolved pair; then one `xcecli_resval` query per pair to confirm it landed; per-pair accept/reject reported to stdout based on that follow-up query, not the update RPC's own reply (which cannot distinguish per-item outcomes — see Responsibilities above).
- Idempotency & recovery: re-running with the same name/value is safe (matches existing `resupdate` semantics — a plain overwrite, no accumulation).
**Implementation Notes**
- Integration: the type-resolution table lookup moves into `resval_typeresolve.c` unchanged in behavior; `cmd_resupdate()` gains the new post-update verification loop described above.
- Validation: the follow-up `xcecli_resval()` query adds one RPC round-trip per submitted resource name — acceptable for a test/verification tool, not a production hot path.
- Risks: a resource that is genuinely registered but whose value legitimately hasn't propagated yet (a race, not a rejection) could be misreported as "rejected" if queried immediately; mitigate with a short bounded retry on the follow-up query before concluding rejection (this codebase's existing resval-propagation timing, confirmed live during the bug-1569 investigation this spec originated from, is sub-second for a controller-local query).
### Scheduler Core / C
#### Scheduler resval trace log
| Field | Detail |
|-------|--------|
| Intent | Make the live scheduler daemon's application of a resource update observable via its own log, without a query RPC or replay |
| Requirements | 4.1, 4.2, 4.3 |
**Responsibilities & Constraints**
- Emit one trace/debug-level log line inside `xce_core_push_events()`'s `XCE_CORE_EVENT_RESVALS_DELTA` case (`src/scheduler/src/core_events.c`), immediately after the resolved worker's live resource value is written — including the resolved host name, resource name, and applied value, in a fixed, greppable format.
- Debug/trace level only (`TRACE`, which expands to `tlog_debug3` — `src/include/utils.h:93`) — never WARN/ERROR, never on any decision path, so normal operation is unaffected; this is purely additive observability.
- Fires on every real resource update in production too (not just this feature's synthetic injections) — acceptable, since it is the same log-volume category as this component's other existing `TRACE` calls (e.g. `apiserver_worker_change`'s `TRACE` in `src/controller/apiserver.c:446`).
- **Load-bearing operational requirement**: `tlog_debug3`/`TRACE` output is gated by the scheduler's own runtime log level (`src/scheduler/src/main.c`'s `-v` flag, cumulative — confirmed by reading `main.c`: `arguments->verbose++` per `-v`, then `verbose_to_loglevel(arguments.verbose)`). At the daemon's default (unset `-v`) log level, this line is silently never written, and the verification skill's log-grep would time out on every run — a false FAIL, not a bug in the feature under test. The skill's scheduler-deploy step MUST start the scheduler daemon with `-vvv` (verbosity 3, matching `tlog_debug3`) — the same convention `xce-deploy-ha`'s generated `start.sh` already uses for the controller (`start.sh ctrl-1 -vvv`) — see task 5.1.
**Dependencies**
- Inbound: fires as part of the existing `XCE_CORE_EVENT_RESVALS_DELTA` handling path (P0) — no new caller, no new event type.
- Outbound: `tinylog` (P0) — existing logging infrastructure, unchanged.
**Contracts**: Service [ ] / API [ ] / Event [x] / Batch [ ] / State [ ]
##### Event Contract
- Trigger: the live scheduler daemon processes a `RESVALS_DELTA` event carrying a `resupdate`-injected (or real) value.
- Output / destination: one line to the scheduler daemon's own log file.
- Idempotency & recovery: log-only; no state changes, safe to fire on every event including retries/duplicates.
### API Server / Go
#### Apiserver resval trace log
| Field | Detail |
|-------|--------|
| Intent | Make the apiserver's storage of a received resource update observable via its own log, without a REST/CLI query |
| Requirements | 5.1, 5.2, 5.4 |
**Responsibilities & Constraints**
- Add one `logger.Debug(...)` call (zap, per `error-handling.md`'s existing convention) inside `OnResourceLocationUpdated` (`src/frontend/apiserver/backend/handler.go:92-95`), immediately after `store.ResourceLocations.Put(key, *res)`, logging the resource name, location/host, and stored value.
- Debug level only — never `Error`/`Warn`, no behavior change to the store write itself.
- **Load-bearing operational requirement**: the apiserver's log level is config-driven (`config/logger.yaml`'s `logging.level`, loaded by `loadLoggerConfig()` in `src/frontend/apiserver/utils/logger.go`). Zap's default level is not Debug, so unless this config explicitly sets `level: debug`, `logger.Debug(...)` output is silently suppressed and the skill's log-grep would time out on every run — a false FAIL, not a bug in the feature under test. `xce-deploy-ha`'s apiserver step (task 3.1) MUST generate a `logger.yaml` with `logging.level: debug`.
**Dependencies**
- Inbound: fires as part of the existing `OnResourceLocationUpdated` notification-handling path (P0) — no new caller.
- Outbound: zap `logger` (P0) — existing logging infrastructure, unchanged.
**Contracts**: Service [ ] / API [ ] / Event [x] / Batch [ ] / State [ ]
##### Event Contract
- Trigger: the live apiserver process receives and stores a resource-location update pushed from the controller.
- Output / destination: one line to the apiserver's own log file.
- Idempotency & recovery: log-only; no state changes.
### Deployment Tooling / Bash
#### `xce-deploy-ha` apiserver step + `start-all.sh`
| Field | Detail |
|-------|--------|
| Intent | Extend `deploy-ha.sh` to (a) conditionally stand up a minimal, boot-only single-node apiserver against an already-deployed controller, and (b) generate a single `start-all.sh` that sequences controller → (worker/scheduler/apiserver, whichever were generated) so callers need no manual multi-script sequencing |
| Requirements | 5.3, 6.1 |
**Responsibilities & Constraints**
- Apiserver step: generate the minimal config needed for the process to **boot and connect to the controller**, plus a `logger.yaml` with `logging.level: debug` (required for task 4.2's trace line to actually be emitted — see that component's Load-bearing operational requirement), modeled on `make test-integration`'s existing `XCE_BIN`/`SRV_URL` convention. Because verification is trace-log-based (Requirement 5, see research.md Design Decisions), this config no longer needs to produce *functional* auth/RBAC — no REST query is ever issued against this instance — only enough to start the process and complete the apiserver↔controller connection that `apiserver_resource_hook_fun`/`OnResourceLocationUpdated` depends on.
- Generated **only if** a pre-built apiserver binary is already present at its conventional output path (e.g. `/tmp/apiserver`, `make build-server`'s default). **Not an exact mirror of the worker/scheduler conditional**: those binaries live in the same meson build directory as the controller and are simple presence-checked; the apiserver is a separate Go build (`make build-server`, `src/frontend/Makefile`) with its own output path, so `deploy-ha.sh` does **not** invoke `make build-server` itself — building the apiserver remains the caller's prerequisite, exactly like the existing "run `/xce-build controller` first if stale" convention in SKILL.md's Prerequisites section, just naming a different build command. This preserves the same "caller opts in by having already built the optional component" semantics the worker/scheduler conditional relies on for backward compatibility — a caller who never runs `make build-server` sees no apiserver step generated, same as one who never builds worker/scheduler today.
- `start-all.sh`: always generated (independent of which optional components exist for a given deploy). Runs `start.sh` for the controller node(s), waits on Step 5's existing health-check, then runs whichever of `start-worker.sh`/`start-scheduler.sh`/`start-apiserver.sh` were actually generated — skipping any that weren't — with the scheduler started at `-vvv` (verbosity 3) specifically so task 4.1's `TRACE`/`tlog_debug3` line is actually emitted (see that component's Load-bearing operational requirement).
- Does not attempt HA/multi-instance apiserver deployment — single instance only, sufficient for this feature's verification needs.
**Dependencies**
- Inbound: `xce-test-resources` skill (P0) — the primary consumer motivating this extension; also available to any other `xce-deploy-ha` caller by construction (same reusability posture as the existing worker/scheduler steps).
- Outbound: `make build-server` (P0), a running controller (P0), `deploy-ha.sh`'s existing Step 3/Step 5 (P0).
**Contracts**: Service [ ] / API [ ] / Event [ ] / Batch [x] / State [ ]
##### Batch / Job Contract
- Trigger: `deploy-ha.sh` invoked normally (apiserver/`start-all.sh` generation happens automatically as part of Step 3, not a separate opt-in flag the caller must pass); `start-all.sh` then invoked directly by the `xce-test-resources` skill, or by an operator.
- Input / validation: target controller host:port, a deploy/scratch directory (same as existing `xce-deploy-ha` inputs — no new required parameters).
- Output / destination: a running apiserver process connected to the controller, logs under the scratch directory (same convention as `xce-deploy-ha`'s `logs/`) — this is the log task 6.4 greps for Requirement 5's trace line.
- Idempotency & recovery: each invocation deploys fresh into a caller-specified scratch directory, matching `xce-deploy-ha`'s existing "every invocation is a fresh, self-contained deploy" convention — no in-place upgrade semantics.
**Implementation Notes**
- Integration: exact minimal-boot config shape is confirmed during implementation, not fully pinned here — see research.md Risks; scope is narrower than originally planned since functional auth/RBAC is no longer required.
- Validation: `start-all.sh`'s own controller health-check gate (reusing Step 5) must complete before it proceeds to start worker/scheduler/apiserver — the skill must additionally confirm the apiserver process has registered with the controller (connection-level, not HTTP-listener) before injecting and grepping for Requirement 5's trace line.
- Risks: apiserver startup/config requirements were previously this design's least-verified part; narrowed by dropping the REST/auth requirement, but boot-time config shape is still unconfirmed until implementation.
### Test Tooling / Bash
#### `xce-test-resources` skill
| Field | Detail |
|-------|--------|
| Intent | Deploy controller+scheduler+apiserver, inject one or more resource values via `resupdate`, verify all three layers, report PASS/FAIL, clean up |
| Requirements | 3.1, 3.2, 4.1, 4.2, 4.3, 5.1, 5.2, 5.4, 6.1, 6.2, 6.3, 6.4, 6.5 |
**Responsibilities & Constraints**
- Deploy controller, scheduler, and apiserver into a fresh scratch directory by invoking `xce-deploy-ha`'s `deploy-ha.sh` (Step 3) then its generated `start-all.sh` — no manual per-component sequencing lives in this skill; `start-all.sh` already handles controller→health-check→scheduler/apiserver ordering (see the `xce-deploy-ha` apiserver step + `start-all.sh` component above).
- Join a synthetic worker and inject one or more resource values via `resupdate` (default set: the 7 names Requirement 1 adds, plus a couple of already-supported names for regression coverage).
- Query the controller (`resval`), and tail/grep the scheduler daemon's and apiserver's own log files (bounded wait each) for the trace lines their respective resval-update handlers now emit; compare all three against the injected value and attribute any mismatch or timeout to a specific layer (Requirement 4.3's distinct-reporting requirement generalizes to all three layers).
- Emit a single machine-parseable VERDICT line, following `xce-test-ha`'s existing convention (e.g. `[xce-test-resources] VERDICT resources=7 layers_checked=3 mismatches=0 result=PASS`), plus the usual terminal PASS/FAIL oracle line.
- Clean up all deployed **processes** and runtime/data state (PIDs, sockets, controller/scheduler data directories) unconditionally — on PASS, on FAIL, and on a mid-deployment abort alike — unless the operator passes a "keep running" flag (Requirement 6.5; see Error Handling's Deployment failure entry for why this must cover the abort path too, not just a completed run). **Log files under the scratch directory are always preserved**, independent of the "keep running" flag — this is a deliberate divergence from `xce-deploy-ha`'s own `cleanup.sh` (which wipes `logs/*.log`), because the Monitoring section below promises logs survive "for post-run inspection," and this feature's own trace-log verdict evidence (the exact scheduler/apiserver lines a FAIL was based on) would otherwise be unrecoverable after a completed run.
**Dependencies**
- Inbound: none (top-level entry point, invoked directly by an operator).
- Outbound: `xce-deploy-ha` (P0, extended with the apiserver step + `start-all.sh`), `xce_test` worker/client contexts (P0), the scheduler daemon's and apiserver's own log files (P0, via the two new trace-log additions).
**Contracts**: Service [ ] / API [ ] / Event [ ] / Batch [x] / State [ ]
##### Batch / Job Contract
- Trigger: `/xce-test-resources [resource=value ...]` (defaults to Requirement 1's gap-resource set if none given).
- Input / validation: optional list of `resource=value` pairs; optional scratch directory; optional "keep running" flag.
- Output / destination: per-layer pass/fail detail (injected vs. observed value, per resource, per layer) plus one VERDICT line and the standard terminal PASS/FAIL exit code, matching `xce-test-ha`'s reporting contract closely enough to be immediately familiar to this codebase's engineers.
- Idempotency & recovery: safe to re-run — fresh scratch directory per run (`xce-deploy-ha` convention), no shared mutable state across runs.
**Implementation Notes**
- Integration: invokes `deploy-ha.sh` then its generated `start-all.sh`, which already gates scheduler/apiserver startup on the controller health-check internally — this skill does not re-implement that sequencing.
- Integration: registers cleanup via a `trap` (or equivalent) as soon as `start-all.sh` is invoked, not just at the end of a successful run — deliberately stronger than `xce-deploy-ha`'s own convention (a separate, manually-invoked `cleanup.sh`), because Requirement 6.5 requires cleanup-by-default on every path including a mid-deployment abort (see Error Handling).
- Validation: the scheduler and apiserver checks are both a bounded-wait poll of a live log file (no replay, no step/continue) — poll with a short interval up to a generous timeout, consistent with `xce-test-ha`'s existing soak-test conventions; a match found before timeout short-circuits the wait.
- Risks: timing-sensitive (bounded waits for controller→scheduler and controller→apiserver propagation); use generous, explicit timeouts, not tight polling that could flake.
## Error Handling
### Error Strategy
This feature is a verification tool, not a production service — "errors" are primarily reportable verification failures (a layer did not observe the injected value) rather than exceptions to recover from mid-run.
### Error Categories and Responses
- **Unrecognized resource name** (Requirement 1.2): `resupdate` reports a clear per-name error, sourced from its own follow-up `xcecli_resval()` verification query (see Components — the controller's update RPC reply cannot carry this signal); the skill treats this as a run-level failure with the specific rejected name(s) named in its report.
- **Layer mismatch** (Requirements 3.2, 4.2, 5.2): the skill reports which layer(s) failed, with both the injected and observed value, per Requirement 6.3.
- **Layer unreachable** (e.g. apiserver never came up, or the expected trace line never appears in the scheduler/apiserver log before the bounded wait elapses): treated the same as a mismatch for verdict purposes (FAIL, layer identified, "timeout" distinguished from "mismatch" in the report), not a silent skip.
- **Deployment failure** (controller/scheduler/apiserver fails to start): the skill aborts before attempting injection, with a clear "deployment failed at step X" message. **Cleanup still runs on this path** — this is a deliberate strengthening of `xce-deploy-ha`'s precedent, not a mirror of it: `xce-deploy-ha`'s `deploy-ha.sh` has no trap-based cleanup and leaves already-started processes running on a mid-deploy failure, requiring the caller to run its separate `cleanup.sh` manually (confirmed against `src/tools/deploy-ha.sh`). Requirement 6.5 promises cleanup by default regardless of outcome, unless the operator explicitly requested the deployment be left running — a deployment failure is not that request, so the skill installs a trap (or equivalent) covering every deployment step from the first process start onward, not just the post-injection verification path.
- **Custom host-attrs resource name false-MISMATCH (known residual risk, not fully closed)**: the default resource set avoids every host-attrs-derived name (`type`/`model`/`cpuf`/`maxcpus`/`maxmem`/`maxswap`/`maxtmp`/`ndisks` — `worker.c`'s `N_HOST_ATTRS`) specifically to route around this; the System Flows section above documents the nested-`!`-escape fix and why it closes the controller-layer check for that default set. If an operator passes a CUSTOM `resource=value` argument naming a host-attrs resource, the controller-layer check will still report a false MISMATCH — not because propagation is wrong, but because that resource is reset to baseline the instant the worker session ends, and no code path today re-checks it before that reset for an arbitrary caller-supplied name. This is a known, documented gap (tasks.md task 6.2), not silently swallowed: a genuine fix needs either a C-level `xce_test` change (no REPL path from `worker` context to `client` context without an equivalent disconnect) or a bash coprocess restructuring to keep the worker connected across every layer's check — out of scope for the current implementation. The skill should warn (not silently mismatch) when a caller-supplied resource name matches a known host-attrs name.
### Monitoring
Not applicable — this is an on-demand, operator-invoked CLI tool, not a running service. Logs are written under the run's scratch directory, following `xce-deploy-ha`/`xce-test-ha`'s existing `logs/` convention, for post-run inspection.
## Testing Strategy
- **Unit Tests**: `resval_typeresolve.c`'s type-resolution fallback — 4 cases: a currently-known numeric name (e.g. `mem`) unchanged; a currently-known string name (`type`) unchanged; an unrecognized name defaults to numeric; an unrecognized name with `-t string` override resolves to string — directly compilable and testable in isolation since the extracted module has no networking, REPL, or `test.c`-global dependency (unlike `test.c` itself, which cannot be white-box included — see File Structure Plan). The skill's verdict-aggregation/comparison step (Requirement 6) is implemented as a self-contained function over (resource, injected value, per-layer observed value) records specifically so its negative path — a genuine per-layer mismatch correctly producing an overall FAIL naming the right layer — can be verified the same way, with synthetic records, without requiring a live deployment.
- **Integration Tests**: the skill's own live run against a real controller+scheduler+apiserver deployment *is* this feature's primary integration/e2e coverage, following this codebase's established precedent for `xce-test-ha`-class tooling (soak/verification tools in this project are validated live, not via `meson test()`). At minimum, one live run per implementation change verifying: (1) a previously-missing resource name (e.g. `r15m`) is now accepted and correctly injected, (2) all three layers' trace lines report the correct value within the bounded wait, (3) a genuinely-unregistered resource name included in the same batch is reported as a specific, clearly-identified rejection (not silently dropped) and correctly drives the run's overall verdict to FAIL, per this design's Error Handling.
- **E2E Path**: the full flow in System Flows' sequence diagram, run end-to-end at least once with the default resource set before this feature is considered done.