feat(resource-value-verification): verdict aggregation, reporting, cleanup (task 7.1)

aggregate_verdict()/build_verdict_record() combine the three per-layer
check results into a single per-resource comparison, self-contained and
invokable with synthetic records (task 8.2). Uses ASCII Unit Separator
(\x1f) as the record delimiter after discovering bash's IFS tab-splitting
silently collapses adjacent empty fields even with IFS set to only a tab
(bug-1581), which corrupted UNREACHABLE-record field alignment.

Emits one machine-parseable VERDICT line (resources/layers_checked/
mismatches/unreachable/rejected/result) plus the terminal PASS/FAIL oracle
line and matching exit code. Cleanup is the same trap-based mechanism task
5.1 registered for the abort path - logs always preserved regardless of
--keep-running.

Round-2 fix: main()'s live-data wiring (build_verdict_records_from_tsv())
derived a layer's status from array-key existence rather than the actual
recorded outcome - since tasks 6.3/6.4 always set the key (even on a
genuine timeout, just with an empty value), every real UNREACHABLE was
silently misclassified as MISMATCH. Fixed to check SCHEDULER_CHECK_RESULTS/
APISERVER_CHECK_RESULTS string content instead (bug-1582). Live-confirmed
by killing the scheduler mid-run: VERDICT now correctly reports
unreachable=9 mismatches=0, not the reverse.

SKILL.md rewritten to describe the now-complete, functional tool.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-08-18 13:18:01 +00:00
parent 0253b4ca09
commit 1364664769
Signed by: rshen
SSH key fingerprint: SHA256:8Vf2pZ3h6rJa7EhGK+WaAQ9Mta/apZvteW6g9U/WKKk
3 changed files with 579 additions and 173 deletions

View file

