Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb77c7d629 | ||
|
|
24294d3b77 | ||
|
|
24caeab057 |
@@ -96,6 +96,10 @@ steps:
|
|||||||
# fail-closed: a seat whose login is missing gets a named error, never a
|
# fail-closed: a seat whose login is missing gets a named error, never a
|
||||||
# borrowed identity. Joins CI directly; its #1007 exclusion is burned down.
|
# borrowed identity. Joins CI directly; its #1007 exclusion is burned down.
|
||||||
- bash packages/mosaic/framework/tools/git/test-gitea-login-resolution.sh
|
- bash packages/mosaic/framework/tools/git/test-gitea-login-resolution.sh
|
||||||
|
# Hermetic regression for issue-view.sh (#1357): mock tea/curl, sandboxed
|
||||||
|
# repo. Pins that comment BODIES render on both paths and that a tea
|
||||||
|
# failure is named as what it was (git-config vs credential).
|
||||||
|
- bash packages/mosaic/framework/tools/git/test-issue-view-comments.sh
|
||||||
# Hermetic behavioural regression for the PreToolUse wrapper guard: proves
|
# Hermetic behavioural regression for the PreToolUse wrapper guard: proves
|
||||||
# it still blocks the three mistakes AND still lets reads, unwrapped
|
# it still blocks the three mistakes AND still lets reads, unwrapped
|
||||||
# endpoints and ordinary commands through. Both directions are asserted —
|
# endpoints and ordinary commands through. Both directions are asserted —
|
||||||
|
|||||||
@@ -24,6 +24,24 @@
|
|||||||
# $HOME points at a per-profile directory that has no credentials file.
|
# $HOME points at a per-profile directory that has no credentials file.
|
||||||
# Operators symlink /etc/mosaic/credentials.json to the host's canonical
|
# Operators symlink /etc/mosaic/credentials.json to the host's canonical
|
||||||
# file once, instead of exporting MOSAIC_CREDENTIALS_FILE per invocation.
|
# file once, instead of exporting MOSAIC_CREDENTIALS_FILE per invocation.
|
||||||
|
#
|
||||||
|
# GITEA SEAT SLOTS (gitea-mosaicstack / gitea-usc arms only):
|
||||||
|
# On a fleet host, a resolved git identity is a SEAT whose live credential is
|
||||||
|
# its slot file, not the shared service store. Resolution, mirroring
|
||||||
|
# get_gitea_token() in tools/git/detect-platform.sh (mosaicstack#1311 lineage):
|
||||||
|
# - MOSAIC_GIT_IDENTITY names a seat with a directory under
|
||||||
|
# ${MOSAIC_BRAIN_HOME:-~/.mosaic}/fleet/agents/<identity>/ → its token is
|
||||||
|
# read from <slot>/secrets/gitea-<instance>-<identity>.token and exported
|
||||||
|
# as GITEA_TOKEN. The URL still comes from credentials.json (it is
|
||||||
|
# provider config, not identity).
|
||||||
|
# - A seat with an EMPTY/missing slot is a REFUSAL (fail loud), not a
|
||||||
|
# fallback: there is no precedence between the seat and service stores,
|
||||||
|
# and a silent service fallback would act as the wrong identity (#1343
|
||||||
|
# family; usc/uconnect#3084 precedent).
|
||||||
|
# - No identity resolved → the service store in credentials.json, exactly
|
||||||
|
# as before. Non-fleet hosts are unchanged.
|
||||||
|
# Other services (woodpecker, authentik, ...) have no seat concept and are
|
||||||
|
# untouched by this.
|
||||||
|
|
||||||
if [[ -z "${MOSAIC_CREDENTIALS_FILE:-}" ]]; then
|
if [[ -z "${MOSAIC_CREDENTIALS_FILE:-}" ]]; then
|
||||||
for _cand in "$HOME/.config/mosaic/credentials.json" "/etc/mosaic/credentials.json"; do
|
for _cand in "$HOME/.config/mosaic/credentials.json" "/etc/mosaic/credentials.json"; do
|
||||||
@@ -94,6 +112,35 @@ _mosaic_load_woodpecker_legacy() {
|
|||||||
_mosaic_sync_woodpecker_env "$WOODPECKER_INSTANCE" "$WOODPECKER_URL" "$WOODPECKER_TOKEN"
|
_mosaic_sync_woodpecker_env "$WOODPECKER_INSTANCE" "$WOODPECKER_URL" "$WOODPECKER_TOKEN"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_gitea_seat_token() {
|
||||||
|
# Echo the seat-slot token path for $1=identity $2=instance-prefix, or rc 1
|
||||||
|
# when the identity is not a seat. Reads nothing; path logic only.
|
||||||
|
local ident="$1" pfx="$2" brain_home slot
|
||||||
|
brain_home="${MOSAIC_BRAIN_HOME:-$HOME/.mosaic}"
|
||||||
|
slot="$brain_home/fleet/agents/$ident/secrets/gitea-$pfx-$ident.token"
|
||||||
|
if [[ -d "$brain_home/fleet/agents/$ident" ]]; then
|
||||||
|
printf '%s' "$slot"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
_gitea_resolve_seat_or_refuse() {
|
||||||
|
# $1=identity $2=instance-prefix $3=service-name (for messages).
|
||||||
|
# Seat with a readable slot → echoes the token (caller exports).
|
||||||
|
# Seat with an empty/missing slot → rc 1 with a named refusal.
|
||||||
|
# Not a seat → rc 2 (caller falls to the service store).
|
||||||
|
local ident="$1" pfx="$2" svc="$3" slot
|
||||||
|
slot="$(_gitea_seat_token "$ident" "$pfx")" || return 2
|
||||||
|
if [[ -r "$slot" ]] && [[ -s "$slot" ]]; then
|
||||||
|
tr -d '\n' <"$slot"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "Error: load_credentials $svc: git identity '$ident' resolves to a SEAT but its slot is empty or unreadable: $slot" >&2
|
||||||
|
echo " Refusing to fall back to the shared service store — that would act as the wrong identity. Provision the slot or unset MOSAIC_GIT_IDENTITY." >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
load_credentials() {
|
load_credentials() {
|
||||||
local service="$1"
|
local service="$1"
|
||||||
|
|
||||||
@@ -183,16 +230,30 @@ EOF
|
|||||||
;;
|
;;
|
||||||
gitea-mosaicstack)
|
gitea-mosaicstack)
|
||||||
export GITEA_URL="${GITEA_URL:-$(_mosaic_read_cred '.gitea.mosaicstack.url')}"
|
export GITEA_URL="${GITEA_URL:-$(_mosaic_read_cred '.gitea.mosaicstack.url')}"
|
||||||
export GITEA_TOKEN="${GITEA_TOKEN:-$(_mosaic_read_cred '.gitea.mosaicstack.token')}"
|
|
||||||
GITEA_URL="${GITEA_URL%/}"
|
GITEA_URL="${GITEA_URL%/}"
|
||||||
[[ -n "$GITEA_URL" ]] || { echo "Error: gitea.mosaicstack.url not found" >&2; return 1; }
|
[[ -n "$GITEA_URL" ]] || { echo "Error: gitea.mosaicstack.url not found" >&2; return 1; }
|
||||||
|
if [[ -z "${GITEA_TOKEN:-}" && -n "${MOSAIC_GIT_IDENTITY:-}" ]]; then
|
||||||
|
local _seat_tok
|
||||||
|
_seat_tok="$(_gitea_resolve_seat_or_refuse "$MOSAIC_GIT_IDENTITY" mosaicstack gitea-mosaicstack)" \
|
||||||
|
&& export GITEA_TOKEN="$_seat_tok" && return 0
|
||||||
|
local _src_rc=$?
|
||||||
|
[[ "$_src_rc" -eq 2 ]] || return 1
|
||||||
|
fi
|
||||||
|
export GITEA_TOKEN="${GITEA_TOKEN:-$(_mosaic_read_cred '.gitea.mosaicstack.token')}"
|
||||||
[[ -n "$GITEA_TOKEN" ]] || { echo "Error: gitea.mosaicstack.token not found" >&2; return 1; }
|
[[ -n "$GITEA_TOKEN" ]] || { echo "Error: gitea.mosaicstack.token not found" >&2; return 1; }
|
||||||
;;
|
;;
|
||||||
gitea-usc)
|
gitea-usc)
|
||||||
export GITEA_URL="${GITEA_URL:-$(_mosaic_read_cred '.gitea.usc.url')}"
|
export GITEA_URL="${GITEA_URL:-$(_mosaic_read_cred '.gitea.usc.url')}"
|
||||||
export GITEA_TOKEN="${GITEA_TOKEN:-$(_mosaic_read_cred '.gitea.usc.token')}"
|
|
||||||
GITEA_URL="${GITEA_URL%/}"
|
GITEA_URL="${GITEA_URL%/}"
|
||||||
[[ -n "$GITEA_URL" ]] || { echo "Error: gitea.usc.url not found" >&2; return 1; }
|
[[ -n "$GITEA_URL" ]] || { echo "Error: gitea.usc.url not found" >&2; return 1; }
|
||||||
|
if [[ -z "${GITEA_TOKEN:-}" && -n "${MOSAIC_GIT_IDENTITY:-}" ]]; then
|
||||||
|
local _seat_tok
|
||||||
|
_seat_tok="$(_gitea_resolve_seat_or_refuse "$MOSAIC_GIT_IDENTITY" usc gitea-usc)" \
|
||||||
|
&& export GITEA_TOKEN="$_seat_tok" && return 0
|
||||||
|
local _src_rc=$?
|
||||||
|
[[ "$_src_rc" -eq 2 ]] || return 1
|
||||||
|
fi
|
||||||
|
export GITEA_TOKEN="${GITEA_TOKEN:-$(_mosaic_read_cred '.gitea.usc.token')}"
|
||||||
[[ -n "$GITEA_TOKEN" ]] || { echo "Error: gitea.usc.token not found" >&2; return 1; }
|
[[ -n "$GITEA_TOKEN" ]] || { echo "Error: gitea.usc.token not found" >&2; return 1; }
|
||||||
;;
|
;;
|
||||||
woodpecker-*)
|
woodpecker-*)
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Hermetic regression for load_credentials gitea seat-slot resolution.
|
||||||
|
# Sandbox brain home + sandbox credentials.json; no real credential is read.
|
||||||
|
#
|
||||||
|
# Pins:
|
||||||
|
# G1 MOSAIC_GIT_IDENTITY naming a seat with a populated slot → GITEA_TOKEN
|
||||||
|
# comes from the SLOT, URL from credentials.json.
|
||||||
|
# G2 seat with an EMPTY slot → rc 1, refusal names the identity and the
|
||||||
|
# slot path, and NO fallback to the service store occurred (the token
|
||||||
|
# must not equal the service-store value).
|
||||||
|
# G3 no identity → service store, unchanged behavior (token from
|
||||||
|
# credentials.json).
|
||||||
|
# G4 identity that is NOT a seat (no directory) → service store (same as
|
||||||
|
# G3; the identity is irrelevant on a non-fleet path).
|
||||||
|
# G5 other services are untouched: woodpecker resolution works the same
|
||||||
|
# with and without MOSAIC_GIT_IDENTITY set.
|
||||||
|
# G6 pre-existing GITEA_TOKEN env is never overridden by the seat path.
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
W="${TMPDIR:-/tmp}/creds-seat-test-$$"
|
||||||
|
BRAIN="$W/brain"; CREDS="$W/credentials.json"
|
||||||
|
mkdir -p "$BRAIN/fleet/agents/live-seat/secrets" "$BRAIN/fleet/agents/empty-seat"
|
||||||
|
printf 'seat-token-value-abc123\n' > "$BRAIN/fleet/agents/live-seat/secrets/gitea-mosaicstack-live-seat.token"
|
||||||
|
cat > "$CREDS" <<'EOF'
|
||||||
|
{"gitea":{"mosaicstack":{"url":"https://gitea.example.test","token":"service-token-value-xyz789"}},
|
||||||
|
"woodpecker":{"default":"mosaic","mosaic":{"url":"https://ci.example.test","token":"wp-token-1"}}}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "$SCRIPT_DIR/credentials.sh"
|
||||||
|
|
||||||
|
fail() { echo "FAIL: $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
load_env() { # $1=service, $2=env-setup; clean subshell; echoes rc then token
|
||||||
|
local svc="$1" setup="$2"
|
||||||
|
(
|
||||||
|
eval "$setup"
|
||||||
|
unset GITEA_TOKEN GITEA_URL
|
||||||
|
export MOSAIC_CREDENTIALS_FILE="$CREDS" MOSAIC_BRAIN_HOME="$BRAIN"
|
||||||
|
load_credentials "$svc" >/dev/null 2>"$W/err"
|
||||||
|
rc=$?
|
||||||
|
printf '%s\n%s\n' "$rc" "${GITEA_TOKEN:-}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
# G1: seat with populated slot
|
||||||
|
out=$(load_env gitea-mosaicstack 'export MOSAIC_GIT_IDENTITY=live-seat')
|
||||||
|
rc=$(printf '%s' "$out" | sed -n 1p); tok=$(printf '%s' "$out" | sed -n 2p)
|
||||||
|
[ "$rc" = 0 ] || fail "G1: rc=$rc err=$(cat "$W/err")"
|
||||||
|
[ "$tok" = "seat-token-value-abc123" ] || fail "G1: token not from slot: ${tok:0:20}"
|
||||||
|
|
||||||
|
# G2: seat with empty slot refuses, no fallback
|
||||||
|
out=$(load_env gitea-mosaicstack 'export MOSAIC_GIT_IDENTITY=empty-seat')
|
||||||
|
rc=$(printf '%s' "$out" | sed -n 1p); tok=$(printf '%s' "$out" | sed -n 2p)
|
||||||
|
[ "$rc" = 1 ] || fail "G2: expected rc=1 refusal, got rc=$rc tok=${tok:0:20}"
|
||||||
|
[ "$tok" != "service-token-value-xyz789" ] || fail "G2: FELL BACK to service store on seat-miss"
|
||||||
|
grep -q "empty-seat" "$W/err" || fail "G2: refusal does not name the identity"
|
||||||
|
grep -q "fleet/agents/empty-seat" "$W/err" || fail "G2: refusal does not name the slot path"
|
||||||
|
|
||||||
|
# G3: no identity → service store
|
||||||
|
out=$(load_env gitea-mosaicstack 'unset MOSAIC_GIT_IDENTITY')
|
||||||
|
rc=$(printf '%s' "$out" | sed -n 1p); tok=$(printf '%s' "$out" | sed -n 2p)
|
||||||
|
[ "$rc" = 0 ] || fail "G3: rc=$rc err=$(cat "$W/err")"
|
||||||
|
[ "$tok" = "service-token-value-xyz789" ] || fail "G3: service-store token not loaded"
|
||||||
|
|
||||||
|
# G4: identity that is not a seat → service store
|
||||||
|
out=$(load_env gitea-mosaicstack 'export MOSAIC_GIT_IDENTITY=nobody')
|
||||||
|
rc=$(printf '%s' "$out" | sed -n 1p); tok=$(printf '%s' "$out" | sed -n 2p)
|
||||||
|
[ "$rc" = 0 ] || fail "G4: rc=$rc err=$(cat "$W/err")"
|
||||||
|
[ "$tok" = "service-token-value-xyz789" ] || fail "G4: non-seat identity broke the service path"
|
||||||
|
|
||||||
|
# G5: woodpecker ignores MOSAIC_GIT_IDENTITY entirely
|
||||||
|
( export MOSAIC_CREDENTIALS_FILE="$CREDS"
|
||||||
|
export MOSAIC_GIT_IDENTITY=live-seat
|
||||||
|
unset WOODPECKER_URL WOODPECKER_TOKEN
|
||||||
|
load_credentials woodpecker >/dev/null 2>&1 || fail "G5: woodpecker load failed with identity set"
|
||||||
|
[ "$WOODPECKER_TOKEN" = "wp-token-1" ] || fail "G5: woodpecker token wrong"
|
||||||
|
[ "$WOODPECKER_URL" = "https://ci.example.test" ] || fail "G5: woodpecker url wrong" )
|
||||||
|
|
||||||
|
# G6: pre-set GITEA_TOKEN env is preserved (both arms)
|
||||||
|
( export MOSAIC_CREDENTIALS_FILE="$CREDS" MOSAIC_BRAIN_HOME="$BRAIN"
|
||||||
|
export MOSAIC_GIT_IDENTITY=live-seat GITEA_TOKEN=already-set-env
|
||||||
|
load_credentials gitea-mosaicstack >/dev/null 2>&1 || fail "G6: load failed"
|
||||||
|
[ "$GITEA_TOKEN" = "already-set-env" ] || fail "G6: seat path overrode existing GITEA_TOKEN" )
|
||||||
|
|
||||||
|
rm -rf "$W"
|
||||||
|
echo "credentials seat-slot regression passed"
|
||||||
@@ -468,6 +468,14 @@ get_gitea_login_for_repo_override() {
|
|||||||
echo "$canon"
|
echo "$canon"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
# Same split as the host path above (#1357 S1): a missing tea binary
|
||||||
|
# is not a missing login, and the "create it with" advice cannot be
|
||||||
|
# followed without tea.
|
||||||
|
if ! command -v tea >/dev/null 2>&1; then
|
||||||
|
echo "Error: git identity '$ident' (via $ident_src) requested for owner '${owner%%/*}', but tea is not installed," >&2
|
||||||
|
echo " so no login can be resolved. Refusing to guess an identity." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
echo "Error: git identity '$ident' (via $ident_src) has no tea login '$canon' for owner '${owner%%/*}'." >&2
|
echo "Error: git identity '$ident' (via $ident_src) has no tea login '$canon' for owner '${owner%%/*}'." >&2
|
||||||
echo " Create it with: ~/.config/mosaic/tools/fleet/seat-logins.sh --apply --seat $ident" >&2
|
echo " Create it with: ~/.config/mosaic/tools/fleet/seat-logins.sh --apply --seat $ident" >&2
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ case "$PLATFORM" in
|
|||||||
gitea)
|
gitea)
|
||||||
if [[ -n "$REPO_OVERRIDE" ]]; then
|
if [[ -n "$REPO_OVERRIDE" ]]; then
|
||||||
GITEA_LOGIN_NAME=$(get_gitea_login_for_repo_override "$REPO_OVERRIDE") || {
|
GITEA_LOGIN_NAME=$(get_gitea_login_for_repo_override "$REPO_OVERRIDE") || {
|
||||||
echo "Error: Could not resolve Gitea login for --repo override. Set GITEA_LOGIN or configure a default tea login." >&2
|
echo "Error: could not resolve a Gitea login for the --repo override (the lines above say why). Set GITEA_LOGIN to name one explicitly." >&2
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# issue-view.sh - View issue details on GitHub or Gitea
|
# issue-view.sh - View issue details, including comments, on GitHub or Gitea
|
||||||
# Usage: issue-view.sh -i <issue_number>
|
# Usage: issue-view.sh -i <issue_number>
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
@@ -28,11 +28,47 @@ gitea_issue_view_api() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
url="https://${host}/api/v1/repos/${repo}/issues/${ISSUE_NUMBER}"
|
url="https://${host}/api/v1/repos/${repo}/issues/${ISSUE_NUMBER}"
|
||||||
if command -v python3 >/dev/null 2>&1; then
|
local -a curl_args=(-fsS -H "User-Agent: curl/8" -H "Authorization: token ${token}")
|
||||||
curl -fsS -H "User-Agent: curl/8" -H "Authorization: token ${token}" "$url" | python3 -m json.tool
|
if ! command -v python3 >/dev/null 2>&1; then
|
||||||
else
|
# No renderer: raw JSON is all this path can give. Comments are a
|
||||||
curl -fsS -H "User-Agent: curl/8" -H "Authorization: token ${token}" "$url"
|
# second resource, so fetch them too rather than only the count.
|
||||||
|
curl "${curl_args[@]}" "$url"
|
||||||
|
curl "${curl_args[@]}" "${url}/comments"
|
||||||
|
return
|
||||||
fi
|
fi
|
||||||
|
# Render issue + comments as text (#1357 F2). The old fallback dumped the
|
||||||
|
# issue JSON, which carries only a comment COUNT, so every comment body was
|
||||||
|
# invisible on this path and the wrapper could never show what
|
||||||
|
# `tea issues --comments` shows.
|
||||||
|
{
|
||||||
|
curl "${curl_args[@]}" "$url"
|
||||||
|
echo
|
||||||
|
echo "__MOSAIC_COMMENTS__"
|
||||||
|
curl "${curl_args[@]}" "${url}/comments"
|
||||||
|
} | python3 -c '
|
||||||
|
import json, sys
|
||||||
|
raw = sys.stdin.read()
|
||||||
|
issue_raw, _, comments_raw = raw.partition("__MOSAIC_COMMENTS__")
|
||||||
|
issue = json.loads(issue_raw)
|
||||||
|
comments = json.loads(comments_raw) if comments_raw.strip() else []
|
||||||
|
print("#%s %s" % (issue["number"], issue["title"]))
|
||||||
|
print("State: %s Author: %s Created: %s" % (issue["state"], issue["user"]["login"], issue["created_at"]))
|
||||||
|
labels = ", ".join(l["name"] for l in issue.get("labels") or [])
|
||||||
|
if labels:
|
||||||
|
print("Labels: " + labels)
|
||||||
|
if issue.get("milestone"):
|
||||||
|
print("Milestone: " + issue["milestone"]["title"])
|
||||||
|
print("URL: " + issue["html_url"])
|
||||||
|
print()
|
||||||
|
print(issue.get("body") or "(no body)")
|
||||||
|
if comments:
|
||||||
|
print()
|
||||||
|
print("--- Comments (%d) ---" % len(comments))
|
||||||
|
for c in comments:
|
||||||
|
print()
|
||||||
|
print("[%s at %s]" % (c["user"]["login"], c["created_at"]))
|
||||||
|
print(c.get("body") or "")
|
||||||
|
'
|
||||||
}
|
}
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
@@ -46,6 +82,8 @@ while [[ $# -gt 0 ]]; do
|
|||||||
echo ""
|
echo ""
|
||||||
echo "Options:"
|
echo "Options:"
|
||||||
echo " -i, --issue Issue number (required)"
|
echo " -i, --issue Issue number (required)"
|
||||||
|
echo ""
|
||||||
|
echo "Comments are always included (tea --comments / Gitea API /comments)."
|
||||||
echo " -h, --help Show this help"
|
echo " -h, --help Show this help"
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
@@ -67,11 +105,30 @@ if [[ "$PLATFORM" == "github" ]]; then
|
|||||||
gh issue view "$ISSUE_NUMBER"
|
gh issue view "$ISSUE_NUMBER"
|
||||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||||
if command -v tea >/dev/null 2>&1; then
|
if command -v tea >/dev/null 2>&1; then
|
||||||
if tea issue "$ISSUE_NUMBER" $(get_gitea_repo_args); then
|
# --comments is what makes tea print the comment bodies (#1357 F3).
|
||||||
|
# Without it tea prompts for them interactively, which in a
|
||||||
|
# non-interactive wrapper means they are silently never shown.
|
||||||
|
tea_err=$(mktemp)
|
||||||
|
if tea issue "$ISSUE_NUMBER" $(get_gitea_repo_args) --comments 2>"$tea_err"; then
|
||||||
|
rm -f "$tea_err"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
echo "Warning: tea issue view failed, trying Gitea API fallback..." >&2
|
# Name the cause tea actually reported, not a guessed one (#1357 F1/F4).
|
||||||
{ declare -F explain_tea_user_does_not_exist >/dev/null && explain_tea_user_does_not_exist; } || true
|
# tea reads the cwd's git config before honouring --repo; a repo with
|
||||||
|
# extensions.worktreeconfig=true makes it exit 1 with a
|
||||||
|
# repositoryformatversion error. That is a git-config condition, not a
|
||||||
|
# credential one. The old path printed the REVOKED OR STALE TOKEN note
|
||||||
|
# here unconditionally, which sent readers to rotate a token that was fine.
|
||||||
|
if grep -q 'repositoryformatversion' "$tea_err"; then
|
||||||
|
echo "Warning: tea cannot read this repo's git config (extensions.worktreeconfig); not a credential problem. Using Gitea API fallback." >&2
|
||||||
|
elif grep -q 'user does not exist' "$tea_err"; then
|
||||||
|
echo "Warning: tea issue view failed, trying Gitea API fallback..." >&2
|
||||||
|
{ declare -F explain_tea_user_does_not_exist >/dev/null && explain_tea_user_does_not_exist; } || true
|
||||||
|
else
|
||||||
|
echo "Warning: tea issue view failed, trying Gitea API fallback..." >&2
|
||||||
|
fi
|
||||||
|
sed 's/^/ tea: /' "$tea_err" >&2
|
||||||
|
rm -f "$tea_err"
|
||||||
fi
|
fi
|
||||||
gitea_issue_view_api
|
gitea_issue_view_api
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ case "$PLATFORM" in
|
|||||||
gitea)
|
gitea)
|
||||||
if [[ -n "$REPO_OVERRIDE" ]]; then
|
if [[ -n "$REPO_OVERRIDE" ]]; then
|
||||||
GITEA_LOGIN_NAME=$(get_gitea_login_for_repo_override "$REPO_OVERRIDE") || {
|
GITEA_LOGIN_NAME=$(get_gitea_login_for_repo_override "$REPO_OVERRIDE") || {
|
||||||
echo "Error: Could not resolve Gitea login for --repo override. Set GITEA_LOGIN or configure a default tea login." >&2
|
echo "Error: could not resolve a Gitea login for the --repo override (the lines above say why). Set GITEA_LOGIN to name one explicitly." >&2
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ if [[ "$PLATFORM" == "github" ]]; then
|
|||||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||||
if [[ -n "$REPO_OVERRIDE" ]]; then
|
if [[ -n "$REPO_OVERRIDE" ]]; then
|
||||||
GITEA_LOGIN_NAME=$(get_gitea_login_for_repo_override "$REPO_OVERRIDE") || {
|
GITEA_LOGIN_NAME=$(get_gitea_login_for_repo_override "$REPO_OVERRIDE") || {
|
||||||
echo "Error: Could not resolve Gitea login for --repo override. Set GITEA_LOGIN or configure a default tea login." >&2
|
echo "Error: could not resolve a Gitea login for the --repo override (the lines above say why). Set GITEA_LOGIN to name one explicitly." >&2
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -463,6 +463,34 @@ if [[ "$override_explicit" != "mosaicstack" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Branch 6 (#1357 S1): with tea ABSENT from PATH, the override path must say tea is
|
||||||
|
# missing, not "no tea login named X exists" (a cause that was never checked) and
|
||||||
|
# not the seat-logins.sh advice, which cannot be followed without tea.
|
||||||
|
NOTEA_BIN="$WORK_DIR/notea-bin"; mkdir -p "$NOTEA_BIN"
|
||||||
|
for t in bash git python3 sed grep cat mktemp dirname basename readlink env sort head tr cut; do
|
||||||
|
_p="$(command -v "$t" 2>/dev/null || true)"; [[ -n "$_p" ]] && ln -sf "$_p" "$NOTEA_BIN/$t"
|
||||||
|
done
|
||||||
|
override_notea_rc=0
|
||||||
|
override_notea_err=$(cd "$REPO_DIR" && env -u GITEA_LOGIN \
|
||||||
|
PATH="$NOTEA_BIN" HOME="$HOME_DIR" MOSAIC_GIT_IDENTITY=testseat \
|
||||||
|
bash -c '
|
||||||
|
command -v tea >/dev/null 2>&1 && { echo "SETUP: tea still on PATH"; exit 99; }
|
||||||
|
source "'"$SCRIPT_DIR"'/detect-platform.sh"
|
||||||
|
get_gitea_login_for_repo_override mosaicstack/stack
|
||||||
|
' 2>&1 >/dev/null) || override_notea_rc=$?
|
||||||
|
if [[ "$override_notea_rc" != 1 ]]; then
|
||||||
|
echo "Expected --repo override path to fail (rc=1) with tea absent; got rc=$override_notea_rc: $override_notea_err" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! grep -q 'tea is not installed' <<<"$override_notea_err"; then
|
||||||
|
echo "Expected --repo override path to name tea as absent; got: $override_notea_err" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if grep -q 'has no tea login\|seat-logins.sh' <<<"$override_notea_err"; then
|
||||||
|
echo "Override path diagnosed a missing LOGIN while tea itself is absent: $override_notea_err" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
git -C "$REPO_DIR" remote set-url origin https://git.uscllc.com/USC/uconnect.git
|
git -C "$REPO_DIR" remote set-url origin https://git.uscllc.com/USC/uconnect.git
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Regression: issue-view.sh must show comment BODIES, on both paths, and must name
|
||||||
|
# the failure tea actually reported instead of guessing a credential cause (#1357).
|
||||||
|
#
|
||||||
|
# Four defects, each with its own case below:
|
||||||
|
# F1 tea exits 1 in any repo with extensions.worktreeconfig=true; the wrapper must
|
||||||
|
# say so (git-config condition) and fall back to the API.
|
||||||
|
# F2 the API fallback dumped raw issue JSON, which carries only a comment COUNT.
|
||||||
|
# F3 the tea path never passed --comments, so tea prompted (non-interactively: nothing).
|
||||||
|
# F4 on ANY tea failure the wrapper printed the REVOKED OR STALE TOKEN note.
|
||||||
|
#
|
||||||
|
# Verification bar (plan §6): assert a real comment BODY appears, not a count and not
|
||||||
|
# `grep -c comment` (that instrument matched the issue title and read inverted).
|
||||||
|
#
|
||||||
|
# Hermetic: mock tea and curl on PATH, sandboxed repo. Resolves no real credentials.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
WORK_ROOT="${AGENT_WORK_ROOT:-${TMPDIR:-/tmp}}"
|
||||||
|
SANDBOX="$WORK_ROOT/issue-view-comments-test-$$"
|
||||||
|
MOCK_BIN="$SANDBOX/bin"; REPO_DIR="$SANDBOX/repo"; CALLS="$SANDBOX/calls.log"
|
||||||
|
cleanup() { rm -rf "$SANDBOX"; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
TARGET="$SCRIPT_DIR/issue-view.sh"
|
||||||
|
[ -f "$TARGET" ] || { echo "FAIL: issue-view.sh not found beside this test"; exit 1; }
|
||||||
|
fail() { echo "FAIL: $*"; exit 1; }
|
||||||
|
|
||||||
|
mkdir -p "$MOCK_BIN" "$REPO_DIR" || fail "setup: cannot create sandbox under $WORK_ROOT"
|
||||||
|
: > "$CALLS" || fail "setup: cannot write calls log at $CALLS"
|
||||||
|
cd "$REPO_DIR" || fail "setup: cannot cd into $REPO_DIR"
|
||||||
|
git init -q || fail "setup: git init failed"
|
||||||
|
git remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git || fail "setup: git remote add failed"
|
||||||
|
export PATH="$MOCK_BIN:$PATH" CALLS
|
||||||
|
export GITEA_URL="https://git.mosaicstack.dev"
|
||||||
|
export GITEA_TOKEN="redacted-test-token"
|
||||||
|
# The identity ladder must not reach for this seat's real login; the mock tea below
|
||||||
|
# defines the only login that exists in this sandbox.
|
||||||
|
unset MOSAIC_GIT_IDENTITY
|
||||||
|
# No fleet in the sandbox: on a host that runs one, get_gitea_token fails closed for an
|
||||||
|
# identity-less caller (by design), which would make this test measure the host, not
|
||||||
|
# the wrapper. An empty brain home makes the sandbox the same on every host.
|
||||||
|
export MOSAIC_BRAIN_HOME="$SANDBOX/brain"
|
||||||
|
mkdir -p "$MOSAIC_BRAIN_HOME" || fail "setup: cannot create sandbox brain home"
|
||||||
|
|
||||||
|
# Distinctive strings: a comment body that appears nowhere else, and an issue title
|
||||||
|
# that contains the word "comment" so a count-of-the-word instrument would misread.
|
||||||
|
BODY_MARKER="zebra-quill-comment-body-7731"
|
||||||
|
ISSUE_TITLE="wrapper never shows a comment"
|
||||||
|
|
||||||
|
# --- mock curl: serves the issue and its comments; logs every call --------------
|
||||||
|
cat > "$MOCK_BIN/curl" <<EOF
|
||||||
|
#!/bin/bash
|
||||||
|
url=""
|
||||||
|
while [ \$# -gt 0 ]; do
|
||||||
|
case "\$1" in
|
||||||
|
http*) url="\$1"; shift ;;
|
||||||
|
*) shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
printf 'curl %s\n' "\$url" >> "$CALLS"
|
||||||
|
case "\$url" in
|
||||||
|
*/issues/77/comments)
|
||||||
|
if [ "\${MOCK_NO_COMMENTS:-}" = "1" ]; then echo '[]'; else
|
||||||
|
echo '[{"id":1,"user":{"login":"alice"},"created_at":"2026-08-21T00:00:00Z","body":"$BODY_MARKER"}]'; fi ;;
|
||||||
|
*/issues/77)
|
||||||
|
echo '{"number":77,"title":"$ISSUE_TITLE","state":"open","user":{"login":"bob"},"created_at":"2026-08-21T00:00:00Z","labels":[],"milestone":null,"html_url":"https://git.mosaicstack.dev/mosaicstack/stack/issues/77","body":"issue body","comments":1}' ;;
|
||||||
|
*) echo '{}' ;;
|
||||||
|
esac
|
||||||
|
exit 0
|
||||||
|
EOF
|
||||||
|
chmod +x "$MOCK_BIN/curl"
|
||||||
|
|
||||||
|
# --- mock tea: MOCK_TEA_MODE selects the behaviour under test --------------------
|
||||||
|
# ok : prints the issue, and the comment body ONLY when --comments is passed (F3)
|
||||||
|
# wtconfig : exits 1 with the repositoryformatversion error (F1/F4)
|
||||||
|
# badtoken : exits 1 with tea's credential error (F4 control: credential wording allowed)
|
||||||
|
cat > "$MOCK_BIN/tea" <<EOF
|
||||||
|
#!/bin/bash
|
||||||
|
printf 'tea %s\n' "\$*" >> "$CALLS"
|
||||||
|
if [[ "\$*" == *"login list"* ]]; then
|
||||||
|
echo '[{"name":"git.mosaicstack.dev","url":"https://git.mosaicstack.dev"}]'; exit 0
|
||||||
|
fi
|
||||||
|
case "\${MOCK_TEA_MODE:-ok}" in
|
||||||
|
wtconfig) echo 'Error: core.repositoryformatversion does not support extension: worktreeconfig' >&2; exit 1 ;;
|
||||||
|
badtoken) echo 'Failed to create Gitea client: invalid username, password or token' >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
echo "# #77 $ISSUE_TITLE (open)"
|
||||||
|
echo "issue body"
|
||||||
|
if [[ "\$*" == *"--comments"* ]]; then echo "$BODY_MARKER"; fi
|
||||||
|
exit 0
|
||||||
|
EOF
|
||||||
|
chmod +x "$MOCK_BIN/tea"
|
||||||
|
|
||||||
|
[ "$(command -v tea)" = "$MOCK_BIN/tea" ] || fail "setup: tea does not resolve inside the sandbox"
|
||||||
|
[ "$(command -v curl)" = "$MOCK_BIN/curl" ] || fail "setup: curl does not resolve inside the sandbox"
|
||||||
|
|
||||||
|
run() { bash "$TARGET" -i 77 >"$SANDBOX/out" 2>"$SANDBOX/err"; echo $?; }
|
||||||
|
|
||||||
|
# F3: tea path shows the comment body, which the mock emits only under --comments.
|
||||||
|
: > "$CALLS"
|
||||||
|
rc=$(MOCK_TEA_MODE=ok run)
|
||||||
|
[ "$rc" = 0 ] || fail "F3: expected rc=0 on the tea path, got $rc: $(cat "$SANDBOX/err")"
|
||||||
|
grep -q -- '--comments' "$CALLS" || fail "F3: tea was not invoked with --comments: $(cat "$CALLS")"
|
||||||
|
grep -q "$BODY_MARKER" "$SANDBOX/out" || fail "F3: comment body missing from tea-path output"
|
||||||
|
if grep -q '^curl' "$CALLS"; then fail "F3: tea path succeeded but the API fallback ran anyway"; fi
|
||||||
|
|
||||||
|
# F1 + F2: worktreeconfig failure is named as a git-config condition, falls back to
|
||||||
|
# the API, and the API rendering includes the comment BODY.
|
||||||
|
: > "$CALLS"
|
||||||
|
rc=$(MOCK_TEA_MODE=wtconfig run)
|
||||||
|
[ "$rc" = 0 ] || fail "F1: expected rc=0 via API fallback, got $rc: $(cat "$SANDBOX/err")"
|
||||||
|
grep -q 'worktreeconfig' "$SANDBOX/err" || fail "F1: stderr does not name the worktreeconfig cause: $(cat "$SANDBOX/err")"
|
||||||
|
grep -q 'not a credential problem' "$SANDBOX/err" || fail "F1: stderr does not rule out the credential cause"
|
||||||
|
grep -q 'issues/77/comments' "$CALLS" || fail "F2: API fallback never fetched /comments: $(cat "$CALLS")"
|
||||||
|
grep -q "$BODY_MARKER" "$SANDBOX/out" || fail "F2: comment body missing from API-path output"
|
||||||
|
grep -q "$ISSUE_TITLE" "$SANDBOX/out" || fail "F2: issue title missing from API-path output"
|
||||||
|
if grep -q 'REVOKED OR STALE' "$SANDBOX/err"; then fail "F4: stale-token note printed for a git-config failure"; fi
|
||||||
|
if grep -q '"comments": 1' "$SANDBOX/out"; then fail "F2: output is still raw JSON (comment count instead of bodies)"; fi
|
||||||
|
|
||||||
|
# F4 control: a real credential error from tea may still carry the credential note,
|
||||||
|
# and tea's own line must be relayed so the reader sees the actual cause.
|
||||||
|
: > "$CALLS"
|
||||||
|
rc=$(MOCK_TEA_MODE=badtoken run)
|
||||||
|
[ "$rc" = 0 ] || fail "F4 control: expected rc=0 via API fallback, got $rc"
|
||||||
|
grep -q 'invalid username, password or token' "$SANDBOX/err" || fail "F4: tea's own error line was not relayed"
|
||||||
|
if grep -q 'worktreeconfig' "$SANDBOX/err"; then fail "F4: git-config wording printed for a credential failure"; fi
|
||||||
|
|
||||||
|
# Negative control: an issue with no comments prints no comment section on the API
|
||||||
|
# path. Without this, a renderer that always prints a section would pass F2.
|
||||||
|
: > "$CALLS"
|
||||||
|
rc=$(MOCK_TEA_MODE=wtconfig MOCK_NO_COMMENTS=1 run)
|
||||||
|
[ "$rc" = 0 ] || fail "negative control: expected rc=0, got $rc"
|
||||||
|
if grep -q -- '--- Comments' "$SANDBOX/out"; then fail "negative control: comment section printed for an issue with no comments"; fi
|
||||||
|
if grep -q "$BODY_MARKER" "$SANDBOX/out"; then fail "negative control: a comment body appeared for an issue with no comments"; fi
|
||||||
|
|
||||||
|
echo "issue-view comments regression harness passed"
|
||||||
@@ -32,7 +32,9 @@
|
|||||||
# 0 delivered (submitted) or queued (agent busy; will process when free)
|
# 0 delivered (submitted) or queued (agent busy; will process when free)
|
||||||
# 1 tmux target not found
|
# 1 tmux target not found
|
||||||
# 2 submission NOT confirmed — either still an unsubmitted draft, or the REPL
|
# 2 submission NOT confirmed — either still an unsubmitted draft, or the REPL
|
||||||
# input prompt could not be located to confirm the message actually landed.
|
# input box could not be located to confirm the message actually landed.
|
||||||
|
# Locating the box is runtime-specific; see locate_input_box() below, and
|
||||||
|
# add a shape there before pointing this tool at a new runtime.
|
||||||
# Delivery is NEVER inferred from absence of evidence: if we cannot positively
|
# Delivery is NEVER inferred from absence of evidence: if we cannot positively
|
||||||
# see the input box clear of the message (or the queued banner), we fail loud
|
# see the input box clear of the message (or the queued banner), we fail loud
|
||||||
# so the sender learns immediately instead of a silent worker->lead stall.
|
# so the sender learns immediately instead of a silent worker->lead stall.
|
||||||
@@ -97,10 +99,50 @@ printf '%s' "$MSG" | "${tmux_cmd[@]}" load-buffer -b "$BUF" -
|
|||||||
# would otherwise accumulate forever.
|
# would otherwise accumulate forever.
|
||||||
sleep 0.5
|
sleep 0.5
|
||||||
|
|
||||||
|
# Locate the REPL input box in a captured pane. Prints the box's contents on
|
||||||
|
# stdout and returns 0 when the box was FOUND; returns 1 when it could not be
|
||||||
|
# located at all. Found-but-empty is a real, distinct answer (an empty input box
|
||||||
|
# is what a submitted message leaves behind), so the caller must branch on the
|
||||||
|
# return code, never on whether the output is empty.
|
||||||
|
#
|
||||||
|
# Two REPL shapes are recognised:
|
||||||
|
# * a prompt-glyph line — `❯`, a leading `>`, or `│ >`. Claude Code and most
|
||||||
|
# readline REPLs.
|
||||||
|
# * a box drawn as two horizontal `─` rules with the input between them and NO
|
||||||
|
# prompt glyph anywhere. pi renders this. Anchoring on the LAST rule pair is
|
||||||
|
# what makes it safe: agent output can contain its own rules, but nothing is
|
||||||
|
# drawn below the input box except the status line.
|
||||||
|
#
|
||||||
|
# Adding a runtime means adding its shape HERE. A shape that is missing does not
|
||||||
|
# degrade gracefully: it turns every send to that runtime into a false
|
||||||
|
# "may be UNDELIVERED", which is what #1362 measured on pi and #1257 on another
|
||||||
|
# arm of the same probe.
|
||||||
|
locate_input_box() {
|
||||||
|
local pane=$1 glyph_line rule_lines top bottom
|
||||||
|
glyph_line=$(printf '%s\n' "$pane" | grep -E '❯|^>|│ >' | tail -1)
|
||||||
|
if [ -n "$glyph_line" ]; then printf '%s\n' "$glyph_line"; return 0; fi
|
||||||
|
rule_lines=$(printf '%s\n' "$pane" | grep -nE '^[[:space:]]*─{4,}[[:space:]]*$' | cut -d: -f1 | tail -2)
|
||||||
|
[ -n "$rule_lines" ] || return 1
|
||||||
|
# Split the (at most two) captured line numbers with parameter expansion. Not
|
||||||
|
# `head -1`: piping into an early-exiting consumer SIGPIPEs the producer, which
|
||||||
|
# under `set -euo pipefail` aborts the caller with rc=141 and no output. The
|
||||||
|
# scripts/pipefail-early-exit.test.mjs guard reds on that shape, correctly.
|
||||||
|
# With one rule captured both halves resolve to the same value and the
|
||||||
|
# ordering test below rejects it, which is the answer we want anyway.
|
||||||
|
top=${rule_lines%%$'\n'*}
|
||||||
|
bottom=${rule_lines##*$'\n'}
|
||||||
|
[ "$top" != "$bottom" ] || return 1
|
||||||
|
[ "$bottom" -gt "$top" ] || return 1
|
||||||
|
# An empty range (adjacent rules) prints nothing and still returns 0: found,
|
||||||
|
# empty, which is the delivered shape.
|
||||||
|
printf '%s\n' "$pane" | sed -n "$((top + 1)),$((bottom - 1))p"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# 2) Submit, then POSITIVELY confirm submission; flush with another Enter if it is
|
# 2) Submit, then POSITIVELY confirm submission; flush with another Enter if it is
|
||||||
# still a draft. Success requires positive evidence — the queued banner, OR the
|
# still a draft. Success requires positive evidence — the queued banner, OR the
|
||||||
# REPL input box located AND clear of our message tail. The historical bug was
|
# REPL input box located AND clear of our message tail. The historical bug was
|
||||||
# treating ABSENCE of a draft as delivery: if the prompt glyph was never matched
|
# treating ABSENCE of a draft as delivery: if the input box was never located
|
||||||
# (wrong pane / prompt-glyph drift), an unsubmitted message read as "delivered"
|
# (wrong pane / prompt-glyph drift), an unsubmitted message read as "delivered"
|
||||||
# and worker->lead relays stalled silently. We now default to UNCONFIRMED and only
|
# and worker->lead relays stalled silently. We now default to UNCONFIRMED and only
|
||||||
# upgrade to delivered on positive evidence; anything we cannot confirm fails loud.
|
# upgrade to delivered on positive evidence; anything we cannot confirm fails loud.
|
||||||
@@ -113,15 +155,14 @@ for attempt in $(seq 1 $((RETRIES + 1))); do
|
|||||||
if grep -qF "$QUEUED_RE" <<<"$pane"; then
|
if grep -qF "$QUEUED_RE" <<<"$pane"; then
|
||||||
status="queued"; break
|
status="queued"; break
|
||||||
fi
|
fi
|
||||||
# Locate the REPL input box (prompt glyph). If we cannot see it, we have NO
|
# If we cannot see the input box, we have NO evidence of submission state —
|
||||||
# evidence of submission state — stay UNCONFIRMED and retry; never infer delivery.
|
# stay UNCONFIRMED and retry; never infer delivery.
|
||||||
promptline=$(printf '%s' "$pane" | grep -E '❯|^>|│ >' | tail -1)
|
if ! inputbox=$(locate_input_box "$pane"); then
|
||||||
if [ -z "$promptline" ]; then
|
|
||||||
status="unconfirmed"; continue
|
status="unconfirmed"; continue
|
||||||
fi
|
fi
|
||||||
# Input box located AND still carrying our tail => unsubmitted draft. Flush + retry.
|
# Input box located AND still carrying our tail => unsubmitted draft. Flush + retry.
|
||||||
# (Submitted messages scroll up into history; a draft stays on the ❯ line.)
|
# (Submitted messages scroll up into history; a draft stays in the box.)
|
||||||
if [ -n "$snippet" ] && grep -qF "$snippet" <<<"$promptline"; then
|
if [ -n "$snippet" ] && grep -qF "$snippet" <<<"$inputbox"; then
|
||||||
status="draft"; continue
|
status="draft"; continue
|
||||||
fi
|
fi
|
||||||
# Input box located AND clear of our tail => positively submitted. This is the
|
# Input box located AND clear of our tail => positively submitted. This is the
|
||||||
@@ -135,6 +176,6 @@ case "$status" in
|
|||||||
delivered) echo "✓ delivered to $TARGET"; exit 0 ;;
|
delivered) echo "✓ delivered to $TARGET"; exit 0 ;;
|
||||||
queued) echo "✓ queued to $TARGET (agent busy — will process when it returns to prompt)"; exit 0 ;;
|
queued) echo "✓ queued to $TARGET (agent busy — will process when it returns to prompt)"; exit 0 ;;
|
||||||
draft) echo "✗ still an unsubmitted draft on $TARGET after $RETRIES flush attempts" >&2; exit 2 ;;
|
draft) echo "✗ still an unsubmitted draft on $TARGET after $RETRIES flush attempts" >&2; exit 2 ;;
|
||||||
unconfirmed) echo "✗ could not confirm submission on $TARGET: REPL input prompt not locatable after $((RETRIES + 1)) attempts — message may be UNDELIVERED (check target/pane, retry, or escalate)" >&2; exit 2 ;;
|
unconfirmed) echo "✗ could not confirm submission on $TARGET: REPL input box not locatable after $((RETRIES + 1)) attempts — message may be UNDELIVERED (check target/pane, retry, or escalate)" >&2; exit 2 ;;
|
||||||
*) echo "✗ could not confirm submission on $TARGET (unexpected state '$status')" >&2; exit 2 ;;
|
*) echo "✗ could not confirm submission on $TARGET (unexpected state '$status')" >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -10,6 +10,13 @@
|
|||||||
# "could not confirm submission").
|
# "could not confirm submission").
|
||||||
# 3. DRAFT — a `❯ `-prompt pane that never submits (message stays on the
|
# 3. DRAFT — a `❯ `-prompt pane that never submits (message stays on the
|
||||||
# input line) => exit 2, stderr "unsubmitted draft".
|
# input line) => exit 2, stderr "unsubmitted draft".
|
||||||
|
# 4. DELIVERED — a pane whose input box is two `─` rules with NO prompt glyph
|
||||||
|
# (box shape) anywhere (pi's shape) and which submits => exit 0. Pre-#1362
|
||||||
|
# the glyph probe could not see this box at all, so EVERY send
|
||||||
|
# to such a pane reported "may be UNDELIVERED" while landing.
|
||||||
|
# 5. DRAFT — the same glyphless box, holding our tail across every flush
|
||||||
|
# (box shape) Enter => exit 2, stderr "unsubmitted draft". Pre-#1362 this
|
||||||
|
# also reported unconfirmed, so the true state was invisible.
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
HERE=$(cd -- "$(dirname -- "$0")" && pwd)
|
HERE=$(cd -- "$(dirname -- "$0")" && pwd)
|
||||||
@@ -69,6 +76,56 @@ else
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- Fixtures 4 and 5: a pi-shaped pane. The input box is two `─` rules with the
|
||||||
|
# text between them and NO prompt glyph anywhere, so the glyph probe alone can
|
||||||
|
# never locate it and every send reports "may be UNDELIVERED" (#1362). The
|
||||||
|
# renderer below is the shape, not the runtime: MODE=clear submits (box empties),
|
||||||
|
# MODE=keep leaves the text sitting in the box.
|
||||||
|
cat > "$TMP/pibox.sh" <<'PIBOX'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
MODE=${1:-clear}
|
||||||
|
RULE=$(printf '─%.0s' $(seq 1 60))
|
||||||
|
buf=""
|
||||||
|
draw() {
|
||||||
|
printf '\033[H\033[2J'
|
||||||
|
printf 'fixture output line\n\n'
|
||||||
|
printf '%s\n' "$RULE"
|
||||||
|
printf '%s\n' "$buf"
|
||||||
|
printf '%s\n' "$RULE"
|
||||||
|
printf '~/fixture (main)\n'
|
||||||
|
printf 'tok 0 model fixture\n'
|
||||||
|
}
|
||||||
|
draw
|
||||||
|
while IFS= read -r line; do
|
||||||
|
# keep: hold the tail across every flush Enter, which is what a stuck draft does.
|
||||||
|
if [ "$MODE" = keep ]; then [ -n "$line" ] && buf=$line; else buf=""; fi
|
||||||
|
draw
|
||||||
|
done
|
||||||
|
PIBOX
|
||||||
|
chmod +x "$TMP/pibox.sh"
|
||||||
|
|
||||||
|
tmux -L "$SOCKET" new-session -d -s pibox -c "$TMP" "exec bash '$TMP/pibox.sh' clear"
|
||||||
|
sleep 0.3
|
||||||
|
out=$("$SEND" -L "$SOCKET" -t "=pibox" -m "pi fixture four delivered ok" 2>"$TMP/e4"); rc=$?
|
||||||
|
if [ "$rc" -eq 0 ] && grep -qF "✓ delivered" <<<"$out"; then
|
||||||
|
ok "delivered: glyphless box-drawn REPL that submits => exit 0 ✓ delivered"
|
||||||
|
else
|
||||||
|
no "delivered: glyphless box-drawn REPL that submits => exit 0 ✓ delivered" "rc=$rc out=[$out] err=[$(cat "$TMP/e4")]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
tmux -L "$SOCKET" new-session -d -s piboxdraft -c "$TMP" "exec bash '$TMP/pibox.sh' keep"
|
||||||
|
sleep 0.3
|
||||||
|
if out=$("$SEND" -L "$SOCKET" -t "=piboxdraft" -r 1 -m "pi fixture five stuck in the box" 2>"$TMP/e5"); then
|
||||||
|
no "draft: glyphless box-drawn pane holding our tail must NOT report success" "expected exit 2, got 0 (out=[$out])"
|
||||||
|
else
|
||||||
|
rc=$?
|
||||||
|
if [ "$rc" -eq 2 ] && grep -qF "unsubmitted draft" "$TMP/e5"; then
|
||||||
|
ok "draft: message left in a glyphless box => exit 2 + 'unsubmitted draft'"
|
||||||
|
else
|
||||||
|
no "draft: message left in a glyphless box => exit 2 + stderr" "rc=$rc err=[$(cat "$TMP/e5")]"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
echo "---"
|
echo "---"
|
||||||
echo "PASS=$PASS FAIL=$FAIL"
|
echo "PASS=$PASS FAIL=$FAIL"
|
||||||
[ "$FAIL" -eq 0 ]
|
[ "$FAIL" -eq 0 ]
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"lint": "eslint src",
|
"lint": "eslint src",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"test": "vitest run --passWithNoTests && pnpm run test:framework-shell",
|
"test": "vitest run --passWithNoTests && pnpm run test:framework-shell",
|
||||||
"test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh"
|
"test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_lib/test-credentials-gitea-seats.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mosaicstack/brain": "workspace:*",
|
"@mosaicstack/brain": "workspace:*",
|
||||||
|
|||||||
@@ -61,7 +61,8 @@ export const STAGES = [
|
|||||||
'bash packages/mosaic/framework/tools/quality/scripts/check-tools-index.sh --self-test',
|
'bash packages/mosaic/framework/tools/quality/scripts/check-tools-index.sh --self-test',
|
||||||
'bash packages/mosaic/framework/tools/quality/scripts/check-tools-index.sh',
|
'bash packages/mosaic/framework/tools/quality/scripts/check-tools-index.sh',
|
||||||
'bash packages/mosaic/framework/tools/git/test-issue-close-fail-closed.sh',
|
'bash packages/mosaic/framework/tools/git/test-issue-close-fail-closed.sh',
|
||||||
'bash packages/mosaic/framework/tools/git/test-gitea-login-resolution.sh',
|
'bash packages/mosaic/framework/tools/git/test-gitea-login-resolution.sh',
|
||||||
|
'bash packages/mosaic/framework/tools/git/test-issue-view-comments.sh',
|
||||||
'bash packages/mosaic/framework/tools/git/test-wrapper-guard.sh',
|
'bash packages/mosaic/framework/tools/git/test-wrapper-guard.sh',
|
||||||
'bash packages/mosaic/framework/tools/git/test-mosaic-worktree-large-repo.sh',
|
'bash packages/mosaic/framework/tools/git/test-mosaic-worktree-large-repo.sh',
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user