Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74b3b9e869 |
File diff suppressed because it is too large
Load Diff
@@ -1,114 +0,0 @@
|
|||||||
# WebUI Fleet Bridge Planning Scratchpad
|
|
||||||
|
|
||||||
**Mode:** Task 0 docs-only decision PR authorized; Task 1 and all executable/runtime work remain blocked.
|
|
||||||
**Owner:** Velma
|
|
||||||
**Opened:** 2026-08-09
|
|
||||||
**Scope:** One enrolled Agent Host launching one native `mosaic claude` OAuth session and streaming it into `apps/web` through `apps/gateway`.
|
|
||||||
|
|
||||||
## Objective
|
|
||||||
|
|
||||||
Turn the approved direction in `jarvis-brain/docs/scratchpads/MOSAIC-WEBUI-FLEET-BRIDGE.md` into a test-first implementation plan while preserving Fred's harness-home/launcher contract and Scooby's greenfield safety findings.
|
|
||||||
|
|
||||||
## Source reconciliation
|
|
||||||
|
|
||||||
- Current `origin/main`: `b0f7d26dd9c14d91eaaefc35d6c9fd6618a0bd92`.
|
|
||||||
- Current `origin/next`: `4df478cdd150fdf8d52ea109f02ade5d85017acd`.
|
|
||||||
- Branches currently diverge (`main` has 11 unique commits; `next` has 13). `next` contains local-tier Redis fix #689; `main` contains later fleet/shell fixes.
|
|
||||||
- Fred's three-root harness-home design and promotion stack are not yet fully present on either baseline.
|
|
||||||
- Therefore neither current SHA is an acceptable implementation pin. Code may begin only from a Fred-certified reconciled SHA containing the required launcher/home contract and safe Gateway startup prerequisites.
|
|
||||||
- The deployed `mosaic.woltje.com` v0.0.20 UI remains reference-only.
|
|
||||||
|
|
||||||
## Verified seams
|
|
||||||
|
|
||||||
- Current browser chat uses an in-process Pi SDK session.
|
|
||||||
- `AgentRuntimeProvider` supports list/tree/stream/send/attach/detach/terminate, but not create/start.
|
|
||||||
- `InteractionController` enrolls an already-existing runtime session; it cannot launch one.
|
|
||||||
- Hermes is the only runtime provider registered in Gateway.
|
|
||||||
- Tmux streaming is explicitly unsupported and remains out of scope.
|
|
||||||
- `mosaic claude` is the authoritative launcher and accepts Claude's machine-facing stream-json flags.
|
|
||||||
- Installed discovery version: Claude Code 2.1.226. Target Distrobox version must be independently pinned and certified.
|
|
||||||
|
|
||||||
## Non-negotiable dependencies
|
|
||||||
|
|
||||||
1. Fred approves the machine-facing launcher/seat-home contract before code.
|
|
||||||
2. No WebUI/Gateway direct read of lease broker state, daemon socket, or state files (F-V3).
|
|
||||||
3. No provider OAuth token leaves the Agent Host.
|
|
||||||
4. No local Gateway/Web startup around the KBN/database hold.
|
|
||||||
5. Greenfield work runs in a Debian Distrobox with an isolated home.
|
|
||||||
6. The initial plan PR targets `next`; Fred binds D1–D15 on its exact head before issue/PRD/tracker completion or implementation work.
|
|
||||||
|
|
||||||
## Reproducible evidence
|
|
||||||
|
|
||||||
Run from a clean Stack clone:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git fetch origin main next
|
|
||||||
git rev-parse origin/main origin/next
|
|
||||||
git rev-list --left-right --count origin/main...origin/next
|
|
||||||
rg -n "interface AgentRuntimeProvider|createSession|streamEvents|terminate" \
|
|
||||||
packages/types/src/agent packages/agent/src apps/gateway/src/agent
|
|
||||||
rg -n "AgentService\.prompt|interaction_sessions|createRuntimeTerminationApproval" \
|
|
||||||
apps/gateway/src packages/db/src/schema.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
Primary inspected source seams:
|
|
||||||
|
|
||||||
- `packages/types/src/agent/agent-runtime-provider.ts`
|
|
||||||
- `packages/agent/src/{runtime-provider-registry,hermes-runtime-provider,matrix-native-runtime-provider,tmux-fleet-runtime-provider}.ts`
|
|
||||||
- `apps/gateway/src/agent/{runtime-provider-registry.service,interaction.controller,durable-session.repository,durable-session.service}.ts`
|
|
||||||
- `apps/gateway/src/chat/chat.gateway.ts`
|
|
||||||
- `packages/mosaic/src/commands/{launch,interaction}.ts`
|
|
||||||
- `packages/mosaic/src/fleet/generated-env-boundary.ts`
|
|
||||||
- `packages/db/src/schema.ts`
|
|
||||||
- `apps/web/src/app/(dashboard)/chat/page.tsx`
|
|
||||||
|
|
||||||
Planning-only investigation transcripts are local and intentionally uncommitted:
|
|
||||||
|
|
||||||
- `/tmp/velma-plan-stack-surface.txt`
|
|
||||||
- `/tmp/velma-plan-structure.txt`
|
|
||||||
- `/tmp/velma-plan-scooby.txt`
|
|
||||||
- `/tmp/velma-plan-runtime-contract.txt`
|
|
||||||
|
|
||||||
## Source findings that constrain the design
|
|
||||||
|
|
||||||
- `interaction_sessions.id` is the stable primary key; there are no create/policy/enrollment/state columns.
|
|
||||||
- `interaction_outbox` has a unique `(session_id, idempotency_key)` index and only `pending | processing | delivered`.
|
|
||||||
- Baseline `DurableSessionRepository.create()` can replace provider/runtime identity for the same owner; M1 must remove that implicit mutation.
|
|
||||||
- Baseline termination approval is Redis-backed and currently consumes separately from PostgreSQL; M1 therefore needs durable authorization acceptance before destructive token deletion/dispatch.
|
|
||||||
- Existing interaction HTTP base is `/api/interaction/:agentName`; the plan extends it rather than inventing a second route family.
|
|
||||||
- Baseline `launch.ts`/lease launcher still use ambient lookup/literal interpreters. Section 4.1 is non-binding consumer input to Fred's W-F design; W-F's final resolved-launch contract must exist in the certified base before Velma can certify it.
|
|
||||||
- Root `pnpm test` is not KBN-safe: it includes PGlite migration and framework-shell/lease-broker suites.
|
|
||||||
|
|
||||||
## Independent draft review
|
|
||||||
|
|
||||||
Seven adversarial review rounds found and drove explicit fixes for:
|
|
||||||
|
|
||||||
- a candidate resolved-seat consumer descriptor and threat model, now explicitly non-binding input to Fred's W-F-owned launcher design;
|
|
||||||
- one active launch per stable conversation, exact pending/failed encodings, durable pre-dispatch reservation, CAS activation, and crash lookup without a migration;
|
|
||||||
- generation-bound enrollment, command/event revalidation, `SIGHUP` config epochs, and stale-epoch rejection;
|
|
||||||
- Redis/PostgreSQL exact-stop crash safety via non-destructive verify, durable authorization acceptance, atomic claim/`GETDEL`, and same-operation status reconciliation;
|
|
||||||
- sequence gaps/reorder limits, deterministic UUIDv5 completion, and Gateway restart fail-closed behavior;
|
|
||||||
- shared streaming redaction before host ring/transport and again before Gateway persistence/browser;
|
|
||||||
- D2/D12-selected path-free provenance and a separate safe browser DTO—never raw or hashed path strings;
|
|
||||||
- exact F-V3 boundary: bridge has no broker API, while Fred's sealed launcher may enforce broker policy internally;
|
|
||||||
- migration-free focused tests in Tasks 1–8, with live OAuth, repository transaction, Gateway/Web, and Playwright restricted to the Fred/Scooby-certified Task 9 path;
|
|
||||||
- candidate private/public commitment and artifact-binding mechanisms that W-F may accept, simplify, replace, or defer;
|
|
||||||
- JCS event-digest recomputation and equal/different duplicate handling in both accepted and future-buffer states;
|
|
||||||
- a safe browser presentation DTO for host/workspace/seat/persona labels, readiness, and connection state;
|
|
||||||
- explicit `OnApplicationBootstrap` create/stop recovery enumeration with no auto-launch;
|
|
||||||
- all POSIX/Windows/UNC/file/tilde path classes in the streaming redactor and definitive failed-stop response semantics;
|
|
||||||
- the full 14-column canonical task schema, fake-only Task 5 repository tests, migration-free certified Task 9 DB test, server-owned operation correlations/routes, and exact merged-SHA smoke;
|
|
||||||
- per-commit independent review, queue guards, exact-head PR review, squash merge, exact merged-next SHA/CI wait, worktree-bound smoke, issue-state readback, and reviewed tracker-closure PR with its own merged-next CI;
|
|
||||||
- a private DB URL loader captured/exported per session without recording or echoing the credential;
|
|
||||||
- an attached Task 9 implementation branch, provider-filtered exact issue-state readback, and machine-verified smoke JSON binding source/worktree/deployed SHA;
|
|
||||||
- a capped, duplicate-key-rejecting, exact-key/type smoke schema so report extensions or JSON boolean/float coercion cannot smuggle data or fake child-count evidence.
|
|
||||||
|
|
||||||
The plan remains deliberately **decision-PR-ready, not implementation-ready**. Fred authorized only Task 0's initial two-document PR on `next`. He must still return every Section 2 value, replace all `[FRED-GATE]` entries, certify storage/startup, and provide the W-F-dependent `IMPLEMENTATION_BASE_SHA` before Task 0 closes or Task 1 starts.
|
|
||||||
|
|
||||||
## Current status
|
|
||||||
|
|
||||||
- Gitea principal verified as `velma`; helper and API wrapper resolution are fail-closed and correct.
|
|
||||||
- Fred authorized the initial Task 0 docs-only PR in `comms/20260809T094952Z__from-fred__ec0e85.md` and confirmed its `next` lane/W-F descriptor corrections in `comms/20260809T095437Z__from-fred__4ee79d.md`.
|
|
||||||
- Provisional decisions: D1/D8/D9/D10/D13/D14 approved; D4 tool labels exactly `{Read, Grep, Glob}`; D5/D12 provisional; D11 remains a single-operator seam; D2/D3/D6/D7 and `IMPLEMENTATION_BASE_SHA` are `BLOCKED-ON-W-F`.
|
|
||||||
- Awaiting exact plan-PR-head review and Fred's complete D1–D15 binding contract.
|
|
||||||
- No source code, database, deployment, or live runtime changes made.
|
|
||||||
@@ -85,10 +85,6 @@ function Get-QueueState {
|
|||||||
$state = "$($Payload.state)".ToLowerInvariant()
|
$state = "$($Payload.state)".ToLowerInvariant()
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($pending -contains $state) { return "pending" }
|
|
||||||
if ($failure -contains $state) { return "terminal-failure" }
|
|
||||||
if ($success -contains $state) { return "terminal-success" }
|
|
||||||
|
|
||||||
$values = @()
|
$values = @()
|
||||||
$statuses = @()
|
$statuses = @()
|
||||||
if ($null -ne $Payload.statuses) { $statuses = @($Payload.statuses) }
|
if ($null -ne $Payload.statuses) { $statuses = @($Payload.statuses) }
|
||||||
@@ -101,7 +97,15 @@ function Get-QueueState {
|
|||||||
if (-not [string]::IsNullOrEmpty($v)) { $values += $v }
|
if (-not [string]::IsNullOrEmpty($v)) { $values += $v }
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($values.Count -eq 0 -and [string]::IsNullOrEmpty($state)) { return "no-status" }
|
# Zero contexts is classified FIRST: Gitea reports a synthetic aggregate
|
||||||
|
# state of "pending" alongside statuses:null / total_count:0 (a commit
|
||||||
|
# with no CI at all), and honoring that aggregate would poll to the
|
||||||
|
# timeout. With zero contexts there is nothing to wait on.
|
||||||
|
if ($values.Count -eq 0) { return "no-status" }
|
||||||
|
|
||||||
|
if ($pending -contains $state) { return "pending" }
|
||||||
|
if ($failure -contains $state) { return "terminal-failure" }
|
||||||
|
if ($success -contains $state) { return "terminal-success" }
|
||||||
if (($values | Where-Object { $pending -contains $_ }).Count -gt 0) { return "pending" }
|
if (($values | Where-Object { $pending -contains $_ }).Count -gt 0) { return "pending" }
|
||||||
if (($values | Where-Object { $failure -contains $_ }).Count -gt 0) { return "terminal-failure" }
|
if (($values | Where-Object { $failure -contains $_ }).Count -gt 0) { return "terminal-failure" }
|
||||||
if ($values.Count -gt 0 -and ($values | Where-Object { -not ($success -contains $_) }).Count -eq 0) { return "terminal-success" }
|
if ($values.Count -gt 0 -and ($values | Where-Object { -not ($success -contains $_) }).Count -eq 0) { return "terminal-success" }
|
||||||
|
|||||||
@@ -52,7 +52,11 @@ except Exception:
|
|||||||
print("malformed")
|
print("malformed")
|
||||||
raise SystemExit(0)
|
raise SystemExit(0)
|
||||||
|
|
||||||
|
# Gitea returns "statuses": null (not []) for a commit with zero status
|
||||||
|
# contexts -- e.g. any repo with no CI configured. Treat null as empty.
|
||||||
raw_statuses = payload.get("statuses", [])
|
raw_statuses = payload.get("statuses", [])
|
||||||
|
if raw_statuses is None:
|
||||||
|
raw_statuses = []
|
||||||
raw_state = payload.get("state", "")
|
raw_state = payload.get("state", "")
|
||||||
if not isinstance(raw_statuses, list) or not isinstance(raw_state, str):
|
if not isinstance(raw_statuses, list) or not isinstance(raw_state, str):
|
||||||
print("malformed")
|
print("malformed")
|
||||||
@@ -75,14 +79,18 @@ for item in statuses:
|
|||||||
raise SystemExit(0)
|
raise SystemExit(0)
|
||||||
values.append(raw_value.lower())
|
values.append(raw_value.lower())
|
||||||
|
|
||||||
if any(value in pending_values for value in values) or state in pending_values:
|
# Zero contexts is classified FIRST: Gitea reports a synthetic aggregate
|
||||||
|
# state of "pending" alongside total_count:0, and an aggregate with no
|
||||||
|
# contexts behind it must not read as an in-flight pipeline (it would poll
|
||||||
|
# to the timeout). With zero contexts there is nothing to wait on.
|
||||||
|
if not values:
|
||||||
|
print("no-status")
|
||||||
|
elif any(value in pending_values for value in values) or state in pending_values:
|
||||||
print("pending")
|
print("pending")
|
||||||
elif any(value in failure_values for value in values) or state in failure_values:
|
elif any(value in failure_values for value in values) or state in failure_values:
|
||||||
print("terminal-failure")
|
print("terminal-failure")
|
||||||
elif values and all(value in success_values for value in values) and state in {"", "success"}:
|
elif all(value in success_values for value in values) and state in {"", "success"}:
|
||||||
print("terminal-success")
|
print("terminal-success")
|
||||||
elif not values:
|
|
||||||
print("no-status")
|
|
||||||
else:
|
else:
|
||||||
print("unknown")
|
print("unknown")
|
||||||
'
|
'
|
||||||
@@ -465,9 +473,18 @@ while true; do
|
|||||||
no-status)
|
no-status)
|
||||||
if [[ "$REQUIRE_STATUS" -eq 1 ]]; then
|
if [[ "$REQUIRE_STATUS" -eq 1 ]]; then
|
||||||
echo "Error: ASSERTED_NOT_READY state=no-status; --require-status was set for ${BRANCH}." >&2
|
echo "Error: ASSERTED_NOT_READY state=no-status; --require-status was set for ${BRANCH}." >&2
|
||||||
else
|
exit 3
|
||||||
echo "Error: ASSERTED_NOT_READY state=no-status purpose=${PURPOSE} branch=${BRANCH}." >&2
|
|
||||||
fi
|
fi
|
||||||
|
# A head with zero status contexts has no CI queue to wait on.
|
||||||
|
# For push, that is queue-clear (a repo with no CI must remain
|
||||||
|
# pushable) -- mirroring record_cannot_assert's dispositions
|
||||||
|
# (push=degraded-pass, merge=hold). Merge stays fail-closed:
|
||||||
|
# no-status there may just mean CI has not reported yet.
|
||||||
|
if [[ "$PURPOSE" == "push" ]]; then
|
||||||
|
echo "[ci-queue-wait] no status contexts on ${BRANCH} head (state=no-status) — no CI queue to wait on; queue clear."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Error: ASSERTED_NOT_READY state=no-status purpose=${PURPOSE} branch=${BRANCH}." >&2
|
||||||
exit 3
|
exit 3
|
||||||
;;
|
;;
|
||||||
terminal-failure|malformed|unknown)
|
terminal-failure|malformed|unknown)
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Regression harness for ci-queue-wait.sh's zero-status-context handling.
|
||||||
|
#
|
||||||
|
# Gitea's combined-status endpoint returns, for a commit with NO status
|
||||||
|
# contexts (e.g. a repo with no CI configured at all):
|
||||||
|
# {"state":"pending","sha":"...","total_count":0,"statuses":null,...}
|
||||||
|
# -- statuses is JSON null (not []), and state is a synthetic "pending"
|
||||||
|
# even though nothing is running. Captured live from a Gitea 1.22 host,
|
||||||
|
# 2026-08-09 (jason.woltje/jarvis-brain, a repo with no pipeline).
|
||||||
|
#
|
||||||
|
# Before the fix, `payload.get("statuses", [])` received null, failed the
|
||||||
|
# isinstance(list) check, and the guard reported ASSERTED_NOT_READY
|
||||||
|
# state=malformed (exit 3) -- blocking every push to a CI-less repo. Had
|
||||||
|
# null been tolerated, the synthetic aggregate "pending" would instead
|
||||||
|
# have polled to the timeout (exit 124). The fix must:
|
||||||
|
# 1. treat statuses:null as an empty list, and
|
||||||
|
# 2. classify zero status VALUES as "no-status" regardless of the
|
||||||
|
# synthetic aggregate state, and
|
||||||
|
# 3. treat no-status for --purpose push (without --require-status) as
|
||||||
|
# queue-clear (exit 0) -- a repo with no CI has no queue to wait on;
|
||||||
|
# this mirrors record_cannot_assert's disposition table
|
||||||
|
# (push=degraded-pass, merge=hold).
|
||||||
|
#
|
||||||
|
# Covers:
|
||||||
|
# (a) statuses:null + synthetic state:pending, purpose=push
|
||||||
|
# -> exit 0, queue-clear/no-status message (THE live fault).
|
||||||
|
# (b) same payload, purpose=merge -> still fail-closed (exit 3).
|
||||||
|
# (c) same payload, push + --require-status -> still fail-closed (exit 3).
|
||||||
|
# (d) statuses:[] + state:"" -> same as (a) (exit 0).
|
||||||
|
# (e) a real pending context -> still polls (times out, 124),
|
||||||
|
# proving the relaxation didn't swallow genuine pending states.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/ci-queue-wait-no-status}"
|
||||||
|
REPO_DIR="$WORK_DIR/repo"
|
||||||
|
STUB_DIR="$WORK_DIR/stubs"
|
||||||
|
|
||||||
|
rm -rf "$WORK_DIR"
|
||||||
|
mkdir -p "$REPO_DIR" "$STUB_DIR"
|
||||||
|
|
||||||
|
git -C "$REPO_DIR" init -q
|
||||||
|
git -C "$REPO_DIR" remote add origin https://git.example.test/acme/widgets.git
|
||||||
|
|
||||||
|
# Minimal curl stub (same conventions as test-ci-queue-wait-branch-absent.sh):
|
||||||
|
# branch lookup answers 200 with a fixed SHA; the status endpoint's payload
|
||||||
|
# is selected by MOSAIC_STUB_STATUS_MODE.
|
||||||
|
cat > "$STUB_DIR/curl" <<'SH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
has_w=0
|
||||||
|
url=""
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
-w) has_w=1 ;;
|
||||||
|
http://*|https://*) url="$arg" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
case "$url" in
|
||||||
|
*/branches/*)
|
||||||
|
body='{"commit":{"id":"deadbeefcafef00d0123456789abcdef01234567"}}'
|
||||||
|
if [[ "$has_w" == 1 ]]; then
|
||||||
|
printf '%s\n200' "$body"
|
||||||
|
else
|
||||||
|
printf '%s' "$body"
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*/status)
|
||||||
|
mode="${MOSAIC_STUB_STATUS_MODE:?MOSAIC_STUB_STATUS_MODE not set}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "curl stub: unrecognized URL: $url" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$mode" in
|
||||||
|
# Verbatim shape of the live Gitea response (repository object elided).
|
||||||
|
gitea-null-statuses)
|
||||||
|
body='{"state":"pending","sha":"deadbeefcafef00d0123456789abcdef01234567","total_count":0,"statuses":null,"url":"","commit_url":""}'
|
||||||
|
;;
|
||||||
|
empty-statuses)
|
||||||
|
body='{"state":"","statuses":[]}'
|
||||||
|
;;
|
||||||
|
real-pending)
|
||||||
|
body='{"state":"pending","statuses":[{"context":"ci/woodpecker","status":"running","target_url":""}]}'
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "curl stub: unknown mode=$mode" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
printf '%s' "$body"
|
||||||
|
SH
|
||||||
|
chmod +x "$STUB_DIR/curl"
|
||||||
|
|
||||||
|
run_ci_queue_wait() {
|
||||||
|
(
|
||||||
|
cd "$REPO_DIR"
|
||||||
|
export PATH="$STUB_DIR:$PATH"
|
||||||
|
export MOSAIC_CREDENTIALS_FILE="$WORK_DIR/no-credentials.json"
|
||||||
|
export MOSAIC_CI_QUEUE_AUDIT_LOG="$WORK_DIR/audit.jsonl"
|
||||||
|
export GITEA_TOKEN="stub-token"
|
||||||
|
export GITEA_URL="https://git.example.test"
|
||||||
|
"$SCRIPT_DIR/ci-queue-wait.sh" -B main -t 3 -i 1 "$@"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fail=0
|
||||||
|
|
||||||
|
# (a) THE live fault: statuses:null + synthetic pending, purpose=push -> exit 0.
|
||||||
|
set +e
|
||||||
|
out_a=$(MOSAIC_STUB_STATUS_MODE=gitea-null-statuses run_ci_queue_wait --purpose push 2>&1)
|
||||||
|
status_a=$?
|
||||||
|
set -e
|
||||||
|
if [[ "$status_a" -ne 0 ]]; then
|
||||||
|
echo "FAIL(a): expected exit 0 for null-statuses/no-CI repo on push, got $status_a" >&2
|
||||||
|
echo "$out_a" >&2
|
||||||
|
fail=1
|
||||||
|
elif [[ "$out_a" == *"malformed"* ]]; then
|
||||||
|
echo "FAIL(a): null statuses must not be classified as malformed" >&2
|
||||||
|
echo "$out_a" >&2
|
||||||
|
fail=1
|
||||||
|
elif [[ "$out_a" != *"no-status"* ]]; then
|
||||||
|
echo "FAIL(a): expected a no-status/queue-clear message, got:" >&2
|
||||||
|
echo "$out_a" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# (b) Same payload, purpose=merge -> hold, fail-closed exit 3.
|
||||||
|
set +e
|
||||||
|
out_b=$(MOSAIC_STUB_STATUS_MODE=gitea-null-statuses run_ci_queue_wait --purpose merge 2>&1)
|
||||||
|
status_b=$?
|
||||||
|
set -e
|
||||||
|
if [[ "$status_b" -ne 3 ]]; then
|
||||||
|
echo "FAIL(b): expected exit 3 for no-status on merge, got $status_b" >&2
|
||||||
|
echo "$out_b" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# (c) Same payload, push + --require-status -> strictness opt-in still fails.
|
||||||
|
set +e
|
||||||
|
out_c=$(MOSAIC_STUB_STATUS_MODE=gitea-null-statuses run_ci_queue_wait --purpose push --require-status 2>&1)
|
||||||
|
status_c=$?
|
||||||
|
set -e
|
||||||
|
if [[ "$status_c" -ne 3 ]]; then
|
||||||
|
echo "FAIL(c): expected exit 3 for --require-status with no contexts, got $status_c" >&2
|
||||||
|
echo "$out_c" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# (d) statuses:[] + state:"" (the shape the code always tolerated) -> exit 0 on push.
|
||||||
|
set +e
|
||||||
|
out_d=$(MOSAIC_STUB_STATUS_MODE=empty-statuses run_ci_queue_wait --purpose push 2>&1)
|
||||||
|
status_d=$?
|
||||||
|
set -e
|
||||||
|
if [[ "$status_d" -ne 0 ]]; then
|
||||||
|
echo "FAIL(d): expected exit 0 for empty-statuses on push, got $status_d" >&2
|
||||||
|
echo "$out_d" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# (e) A REAL pending context must still block: polls to timeout, exit 124.
|
||||||
|
set +e
|
||||||
|
out_e=$(MOSAIC_STUB_STATUS_MODE=real-pending run_ci_queue_wait --purpose push 2>&1)
|
||||||
|
status_e=$?
|
||||||
|
set -e
|
||||||
|
if [[ "$status_e" -ne 124 ]]; then
|
||||||
|
echo "FAIL(e): expected exit 124 (timeout) for a genuinely pending context, got $status_e" >&2
|
||||||
|
echo "$out_e" >&2
|
||||||
|
fail=1
|
||||||
|
elif [[ "$out_e" != *"ci/woodpecker=running"* ]]; then
|
||||||
|
echo "FAIL(e): expected the pending context to be reported, got:" >&2
|
||||||
|
echo "$out_e" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$fail" -eq 0 ]]; then
|
||||||
|
echo "ci-queue-wait no-status regression passed (5/5 cases)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit "$fail"
|
||||||
Reference in New Issue
Block a user