@ -6,80 +6,70 @@ model: haiku
# XCE Resource-Value Verification
## Status: deployment step functional; injection/verification not yet built
## Status: complete and functional (tasks 1.1-7.1 all done)
`run-resource-verify.sh` now exists (task 5.1) and implements the
**deployment step only**: it invokes `xce-deploy-ha`'s `deploy-ha.sh` into a
fresh scratch directory, then its generated `start-all.sh`, confirms
controller + scheduler (`-vvv`) + apiserver (debug logging) are all
genuinely up, and cleans up (trap-based, covers a mid-deployment abort, not
just a completed run — see Error handling below). Invoking
`/xce-test-resources` today runs a real deploy-confirm-cleanup cycle but
does **not yet** inject a resource value or check any layer — it exits 0 on
a successful deployment/cleanup and non-zero on a deployment failure, not on
an injection PASS/FAIL verdict. Report this gap rather than fabricate a
verdict if asked to check actual resource-value propagation.
The rest of the orchestration logic is built incrementally by later tasks in
the same spec:
- **Task 2.1/2.2** — `resupdate` accepts any registered resource name and
reports per-name accept/reject (closes the gap this whole feature exists to
close; `resval_typeresolve.c`'s fallback path). Done.
- **Task 3.1** — `xce-deploy-ha`'s `deploy-ha.sh` grows a conditional apiserver
step + a combined `start-all.sh` (controller → scheduler → apiserver). Done.
- **Task 4.1/4.2** — one trace-log line each in the scheduler
(`core_events.c`, `XCE_CORE_EVENT_RESVALS_DELTA`) and the apiserver
(`handler.go`'s `OnResourceLocationUpdated`) — what this skill will
tail/grep for. Done.
- **Task 5.1** — wires `start-all.sh` + trap-based cleanup into this skill.
Done (this is the deployment step described above).
- **Task 6.1–6.4** — worker join + injection, and the controller/scheduler/
apiserver checks themselves. Not yet built.
- **Task 7.1** — verdict aggregation, the VERDICT line, and cleanup-on-every-path.
Not yet built.
Once tasks 6.1–7.1 land, this section (and the rest of this document's
hedging about the VERDICT line/PASS-FAIL oracle being aspirational) should
be removed.
## What this skill will do (target behavior)
`run-resource-verify.sh` implements the full flow described below: deploy
controller + scheduler + apiserver, join a synthetic worker, inject one or
more resource values, verify all three layers, emit a VERDICT line plus the
standard terminal PASS/FAIL oracle line, and clean up. Invoking
`/xce-test-resources` runs the whole thing end-to-end and its exit code
reflects the real overall verdict (0 = PASS, non-zero = FAIL) -- not just
whether the deployment stood up.
Bug-1569 required deploying real worker/controller/scheduler/apiserver
daemons and waiting on periodic reporting cycles just to confirm a
resource-value routing fix. This skill turns that multi-minute manual
investigation into one command:
investigation into one command.
## What this skill does
1. **Deploy** controller + scheduler + apiserver into a fresh scratch
directory by invoking `xce-deploy-ha`'s `deploy-ha.sh` (Step 3) and then
its generated `start-all.sh` — no manual per-component sequencing lives in
this skill; `start-all.sh` already gates scheduler/apiserver startup on
`xce-deploy-ha`'s existing controller health-check.
2. **Inject** — join a synthetic worker through `xce_test`'s worker context
its generated `start-all.sh` -- no manual per-component sequencing lives
in this skill; `start-all.sh` already gates scheduler/apiserver startup
on `xce-deploy-ha`'s existing controller health-check. A `trap`-based
cleanup is registered before the first process starts, so it fires on
every exit path -- a completed PASS/FAIL run, a mid-deployment abort, or
an external SIGINT/SIGTERM -- not just a successful completion.
2. **Inject** -- join a synthetic worker through `xce_test`'s worker context
(no real worker daemon process) and run `resupdate -n <name> -v <value>`
for one or more resource names.
3. **Verify all three layers**:
- **Controller** — query via `xce_test`'s client-context `resval` and
compare against the injected value.
- **Scheduler** — tail/grep the live scheduler daemon's own log file for
the trace line task 4.1 adds, with a short poll interval up to a
generous bounded wait (no record/replay/REPL).
- **Apiserver** — tail/grep the apiserver's own log file for the trace
line task 4.2 adds, same bounded-wait posture.
Each layer's observed value is reported distinctly, so a mismatch is
attributable to a specific layer, not just "something failed."
4. **Report** a single VERDICT line plus the standard terminal PASS/FAIL
oracle line and exit code.
for one or more resource names in a single RPC round trip. Every
submitted name gets a definitive "accepted" or "rejected: \<reason\>"
outcome.
3. **Verify all three layers**, for every accepted name:
- **Controller** -- query via `xce_test`'s client-context `resval` and
compare against the injected value (run nested inside the still-open
injecting worker session, so host-attrs resources like `cpuf` are
never reset by a premature disconnect -- see bug-1579).
- **Scheduler** -- tail/grep the live scheduler daemon's own log file
(`resval applied host=<H> resource=<R> value=<V>`, `-vvv`) with a short
poll interval up to a 15s bounded wait (no record/replay/REPL).
- **Apiserver** -- tail/grep the apiserver's own log file (zap
`"resource-location stored"` with a trailing JSON field block), same
bounded-wait posture.
Each layer's observed value is reported distinctly (MATCH / MISMATCH /
UNREACHABLE), so a failure is attributable to a specific layer, not just
"something failed" -- UNREACHABLE (bounded wait timed out, no trace line
ever appeared) is always reported distinctly from MISMATCH (a trace line
appeared with the wrong value).
4. **Aggregate and report** -- combine the injection accept/reject outcome
and all three layers' results into one machine-parseable VERDICT line
plus the standard terminal PASS/FAIL oracle line (see below). A rejected
resource name is a run-level failure in its own right, independent of
whatever the (nonexistent, since a rejected name is never actually
applied anywhere) per-layer results would say.
5. **Clean up** every process and runtime/data-directory artifact this run
created — unconditionally, on PASS, FAIL, or a mid-deployment abort alike
— unless the operator explicitly asked for the deployment to be left
running. **Log files under the scratch directory are always preserved**,
independent of that flag, so a FAIL verdict's underlying trace-log
evidence stays inspectable after the run (a deliberate divergence from
`xce-deploy-ha`'s own `cleanup.sh`, which wipes `logs/*.log`).
created -- unconditionally, on PASS, FAIL, or a mid-deployment abort
alike -- unless the operator explicitly asked for the deployment to be
left running (`--keep-running`). **Log files under the scratch directory
are always preserved**, independent of that flag, so a FAIL verdict's
underlying trace-log evidence stays inspectable after the run (a
deliberate divergence from `xce-deploy-ha`'s own `cleanup.sh`, which
wipes `logs/*.log`).
**Idempotency & recovery**: every invocation is safe to re-run — a fresh
**Idempotency & recovery**: every invocation is safe to re-run -- a fresh
scratch directory per run, following `xce-deploy-ha`'s existing "every
invocation is a fresh, self-contained deploy" convention — with no shared
invocation is a fresh, self-contained deploy" convention -- with no shared
mutable state carried across runs.
## Usage
@ -91,124 +81,176 @@ run-resource-verify.sh [--keep-running] [--dir DIR] [resource=value ...]
(the `/xce-test-resources [resource=value ...]` form above dispatches to this
script.)
- No arguments: defaults to Requirement 1's gap-resource set — the 7 names
`resupdate` is currently missing (`r15m`, `pg`, `ls`, `it`, `swp`, `io`,
`tmp`) — plus a couple of already-supported names for regression coverage
(e.g. `mem` for the numeric path, `type` for the string path), so a default
run also proves the fix didn't disturb existing behavior.
- No arguments: defaults to `DEFAULT_RESOURCE_ARGS` -- Requirement 1's
gap-resource set (`r15m`, `pg`, `ls`, `it`, `swp`, `io`, `tmp`) plus two
already-supported NUMERIC names for regression coverage (`mem`, `cpuf`),
so a default run also proves the `resupdate` fix didn't disturb existing
behavior. Deliberately excludes `type`/`model` (string-typed) from the
default set -- see bug-1574 below.
- One or more `resource=value` pairs: checks exactly those, e.g.
`/xce-test-resources r15m=1.5 pg=3`. **Not yet implemented** — task 5.1
parses and accepts these but does not act on them yet; injection lands in
task 6.1.
- `--dir DIR` — scratch/deploy directory. Default: a fresh directory
`/xce-test-resources r15m=1.5 pg=3`. A name the cluster doesn't recognize
is reported as a rejection and drives the overall verdict to FAIL,
identifying the specific name -- it is never silently dropped or ignored.
- `--dir DIR` -- scratch/deploy directory. Default: a fresh directory
generated via `mktemp -d` (xce-deploy-ha's "every invocation is a fresh,
self-contained deploy" convention), or `$XCE_RESVERIFY_TOP` when set and
`--dir` is not given (same precedence pattern as `xce-test-ha`'s
`$XCE_HA_TOP`).
- `--keep-running` — skip PROCESS teardown in cleanup, leaving the
- `--keep-running` -- skip PROCESS teardown in cleanup, leaving the
deployment running under `DIR` for further inspection. Log files under
`DIR` are ALWAYS preserved regardless of this flag (see Error handling
below); it only controls whether controller/scheduler/apiserver processes
are killed.
`DIR` are ALWAYS preserved regardless of this flag; it only controls
whether controller/scheduler/apiserver processes are killed. The verdict
is still computed and reported normally either way -- `--keep-running`
does not change PASS/FAIL or the exit code, only what happens to the
processes afterward. Tear the deployment down manually afterward (its PIDs
are visible via `ps`/`pgrep` scoped to `DIR`).
- Other overridable env vars (all optional, default to this repo's own
meson build dir / `/tmp/apiserver`): `$XCE_RESVERIFY_BUILD_DIR`,
`$XCE_RESVERIFY_BIN` (controller), `$XCE_RESVERIFY_SCHED_BIN`,
`$XCE_RESVERIFY_APISERVER_BIN`, `$XCE_RESVERIFY_PEERS` (defaults to a
single-node `localhost:9991` — this skill verifies value propagation, not
raft/HA failover, so a single node is deliberate and avoids
election-timing flakiness; xce-test-ha remains the tool for HA testing).
`$XCE_RESVERIFY_APISERVER_BIN`, `$XCE_RESVERIFY_XT` (the `xce_test`
binary), `$XCE_RESVERIFY_PEERS` (defaults to a single-node
`localhost:9991` -- this skill verifies value propagation, not raft/HA
failover, so a single node is deliberate and avoids election-timing
flakiness; xce-test-ha remains the tool for HA testing).
Must be invoked under an environment with `nix` on `PATH` -- the script
shells out to `nix develop` itself for the steps that need it, so it does
not need to be pre-wrapped by the caller.
## VERDICT line and PASS/FAIL oracle
Following `xce-test-ha`'s machine-parseable VERDICT convention:
Following `xce-test-ha`'s machine-parseable VERDICT convention, emitted by
`aggregate_verdict()`:
```
[xce-test-resources] VERDICT resources=7 layers_checked=3 mismatches=0 result=PASS
[xce-test-resources] VERDICT resources=9 layers_checked=3 mismatches=0 unreachable=0 rejected=0 result=PASS
```
- `resources` — count of resource names checked this run (7 for the
canonical Requirement-1-gap-only run shown above; a default no-args run
that also includes the regression names, or an explicit `resource=value`
list, reports whatever count was actually checked).
- `layers_checked` — always 3 (controller, scheduler, apiserver) for a full
run.
- `mismatches` — count of (resource, layer) pairs where the observed value
did not match the injected value, or the layer was unreachable
(trace-log grep timed out). `result=PASS` iff `mismatches == 0`.
- `result` — `PASS` or `FAIL`.
- `resources` -- total count of resource names checked this run (accepted
and rejected alike).
- `layers_checked` -- always 3 (controller, scheduler, apiserver).
- `mismatches` -- count of (resource, layer) pairs where a trace
line/query DID return a value but it did not match the injected value
(within numeric tolerance; exact string match for non-numeric values).
- `unreachable` -- count of (resource, layer) pairs where the bounded wait
timed out with NO trace line ever observed -- distinct from `mismatches`
per Requirement 4.3/5.4 (the two point at different bugs: a
value-propagation defect vs. a layer that never came up or never logged).
- `rejected` -- count of resource names the injection step itself rejected
(Requirement 1.2) -- never checked at any layer, since nothing was ever
applied anywhere for a rejected name.
- `result` -- `PASS` iff `mismatches == 0 AND unreachable == 0 AND
rejected == 0`; `FAIL` otherwise.
Per-resource, per-layer detail (injected vs. observed value) is reported
above the VERDICT line for anything that mismatched, distinguishing a genuine
**mismatch** (wrong value observed) from **unreachable** (bounded wait timed
out with no matching trace line) — the same distinction Requirement 4.3/5.4
and design.md's Error Handling require, since the two point at different bugs
(a value-propagation defect vs. a layer that never came up or never logged).
Alongside the VERDICT line, the skill emits the usual terminal oracle line so
a goal-loop or CI-style caller can branch on it without parsing the VERDICT
line's fields:
Immediately above the VERDICT line, one `FAILURE:` line is printed per
failing (resource, layer) pair or rejected name, naming the specific
resource and layer (or `layer=injection` for a rejection) plus the injected
and observed values -- e.g.:
```
[xce-test-resources] PASS: 7 resources, 3 layers checked, zero mismatches
[xce-test-resources] FAIL: resource=r15m layer=scheduler mismatch (injected=1.5 observed=timeout)
[xce-test-resources] FAILURE: resource=r15m layer=scheduler status=MISMATCH injected=1.5 observed=1.2
[xce-test-resources] FAILURE: resource=not-a-real-resource layer=injection status=REJECTED detail="rejected: unknown resource"
```
PASS ⇒ exit `0`. Any FAIL condition ⇒ non-zero exit.
Then exactly one terminal oracle line:
## Error handling this skill will implement
```
[xce-test-resources] PASS: 9 resource(s), 3 layers checked, zero mismatches, zero unreachable, zero rejected
[xce-test-resources] FAIL: 2 resource(s), 3 layers checked, 0 mismatch(es), 0 unreachable, 1 rejected -- see FAILURE lines above
```
PASS ⇒ exit `0`. Any FAIL condition ⇒ exit `1`. A structural failure before
the aggregation step is reached (deployment failure, worker-join failure, a
malformed `--dir`/argument, etc.) still exits non-zero via this script's
`fail()`/`usage()` (1, or 2 for a usage/argument error) -- unrelated to the
verdict's own `result=` field, since aggregation never ran.
A full run against the default resource set produces **exactly one** VERDICT
line and a matching process exit code; no controller/scheduler/apiserver
processes remain afterward (unless `--keep-running` was passed); the run's
log files are present on disk regardless.
### The comparison/aggregation step is self-contained and independently testable
`aggregate_verdict()` (and its `build_verdict_record()` formatting helper)
takes plain records as function arguments -- no live command, no file I/O,
no global deployment state -- so it can be invoked directly with
hand-written synthetic records, with no live deployment required. This is
what task 8.2's dedicated test harness exercises; source the script
(`source run-resource-verify.sh`) to get `aggregate_verdict()`,
`build_verdict_record()`, and `controller_values_match()` in scope without
triggering `main()` or any live deployment -- the bottom of the file is
guarded (`[[ "${BASH_SOURCE[0]}" == "${0}" ]]`) specifically so sourcing it
has no side effects.
## Error handling this skill implements
Per design.md's Error Handling section:
- **Unrecognized resource name** — `resupdate`'s own follow-up
`xcecli_resval()` verification (task 2.2) reports it as rejected; the skill
treats this as a run-level failure and names the specific rejected
resource(s) in its report.
- **Layer mismatch** — reported with both the injected and observed value,
- **Unrecognized resource name** -- `resupdate`'s own follow-up
`xcecli_resval()` verification (task 2.2) reports it as rejected; the
aggregation step (task 7.1) treats this as a run-level failure and names
the specific rejected resource(s) in its FAILURE lines.
- **Layer mismatch** -- reported with both the injected and observed value,
attributed to the specific layer (controller/scheduler/apiserver).
- **Layer unreachable** (e.g. apiserver never came up, or the expected
trace line never appears before the bounded wait elapses) — treated the
same as a mismatch for verdict purposes (FAIL), but labeled "timeout"
distinctly from "mismatch" in the report, never a silent skip.
- **Deployment failure** (controller/scheduler/apiserver fails to start) —
trace line never appears before the bounded wait elapses) -- treated the
same as a mismatch for verdict purposes (FAIL), but labeled UNREACHABLE
distinctly from MISMATCH in the report, never 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: a trap (or
equivalent) is installed covering every deployment step from the first
process start onward, not just the post-injection verification path —
deliberately stronger than `xce-deploy-ha`'s own precedent (a separate,
manually-invoked `cleanup.sh` that a mid-deploy failure would otherwise
leave for the caller to run by hand).
failed at step X" message. Cleanup still runs on this path: a trap
registered before the first process starts covers every deployment step
from the first process start onward, not just the post-injection
verification path.
## Not using `context: fork` for the eventual orchestration script
## Known pre-existing bugs relevant to this skill (not fixed here)
Neither this skill's frontmatter nor the invocation instructions below will
set `context: fork`, for the same reason `xce-test-ha` and `xce-deploy-ha`
both call out explicitly in their own `SKILL.md`s: this skill's
`run-resource-verify.sh` will invoke `xce-deploy-ha`'s `start-all.sh`
internally, starting real backgrounded controller/scheduler/apiserver
processes for the duration of the run. `context: fork`'s own teardown sends
SIGHUP through the whole process group on return — the same confirmed-broken
mechanism `xce-test-ha`'s soak driver and `xce-deploy-ha` both hit (bug
confirmed 2026-07-08) — that would silently kill those processes mid-run,
before cleanup or the verdict line ever fires. Dispatch it directly via Bash,
or via the `Agent` tool
(empirically verified safe for this exact pattern — see `xce-test-ha`'s and
- **bug-1572** (open): the live scheduler daemon can double-free/crash if
the SAME resource is re-pushed to it a second time while it's running
(e.g. left alive for a while under `--keep-running`, or a controller
periodic resync). A single run's own injection (one `resupdate` per
resource, one verification pass) does not trigger this, but don't rely on
a `--keep-running` deployment staying up indefinitely once left running.
- **bug-1574** (open): `xce_test` can non-deterministically SEGFAULT on a
string-typed `resupdate` (`-n type`/`-n model`) -- uninitialized-memory
UB, not this feature's own bug. This is why the default resource set is
numeric-only; avoid adding `type`/`model` to a custom `resource=value`
argument until this is fixed.
- **bug-1579** (fixed): host-attrs resources (`cpuf`, `maxcpus`, etc.) used
to false-MISMATCH at the controller layer because the synthetic worker
disconnected before the controller-side check ran. Fixed by nesting the
controller check inside the still-open injecting worker session.
## Not using `context: fork` for the orchestration script
Neither this skill's frontmatter nor the invocation instructions above set
`context: fork`, for the same reason `xce-test-ha` and `xce-deploy-ha` both
call out explicitly in their own `SKILL.md`s: `run-resource-verify.sh`
starts real backgrounded controller/scheduler/apiserver processes for the
duration of the run, and `context: fork`'s own teardown sends SIGHUP through
the whole process group on return -- the same confirmed-broken mechanism
`xce-test-ha`'s soak driver and `xce-deploy-ha` both hit -- which would
silently kill those processes mid-run, before cleanup or the verdict line
ever fires. Dispatch it directly via Bash, or via the `Agent` tool
(empirically verified safe for this exact pattern -- see `xce-test-ha`'s and
`xce-deploy-ha`'s own `SKILL.md`s), not via a forked Skill-tool context.
## Dependencies
- `xce_test`'s worker-context `resupdate`/`join` and client-context `resval`
— resource-name table gap closed (task 2.1/2.2, done).
- `xce-deploy-ha`'s `deploy-ha.sh` / `start-all.sh` — apiserver step and
combined `start-all.sh` done (task 3.1).
- Scheduler and apiserver trace-log lines — done (tasks 4.1 and 4.2).
- This skill's own `run-resource-verify.sh` — deployment step done (task
5.1); injection + per-layer checks + verdict aggregation not yet built
(tasks 6.1–7.1).
-- resource-name table gap closed (tasks 2.1/2.2).
- `xce-deploy-ha`'s `deploy-ha.sh` / `start-all.sh` -- apiserver step and
combined `start-all.sh` (task 3.1).
- Scheduler and apiserver trace-log lines (tasks 4.1 and 4.2).
- This skill's own `run-resource-verify.sh` -- deployment (task 5.1),
injection + per-layer checks (tasks 6.1-6.4), and verdict aggregation +
cleanup-on-every-path (task 7.1) -- all done.
## Requirements traceability
This stub documents the contract for design.md's "`xce-test-resources`
skill" component (Components and Interfaces → Test Tooling / Bash), covering
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.
See `.kiro/specs/resource-value-verification/requirements.md` and `design.md`
This document describes design.md's "`xce-test-resources` skill" component
(Components and Interfaces → Test Tooling / Bash), covering 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. See
`.kiro/specs/resource-value-verification/requirements.md` and `design.md`
for the full text.

