Compare commits

...
10 changed files with 429 additions and 100 deletions
+70 -31
View File
@@ -11,48 +11,87 @@
## Project Context ## Project Context
Mosaic Stack is a self-hosted, multi-user AI agent platform. TypeScript monorepo with NestJS gateway, Next.js web dashboard, Pi SDK agent runtime, and plugin architecture for Discord/Telegram. Mosaic Stack is a self-hosted, multi-user AI agent platform. It is a TypeScript monorepo with a NestJS gateway, Next.js dashboard, Pi SDK agent runtime, and Discord/Telegram plugin architecture.
## Package Map ### Stack
| Package | Purpose | Key Dependencies | - **API:** NestJS with Fastify (`apps/gateway`)
| ------------------ | ------------------------------- | -------------------------------- | - **Web:** Next.js 16 with React 19 (`apps/web`)
| `apps/gateway` | NestJS API + WebSocket hub | Fastify, Socket.IO, Pi SDK, OTEL | - **ORM and database:** Drizzle ORM, PostgreSQL 17, and pgvector (`packages/db`)
| `apps/web` | Next.js dashboard | React 19, Tailwind | - **Authentication:** BetterAuth (`packages/auth`)
| `packages/types` | Shared TypeScript contracts | class-validator | - **Agent runtime:** Pi SDK (`apps/gateway`, `packages/mosaic`)
| `packages/db` | Drizzle ORM schema + migrations | drizzle-orm, postgres | - **Queue:** Valkey 8 (`packages/queue`)
| `packages/auth` | BetterAuth configuration | better-auth, @mosaicstack/db | - **Build:** pnpm workspaces and Turborepo
| `packages/brain` | Data layer (PG-backed) | @mosaicstack/db | - **CI:** Woodpecker CI
| `packages/queue` | Valkey task queue + MCP | ioredis | - **Observability:** OpenTelemetry and Jaeger
| `packages/coord` | Mission coordination | @mosaicstack/queue |
| `packages/mosaic` | Unified `mosaic` CLI + TUI | Ink, Pi SDK, commander |
| `plugins/discord` | Discord channel plugin | discord.js |
| `plugins/telegram` | Telegram channel plugin | Telegraf |
## Architecture Rules ### Package Map
1. Gateway is the single API surface — all clients connect through it | Package | Purpose | Key Dependencies |
2. Pi SDK is ESM-only — gateway and CLI must use ESM | ------------------ | ----------------------------- | -------------------------------- |
3. Socket.IO typed events defined in `@mosaicstack/types` enforce compile-time contracts | `apps/gateway` | NestJS API + WebSocket hub | Fastify, Socket.IO, Pi SDK, OTEL |
4. OTEL auto-instrumentation loads before NestJS bootstrap | `apps/web` | Next.js dashboard | React 19, Tailwind |
5. BetterAuth manages auth tables; schema defined in `@mosaicstack/db` | `packages/types` | Shared TypeScript contracts | class-validator |
6. Docker Compose provides PG (5433), Valkey (6380), OTEL Collector (4317/4318), Jaeger (16686) | `packages/db` | Drizzle schema and migrations | drizzle-orm, postgres |
7. Explicit `@Inject()` decorators required in NestJS (tsx/esbuild doesn't emit decorator metadata) | `packages/auth` | BetterAuth configuration | better-auth, @mosaicstack/db |
| `packages/brain` | Structured data layer | @mosaicstack/db |
| `packages/queue` | Valkey task queue and MCP | ioredis |
| `packages/coord` | Mission coordination | @mosaicstack/queue |
| `packages/mosaic` | Unified `mosaic` CLI and TUI | Ink, Pi SDK, commander |
| `plugins/discord` | Discord channel plugin | discord.js |
| `plugins/telegram` | Telegram channel plugin | Telegraf |
## Architecture and Code Conventions
1. Gateway is the single API surface; all clients connect through it.
2. Pi SDK is ESM-only; gateway and CLI code must remain ESM.
3. Use `"type": "module"`, NodeNext module resolution, and `.js` extensions in imports.
4. Keep typed Socket.IO events in `@mosaicstack/types` to enforce client/server contracts.
5. Import OTEL tracing before NestJS bootstrap (`import './tracing.js'`).
6. Use explicit `@Inject()` decorators in NestJS because tsx/esbuild does not emit decorator metadata.
7. Keep DTOs in `*.dto.ts` files at module boundaries.
8. BetterAuth owns authentication tables; their schema is defined in `@mosaicstack/db`.
9. Create a task-specific scratchpad for non-trivial work.
## Development Workflow ## Development Workflow
Requirements: Node.js 20+, pnpm 10.6.2, and Docker Compose when optional local services are needed.
```bash ```bash
docker compose up -d # Infrastructure pnpm install --frozen-lockfile
pnpm install # Dependencies pnpm preflight
pnpm typecheck && pnpm lint && pnpm format:check # Quality gates
# Optional local queue service only; do not start the full Compose stack.
docker compose up -d valkey
``` ```
## Repo-Specific Notes The pre-push hook requires:
- DTOs in `*.dto.ts` files at module boundaries ```bash
- ESM everywhere (`"type": "module"`, `.js` extensions in imports) pnpm preflight && pnpm typecheck && pnpm lint && pnpm format:check
- NodeNext module resolution in all tsconfigs ```
- Scratchpads are mandatory for non-trivial tasks
Software delivery also requires the applicable tests. Common repository commands are:
```bash
pnpm typecheck # TypeScript checks across the workspace
pnpm lint # ESLint across the workspace
pnpm test # Checkout tests and package Vitest suites
pnpm format:check # Prettier check
pnpm build # Build all packages and applications
```
## Database and Local Runtime Safety
- Current local data-layer work uses in-process PGlite; leave `DATABASE_URL` unset.
- PostgreSQL execution is held until KBN-101-00, KBN-101-03, and KBN-101-05 land.
- Do not invoke a migration runner, initialization SQL, or the Compose PostgreSQL service from this checkout.
- Do not start Gateway/Web or run root `pnpm dev` as a local PGlite route. The current dotenv loader can inherit a daemon PostgreSQL DSN; KBN-101-02 must make that path fail closed first.
- Migration artifact generation is offline and does not authorize PostgreSQL access:
```bash
pnpm --filter @mosaicstack/db db:generate
```
## docs/TASKS.md — Schema (CANONICAL) ## docs/TASKS.md — Schema (CANONICAL)
+3 -44
View File
@@ -1,46 +1,5 @@
# CLAUDE.md — Mosaic Stack # Claude Compatibility Pointer
## Project @AGENTS.md
Self-hosted, multi-user AI agent platform. TypeScript monorepo. Do not add project guidance here. Keep `AGENTS.md` authoritative so every agent runtime receives the same instructions.
## Stack
- **API**: NestJS + Fastify adapter (`apps/gateway`)
- **Web**: Next.js 16 + React 19 (`apps/web`)
- **ORM**: Drizzle ORM + PostgreSQL 17 + pgvector (`packages/db`)
- **Auth**: BetterAuth (`packages/auth`)
- **Agent**: Pi SDK (`packages/agent`, `packages/mosaic`)
- **Queue**: Valkey 8 (`packages/queue`)
- **Build**: pnpm workspaces + Turborepo
- **CI**: Woodpecker CI
- **Observability**: OpenTelemetry → Jaeger
## Commands
```bash
pnpm typecheck # TypeScript check (all packages)
pnpm lint # ESLint (all packages)
pnpm format:check # Prettier check
pnpm test # Vitest (all packages)
pnpm build # Build all packages
# Database
pnpm --filter @mosaicstack/db db:generate # Offline migration artifact generation only
# PostgreSQL execution is held until KBN-101-00/-03/-05 land. Do not invoke a runner,
# init SQL, or Compose PostgreSQL service from this checkout.
# Dev: local PGlite data-layer work needs no PostgreSQL. Optional local queue service only:
docker compose up -d valkey
# Do not start Gateway/Web or root pnpm dev as a local PGlite route: the current unguarded dotenv
# loader can inherit a daemon PostgreSQL DSN. KBN-101-02 must make that state fail closed first.
```
## Conventions
- ESM everywhere (`"type": "module"`, `.js` extensions in imports)
- NodeNext module resolution
- Explicit `@Inject()` decorators in NestJS (tsx/esbuild doesn't support emitDecoratorMetadata)
- DTOs in `*.dto.ts` files at module boundaries
- OTEL tracing imported before NestJS bootstrap (`import './tracing.js'`)
- All three gates must pass before push: typecheck, lint, format:check
@@ -0,0 +1,33 @@
# CI Queue Guard Purpose Semantics
- **Issue:** #1146
- **Target branch:** `next`
## Problem
`ci-queue-wait.sh` treats any result other than terminal success as asserted non-readiness. That is correct for merge readiness, but incorrect for the pre-push queue guard: a terminal failure or an empty status set means no pipeline is queued or running, so the queue is clear.
## Design
Make final-state handling purpose-sensitive while preserving the existing provider and payload safeguards:
- `--purpose push`
- wait while state is `pending`;
- return success for `terminal-success`, `terminal-failure`, and `no-status`;
- continue rejecting `malformed`, `unknown`, and unrecognized states.
- `--purpose merge`
- return success only for `terminal-success`;
- continue rejecting `terminal-failure`, `no-status`, malformed, unknown, and unrecognized states.
- `--require-status` remains authoritative: `no-status` fails for either purpose when it is supplied.
Diagnostics will explicitly distinguish a queue-clear push result from successful CI so callers cannot mistake an old failure for a green pipeline.
## Testing
Extend the process-level tri-state regression harness with separate push and merge assertions:
1. Push passes for terminal success, terminal failure, and no status.
2. Push still fails for pending, malformed, and unknown states.
3. `--require-status` makes push/no-status fail.
4. Merge behavior remains fail-closed except for terminal success.
5. Existing provider-unavailable audit behavior remains unchanged.
@@ -0,0 +1,208 @@
# CI Queue Guard Purpose Semantics Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Make the pre-push CI queue guard pass when no pipeline is queued or running while preserving fail-closed merge readiness.
**Architecture:** Keep provider lookup and tri-state classification unchanged. Make only the final state dispatch purpose-sensitive: push treats valid non-pending states as queue-clear, while merge continues to require terminal success. Preserve `--require-status`, malformed-payload rejection, unknown-state rejection, and audited provider-unavailable behavior.
**Tech Stack:** Bash, process-level shell regression harnesses, Gitea/GitHub status APIs.
---
### Task 1: Freeze Purpose-Specific State Semantics
**Files:**
- Modify: `packages/mosaic/framework/tools/git/test-ci-queue-wait-tristate.sh`
- Test: `packages/mosaic/framework/tools/git/test-ci-queue-wait-tristate.sh`
**Step 1: Add failing push assertions**
Change push expectations so `terminal-failure` and `no-status` require exit 0 plus an explicit `queue-clear` diagnostic. Add a `--require-status` assertion that keeps push/no-status non-zero.
**Step 2: Add failing merge assertions**
Invoke the same harness with `MOSAIC_TEST_PURPOSE=merge` and assert terminal failure and no status remain non-zero while terminal success remains zero.
**Step 3: Add unknown-state coverage**
Add a stub payload with a syntactically valid but unsupported status value and assert both purposes reject it.
**Step 4: Run the focused test and verify RED**
Run:
```bash
bash packages/mosaic/framework/tools/git/test-ci-queue-wait-tristate.sh
```
Expected: failures showing push terminal-failure and no-status returned exit 3 instead of exit 0 or lacked `queue-clear` diagnostics.
**Step 5: Commit the failing tests**
```bash
git add packages/mosaic/framework/tools/git/test-ci-queue-wait-tristate.sh
git commit -m "test(ci): define purpose-aware queue readiness"
```
### Task 2: Implement Purpose-Sensitive Final-State Dispatch
**Files:**
- Modify: `packages/mosaic/framework/tools/git/ci-queue-wait.sh:458-481`
- Test: `packages/mosaic/framework/tools/git/test-ci-queue-wait-tristate.sh`
- Test: `packages/mosaic/framework/tools/git/test-ci-queue-wait-github-checks.sh`
**Step 1: Implement push queue-clear behavior**
For `no-status`, retain the existing `--require-status` failure. Otherwise, return success for push with an explicit diagnostic such as:
```text
[ci-queue-wait] queue-clear state=no-status purpose=push branch=<branch>; no queued or running CI.
```
For `terminal-failure`, return success only for push with the same queue-clear wording. Merge must continue returning asserted non-readiness.
**Step 2: Preserve malformed and unknown rejection**
Keep `malformed`, `unknown`, and unrecognized states non-zero for both purposes.
**Step 3: Run focused tests and verify GREEN**
Run:
```bash
bash packages/mosaic/framework/tools/git/test-ci-queue-wait-tristate.sh
bash packages/mosaic/framework/tools/git/test-ci-queue-wait-github-checks.sh
```
Expected: both scripts exit 0 and report their regression suites passed.
**Step 4: Commit implementation**
```bash
git add packages/mosaic/framework/tools/git/ci-queue-wait.sh
git commit -m "fix(ci): separate push queue clearance from merge readiness"
```
### Task 3: Verify, Review, and Document Evidence
**Files:**
- Modify: `docs/scratchpads/1146-ci-queue-purpose.md`
**Step 1: Run shell syntax and focused regressions**
```bash
bash -n packages/mosaic/framework/tools/git/ci-queue-wait.sh
bash -n packages/mosaic/framework/tools/git/test-ci-queue-wait-tristate.sh
bash packages/mosaic/framework/tools/git/test-ci-queue-wait-tristate.sh
bash packages/mosaic/framework/tools/git/test-ci-queue-wait-github-checks.sh
```
**Step 2: Run repository quality gates**
```bash
pnpm preflight
pnpm typecheck
pnpm lint
pnpm test
pnpm format:check
```
Expected: every command exits 0.
**Step 3: Obtain independent review**
Request review of the exact branch head. Remediate all blocking findings and rerun focused and baseline gates.
**Step 4: Record evidence and commit**
Update the scratchpad with test output, review result, and residual risk, then commit it:
```bash
git add docs/scratchpads/1146-ci-queue-purpose.md
git commit -m "docs(ci): record queue guard verification"
```
### Task 4: Keep the Merge Wrapper Aligned with the `next` Lane
**Files:**
- Modify: `packages/mosaic/framework/tools/git/pr-merge.sh:97-101`
- Test: `packages/mosaic/framework/tools/git/test-pr-merge-head-pin.sh`
**Step 1: Write the failing regression**
Run the exact-head merge regression with its Gitea fixture targeting `next` and confirm the current wrapper rejects it because it only permits `main`.
**Step 2: Allow only documented integration targets**
Permit `main` and `next`; reject every other target. Do not alter exact-head pinning, queue-guard invocation, provider selection, or merge method enforcement.
**Step 3: Run focused merge regressions**
```bash
bash packages/mosaic/framework/tools/git/test-pr-merge-head-pin.sh
bash packages/mosaic/framework/tools/git/test-pr-merge-queue-branch.sh
bash packages/mosaic/framework/tools/git/test-pr-merge-gitea-empty-uid.sh
```
Expected: all pass, including a Gitea merge fixture targeting `next`.
**Step 4: Commit**
```bash
git add packages/mosaic/framework/tools/git/pr-merge.sh packages/mosaic/framework/tools/git/test-pr-merge-head-pin.sh
git commit -m "fix(ci): allow reviewed merges into next"
```
### Task 5: Activate and Deliver Through `next`
**Files:**
- Installed output: `~/.config/mosaic/tools/git/ci-queue-wait.sh`
**Step 1: Activate through the canonical installer**
From the reviewed worktree, run the framework installer in sync-only keep mode so operator files remain protected:
```bash
MOSAIC_SYNC_ONLY=1 MOSAIC_INSTALL_MODE=keep MOSAIC_SKIP_SKILLS_SYNC=1 \
bash packages/mosaic/framework/install.sh
```
**Step 2: Verify installed/source parity**
```bash
cmp -s \
packages/mosaic/framework/tools/git/ci-queue-wait.sh \
~/.config/mosaic/tools/git/ci-queue-wait.sh
```
Expected: exit 0.
**Step 3: Run mandatory pre-push queue guard**
```bash
~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push -B fix/1146-ci-queue-purpose
```
Expected: branch-absent or queue-clear success.
**Step 4: Push and open a PR against `next`**
```bash
git push -u origin fix/1146-ci-queue-purpose
~/.config/mosaic/tools/git/pr-create.sh \
-t "fix(ci): make queue guard purpose-sensitive" \
-b "Closes #1146" \
-B next \
-H fix/1146-ci-queue-purpose \
-i 1146
```
**Step 5: Complete reviewed integration**
Wait for exact-head terminal-green CI, obtain the required review, merge via the Mosaic wrapper, verify merged CI, and close #1146. Do not bypass any gate.
+65
View File
@@ -0,0 +1,65 @@
# #1146 — CI Queue Guard Purpose Semantics
## Objective
Make the pre-push queue guard wait for queued/running CI without requiring the previous remote head to have successful CI. Preserve fail-closed merge readiness.
## Scope
- `packages/mosaic/framework/tools/git/ci-queue-wait.sh`
- focused queue-guard regression tests
- design and scratchpad documentation
- local framework activation required before the fixed guard can authorize this branch's push
## Plan
1. Freeze purpose-specific behavior in failing process-level tests.
2. Implement the smallest state-dispatch change.
3. Run focused shell tests and repository quality gates.
4. Obtain independent review and remediate findings.
5. Install the reviewed framework source locally, run the mandatory pre-push queue guard, and push.
6. Open a PR against `next`, verify terminal-green CI, and close #1146 after merge.
## Budget
- ASSUMPTION: no explicit token cap was provided.
- Working estimate: 12K tokens.
- Scope reduction: change only final-state dispatch and focused tests; do not redesign provider adapters.
## Progress
- Confirmed source and installed guards are byte-identical.
- Reproduced `terminal-failure` blocking `--purpose push`.
- Root cause: final-state dispatch requires terminal success for both push and merge.
- Design approved: push is queue-clear on valid non-pending states; merge remains fail-closed.
## Tests
- RED confirmed before implementation: the focused tri-state harness reported push `terminal-failure` and `no-status` as `ASSERTED_NOT_READY`.
- GREEN: `bash packages/mosaic/framework/tools/git/test-ci-queue-wait-tristate.sh` — all outcome classes passed.
- GREEN: `bash packages/mosaic/framework/tools/git/test-ci-queue-wait-github-checks.sh` — 6/6 purpose-aware cases passed.
- GREEN: `bash -n` passed for the changed guard and both focused harnesses.
- GREEN: `pnpm preflight`, `pnpm typecheck`, and `pnpm lint` passed.
- `pnpm test` ran 45/46 workspace test tasks successfully, but the pre-existing Gateway `cross-user-isolation.test.ts` failed during cleanup with PostgreSQL error `28P01` (local `mosaic` password authentication failure). The changed Mosaic framework test task passed within that run.
- GREEN: focused queue and merge shell regressions passed after the wrapper change.
- GREEN: isolated Mosaic Vitest run passed (81 files, 1,514 tests).
- The normal parallel Mosaic Vitest run has an environment-sensitive pre-existing failure in `install-ordering-guard.spec.ts`: the real activation probe changes between two calls while other suites run concurrently. Running the same spec alone and the complete Vitest suite with one fork passes.
- The framework shell suite's pre-existing `version_coupling_unittest.py` also fails locally because the newly installed `mosaic` is now on PATH despite the test injecting a nonexistent PATH; CI's clean image does not have this global CLI. All changed queue/merge harnesses pass.
- GREEN: `pnpm format:check` passed.
- Note: an additional ad hoc Prettier command was not applicable to shell files because Prettier has no shell parser; the repository-wide format check passed using its configured file globs.
## Review
- Independent Codex review of the six-file diff: approved, confidence 0.84, zero blockers/should-fix/suggestions.
- Review confirmed push queue-clear behavior, merge fail-closed behavior, and `--require-status` coverage.
## Risks and Blockers
- Canonical framework activation completed with `MOSAIC_SYNC_ONLY=1 MOSAIC_INSTALL_MODE=keep MOSAIC_SKIP_SKILLS_SYNC=1 bash packages/mosaic/framework/install.sh`.
- Source and installed queue guards are byte-identical (`cmp` and SHA-256 parity passed).
- The installed pre-push guard now passes for the not-yet-remote feature branch with `queue clear`.
- The required merge wrapper then exposed a second bootstrap defect: `pr-merge.sh` hardcoded `main`, contradicting the documented PR-based `next` integration lane. Tracked as #1149 and fixed in the same delivery branch with a regression fixture targeting `next`.
- Activation emitted the existing manifest-safety warning that six `fleet/run/*.hb*` operator files were touched then restored; no data loss was observed, but this remains a pre-existing framework-manifest defect to report separately.
- The first activation attempt timed out after 600 seconds while copying the 113K-file operator snapshot; the bounded 1,800-second retry completed successfully. It left a partial durable snapshot from the interrupted attempt in the normal backup directory; the completed snapshot is the newer `pre-update-20260810T195317Z` entry.
- Full baseline test completion is blocked by the unrelated local PostgreSQL authentication/cleanup failure described above; CI has its own disposable PostgreSQL service.
- Existing `.mosaic/orchestrator/*` working-tree changes are unrelated and must remain unstaged.
@@ -465,12 +465,24 @@ 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
if [[ "$PURPOSE" == "push" ]]; then
echo "[ci-queue-wait] queue-clear state=no-status purpose=push branch=${BRANCH}; no queued or running CI."
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)
if [[ "$PURPOSE" == "push" ]]; then
echo "[ci-queue-wait] queue-clear state=terminal-failure purpose=push branch=${BRANCH}; no queued or running CI."
exit 0
fi
echo "Error: ASSERTED_NOT_READY state=terminal-failure purpose=${PURPOSE} branch=${BRANCH}." >&2
exit 3
;;
malformed|unknown)
echo "Error: ASSERTED_NOT_READY state=${STATE} purpose=${PURPOSE} branch=${BRANCH}." >&2 echo "Error: ASSERTED_NOT_READY state=${STATE} purpose=${PURPOSE} branch=${BRANCH}." >&2
exit 3 exit 3
;; ;;
@@ -94,8 +94,8 @@ BASE_BRANCH="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; print(
HEAD_BRANCH="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; print((json.load(sys.stdin).get("headRefName") or "").strip())')" HEAD_BRANCH="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; print((json.load(sys.stdin).get("headRefName") or "").strip())')"
HEAD_SHA="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; print((json.load(sys.stdin).get("headRefOid") or "").strip())')" HEAD_SHA="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; print((json.load(sys.stdin).get("headRefOid") or "").strip())')"
HEAD_REPO="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; value=json.load(sys.stdin).get("headRepository") or ""; print((value.get("nameWithOwner") or value.get("full_name") or "") if isinstance(value, dict) else str(value).strip())')" HEAD_REPO="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; value=json.load(sys.stdin).get("headRepository") or ""; print((value.get("nameWithOwner") or value.get("full_name") or "") if isinstance(value, dict) else str(value).strip())')"
if [[ "$BASE_BRANCH" != "main" ]]; then if [[ "$BASE_BRANCH" != "main" && "$BASE_BRANCH" != "next" ]]; then
echo "Error: Mosaic policy allows merges only for PRs targeting 'main' (found '$BASE_BRANCH')." >&2 echo "Error: Mosaic policy allows merges only for PRs targeting 'main' or 'next' (found '$BASE_BRANCH')." >&2
exit 1 exit 1
fi fi
@@ -43,22 +43,22 @@ SH
chmod +x "$STUB_DIR/gh" chmod +x "$STUB_DIR/gh"
run_guard() { run_guard() {
local mode="$1" local mode="$1" purpose="${2:-push}"
( (
cd "$REPO_DIR" || exit cd "$REPO_DIR" || exit
export PATH="$STUB_DIR:$PATH" export PATH="$STUB_DIR:$PATH"
export MOSAIC_GH_CHECK_MODE="$mode" export MOSAIC_GH_CHECK_MODE="$mode"
export MOSAIC_GH_CALL_LOG="$WORK_DIR/gh-calls.log" export MOSAIC_GH_CALL_LOG="$WORK_DIR/gh-calls.log"
export MOSAIC_CI_QUEUE_AUDIT_LOG="$WORK_DIR/audit.jsonl" export MOSAIC_CI_QUEUE_AUDIT_LOG="$WORK_DIR/audit.jsonl"
"$SCRIPT_DIR/ci-queue-wait.sh" --purpose push -t 0 -i 0 "$SCRIPT_DIR/ci-queue-wait.sh" --purpose "$purpose" -t 0 -i 0
) )
} }
failures=0 failures=0
assert_case() { assert_case() {
local mode="$1" expected_rc="$2" expected_state="$3" output rc local mode="$1" expected_rc="$2" expected_state="$3" purpose="${4:-push}" output rc
set +e set +e
output=$(run_guard "$mode" 2>&1) output=$(run_guard "$mode" "$purpose" 2>&1)
rc=$? rc=$?
set -e set -e
if [[ "$expected_rc" == zero && "$rc" -ne 0 ]]; then if [[ "$expected_rc" == zero && "$rc" -ne 0 ]]; then
@@ -79,10 +79,12 @@ set -e
: > "$WORK_DIR/gh-calls.log" : > "$WORK_DIR/gh-calls.log"
assert_case success zero terminal-success assert_case success zero terminal-success
assert_case pending nonzero pending assert_case pending nonzero pending
assert_case failure nonzero terminal-failure assert_case failure zero terminal-failure
assert_case late-failure nonzero terminal-failure assert_case late-failure zero terminal-failure
assert_case failure nonzero terminal-failure merge
assert_case late-failure nonzero terminal-failure merge
if [[ $(grep -c 'check-runs?per_page=100&filter=latest' "$WORK_DIR/gh-calls.log") -lt 4 ]]; then if [[ $(grep -c 'check-runs?per_page=100&filter=latest' "$WORK_DIR/gh-calls.log") -lt 6 ]]; then
echo "FAIL: expected every case to query all Checks API pages" >&2 echo "FAIL: expected every case to query all Checks API pages" >&2
failures=$((failures + 1)) failures=$((failures + 1))
fi fi
@@ -92,4 +94,4 @@ if [[ "$failures" -ne 0 ]]; then
exit 1 exit 1
fi fi
echo "GitHub check-runs regression passed (4/4 cases, including later-page failure)" echo "GitHub check-runs regression passed (6/6 purpose-aware cases, including later-page failure)"
@@ -53,6 +53,7 @@ case "$url" in
malformed) printf '%s' 'not-json' ;; malformed) printf '%s' 'not-json' ;;
malformed-statuses-type) printf '%s' '{"state":"success","statuses":"corrupt"}' ;; malformed-statuses-type) printf '%s' '{"state":"success","statuses":"corrupt"}' ;;
malformed-status-entry) printf '%s' '{"state":"success","statuses":[null]}' ;; malformed-status-entry) printf '%s' '{"state":"success","statuses":[null]}' ;;
unknown) printf '%s' '{"state":"success","statuses":[{"status":"cancelled"}]}' ;;
large-success) large-success)
python3 -c 'import json; print(json.dumps({"state":"success", "statuses":[{"status":"success"}], "padding":"x" * (160 * 1024)}), end="")' python3 -c 'import json; print(json.dumps({"state":"success", "statuses":[{"status":"success"}], "padding":"x" * (160 * 1024)}), end="")'
;; ;;
@@ -128,18 +129,27 @@ run_assertion() {
set -e set -e
: > "$WORK_DIR/urls.log" : > "$WORK_DIR/urls.log"
run_assertion success zero success 'state=terminal-success' # Push readiness is queue clearance, not proof that prior CI succeeded.
run_assertion pending nonzero pending 'ASSERTED_NOT_READY' run_assertion push-success zero success 'state=terminal-success'
run_assertion failure nonzero failure 'ASSERTED_NOT_READY' run_assertion push-pending nonzero pending 'ASSERTED_NOT_READY'
run_assertion no-status nonzero no-status 'ASSERTED_NOT_READY' run_assertion push-failure zero failure 'queue-clear state=terminal-failure purpose=push'
run_assertion aggregate-success-no-status nonzero aggregate-success-no-status 'ASSERTED_NOT_READY' run_assertion push-no-status zero no-status 'queue-clear state=no-status purpose=push'
run_assertion malformed nonzero malformed 'ASSERTED_NOT_READY' run_assertion push-aggregate-success-no-status zero aggregate-success-no-status 'queue-clear state=no-status purpose=push'
run_assertion malformed-statuses-type nonzero malformed-statuses-type 'ASSERTED_NOT_READY' run_assertion push-require-status nonzero no-status 'ASSERTED_NOT_READY state=no-status' --require-status
run_assertion malformed-status-entry nonzero malformed-status-entry 'ASSERTED_NOT_READY' run_assertion push-malformed nonzero malformed 'ASSERTED_NOT_READY'
run_assertion large-payload not126 large-success 'state=terminal-success' run_assertion push-malformed-statuses-type nonzero malformed-statuses-type 'ASSERTED_NOT_READY'
run_assertion push-malformed-status-entry nonzero malformed-status-entry 'ASSERTED_NOT_READY'
run_assertion push-unknown nonzero unknown 'ASSERTED_NOT_READY'
run_assertion push-large-payload not126 large-success 'state=terminal-success'
run_assertion credential-unresolvable zero credential-unresolvable 'CANNOT_ASSERT' run_assertion credential-unresolvable zero credential-unresolvable 'CANNOT_ASSERT'
run_assertion provider-unreachable zero unreachable 'CANNOT_ASSERT' run_assertion provider-unreachable zero unreachable 'CANNOT_ASSERT'
# Merge readiness remains fail-closed and requires exact-head terminal success.
MOSAIC_TEST_PURPOSE=merge run_assertion merge-success zero success 'state=terminal-success'
MOSAIC_TEST_PURPOSE=merge run_assertion merge-failure nonzero failure 'ASSERTED_NOT_READY state=terminal-failure'
MOSAIC_TEST_PURPOSE=merge run_assertion merge-no-status nonzero no-status 'ASSERTED_NOT_READY state=no-status'
MOSAIC_TEST_PURPOSE=merge run_assertion merge-unknown nonzero unknown 'ASSERTED_NOT_READY state=unknown'
if [[ ! -s "$AUDIT_LOG" ]] || ! grep -q '"outcome":"CANNOT_ASSERT"' "$AUDIT_LOG"; then if [[ ! -s "$AUDIT_LOG" ]] || ! grep -q '"outcome":"CANNOT_ASSERT"' "$AUDIT_LOG"; then
echo "FAIL provider-unreachable-audit: expected durable CANNOT_ASSERT JSONL record" >&2 echo "FAIL provider-unreachable-audit: expected durable CANNOT_ASSERT JSONL record" >&2
failures=$((failures + 1)) failures=$((failures + 1))
@@ -17,9 +17,10 @@ make_fixture() {
cp "$SCRIPT_DIR/detect-platform.sh" "$tools/detect-platform.sh" cp "$SCRIPT_DIR/detect-platform.sh" "$tools/detect-platform.sh"
git -C "$root/repo" init -q git -C "$root/repo" init -q
git -C "$root/repo" remote add origin "$remote" git -C "$root/repo" remote add origin "$remote"
local base_branch="${3:-main}"
cat > "$tools/pr-metadata.sh" <<SH cat > "$tools/pr-metadata.sh" <<SH
#!/usr/bin/env bash #!/usr/bin/env bash
printf '%s\n' '{"baseRefName":"main","headRefName":"fix/pinned","headRefOid":"$SHA","headRepository":"contributor/widgets-fork"}' printf '%s\n' '{"baseRefName":"$base_branch","headRefName":"fix/pinned","headRefOid":"$SHA","headRepository":"contributor/widgets-fork"}'
SH SH
cat > "$tools/ci-queue-wait.sh" <<'SH' cat > "$tools/ci-queue-wait.sh" <<'SH'
#!/usr/bin/env bash #!/usr/bin/env bash
@@ -29,8 +30,8 @@ SH
} }
rm -rf "$WORK_DIR" rm -rf "$WORK_DIR"
make_fixture gitea https://git.example.test/acme/widgets.git make_fixture gitea https://git.example.test/acme/widgets.git next
make_fixture github https://github.com/acme/widgets.git make_fixture github https://github.com/acme/widgets.git main
cat > "$WORK_DIR/gitea/curl" <<'SH' cat > "$WORK_DIR/gitea/curl" <<'SH'
#!/usr/bin/env bash #!/usr/bin/env bash