test(installer): preregister P0-P9 greenfield RED
This commit is contained in:
+33
@@ -1368,3 +1368,36 @@ All work is **alpha** (< 0.1.0) until Jason approves 0.1.0 beta release.
|
||||
10. ASSUMPTION: **Conversations and messages get their own PG tables** (not stored in brain's entity model). They follow a chat-specific schema with proper foreign keys to users and projects. Rationale: Chat has different access patterns (streaming, pagination, search) than brain entities.
|
||||
|
||||
11. RESOLVED: **Pi handles all target LLM providers natively.** Anthropic, OpenAI/Codex, Z.ai, Ollama, LM Studio, and llama.cpp are all supported via Pi's built-in providers or `models.json` configuration with `openai-completions` API type. No custom provider adapters needed in @mosaicstack/agent — only configuration management.
|
||||
|
||||
---
|
||||
|
||||
## Greenfield install correctness — C1 (#1050)
|
||||
|
||||
### Problem and objective
|
||||
|
||||
A from-zero install can report success while leaving the target host unusable because the installer has no transactional state machine capable of certifying its own postconditions. C1 supplies the structural spine and red-first fixture; later cards repair the individual failed postconditions.
|
||||
|
||||
### Normative requirements
|
||||
|
||||
1. The installer SHALL implement the canonical P0–P9 numbering from the greenfield-install PRD v2: P0 Resolve context; P1 Preflight; P2 Acquire artifacts; P3 Install CLI; P4 Install framework + skills; P5 Identity; P6 Runtime linking / activation; P7 Services; P8 Shell discoverability; P9 Verify + commit.
|
||||
2. Every phase SHALL declare preconditions, action, committed postconditions, and rollback. An unverifiable postcondition SHALL fail the install non-zero with the named phase and a remediation line; no best-effort failure may still certify success. P1's required-tool closure includes tools invoked by later phases, including `git`; a downstream prerequisite may not remain undeclared and degrade silently.
|
||||
3. A durable mutation journal SHALL open before the first mutation and commit at P9. Fallible command output needed to diagnose a phase SHALL be journaled and surfaced, never discarded.
|
||||
4. `--check` SHALL run exactly the P0–P8 postcondition predicates without mutation, report each phase PASS/FAIL, and exit non-zero if any predicate fails.
|
||||
5. P4 SHALL consume a checkout-free, lane/versioned shipped-set declaration published by the installer. C1 SHALL NOT select among the currently disagreeing framework-payload, repository-root, sync-source, and W-jarvis populations; while no declaration exists, P4 reports `NOT-MEASURED / UNDECLARED` and remains blocking rather than fabricating a count. C5 owns the declaration's contents and containment/loadability fulfillment.
|
||||
6. The from-zero fixture SHALL be lane-parametric, use Debian/glibc, run the documented install command as a non-root target user with an isolated HOME, and inherit no host credentials, npm cache, home directory, or runtime configuration.
|
||||
7. The fixture SHALL select `next` with `--next` or `MOSAIC_NEXT=1` and assert the resolved lane version. Internal predicates use P3's absolute CLI path; shell discoverability is tested only at P8.
|
||||
8. Fault injection after each P2–P8 phase SHALL prove either clean rollback or a durable, honestly reported resumable partial state, with no journal incorrectly left in progress.
|
||||
9. Unsupported musl/Alpine and unavailable Docker SHALL fail loudly rather than skip as pass.
|
||||
|
||||
### C1 acceptance criteria
|
||||
|
||||
1. The pre-C1 from-zero matrix records both discriminating controls: with `git` absent, the legacy installer still exits zero while P1 fails and skill sync degrades; with `git` present, P1 passes and the observed sync store/runtime links are 101/101. The C1 installer must fail at P1 before mutation when `git` is absent.
|
||||
2. The discriminating P3 row passes: the binary exists at the expected absolute path and reports exactly the resolved `next` lane version, while P4, P5, and P8 fail.
|
||||
3. The `--check` mutation negative control proves host fingerprints are byte-identical before and after observation.
|
||||
4. Woodpecker executes and validates the expected RED fixture; C1 does not repair P4/P5/P8 or activate #869.
|
||||
|
||||
### Explicit exclusions and dependencies
|
||||
|
||||
- C2 owns P8/PATH, C3 owns P5/headless identity, C4 owns P6 activation policy, and C5 owns P4/skills.
|
||||
- Main-lane execution is a promotion precondition owned by #1037; C1 only makes the fixture lane-parametric.
|
||||
- RM-02 and #869 activation are out of scope.
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# #1050 — Installer P0–P9 state machine and red-first fixture
|
||||
|
||||
## Objective
|
||||
|
||||
Implement C1 from the canonical greenfield-install PRD v2: a transactional P0–P9 installer spine, a side-effect-free P0–P8 `--check`, and a lane-parametric Debian/glibc non-root from-zero fixture. The acceptance milestone is an attributable RED on the pre-C1 installer while preserving P3 PASS.
|
||||
|
||||
## Authority and scope
|
||||
|
||||
- Canonical requirements: `jason.woltje/jarvis-brain` `docs/plans/2026-08-04-greenfield-install-blockers-PRD-v2.md`, read from local `origin/main` object `b2b6ed41f5aff5ea964e69b7c701cb45718742fa`; remote currency is **unestablished** because authenticated fetch returned repository-not-found.
|
||||
- Tracking: `mosaicstack/stack#1050` on `git.mosaicstack.dev` (author read back as `be-coder-05`).
|
||||
- Base: `origin/next` `4df478cdd150fdf8d52ea109f02ade5d85017acd`.
|
||||
- Out of scope: PATH, skills, headless wizard/identity, activation remediation, #869 wiring, RM-02, main promotion.
|
||||
- `docs/TASKS.md` is orchestrator-single-writer and is not modified by this worker.
|
||||
|
||||
## Plan
|
||||
|
||||
1. Pre-register the canonical phase/output/side-effect-free/fault-injection checks and observe RED against the base installer.
|
||||
2. Commit the immutable red-first acceptance fixture before implementation.
|
||||
3. Add the state-machine/journal/postcondition spine without repairing P4/P5/P8 symptoms.
|
||||
4. Wire the expected-RED from-zero fixture into Woodpecker using Debian/glibc and a non-root target user.
|
||||
5. Run shell/static baselines, situational container validation, code review, security review, then deliver through a PR to `next` under the coordinator-owned merge path.
|
||||
|
||||
## Budget
|
||||
|
||||
- Working estimate: 32K reasoning/output tokens.
|
||||
- Hard external cap: none stated.
|
||||
- Adaptation: keep implementation in shell surfaces already in scope; no package dependency install unless repository gates require it.
|
||||
|
||||
## Pre-registered acceptance checks
|
||||
|
||||
| ID | Exact case | Expected pre-fix result |
|
||||
|---|---|---|
|
||||
| C1-R1 | `tools/e2e-install-test.sh --lane next` in a clean Debian 12 container as uid 1001 | non-zero; P3 PASS; P4 `NOT-MEASURED / UNDECLARED`; P5/P6/P8 FAIL with own reasons |
|
||||
| C1-R2 | `tools/install-state-machine.test.sh` phase table case | RED because base installer does not enumerate canonical P0–P9 contracts |
|
||||
| C1-R3 | side-effect-free `--check` case over a fingerprinted HOME | RED because base `--check` is version-only rather than P0–P8 predicates |
|
||||
| C1-R4 | fault injection after each P2…P8 | RED because base installer has no injectable durable journal/rollback state |
|
||||
| C1-R5 | Docker unavailable | base harness incorrectly exits 0; replacement must fail non-zero |
|
||||
| C1-R6 | lane resolution | bare checkout is forbidden; fixture must pass `--next` and assert the resolved prerelease version |
|
||||
| C1-R7 | same Debian fixture with `git` absent vs present | absent: P1 FAIL while legacy installer exits 0 and sync degrades; present: P1 PASS and observed store/runtime containment 101/101 |
|
||||
|
||||
## Progress
|
||||
|
||||
- [x] Charter, doctrine, delivery/CI/QA/docs guides read.
|
||||
- [x] Canonical PRD v2 and charters read from local origin object; numbering reconciles with the TL spec. No numbering conflict found. TL additions (early durable journal and INV-C) are additive, not contradictory.
|
||||
- [x] Target base reachability verified with `merge-base --is-ancestor`.
|
||||
- [x] Issue #1050 created and provider author read back.
|
||||
- [x] Initial RED captured; TL rejected P4's repo-root count as a false RED. Four populations disagree (framework payload 1, repo root 13, sync store 101 in the fixture, W-jarvis observation 7), so C1 now requires a checkout-free declared shipped-set artifact and reports P4 `NOT-MEASURED / UNDECLARED` until C5 supplies it.
|
||||
- [x] P6 strengthens #869: the two dead enforcement hooks reproduce from zero on a clean broker-less container. C1 asserts the breach but neither wires nor unwires it.
|
||||
- [x] P1 false pass identified from the P4 evidence row: `git` is absent from the Debian base and was undeclared even though skill sync shells out to it. C1 adds `git` to P1; the fixture matrix preserves absent/present controls. The prior claim that web1's missing runtime skills reproduce this greenfield mechanism is withdrawn by the TL and is not carried here.
|
||||
- [ ] Corrected RED transcript captured and reported.
|
||||
- [ ] State machine implemented.
|
||||
- [ ] Reviews complete.
|
||||
|
||||
## Risks / blockers
|
||||
|
||||
- The deployed create wrappers do not expose `--dry-run`; identity preflight was performed through `pr-merge.sh --dry-run` on the same HOMELAB repo, which resolved `git.mosaicstack.dev` + `be-coder-05`. The issue create then fell back from tea to the API but provider read-back confirmed author `be-coder-05`.
|
||||
- `next` is an integration lane; `main` promotion remains #1037-owned.
|
||||
- #869 must remain staged and inactive.
|
||||
|
||||
## Verification log
|
||||
|
||||
(To be updated with exact commands and resulting objects.)
|
||||
+254
-163
@@ -1,184 +1,275 @@
|
||||
#!/usr/bin/env bash
|
||||
# ─── Mosaic Stack — End-to-End Install Test ────────────────────────────────────
|
||||
# Greenfield installer acceptance fixture.
|
||||
#
|
||||
# Runs a clean-container install test to verify the full first-run flow:
|
||||
# tools/install.sh -> mosaic wizard (non-interactive)
|
||||
# -> mosaic gateway install
|
||||
# -> mosaic gateway verify
|
||||
#
|
||||
# Usage:
|
||||
# bash tools/e2e-install-test.sh
|
||||
#
|
||||
# Requirements:
|
||||
# - Docker (skips gracefully if not available)
|
||||
# - Run from the repository root
|
||||
#
|
||||
# How it works:
|
||||
# 1. Mounts the repository into a node:22-alpine container.
|
||||
# 2. Installs prerequisites (bash, curl, jq, git) inside the container.
|
||||
# 3. Runs `bash tools/install.sh --yes --no-auto-launch` to install the
|
||||
# framework and CLI from the Gitea registry.
|
||||
# 4. Runs `mosaic wizard --non-interactive` to set up SOUL/USER.
|
||||
# 5. Runs `mosaic gateway install` with piped defaults (non-interactive).
|
||||
# 6. Runs `mosaic gateway verify` and checks its exit code.
|
||||
# NOTE: `mosaic gateway verify` is a new command added in the
|
||||
# feat/mosaic-first-run-ux branch. If the installed CLI version
|
||||
# pre-dates this branch (does not have `gateway verify`), the test
|
||||
# marks this step as EXPECTED-SKIP and reports the installed version.
|
||||
# 7. Reports PASS or FAIL with a summary.
|
||||
#
|
||||
# To run manually:
|
||||
# cd /path/to/mosaic-stack
|
||||
# bash tools/e2e-install-test.sh
|
||||
#
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# The fixture itself is intentionally RED until the C2-C5 phase owners repair
|
||||
# their postconditions. C1's CI gate executes it and validates that the RED is
|
||||
# attributable (including the discriminating P3 PASS); it does not turn the
|
||||
# failed install into a false green.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
IMAGE="node:22-alpine"
|
||||
CONTAINER_NAME="mosaic-e2e-install-$$"
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
LANE="${MOSAIC_INSTALL_LANE:-next}"
|
||||
SOURCE="${MOSAIC_INSTALL_SOURCE:-checkout}"
|
||||
IMAGE="${MOSAIC_INSTALL_IMAGE:-node:22-bookworm-slim}"
|
||||
GIT_MODE="${MOSAIC_INSTALL_GIT_MODE:-present}"
|
||||
INSTALLER_FILE="${MOSAIC_FIXTURE_INSTALLER_FILE:-$ROOT/tools/install.sh}"
|
||||
|
||||
# ─── Colour helpers ───────────────────────────────────────────────────────────
|
||||
if [[ -t 1 ]]; then
|
||||
R=$'\033[0;31m' G=$'\033[0;32m' Y=$'\033[0;33m' BOLD=$'\033[1m' RESET=$'\033[0m'
|
||||
else
|
||||
R="" G="" Y="" BOLD="" RESET=""
|
||||
fi
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: tools/e2e-install-test.sh [--lane next|main] [--source checkout|remote] [--git present|absent]
|
||||
|
||||
info() { echo "${BOLD}[e2e]${RESET} $*"; }
|
||||
ok() { echo "${G}[PASS]${RESET} $*"; }
|
||||
fail() { echo "${R}[FAIL]${RESET} $*" >&2; }
|
||||
warn() { echo "${Y}[WARN]${RESET} $*"; }
|
||||
|
||||
# ─── Docker availability check ────────────────────────────────────────────────
|
||||
if ! command -v docker &>/dev/null; then
|
||||
warn "Docker not found — skipping e2e install test."
|
||||
warn "Install Docker and re-run this script to exercise the full install flow."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! docker info &>/dev/null 2>&1; then
|
||||
warn "Docker daemon is not running or not accessible — skipping e2e install test."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
info "Docker available — proceeding with e2e install test."
|
||||
info "Repo root: ${REPO_ROOT}"
|
||||
info "Container image: ${IMAGE}"
|
||||
|
||||
# ─── Inline script that runs INSIDE the container ────────────────────────────
|
||||
INNER_SCRIPT="$(mktemp /tmp/mosaic-e2e-inner-XXXXXX.sh)"
|
||||
trap 'rm -f "$INNER_SCRIPT"' EXIT
|
||||
|
||||
cat > "$INNER_SCRIPT" <<'INNER_SCRIPT_EOF'
|
||||
#!/bin/sh
|
||||
# Bootstrap: /bin/sh until bash is installed, then re-exec.
|
||||
set -e
|
||||
|
||||
echo "=== [inner] Installing system prerequisites ==="
|
||||
apk add --no-cache bash curl jq git 2>/dev/null || \
|
||||
apt-get install -y -q bash curl jq git 2>/dev/null || true
|
||||
|
||||
# Re-exec under bash.
|
||||
if [ -z "${BASH_VERSION:-}" ] && command -v bash >/dev/null 2>&1; then
|
||||
exec bash "$0" "$@"
|
||||
fi
|
||||
|
||||
# ── bash from here ────────────────────────────────────────────────────────────
|
||||
set -euo pipefail
|
||||
|
||||
echo "=== [inner] Node.js / npm versions ==="
|
||||
node --version
|
||||
npm --version
|
||||
|
||||
echo "=== [inner] Setting up npm global prefix ==="
|
||||
export NPM_PREFIX="/root/.npm-global"
|
||||
mkdir -p "$NPM_PREFIX/bin"
|
||||
npm config set prefix "$NPM_PREFIX" 2>/dev/null || true
|
||||
export PATH="$NPM_PREFIX/bin:$PATH"
|
||||
|
||||
echo "=== [inner] Running install.sh --yes --no-auto-launch ==="
|
||||
# Install both framework and CLI from the Gitea registry.
|
||||
MOSAIC_SKIP_SKILLS_SYNC=1 \
|
||||
MOSAIC_ASSUME_YES=1 \
|
||||
bash /repo/tools/install.sh --yes --no-auto-launch
|
||||
|
||||
INSTALLED_VERSION="$(mosaic --version 2>/dev/null || echo 'unknown')"
|
||||
echo "[inner] mosaic CLI installed: ${INSTALLED_VERSION}"
|
||||
|
||||
echo "=== [inner] Running mosaic wizard (non-interactive) ==="
|
||||
mosaic wizard \
|
||||
--non-interactive \
|
||||
--name "test-agent" \
|
||||
--user-name "tester" \
|
||||
--pronouns "they/them" \
|
||||
--timezone "UTC" || {
|
||||
echo "[WARN] mosaic wizard exited non-zero — continuing"
|
||||
Runs the documented installer command from zero in Debian/glibc as a non-root
|
||||
uid with an isolated HOME. The fixture exits non-zero when any P0-P8
|
||||
postcondition fails. `next` is always selected with the --next installer flag.
|
||||
EOF
|
||||
}
|
||||
|
||||
echo "=== [inner] Running mosaic gateway install ==="
|
||||
# Feed non-interactive answers:
|
||||
# "1" → storage tier: local
|
||||
# "" → port: accept default (14242)
|
||||
# "" → ANTHROPIC_API_KEY: skip
|
||||
# "" → CORS origin: accept default
|
||||
# Then admin bootstrap: name, email, password
|
||||
printf '1\n\n\n\nTest Admin\[email protected]\ntestpassword123\n' \
|
||||
| mosaic gateway install
|
||||
INSTALL_EXIT="$?"
|
||||
if [ "${INSTALL_EXIT}" -ne 0 ]; then
|
||||
echo "[ERR] mosaic gateway install exited ${INSTALL_EXIT}"
|
||||
mosaic gateway status 2>/dev/null || true
|
||||
exit "${INSTALL_EXIT}"
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--lane) LANE="${2:-}"; shift 2 ;;
|
||||
--source) SOURCE="${2:-}"; shift 2 ;;
|
||||
--git) GIT_MODE="${2:-}"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "[fixture] unknown argument: $1" >&2; usage >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$LANE" in next|main) ;; *) echo "[fixture] unsupported lane '$LANE' (expected next|main)" >&2; exit 2 ;; esac
|
||||
case "$SOURCE" in checkout|remote) ;; *) echo "[fixture] unsupported source '$SOURCE' (expected checkout|remote)" >&2; exit 2 ;; esac
|
||||
case "$GIT_MODE" in present|absent) ;; *) echo "[fixture] unsupported git mode '$GIT_MODE' (expected present|absent)" >&2; exit 2 ;; esac
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "[fixture] FAIL: Docker is required; greenfield validation was NOT RUN." >&2
|
||||
exit 2
|
||||
fi
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
echo "[fixture] FAIL: Docker daemon is unavailable; greenfield validation was NOT RUN." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
echo "=== [inner] Running mosaic gateway verify ==="
|
||||
# `gateway verify` was added in feat/mosaic-first-run-ux.
|
||||
# If the installed version pre-dates this, skip gracefully.
|
||||
if ! mosaic gateway --help 2>&1 | grep -q 'verify'; then
|
||||
echo "[SKIP] 'mosaic gateway verify' not available in installed version ${INSTALLED_VERSION}."
|
||||
echo "[SKIP] This command was added in the feat/mosaic-first-run-ux release."
|
||||
echo "[SKIP] Re-run after the new version is published to validate this step."
|
||||
# Treat as pass — the install flow itself worked.
|
||||
exit 0
|
||||
installer_b64=""
|
||||
framework_payload_count="NOT-MEASURED"
|
||||
repo_root_count="NOT-MEASURED"
|
||||
if [[ "$SOURCE" == "checkout" ]]; then
|
||||
installer_b64="$(base64 -w0 "$INSTALLER_FILE")"
|
||||
[[ -d "$ROOT/packages/mosaic/framework/skills" ]] \
|
||||
&& framework_payload_count="$(find "$ROOT/packages/mosaic/framework/skills" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')"
|
||||
[[ -d "$ROOT/skills" ]] \
|
||||
&& repo_root_count="$(find "$ROOT/skills" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')"
|
||||
fi
|
||||
|
||||
mosaic gateway verify
|
||||
VERIFY_EXIT="$?"
|
||||
echo "=== [inner] verify exit code: ${VERIFY_EXIT} ==="
|
||||
exit "${VERIFY_EXIT}"
|
||||
INNER_SCRIPT_EOF
|
||||
inner="$(mktemp "${TMPDIR:-/tmp}/mosaic-greenfield-inner.XXXXXX.sh")"
|
||||
trap 'rm -f "$inner"' EXIT
|
||||
cat > "$inner" <<'INNER'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
chmod +x "$INNER_SCRIPT"
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
packages=(bash ca-certificates curl jq passwd util-linux)
|
||||
[[ "$FIXTURE_GIT_MODE" == "present" ]] && packages+=(git)
|
||||
apt-get install -y -qq "${packages[@]}" >/dev/null
|
||||
|
||||
# ─── Pull image ───────────────────────────────────────────────────────────────
|
||||
info "Pulling ${IMAGE}…"
|
||||
docker pull "${IMAGE}" --quiet
|
||||
useradd --create-home --uid 1001 --shell /bin/bash mosaic
|
||||
install -d -o mosaic -g mosaic /home/mosaic/work
|
||||
|
||||
# ─── Run container ────────────────────────────────────────────────────────────
|
||||
info "Starting container ${CONTAINER_NAME}…"
|
||||
case "$FIXTURE_SOURCE" in
|
||||
checkout)
|
||||
printf '%s' "$FIXTURE_INSTALLER_B64" | base64 -d > /tmp/install.sh
|
||||
;;
|
||||
remote)
|
||||
curl -fsSL "https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/${FIXTURE_LANE}/tools/install.sh" > /tmp/install.sh
|
||||
;;
|
||||
esac
|
||||
chmod 0755 /tmp/install.sh
|
||||
sha256sum /tmp/install.sh | sed 's/^/[fixture] installer sha256: /'
|
||||
|
||||
EXIT_CODE=0
|
||||
docker run --rm \
|
||||
--name "${CONTAINER_NAME}" \
|
||||
--volume "${REPO_ROOT}:/repo:ro" \
|
||||
--volume "${INNER_SCRIPT}:/e2e-inner.sh:ro" \
|
||||
--network host \
|
||||
"${IMAGE}" \
|
||||
/bin/sh /e2e-inner.sh \
|
||||
|| EXIT_CODE=$?
|
||||
cat > /tmp/run-as-target.sh <<'TARGET'
|
||||
#!/usr/bin/env bash
|
||||
set -uo pipefail
|
||||
|
||||
# ─── Report ───────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
if [[ "$EXIT_CODE" -eq 0 ]]; then
|
||||
ok "End-to-end install test PASSED (exit ${EXIT_CODE})"
|
||||
lane="$FIXTURE_LANE"
|
||||
home="$HOME"
|
||||
prefix="$home/.npm-global"
|
||||
mosaic_home="$home/.config/mosaic"
|
||||
install_log="$home/install.log"
|
||||
failures=0
|
||||
|
||||
phase_pass() { printf '[%s] PASS: %s\n' "$1" "$2"; }
|
||||
phase_fail() { printf '[%s] FAIL: %s\n' "$1" "$2"; failures=$((failures + 1)); }
|
||||
|
||||
lane_args=()
|
||||
resolved_spec='@mosaicstack/mosaic'
|
||||
if [[ "$lane" == "next" ]]; then
|
||||
lane_args+=(--next)
|
||||
resolved_spec='@mosaicstack/mosaic@next'
|
||||
fi
|
||||
|
||||
resolved_version="$(npm view "$resolved_spec" version --registry=https://git.mosaicstack.dev/api/packages/mosaicstack/npm/ 2>/dev/null || true)"
|
||||
printf '[fixture] resolved lane=%s package=%s version=%s\n' "$lane" "$resolved_spec" "${resolved_version:-UNRESOLVED}"
|
||||
|
||||
set +e
|
||||
MOSAIC_NO_COLOR=1 MOSAIC_ASSUME_YES=1 \
|
||||
bash /tmp/install.sh "${lane_args[@]}" --yes --no-auto-launch >"$install_log" 2>&1
|
||||
install_status=$?
|
||||
set -e
|
||||
cat "$install_log"
|
||||
printf '[fixture] installer_exit=%d done_claims=%s\n' \
|
||||
"$install_status" "$(grep -cF 'Done.' "$install_log" || true)"
|
||||
|
||||
# P0 Resolve context
|
||||
shell="$(getent passwd "$(id -u)" | cut -d: -f7)"
|
||||
if [[ "$(id -u)" -ne 0 && "$home" == "/home/mosaic" && "$shell" == "/bin/bash" ]] \
|
||||
&& ldd --version 2>&1 | grep -qi 'glibc\|gnu libc' \
|
||||
&& [[ "$(node -p 'Number(process.versions.node.split(".")[0])')" -ge 20 ]]; then
|
||||
phase_pass P0 "target=mosaic uid=$(id -u) HOME=$home shell=$shell libc=glibc node=$(node --version)"
|
||||
else
|
||||
fail "End-to-end install test FAILED (exit ${EXIT_CODE})"
|
||||
echo ""
|
||||
echo " Troubleshooting:"
|
||||
echo " - Review the output above for the failing step."
|
||||
echo " - Re-run with bash -x tools/e2e-install-test.sh for verbose trace."
|
||||
echo " - Run mosaic gateway logs inside a manual container for daemon output."
|
||||
phase_fail P0 "context unresolved or unsupported (uid=$(id -u) HOME=$home shell=${shell:-unknown})"
|
||||
fi
|
||||
|
||||
# P1 Preflight
|
||||
missing_tools=()
|
||||
for tool in bash curl git node npm tar; do
|
||||
command -v "$tool" >/dev/null 2>&1 || missing_tools+=("$tool")
|
||||
done
|
||||
if [[ "${#missing_tools[@]}" -eq 0 && -n "$resolved_version" && -w "$home" ]]; then
|
||||
phase_pass P1 "required tools present (including downstream git); target HOME writable; registry lane resolved"
|
||||
else
|
||||
phase_fail P1 "undeclared/missing prerequisite(s)=${missing_tools[*]:-none}; target_writable=$([[ -w "$home" ]] && echo yes || echo no) registry_resolved=$([[ -n "$resolved_version" ]] && echo yes || echo no)"
|
||||
fi
|
||||
|
||||
# P2 Acquire artifacts
|
||||
if [[ -n "$resolved_version" ]] && grep -qF "$resolved_version" "$install_log"; then
|
||||
phase_pass P2 "lane=$lane pinned_version=$resolved_version recorded in installer transcript"
|
||||
else
|
||||
phase_fail P2 "lane=$lane did not resolve and record a pinned artifact version"
|
||||
fi
|
||||
|
||||
# P3 Install CLI — the discriminating row. Use the known absolute path only.
|
||||
cli="$prefix/bin/mosaic"
|
||||
cli_version=""
|
||||
if [[ -x "$cli" ]]; then
|
||||
cli_version="$($cli --version 2>/dev/null | tail -n 1 | tr -d '\r' || true)"
|
||||
fi
|
||||
if [[ -x "$cli" && "$cli_version" == "$resolved_version" ]]; then
|
||||
phase_pass P3 "absolute_path=$cli version=$cli_version equals resolved lane version"
|
||||
else
|
||||
phase_fail P3 "absolute_path=$cli executable=$([[ -x "$cli" ]] && echo yes || echo no) got=${cli_version:-missing} expected=${resolved_version:-unresolved}"
|
||||
fi
|
||||
|
||||
# P4 Framework + skills. C1 does not choose among the four disagreeing
|
||||
# candidate populations. It requires the installer to publish a lane/versioned
|
||||
# shipped-set declaration that a checkout-free install can resolve; C5 owns its
|
||||
# contents. Without that artifact P4 is NOT-MEASURED, never a fabricated count.
|
||||
declared_set="$mosaic_home/.install-shipped-skills.json"
|
||||
sync_store_count=0
|
||||
runtime_link_count=0
|
||||
[[ -d "$mosaic_home/skills" ]] \
|
||||
&& sync_store_count="$(find "$mosaic_home/skills" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')"
|
||||
[[ -d "$home/.pi/agent/skills" ]] \
|
||||
&& runtime_link_count="$(find "$home/.pi/agent/skills" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) | wc -l | tr -d ' ')"
|
||||
printf '[P4-EVIDENCE] candidate_populations framework_payload=%s repo_root=%s sync_store=%s jarvis_W-jarvis_observation=7 runtime_links=%s\n' \
|
||||
"$FIXTURE_FRAMEWORK_PAYLOAD_COUNT" "$FIXTURE_REPO_ROOT_COUNT" "$sync_store_count" "$runtime_link_count"
|
||||
if [[ ! -s "$declared_set" ]]; then
|
||||
phase_fail P4 "NOT-MEASURED / UNDECLARED: installer published no checkout-free, lane/versioned shipped-set artifact at $declared_set"
|
||||
elif node - "$declared_set" <<'NODE'
|
||||
const fs = require('fs');
|
||||
const data = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
||||
if (!data || typeof data !== 'object' || !['latest', 'next'].includes(data.lane) ||
|
||||
typeof data.version !== 'string' || !data.version || !Array.isArray(data.skills) || data.skills.length === 0 ||
|
||||
data.skills.some((name) => typeof name !== 'string' || !name)) process.exit(1);
|
||||
NODE
|
||||
then
|
||||
declared_count="$(node -p "require('$declared_set').skills.length")"
|
||||
phase_pass P4 "declared shipped-set artifact parses (declared_count=$declared_count); C5 owns containment/loadability fulfillment"
|
||||
else
|
||||
phase_fail P4 "NOT-MEASURED / UNDECLARED: shipped-set artifact exists but is empty, malformed, or lacks lane/version"
|
||||
fi
|
||||
|
||||
# P5 Identity
|
||||
identity_ok=true
|
||||
identity_reason=()
|
||||
for f in SOUL.md USER.md; do
|
||||
path="$mosaic_home/$f"
|
||||
if [[ ! -s "$path" ]]; then
|
||||
identity_ok=false; identity_reason+=("$f missing-or-empty"); continue
|
||||
fi
|
||||
owner="$(stat -c '%u' "$path")"; mode="$(stat -c '%a' "$path")"
|
||||
if [[ "$owner" != "$(id -u)" || "$mode" =~ [2367]$ ]]; then
|
||||
identity_ok=false; identity_reason+=("$f owner=$owner mode=$mode")
|
||||
fi
|
||||
done
|
||||
if [[ "$identity_ok" == true ]]; then
|
||||
phase_pass P5 "SOUL.md and USER.md are non-empty and target-user owned with non-world-writable modes"
|
||||
else
|
||||
phase_fail P5 "${identity_reason[*]}"
|
||||
fi
|
||||
|
||||
# P6 Runtime linking / activation. #869 must remain unwired without its broker.
|
||||
broker_present=false
|
||||
[[ -S "${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/mosaic-lease/broker.sock" ]] && broker_present=true
|
||||
dead_hooks=0
|
||||
if [[ -f "$home/.claude/settings.json" ]]; then
|
||||
dead_hooks="$(grep -Ec 'mutator-gate\.py|receipt-observer-client\.py' "$home/.claude/settings.json" || true)"
|
||||
fi
|
||||
if [[ "$broker_present" == false && "$dead_hooks" -eq 0 ]]; then
|
||||
phase_pass P6 "broker absent and #869 enforcement hooks remain inactive"
|
||||
elif [[ "$broker_present" == true ]]; then
|
||||
phase_pass P6 "activation broker present; hook state is evaluable"
|
||||
else
|
||||
phase_fail P6 "broker absent but dead enforcement hooks are active (count=$dead_hooks)"
|
||||
fi
|
||||
|
||||
# P7 Services — none requested by --no-auto-launch.
|
||||
phase_pass P7 "no services requested by this fixture"
|
||||
|
||||
# P8 Shell discoverability — actual target shell, fresh login and non-login.
|
||||
base_env=(env -i HOME="$home" USER=mosaic LOGNAME=mosaic SHELL=/bin/bash PATH=/usr/local/bin:/usr/bin:/bin)
|
||||
login_path="$("${base_env[@]}" /bin/bash -lc 'command -v mosaic' 2>/dev/null || true)"
|
||||
nonlogin_path="$("${base_env[@]}" /bin/bash -c 'command -v mosaic' 2>/dev/null || true)"
|
||||
if [[ "$login_path" == "$cli" && "$nonlogin_path" == "$cli" ]]; then
|
||||
phase_pass P8 "login=$login_path nonlogin=$nonlogin_path equals P3 path"
|
||||
else
|
||||
phase_fail P8 "fresh bash login=${login_path:-missing} nonlogin=${nonlogin_path:-missing} expected=$cli"
|
||||
fi
|
||||
|
||||
manifest="$mosaic_home/.install-manifest.json"
|
||||
p0_p8_failures="$failures"
|
||||
if [[ "$p0_p8_failures" -eq 0 && -s "$manifest" ]]; then
|
||||
phase_pass P9 "P0-P8 reasserted; manifest present"
|
||||
else
|
||||
phase_fail P9 "P0-P8_failed_postconditions=$p0_p8_failures manifest=$([[ -s "$manifest" ]] && echo present || echo missing); install must not certify success"
|
||||
fi
|
||||
|
||||
printf '[fixture] P0-P9_failed_rows=%d (includes P9 aggregate row)\n' "$failures"
|
||||
if [[ "$failures" -ne 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
TARGET
|
||||
chmod 0755 /tmp/run-as-target.sh
|
||||
chown mosaic:mosaic /tmp/run-as-target.sh
|
||||
|
||||
exec runuser -u mosaic -- env -i \
|
||||
HOME=/home/mosaic USER=mosaic LOGNAME=mosaic SHELL=/bin/bash \
|
||||
PATH=/usr/local/bin:/usr/bin:/bin \
|
||||
FIXTURE_LANE="$FIXTURE_LANE" \
|
||||
FIXTURE_GIT_MODE="$FIXTURE_GIT_MODE" \
|
||||
FIXTURE_FRAMEWORK_PAYLOAD_COUNT="$FIXTURE_FRAMEWORK_PAYLOAD_COUNT" \
|
||||
FIXTURE_REPO_ROOT_COUNT="$FIXTURE_REPO_ROOT_COUNT" \
|
||||
/bin/bash /tmp/run-as-target.sh
|
||||
INNER
|
||||
chmod 0755 "$inner"
|
||||
|
||||
printf '[fixture] platform=Debian/glibc image=%s target_uid=1001 lane=%s source=%s git=%s\n' "$IMAGE" "$LANE" "$SOURCE" "$GIT_MODE"
|
||||
printf '[fixture] host inheritance: no bind mounts, no host HOME, no npm cache, no credentials\n'
|
||||
|
||||
docker run --rm -i \
|
||||
--network bridge \
|
||||
--env FIXTURE_LANE="$LANE" \
|
||||
--env FIXTURE_SOURCE="$SOURCE" \
|
||||
--env FIXTURE_GIT_MODE="$GIT_MODE" \
|
||||
--env FIXTURE_INSTALLER_B64="$installer_b64" \
|
||||
--env FIXTURE_FRAMEWORK_PAYLOAD_COUNT="$framework_payload_count" \
|
||||
--env FIXTURE_REPO_ROOT_COUNT="$repo_root_count" \
|
||||
"$IMAGE" /bin/bash -s < "$inner"
|
||||
|
||||
Executable
+125
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env bash
|
||||
# Red-first acceptance checks for #1050. This file is committed before the
|
||||
# installer implementation. Do not weaken these properties to make it green.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
TMP="$(mktemp -d "${TMPDIR:-/tmp}/mosaic-install-state-test.XXXXXX")"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
failures=0
|
||||
|
||||
fail_case() { printf '[test] FAIL: %s\n' "$*" >&2; failures=$((failures + 1)); }
|
||||
pass_case() { printf '[test] PASS: %s\n' "$*"; }
|
||||
|
||||
fingerprint() {
|
||||
local dir="$1"
|
||||
if [[ ! -d "$dir" ]]; then printf 'ABSENT\n'; return; fi
|
||||
(
|
||||
cd "$dir" || exit 1
|
||||
find . -mindepth 1 -printf '%P|%y|%m|%u|%g|%l\n' | LC_ALL=C sort
|
||||
find . -type f -print0 | LC_ALL=C sort -z | xargs -0 -r sha256sum
|
||||
) | sha256sum | awk '{print $1}'
|
||||
}
|
||||
|
||||
make_fake_npm() {
|
||||
local bin="$1"
|
||||
mkdir -p "$bin"
|
||||
cat > "$bin/npm" <<'FAKE'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
case "${1:-} ${2:-} ${3:-}" in
|
||||
'view @mosaicstack/mosaic@next version') echo '0.0.50-next.999' ;;
|
||||
'view @mosaicstack/gateway@next version') echo '0.0.7-next.999' ;;
|
||||
'view @mosaicstack/mosaic version') echo '0.0.49' ;;
|
||||
'ls -g --depth=0'|'ls -g --json') echo '{"dependencies":{"@mosaicstack/mosaic":{"version":"0.0.50-next.999"},"@mosaicstack/gateway":{"version":"0.0.7-next.999"}}}' ;;
|
||||
ls*) echo '{"dependencies":{"@mosaicstack/mosaic":{"version":"0.0.50-next.999"},"@mosaicstack/gateway":{"version":"0.0.7-next.999"}}}' ;;
|
||||
*) echo "unexpected fake npm command: $*" >&2; exit 1 ;;
|
||||
esac
|
||||
FAKE
|
||||
chmod 0755 "$bin/npm"
|
||||
}
|
||||
|
||||
printf '[test] case: --check enumerates exactly P0-P8, discriminates, and mutates nothing\n'
|
||||
check_home="$TMP/check-home"
|
||||
check_bin="$TMP/check-bin"
|
||||
mkdir -p "$check_home/.config/mosaic/skills/alpha" "$check_home/.npm-global/bin" "$check_bin"
|
||||
printf '# framework\n' > "$check_home/.config/mosaic/AGENTS.md"
|
||||
printf '# skill\n' > "$check_home/.config/mosaic/skills/alpha/SKILL.md"
|
||||
cat > "$check_home/.npm-global/bin/mosaic" <<'CLI'
|
||||
#!/usr/bin/env bash
|
||||
printf '0.0.50-next.999\n'
|
||||
CLI
|
||||
chmod 0755 "$check_home/.npm-global/bin/mosaic"
|
||||
make_fake_npm "$check_bin"
|
||||
before="$(fingerprint "$check_home")"
|
||||
set +e
|
||||
HOME="$check_home" MOSAIC_HOME="$check_home/.config/mosaic" MOSAIC_PREFIX="$check_home/.npm-global" \
|
||||
MOSAIC_NO_COLOR=1 PATH="$check_bin:/usr/local/bin:/usr/bin:/bin" \
|
||||
bash "$ROOT/tools/install.sh" --check --next >"$TMP/check.log" 2>&1
|
||||
check_status=$?
|
||||
set -e
|
||||
after="$(fingerprint "$check_home")"
|
||||
|
||||
[[ "$before" == "$after" ]] && pass_case '--check left the complete HOME fingerprint unchanged' \
|
||||
|| fail_case "--check mutated HOME (before=$before after=$after)"
|
||||
[[ "$check_status" -ne 0 ]] && pass_case '--check exited non-zero for failed P4/P5/P8 predicates' \
|
||||
|| fail_case '--check returned zero on the deliberately broken host'
|
||||
|
||||
phase_rows=0
|
||||
for phase in P0 P1 P2 P3 P4 P5 P6 P7 P8; do
|
||||
count="$(grep -Ec "^\[$phase\] (PASS|FAIL):" "$TMP/check.log" || true)"
|
||||
[[ "$count" -eq 1 ]] || fail_case "$phase expected exactly one PASS/FAIL row, got $count"
|
||||
phase_rows=$((phase_rows + count))
|
||||
done
|
||||
[[ "$phase_rows" -eq 9 ]] && pass_case '--check emitted exactly nine P0-P8 result rows' \
|
||||
|| fail_case "--check emitted $phase_rows canonical rows instead of 9"
|
||||
grep -q '^\[P3\] PASS:.*0\.0\.50-next\.999' "$TMP/check.log" \
|
||||
&& pass_case 'P3 preserves the absolute-path exact-version discriminator' \
|
||||
|| fail_case 'P3 did not PASS with the exact resolved next-lane version'
|
||||
grep -q '^\[P4\] FAIL: NOT-MEASURED / UNDECLARED:' "$TMP/check.log" \
|
||||
&& pass_case 'P4 refuses fabricated precision when no shipped-set declaration exists' \
|
||||
|| fail_case 'P4 did not report the declared-set population as NOT-MEASURED / UNDECLARED'
|
||||
for phase in P5 P8; do
|
||||
grep -q "^\[$phase\] FAIL:" "$TMP/check.log" \
|
||||
&& pass_case "$phase remains an attributable expected RED" \
|
||||
|| fail_case "$phase did not report its own expected failure"
|
||||
done
|
||||
|
||||
printf '[test] case: per-phase P2-P8 fault injection restores representative host mutations\n'
|
||||
for phase in P2 P3 P4 P5 P6 P7 P8; do
|
||||
home="$TMP/fault-$phase/home"
|
||||
state="$TMP/fault-$phase/state"
|
||||
mkdir -p "$home/.config/mosaic" "$home/.npm-global/bin" "$home/.claude" "$state"
|
||||
printf 'operator-framework-sentinel\n' > "$home/.config/mosaic/operator.txt"
|
||||
printf '@scope:registry=https://pre.example.invalid/\n' > "$home/.npmrc"
|
||||
printf 'old-cli\n' > "$home/.npm-global/bin/mosaic"
|
||||
printf '{"hooks":{"safe":true}}\n' > "$home/.claude/settings.json"
|
||||
before="$(fingerprint "$home")"
|
||||
set +e
|
||||
HOME="$home" MOSAIC_HOME="$home/.config/mosaic" MOSAIC_PREFIX="$home/.npm-global" \
|
||||
MOSAIC_INSTALL_STATE_DIR="$state" MOSAIC_INSTALL_FAULT_AFTER="$phase" \
|
||||
MOSAIC_NO_COLOR=1 bash "$ROOT/tools/install.sh" --state-machine-self-test \
|
||||
>"$TMP/fault-$phase.log" 2>&1
|
||||
status=$?
|
||||
set -e
|
||||
after="$(fingerprint "$home")"
|
||||
[[ "$status" -ne 0 ]] || fail_case "$phase injected fault returned zero"
|
||||
grep -q "phase=$phase" "$TMP/fault-$phase.log" \
|
||||
|| fail_case "$phase fault transcript did not name the injected phase"
|
||||
[[ "$before" == "$after" ]] \
|
||||
&& pass_case "$phase rollback restored framework/npmrc/prefix/runtime representative state" \
|
||||
|| fail_case "$phase rollback mismatch (before=$before after=$after)"
|
||||
if find "$state" -type f -exec grep -l '"status"[[:space:]]*:[[:space:]]*"in-progress"' {} + 2>/dev/null | grep -q .; then
|
||||
fail_case "$phase left a journal in-progress"
|
||||
else
|
||||
pass_case "$phase left no journal falsely in-progress"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$failures" -ne 0 ]]; then
|
||||
printf '[test] install state-machine acceptance RED: %d failed assertion(s)\n' "$failures" >&2
|
||||
printf '[test] --check transcript: %s\n' "$TMP/check.log" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '[test] installer state-machine acceptance passed\n'
|
||||
Reference in New Issue
Block a user