View file

@ -5,12 +5,14 @@
# resource value via xce_test's resupdate, confirm it is visible at the
# controller, live scheduler, and Go apiserver).
#
# resource-value-verification spec, task 5.1+6.1+6.2+6.3+6.4 SCOPE (+
# bug-1579 follow-up fix): this file currently implements deployment (task
# 5.1), worker join + resource injection (task 6.1), the controller-side
# per-layer verification check (task 6.2), the scheduler-side per-layer
# verification check (task 6.3), AND the apiserver-side per-layer
# verification check (task 6.4) -- stand up a fresh controller + scheduler
# resource-value-verification spec, task 5.1+6.1+6.2+6.3+6.4+7.1 SCOPE (+
# bug-1579 follow-up fix): this file implements the FULL skill -- deployment
# (task 5.1), worker join + resource injection (task 6.1), the
# controller-side per-layer verification check (task 6.2), the
# scheduler-side per-layer verification check (task 6.3), the apiserver-side
# per-layer verification check (task 6.4), AND the final verdict aggregation
# / VERDICT line / PASS-FAIL oracle / cleanup-on-every-path (task 7.1) --
# stand up a fresh controller + scheduler
# (-vvv) + apiserver (debug logging) deployment via xce-deploy-ha's
# deploy-ha.sh + generated start-all.sh, confirm all three components are
# actually up, join a SYNTHETIC worker (no real worker daemon process)
@ -34,12 +36,29 @@
# internal_controller_check() function and the top-of-file bug-1579 note
# below for why this matters and exactly how it works; DEFAULT_RESOURCE_ARGS
# now includes `cpuf` again as proof the fix is real (see that note).
# Verdict aggregation / cleanup-on-every-path (task 7.1) is added by a LATER
# task -- see the "Extension point" marker in main() below for exactly where
# it hooks in.
# See .kiro/specs/resource-value-verification/{requirements,design}.md,
# Requirements 2.1, 2.2, 3.1, 3.2, 4.1, 4.2, 4.3, 5.3, 6.1, 6.4, 6.5 for these
# tasks; the full skill covers 3.1-6.5.
# Requirements 2.1, 2.2, 3.1, 3.2, 4.1, 4.2, 4.3, 5.3, 6.1, 6.2, 6.3, 6.4,
# 6.5 for these tasks; the full skill covers 3.1-6.5 and is now complete.
#
# Task 7.1 (this revision): adds the final aggregation step. Combines the
# controller/scheduler/apiserver per-layer results (tasks 6.2-6.4) plus the
# injection accept/reject outcome (tasks 2.1/2.2/6.1) into a single overall
# verdict, via a SELF-CONTAINED comparison/aggregation function
# (aggregate_verdict(), see its own header comment below) that takes plain
# TSV "records" as arguments -- no live command, no file I/O, no global
# deployment state -- so it is directly callable with hand-written synthetic
# records for testing (this task's own sanity-check invocations, and task
# 8.2's dedicated harness, which depends on this exact function/record
# format) as well as from main()'s live-run path. Emits exactly one
# machine-parseable VERDICT line (xce-test-ha's convention) plus the
# standard terminal PASS/FAIL oracle line; exit code is 0 for an overall
# PASS, 1 for an overall FAIL -- this is now the script's REAL exit code,
# not the deployment-only placeholder described in the "Exit 0/Non-zero"
# note below the Usage block (kept for historical/task-5.1-6.4 context, see
# the correction directly beneath it). The bottom of this file is also now
# guarded so it can be `source`d (e.g. by task 8.2's harness) without
# executing main()/internal_controller_check() as a side effect -- see the
# `[[ "${BASH_SOURCE[0]}" == "${0}" ]]` guard at the very end.
#
# Task 6.1 deliberate deviation from design.md: the default (no-args)
# resource set is Requirement 1's gap-resource set (r15m, pg, ls, it, swp,
@ -126,6 +145,8 @@
# coverage (see the top-of-file bug-1574 note for why
# the default deliberately excludes type/model).
#
# --- SUPERSEDED by task 7.1 (kept for historical context -- this described
# the script's exit code BEFORE the final aggregation step existed) ---
# Exit 0 == this task's scope succeeded (deployment stood up, all three
# components confirmed up, worker joined, resupdate ran for every requested
# resource -- accepted AND rejected outcomes are both a successful run of
@ -148,6 +169,21 @@
# MISMATCH/UNREACHABLE yet (see ANY_CONTROLLER_MISMATCH/
# ANY_SCHEDULER_MISMATCH/ANY_SCHEDULER_UNREACHABLE/ANY_APISERVER_MISMATCH/
# ANY_APISERVER_UNREACHABLE, groundwork for task 7.1's aggregation).
# --- CURRENT (task 7.1, this revision) ---
# Exit 0 == overall verdict PASS: every requested resource name was accepted
# (Requirement 1.2's rejection path never fired) AND every accepted
# resource's controller/scheduler/apiserver observed value matched its
# injected value (see aggregate_verdict() below) -- exactly what the
# script's single VERDICT line's `result=PASS` reports.
# Non-zero (1) == overall verdict FAIL: at least one requested resource name
# was rejected, OR at least one accepted resource's value did not match (or
# was UNREACHABLE/timed out) at some layer -- exactly what `result=FAIL`
# reports, with every specific (resource, layer) failure named in the
# FAILURE lines aggregate_verdict() logs immediately above the VERDICT line.
# A structural failure before aggregation is reached (deployment failure,
# worker-join failure, a resupdate with no observable accept/reject outcome,
# a malformed `--dir`/argument, etc.) still exits non-zero via fail()/usage()
# (1, or 2 for a usage/argument error) -- unchanged from before this task.
#
set -u
@ -1133,6 +1169,318 @@ query_apiserver_resvals() {
done
}
# ── Step 8: verdict aggregation, VERDICT line, PASS/FAIL oracle (task 7.1) ──
# Requirement 6.2/6.3/6.4: combine the controller/scheduler/apiserver
# per-layer results (tasks 6.2-6.4) plus the injection accept/reject outcome
# (tasks 2.1/2.2/6.1) into a single overall verdict. Deliberately
# implemented as a SELF-CONTAINED comparison/aggregation function --
# aggregate_verdict() below takes an array of plain TSV "records" (one per
# checked resource, see the format comment on aggregate_verdict() itself)
# and does its own MATCH/MISMATCH/UNREACHABLE classification via
# controller_values_match() (task 6.2's own pure comparison helper, reused
# here rather than re-parsing/re-trusting each layer's already-rendered
# CHECK_RESULTS string) -- it runs no live command, reads no file, and
# touches no global deployment state, so it is directly callable with
# hand-written SYNTHETIC records for testing (this task's own sanity-check
# invocations during implementation, and task 8.2's dedicated harness, which
# depends on this exact function/record format). main() below builds the
# record array from the ALREADY-POPULATED INJECTED_VALUES/INJECTED_RESULTS
# (task 6.1) and CONTROLLER_OBSERVED/SCHEDULER_OBSERVED/APISERVER_OBSERVED
# (tasks 6.2-6.4) associative arrays for the live-run path; a synthetic
# caller builds the same shape by hand, with no deployment involved at all.
#
# Per design.md's Error Handling, a REJECTED resource name (Requirement 1.2)
# is a run-level failure in its own right -- reported as its own failure
# category ("REJECTED", distinct from a per-layer MISMATCH/UNREACHABLE) and
# always drives the overall verdict to FAIL, independent of the (nonexistent
# -- a rejected name is never actually applied anywhere, see
# query_controller_resvals()/query_scheduler_resvals()/
# query_apiserver_resvals()'s shared skip rule) per-layer results.
# Record format (9 fields, separated by VERDICT_FIELD_SEP -- ASCII Unit
# Separator 0x1F, deliberately NOT a tab, see that constant's own comment
# below), one record string per checked resource:
# resource<SEP>injected<SEP>injection_status<SEP>
# ctrl_reachable<SEP>ctrl_observed<SEP>
# sched_reachable<SEP>sched_observed<SEP>
# api_reachable<SEP>api_observed
#
# resource -- resource name.
# injected -- the value that was (or would have been) injected.
# injection_status -- "accepted" or "rejected: <reason>"
# (INJECTED_RESULTS' own vocabulary, tasks 2.2/6.1)
# -- anything other than the literal string
# "accepted" is treated as a rejection, independent
# of the reason text.
# *_reachable -- "1" if that layer produced an observed value
# within scope/bounded wait, "0" if it did not
# (timeout, or simply never checked). Ignored when
# injection_status != "accepted" -- nothing was ever
# applied anywhere for a rejected name, so no layer
# is "checked" for it at all (matches tasks
# 6.2-6.4's own skip rule).
# *_observed -- observed value string for that layer, meaningful
# only when the paired *_reachable flag is "1".
#
# A record for a rejected resource still needs all 9 fields present
# (positional read) -- pass "0"/"" for each of the 6 layer fields by
# convention (see build_verdict_record() below); their content is ignored.
#
# VERDICT_FIELD_SEP: ASCII Unit Separator (0x1F), NOT a tab. Found live
# (this task's own synthetic sanity-check run, scenario (c) below) that a
# plain `IFS=$'\t' read` -- the convention every OTHER TSV in this file
# uses -- silently COLLAPSES a genuinely-empty field: bash treats tab (like
# space/newline) as "IFS whitespace" for splitting purposes even when IFS is
# set to ONLY a tab character, so two adjacent delimiters (an empty
# *_observed field, which legitimately happens both for an UNREACHABLE
# layer and for a reachable-but-empty-value MISMATCH, e.g. controller "no
# value" -- see query_controller_resvals()) collapse into one, shifting
# every field after it and silently corrupting the record. None of this
# file's other TSVs (injection-results.tsv, *-check-results.tsv) ever carry
# a genuinely-empty field, which is why this gotcha never surfaced before
# aggregate_verdict()'s record format, the first place in this script where
# an empty field is a legitimate, expected value. 0x1F is not "IFS
# whitespace" and is never expected to appear in a resource name, value, or
# status string, so it splits/preserves empty fields correctly with a plain
# `IFS=$VERDICT_FIELD_SEP read`.
VERDICT_FIELD_SEP=$'\x1f'
build_verdict_record() {
# build_verdict_record NAME INJECTED INJ_STATUS CTRL_OK CTRL_OBS SCHED_OK SCHED_OBS API_OK API_OBS
# Thin formatting helper so main()'s live-run wiring and any hand-built
# synthetic record share one place that defines field order/count -- not
# itself part of the "self-contained comparison step" (aggregate_verdict()
# is that step); a caller may also just printf/construct the
# VERDICT_FIELD_SEP-joined string directly, this exists purely for
# readability at call sites.
local sep="$VERDICT_FIELD_SEP"
printf '%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s' \
"$1" "$sep" "$2" "$sep" "$3" "$sep" "$4" "$sep" "$5" "$sep" \
"$6" "$sep" "$7" "$sep" "$8" "$sep" "$9"
}
# aggregate_verdict RECORD...
# THE self-contained comparison/aggregation step (Requirement 6.2/6.3/6.4).
# Pure function over its arguments: no network I/O, no file reads, no
# subprocess -- every classification decision is either a direct field
# comparison or a call to controller_values_match() (also pure). Safe and
# deterministic to call with hand-built synthetic records, live-run-derived
# records, or a mix, with no live deployment required either way.
#
# Resets and repopulates these globals on every call (so repeated calls in
# the same shell/test session -- e.g. one per synthetic scenario -- never
# see a stale result from a previous invocation):
# AGG_RESOURCE_STATUS["<name>"] = "PASS" | "FAIL"
# AGG_FAILURE_LINES[] = one human-readable "resource=... layer=...
# status=... ..." string per failing
# (resource, layer) pair or rejected name
# AGG_TOTAL_RESOURCES = count of records passed in
# AGG_MISMATCHES = count of (resource, layer) pairs
# classified MISMATCH
# AGG_UNREACHABLE = count of (resource, layer) pairs
# classified UNREACHABLE
# AGG_REJECTED = count of records whose injection_status
# was not "accepted"
# AGG_RESULT = "PASS" | "FAIL"
#
# Prints one FAILURE line per failing (resource, layer) pair or rejected
# name (naming the specific resource + layer, per Requirement 6.3), then
# exactly one VERDICT line, then exactly one terminal PASS/FAIL oracle line
# -- xce-test-ha's machine-parseable convention, see SKILL.md.
#
# Returns (exit code) 0 when AGG_RESULT=PASS, 1 when AGG_RESULT=FAIL -- so a
# caller (main(), or a synthetic test) can branch directly on the call
# itself: `if aggregate_verdict "${records[@]}"; then ... fi`.
declare -A AGG_RESOURCE_STATUS=()
AGG_FAILURE_LINES=()
AGG_TOTAL_RESOURCES=0
AGG_MISMATCHES=0
AGG_UNREACHABLE=0
AGG_REJECTED=0
AGG_RESULT="PASS"
aggregate_verdict() {
AGG_RESOURCE_STATUS=()
AGG_FAILURE_LINES=()
AGG_TOTAL_RESOURCES=0
AGG_MISMATCHES=0
AGG_UNREACHABLE=0
AGG_REJECTED=0
AGG_RESULT="PASS"
local rec resource injected inj_status
local ctrl_ok ctrl_obs sched_ok sched_obs api_ok api_obs
local layer ok obs resource_failed
for rec in "$@"; do
IFS="$VERDICT_FIELD_SEP" read -r resource injected inj_status \
ctrl_ok ctrl_obs sched_ok sched_obs api_ok api_obs <<< "$rec"
AGG_TOTAL_RESOURCES=$((AGG_TOTAL_RESOURCES + 1))
resource_failed=0
if [ "$inj_status" != "accepted" ]; then
AGG_REJECTED=$((AGG_REJECTED + 1))
resource_failed=1
AGG_FAILURE_LINES+=("resource=$resource layer=injection status=REJECTED detail=\"$inj_status\"")
else
for layer in controller scheduler apiserver; do
case "$layer" in
controller) ok="$ctrl_ok"; obs="$ctrl_obs" ;;
scheduler) ok="$sched_ok"; obs="$sched_obs" ;;
apiserver) ok="$api_ok"; obs="$api_obs" ;;
esac
if [ "$ok" != "1" ]; then
AGG_UNREACHABLE=$((AGG_UNREACHABLE + 1))
resource_failed=1
AGG_FAILURE_LINES+=("resource=$resource layer=$layer status=UNREACHABLE injected=$injected observed=<none>")
elif ! controller_values_match "$injected" "$obs"; then
AGG_MISMATCHES=$((AGG_MISMATCHES + 1))
resource_failed=1
AGG_FAILURE_LINES+=("resource=$resource layer=$layer status=MISMATCH injected=$injected observed=$obs")
fi
done
fi
if [ "$resource_failed" -eq 1 ]; then
AGG_RESOURCE_STATUS["$resource"]="FAIL"
AGG_RESULT="FAIL"
else
AGG_RESOURCE_STATUS["$resource"]="PASS"
fi
done
if [ "$AGG_TOTAL_RESOURCES" -eq 0 ]; then
# No resources at all is not a legitimate PASS -- nothing was
# actually verified. Structural/degenerate case, not expected from
# main()'s own live-run wiring (inject_resources() already fail()s
# the run before this point if RESOURCE_ARGS somehow resolved empty
# -- DEFAULT_RESOURCE_ARGS is always non-empty), but a synthetic
# caller could legitimately probe this edge, so classify it
# explicitly rather than silently reporting PASS on zero input.
AGG_RESULT="FAIL"
AGG_FAILURE_LINES+=("no resources were checked -- nothing to verify")
fi
local line
for line in "${AGG_FAILURE_LINES[@]}"; do
log "FAILURE: $line"
done
log "VERDICT resources=$AGG_TOTAL_RESOURCES layers_checked=3 mismatches=$AGG_MISMATCHES unreachable=$AGG_UNREACHABLE rejected=$AGG_REJECTED result=$AGG_RESULT"
if [ "$AGG_RESULT" = "PASS" ]; then
log "PASS: $AGG_TOTAL_RESOURCES resource(s), 3 layers checked, zero mismatches, zero unreachable, zero rejected"
return 0
else
log "FAIL: $AGG_TOTAL_RESOURCES resource(s), 3 layers checked, $AGG_MISMATCHES mismatch(es), $AGG_UNREACHABLE unreachable, $AGG_REJECTED rejected -- see FAILURE lines above"
return 1
fi
}
# build_verdict_records_from_tsv TSV_PATH
# Round-2 remediation of the round-1 REJECTED verdict (see the sched_ok/
# api_ok derivation comment inside for the actual bug/fix). This is main()'s
# own record-building loop (Task 7.1), extracted verbatim from main() into a
# standalone function so it is independently callable -- the round-1
# reviewer's exact finding was that a hand-built-synthetic-record test of
# aggregate_verdict() alone (task 8.2's harness) could never have caught this
# bug, because the bug lived entirely in main()'s data-marshaling code
# (reading $TOP/logs/injection-results.tsv plus the CONTROLLER_OBSERVED/
# SCHEDULER_OBSERVED/SCHEDULER_CHECK_RESULTS/APISERVER_OBSERVED/
# APISERVER_CHECK_RESULTS globals into aggregate_verdict()-shaped records),
# which a bypass-and-call-aggregate_verdict()-directly test never exercises.
# Extracting it here means a synthetic test can source this file, hand-
# populate the same globals + a synthetic TSV, call this function directly,
# and inspect its output -- exercising the EXACT SAME code main() itself
# runs, not a reimplementation of it. No behavior change from the inline
# version: same reads, same globals, same build_verdict_record() calls.
#
# Populates the global VERDICT_RECORDS array (reset on every call, mirroring
# aggregate_verdict()'s own reset-globals-on-every-call convention) with one
# build_verdict_record()-shaped record per line of TSV_PATH. Build one record
# per INJECTED name, in injection order -- read back from
# $TOP/logs/injection-results.tsv, the same durable, order-preserving source
# query_controller_resvals()/query_scheduler_resvals()/
# query_apiserver_resvals() already read (bash associative arrays have no
# defined iteration order, the TSV does). ANY_CONTROLLER_MISMATCH/
# ANY_SCHEDULER_MISMATCH/ANY_SCHEDULER_UNREACHABLE/ANY_APISERVER_MISMATCH/
# ANY_APISERVER_UNREACHABLE (set by the three query_*_resvals() calls) are
# NOT consulted here -- aggregate_verdict() does its own independent
# MATCH/MISMATCH/UNREACHABLE classification straight from the *_OBSERVED
# values via controller_values_match(), the same pure comparison helper each
# query_*_resvals() already uses, so a synthetic caller and this live-run
# path share identical classification logic.
VERDICT_RECORDS=()
build_verdict_records_from_tsv() {
local verdict_tsv="$1"
[ -f "$verdict_tsv" ] || fail "verdict aggregation: $verdict_tsv not found -- inject_resources() must run first"
VERDICT_RECORDS=()
local vname vvalue vstatus
local ctrl_ok ctrl_obs sched_ok sched_obs api_ok api_obs
while IFS=$'\t' read -r vname vvalue vstatus; do
[ -n "$vname" ] || continue
if [ "$vstatus" = "accepted" ]; then
# ctrl_ok: derived from CONTROLLER_OBSERVED array-key existence.
# This is fine as-is (round-2 review confirmed no change needed)
# -- query_controller_resvals()/internal_controller_check() has
# no separate UNREACHABLE concept at all; a query failure there
# aborts the whole run via fail() rather than leaving a
# placeholder entry, so "no observed value" legitimately means
# MISMATCH for this layer specifically, and array-key existence
# is an accurate proxy for "was queried".
if [ -n "${CONTROLLER_OBSERVED[$vname]+x}" ]; then
ctrl_ok=1; ctrl_obs="${CONTROLLER_OBSERVED[$vname]}"
else
ctrl_ok=0; ctrl_obs=""
fi
# sched_ok/api_ok: round-2 fix (remediation of the round-1
# REJECTED verdict). query_scheduler_resvals()/
# query_apiserver_resvals() (tasks 6.3/6.4) unconditionally set
# SCHEDULER_OBSERVED[$vname]/APISERVER_OBSERVED[$vname] for
# EVERY accepted+checked name, including on a genuine
# UNREACHABLE/timeout -- where the value is just set to "" (see
# the `observed=""` branch in both functions). So deriving
# sched_ok/api_ok from *_OBSERVED array-key existence (the old,
# rejected approach) was ALWAYS 1 for a checked name -- it could
# never see the genuine UNREACHABLE status, and aggregate_verdict
# fell through to comparing the injected value against an empty
# string, misreporting UNREACHABLE as MISMATCH. Fix: derive
# sched_ok/api_ok from SCHEDULER_CHECK_RESULTS[$vname]/
# APISERVER_CHECK_RESULTS[$vname] instead -- both are always
# populated (same unconditional-set guarantee, task 6.3/6.4) with
# one of exactly "MATCH", "MISMATCH: injected=... observed=...",
# or "UNREACHABLE: ..." (see the declare -A comments above each
# array), so a leading "UNREACHABLE:" is the correct, exact
# signal -- not the presence/absence of the OBSERVED entry.
# The `__UNCHECKED__` fallback below is defensive-only (not
# expected to trigger given query_scheduler_resvals()/
# query_apiserver_resvals() iterate the exact same accepted-name
# list this loop reads from the same injection-results.tsv, so
# CHECK_RESULTS is always populated by the time main() reaches
# this loop) -- but if it ever DID trigger, treating "never
# checked" as ok=1/empty-observed would silently reproduce this
# exact bug class, so it's treated as unreachable instead.
case "${SCHEDULER_CHECK_RESULTS[$vname]-__UNCHECKED__}" in
UNREACHABLE:*|__UNCHECKED__) sched_ok=0; sched_obs="" ;;
*) sched_ok=1; sched_obs="${SCHEDULER_OBSERVED[$vname]-}" ;;
esac
case "${APISERVER_CHECK_RESULTS[$vname]-__UNCHECKED__}" in
UNREACHABLE:*|__UNCHECKED__) api_ok=0; api_obs="" ;;
*) api_ok=1; api_obs="${APISERVER_OBSERVED[$vname]-}" ;;
esac
else
ctrl_ok=0; ctrl_obs=""
sched_ok=0; sched_obs=""
api_ok=0; api_obs=""
fi
VERDICT_RECORDS+=("$(build_verdict_record "$vname" "$vvalue" "$vstatus" \
"$ctrl_ok" "$ctrl_obs" "$sched_ok" "$sched_obs" "$api_ok" "$api_obs")")
done < "$verdict_tsv"
}
main() {
parse_args "$@"
resolve_top
@ -1158,18 +1506,21 @@ main() {
query_apiserver_resvals
# --- Extension point (task 7.1): aggregate results into a VERDICT line
# plus the terminal PASS/FAIL oracle line, replacing this task's
# placeholder success message and exit code below.
# ANY_CONTROLLER_MISMATCH/ANY_SCHEDULER_MISMATCH/
# ANY_SCHEDULER_UNREACHABLE/ANY_APISERVER_MISMATCH/
# ANY_APISERVER_UNREACHABLE (set by query_controller_resvals()/
# query_scheduler_resvals()/query_apiserver_resvals() above) are
# groundwork for this aggregation -- tasks 6.2/6.3/6.4 themselves
# deliberately do not act on them. ---
# --- Task 7.1: aggregate the controller/scheduler/apiserver per-layer
# results plus the injection accept/reject outcome into one VERDICT
# line + terminal PASS/FAIL oracle line (aggregate_verdict() above).
# build_verdict_records_from_tsv() (round-2 extraction, see its own
# header comment) does the data-marshaling; main() is just a thin
# caller. ---
build_verdict_records_from_tsv "$TOP/logs/injection-results.tsv"
log "PASS (task 5.1+6.1+6.2+6.3+6.4 scope): controller, scheduler, and apiserver confirmed up; worker '$WORKER_HOST' joined and resupdate ran for ${#INJECTED_RESULTS[@]} resource(s) under $TOP (see $TOP/logs/injection-results.tsv for the accept/reject detail per name -- a rejection there is a captured outcome, not a script failure at this stage); controller-side check ran for every accepted resource (see $TOP/logs/controller-check-results.tsv -- a MISMATCH there is also a captured outcome, not a script failure at this stage, see ANY_CONTROLLER_MISMATCH); scheduler-side check ran for every accepted resource (see $TOP/logs/scheduler-check-results.tsv -- a MISMATCH or UNREACHABLE there is also a captured outcome, not a script failure at this stage, see ANY_SCHEDULER_MISMATCH/ANY_SCHEDULER_UNREACHABLE); apiserver-side check ran for every accepted resource (see $TOP/logs/apiserver-check-results.tsv -- a MISMATCH or UNREACHABLE there is also a captured outcome, not a script failure at this stage, see ANY_APISERVER_MISMATCH/ANY_APISERVER_UNREACHABLE); the overall PASS/FAIL verdict lands in task 7.1; cleaning up"
exit 0
log "aggregating verdict across ${#VERDICT_RECORDS[@]} resource(s) under $TOP; cleanup (trap-based, same mechanism as a mid-deployment abort) fires on exit below regardless of PASS/FAIL, unless --keep-running was passed -- logs under $TOP/logs are preserved either way"
if aggregate_verdict "${VERDICT_RECORDS[@]}"; then
exit 0
else
exit 1
fi
}
# Hidden internal entry point (bug-1579 fix): inject_resources()'s `!`
@ -1179,10 +1530,22 @@ main() {
# (which has no knowledge of this flag and would otherwise reject it as an
# "Unknown argument") -- runs only internal_controller_check(), none of the
# normal deploy/inject/trap flow.
if [ "${1:-}" = "--internal-controller-check" ]; then
shift
internal_controller_check "$@"
exit $?
fi
#
# Guarded so this file can be `source`d (e.g. a synthetic test harness that
# wants aggregate_verdict()/controller_values_match()/build_verdict_record()
# in scope, task 8.2's use case) without executing main() or
# internal_controller_check() as a side effect -- ${BASH_SOURCE[0]} (this
# file's own path) only equals ${0} when the script is the thing actually
# being run (directly executed, or `bash run-resource-verify.sh ...`,
# including the `--internal-controller-check` recursive invocation above,
# which is a fresh `bash "$SELF_SCRIPT" ...` process, not a source), not
# when it's sourced into another shell/script.
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
if [ "${1:-}" = "--internal-controller-check" ]; then
shift
internal_controller_check "$@"
exit $?
fi
main "$@"
main "$@"
fi

View file

@ -93,7 +93,7 @@
- _Boundary: xce-test-resources skill (apiserver check) — same file as 6.2/6.3, sequential_
- [ ] 7. Integration: verdict aggregation, reporting, and cleanup
- [ ] 7.1 Aggregate layer results, emit the verdict, and clean up
- [x] 7.1 Aggregate layer results, emit the verdict, and clean up
- Combine the controller/scheduler/apiserver check results into a single per-resource, per-layer comparison, implemented as a self-contained comparison step that accepts a set of (resource, injected value, per-layer observed value) records — so it can be invoked directly with synthetic records for testing (task 8.2), not only reachable from a live run
- Emit a machine-parseable VERDICT line (following `xce-test-ha`'s convention) plus the standard terminal PASS/FAIL oracle line, naming every layer that mismatched or was unreachable (timed out)
- Support checking multiple resource names in one run, defaulting to the resource names added in task 2.1 when none are supplied
@ -116,6 +116,7 @@
- _Depends: 7.1_
## Implementation Notes
- Task 7.1 (round 1 REJECTED, round 2 fixed): `aggregate_verdict()`/`build_verdict_record()` (VERDICT_FIELD_SEP=`$'\x1f'`, ASCII Unit Separator — bash's IFS tab-splitting silently collapses adjacent empty fields even with IFS set to only a tab, bug-1581) are the self-contained, synthetic-testable comparison step task 8.2 needs. `main()`'s live-data wiring is `build_verdict_records_from_tsv()` — derive a layer's "ok" status from `SCHEDULER_CHECK_RESULTS[$name]`/`APISERVER_CHECK_RESULTS[$name]`'s STRING CONTENT (`UNREACHABLE:*` prefix check), never from array-key existence — tasks 6.3/6.4 always set the key, even on a genuine timeout (just with an empty value), so key-existence can never distinguish UNREACHABLE from MISMATCH (bug-1582). `CONTROLLER_OBSERVED`'s key-existence check IS correct for the controller layer specifically (task 6.2 has no separate UNREACHABLE concept — a query failure there aborts the whole run). VERDICT line format: `[xce-test-resources] VERDICT resources=<N> layers_checked=3 mismatches=<M> unreachable=<U> rejected=<R> result=PASS|FAIL` — `rejected=` is an intentional addition beyond design.md's illustrative template (Requirement 6.3 needs rejected names attributable in the machine-parseable line itself); `layers_checked=3` is a fixed constant, not a per-run computed count. SKILL.md fully rewritten to describe the now-complete, functional tool.
- Task 6.4: `poll_apiserver_log()`/`query_apiserver_resvals()` mirror task 6.3's scheduler-check pattern exactly (same bounded-wait/batched-polling shape). The apiserver's zap `console` encoder renders structured fields as a trailing JSON object (`resource-location stored {"resource": "r15m", "host": "...", "value": 1.5}`), not classic `key=value` text — grep for the message string, then parse the trailing JSON block. All three layers (controller/scheduler/apiserver) confirmed simultaneously MATCH on the full default set including `cpuf`, proving the bug-1579 fix holds end-to-end.
- **bug-1579 is now FULLY fixed** (retrofit on top of tasks 6.1/6.2, not a numbered task — user-requested follow-up). `inject_resources()`'s worker-context `xce_test` script now inserts a `!<shell-command>` line between `resupdate` and `quit`: `! bash <script> --internal-controller-check <top> <peers> <worker-host> <names...>` — this runs the controller-check query via `xce_test`'s `!` shell-escape (`preprocess_and_run()`, handled in the single top-level REPL loop before context dispatch, so it works in ANY context including `worker` mode without disconnecting). The controller check now runs WHILE the injecting worker's connection is still open, so host-attrs resources are never reset before being checked. `DEFAULT_RESOURCE_ARGS` now includes `cpuf` again (proof the fix works: `cpuf` now MATCHes at the controller layer, not the old false MISMATCH). `query_controller_resvals()`'s comparison logic (`controller_values_match()`) is confirmed byte-for-byte unchanged — this was a pure sequencing fix. Any future task touching `inject_resources()`/the controller-check flow should be aware of this nested-process structure (`--internal-controller-check` is a hidden internal dispatch flag, gated before `main()`/`parse_args()` runs — do not remove it thinking it's dead code).
- Non-blocking notes from the retrofit's review: `internal_controller_check()` queries ALL requested resource names (including ones that will later be rejected), not just accepted ones like the original design — forced by sequencing (accept/reject isn't known until the whole injecting session's output is parsed after it returns); confirmed benign, the final `controller-check-results.tsv` still correctly omits rejected names. Also: invoking `--internal-controller-check` directly with fewer than 3 args hits `set -u` and dies with a raw "unbound variable" message instead of a clean `fail()` — cosmetic only, this path is unreachable from any normal invocation.