Compare commits
1 Commits
main
...
feat/633-c
| Author | SHA1 | Date | |
|---|---|---|---|
| 1288c4bc2c |
4
.npmrc
4
.npmrc
@@ -1,5 +1 @@
|
|||||||
@mosaicstack:registry=https://git.mosaicstack.dev/api/packages/mosaicstack/npm/
|
@mosaicstack:registry=https://git.mosaicstack.dev/api/packages/mosaicstack/npm/
|
||||||
# Pin the pnpm store to the same path the ci-base image warms (Dockerfile.ci),
|
|
||||||
# so the pipeline `pnpm install --prefer-offline` consumes the baked store
|
|
||||||
# instead of repopulating a fresh one.
|
|
||||||
store-dir=/root/.local/share/pnpm/store
|
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
# Build & push the pre-baked CI base image (Dockerfile.ci) to the Gitea
|
|
||||||
# registry CI already publishes to. Reuses the exact kaniko + auth pattern
|
|
||||||
# from publish.yml (REGISTRY_USER/REGISTRY_PASS from_secret, /kaniko/.docker
|
|
||||||
# config.json). Other pipelines (ci.yml, publish.yml) pull `ci-base:latest`
|
|
||||||
# for their install step.
|
|
||||||
#
|
|
||||||
# Rebuild ONLY when the dependency set or the image recipe changes — a normal
|
|
||||||
# code push must not trigger a 25-min image build. `path` applies to push/PR
|
|
||||||
# events; `event: tag` (releases) rebuilds unconditionally so a tagged release
|
|
||||||
# always ships a fresh base.
|
|
||||||
when:
|
|
||||||
- event: tag
|
|
||||||
- event: [push, manual]
|
|
||||||
branch: main
|
|
||||||
path:
|
|
||||||
include:
|
|
||||||
- 'pnpm-lock.yaml'
|
|
||||||
- 'Dockerfile.ci'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
build-ci-base:
|
|
||||||
image: gcr.io/kaniko-project/executor:debug
|
|
||||||
environment:
|
|
||||||
REGISTRY_USER:
|
|
||||||
from_secret: gitea_username
|
|
||||||
REGISTRY_PASS:
|
|
||||||
from_secret: gitea_password
|
|
||||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
|
||||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
|
||||||
CI_COMMIT_SHA: ${CI_COMMIT_SHA}
|
|
||||||
commands:
|
|
||||||
- mkdir -p /kaniko/.docker
|
|
||||||
- echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASS\"}}}" > /kaniko/.docker/config.json
|
|
||||||
- |
|
|
||||||
# Lockfile-hash tag: an immutable identity for the exact dep set baked
|
|
||||||
# into this image. `:latest` is the mutable pointer pipelines consume.
|
|
||||||
LOCK_HASH=$(sha256sum pnpm-lock.yaml | cut -c1-12)
|
|
||||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaicstack/stack/ci-base:latest"
|
|
||||||
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/ci-base:lock-$LOCK_HASH"
|
|
||||||
/kaniko/executor --context . --dockerfile Dockerfile.ci $DESTINATIONS
|
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
# &node_image is the pre-baked CI base built by .woodpecker/ci-image.yml:
|
|
||||||
# node:24-alpine + python3/make/g++/postgresql-client + pnpm + a warm pnpm
|
|
||||||
# store. The install step resolves from the baked store (--prefer-offline)
|
|
||||||
# instead of paying a ~731s cold fetch + native compile every run.
|
|
||||||
variables:
|
variables:
|
||||||
- &node_image 'git.mosaicstack.dev/mosaicstack/stack/ci-base:latest'
|
- &node_image 'node:22-alpine'
|
||||||
- &enable_pnpm 'corepack enable'
|
- &enable_pnpm 'corepack enable'
|
||||||
|
|
||||||
when:
|
when:
|
||||||
@@ -19,9 +15,8 @@ steps:
|
|||||||
image: *node_image
|
image: *node_image
|
||||||
commands:
|
commands:
|
||||||
- corepack enable
|
- corepack enable
|
||||||
# python3/make/g++ are baked into ci-base; --prefer-offline resolves from
|
- apk add --no-cache python3 make g++
|
||||||
# the baked pnpm store.
|
- pnpm install --frozen-lockfile
|
||||||
- pnpm install --frozen-lockfile --prefer-offline
|
|
||||||
|
|
||||||
# Blocking gate: public framework package must contain no operator-specific
|
# Blocking gate: public framework package must contain no operator-specific
|
||||||
# personal data or private $HOME defaults. Runs early (no node_modules needed).
|
# personal data or private $HOME defaults. Runs early (no node_modules needed).
|
||||||
@@ -69,7 +64,8 @@ steps:
|
|||||||
DATABASE_URL: postgresql://mosaic:mosaic@ci-postgres:5432/mosaic
|
DATABASE_URL: postgresql://mosaic:mosaic@ci-postgres:5432/mosaic
|
||||||
commands:
|
commands:
|
||||||
- *enable_pnpm
|
- *enable_pnpm
|
||||||
# postgresql-client (pg_isready) is baked into ci-base.
|
# Install postgresql-client for pg_isready
|
||||||
|
- apk add --no-cache postgresql-client
|
||||||
# Wait up to 60s for CI postgres to be ready; fail fast if it never comes up.
|
# Wait up to 60s for CI postgres to be ready; fail fast if it never comes up.
|
||||||
- |
|
- |
|
||||||
ready=0
|
ready=0
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
# Runs only on main branch push/tag
|
# Runs only on main branch push/tag
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
# Pre-baked CI base (see .woodpecker/ci-image.yml): node:24-alpine +
|
- &node_image 'node:22-alpine'
|
||||||
# toolchain + warm pnpm store. Kills the second cold install publish pays.
|
|
||||||
- &node_image 'git.mosaicstack.dev/mosaicstack/stack/ci-base:latest'
|
|
||||||
- &enable_pnpm 'corepack enable'
|
- &enable_pnpm 'corepack enable'
|
||||||
# Heavy kaniko image builds (~25 min) — gate them so a merge that only touches
|
# Heavy kaniko image builds (~25 min) — gate them so a merge that only touches
|
||||||
# the npm-only CLI (@mosaicstack/mosaic) or docs does NOT rebuild the platform
|
# the npm-only CLI (@mosaicstack/mosaic) or docs does NOT rebuild the platform
|
||||||
@@ -33,8 +31,7 @@ steps:
|
|||||||
image: *node_image
|
image: *node_image
|
||||||
commands:
|
commands:
|
||||||
- corepack enable
|
- corepack enable
|
||||||
# Resolve from the baked pnpm store instead of a cold network fetch.
|
- pnpm install --frozen-lockfile
|
||||||
- pnpm install --frozen-lockfile --prefer-offline
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
image: *node_image
|
image: *node_image
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
# Pre-baked CI base image for Woodpecker pipelines.
|
|
||||||
#
|
|
||||||
# Purpose: eliminate the cold `pnpm install` that dominates every pipeline
|
|
||||||
# (~731s median). This image ships the native toolchain (no per-run `apk add`)
|
|
||||||
# AND a warm, content-addressable pnpm store with the dependency-tree tarballs
|
|
||||||
# already fetched at build time. `pnpm fetch` only populates the store from the
|
|
||||||
# lockfile — it does NOT run the native node-gyp builds (better-sqlite3,
|
|
||||||
# node-pty, sqlite3, canvas, sharp); those still compile at `pnpm install`,
|
|
||||||
# which is exactly why the musl toolchain stays baked into this image. A
|
|
||||||
# pipeline `pnpm install --frozen-lockfile --prefer-offline` then resolves
|
|
||||||
# tarballs from local hard-links (no network) and compiles natives against the
|
|
||||||
# already-present toolchain, in tens of seconds instead of ~731s.
|
|
||||||
#
|
|
||||||
# Rebuilt only when `pnpm-lock.yaml` or this Dockerfile change
|
|
||||||
# (see .woodpecker/ci-image.yml).
|
|
||||||
#
|
|
||||||
# Node version is pinned to 24 (Active LTS). This is the follow-up bump from
|
|
||||||
# node:22 — sequenced AFTER the CI cache work landed so the runtime change
|
|
||||||
# carries zero cache variables. node:26 stays held until it reaches LTS
|
|
||||||
# (Oct 2026); the Current line risks native-module (node-gyp) breakage on a
|
|
||||||
# runner that compiles better-sqlite3 / canvas / sharp / node-pty from source.
|
|
||||||
FROM node:24-alpine
|
|
||||||
|
|
||||||
# Native toolchain required to compile node-gyp deps on musl, plus the
|
|
||||||
# postgresql-client used by the test step's pg_isready readiness probe. `bash`
|
|
||||||
# is baked here too — the sanitization step in ci.yml otherwise does a per-run
|
|
||||||
# `apk add bash`.
|
|
||||||
RUN apk add --no-cache python3 make g++ postgresql-client bash
|
|
||||||
|
|
||||||
# Pin pnpm to the repo's packageManager version via corepack.
|
|
||||||
RUN corepack enable && corepack prepare pnpm@10.6.2 --activate
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Pin the store location so the pipeline can point `store-dir` at the same path.
|
|
||||||
ENV PNPM_HOME=/root/.local/share/pnpm
|
|
||||||
RUN pnpm config set store-dir /root/.local/share/pnpm/store
|
|
||||||
|
|
||||||
# Warm the store. `pnpm fetch` populates the content-addressable store with the
|
|
||||||
# dependency tarballs directly from the lockfile (no package.json / workspace
|
|
||||||
# needed), so a baked store stays valid until the lockfile changes. Note:
|
|
||||||
# `fetch` does NOT compile native modules — that happens later at `pnpm install`
|
|
||||||
# in the pipeline, against the toolchain baked above.
|
|
||||||
COPY pnpm-lock.yaml ./
|
|
||||||
RUN pnpm fetch --frozen-lockfile
|
|
||||||
@@ -2,20 +2,12 @@
|
|||||||
when:
|
when:
|
||||||
- event: [push, pull_request, manual]
|
- event: [push, pull_request, manual]
|
||||||
|
|
||||||
# Dependencies are installed ONCE in the `install` step and every downstream
|
|
||||||
# step depends on it, reusing the populated node_modules from the shared
|
|
||||||
# workspace volume. Do NOT re-run `npm ci` per step — that pays the full cold
|
|
||||||
# install (network fetch + native rebuilds) N times and is the dominant cost
|
|
||||||
# in a pipeline.
|
|
||||||
#
|
|
||||||
# For best results, replace `&node_image` with a pre-baked CI base image that
|
|
||||||
# ships your toolchain (python3/make/g++ for native modules) and a warm npm
|
|
||||||
# cache, then keep `--prefer-offline` so installs resolve from the cache. See
|
|
||||||
# the Mosaic Stack repo's Dockerfile.ci + .woodpecker/ci-image.yml for the
|
|
||||||
# baked-image pattern.
|
|
||||||
variables:
|
variables:
|
||||||
- &node_image 'node:20-alpine'
|
- &node_image 'node:20-alpine'
|
||||||
- &gitleaks_image 'ghcr.io/gitleaks/gitleaks:v8.24.0'
|
- &gitleaks_image 'ghcr.io/gitleaks/gitleaks:v8.24.0'
|
||||||
|
- &install_deps |
|
||||||
|
corepack enable
|
||||||
|
npm ci --ignore-scripts
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# Secret scanning (runs in parallel with install, no deps)
|
# Secret scanning (runs in parallel with install, no deps)
|
||||||
@@ -25,18 +17,15 @@ steps:
|
|||||||
- gitleaks git --redact --verbose --log-opts="HEAD~1..HEAD"
|
- gitleaks git --redact --verbose --log-opts="HEAD~1..HEAD"
|
||||||
depends_on: []
|
depends_on: []
|
||||||
|
|
||||||
# Single cached install. Every other step depends on this and reuses the
|
|
||||||
# node_modules it produces in the shared workspace.
|
|
||||||
install:
|
install:
|
||||||
image: *node_image
|
image: *node_image
|
||||||
commands:
|
commands:
|
||||||
- corepack enable
|
- *install_deps
|
||||||
- npm ci --ignore-scripts --prefer-offline
|
|
||||||
depends_on: []
|
|
||||||
|
|
||||||
security-audit:
|
security-audit:
|
||||||
image: *node_image
|
image: *node_image
|
||||||
commands:
|
commands:
|
||||||
|
- *install_deps
|
||||||
- npm audit --audit-level=high
|
- npm audit --audit-level=high
|
||||||
depends_on:
|
depends_on:
|
||||||
- install
|
- install
|
||||||
@@ -46,6 +35,7 @@ steps:
|
|||||||
environment:
|
environment:
|
||||||
SKIP_ENV_VALIDATION: 'true'
|
SKIP_ENV_VALIDATION: 'true'
|
||||||
commands:
|
commands:
|
||||||
|
- *install_deps
|
||||||
- npm run lint
|
- npm run lint
|
||||||
depends_on:
|
depends_on:
|
||||||
- install
|
- install
|
||||||
@@ -55,6 +45,7 @@ steps:
|
|||||||
environment:
|
environment:
|
||||||
SKIP_ENV_VALIDATION: 'true'
|
SKIP_ENV_VALIDATION: 'true'
|
||||||
commands:
|
commands:
|
||||||
|
- *install_deps
|
||||||
- npm run type-check
|
- npm run type-check
|
||||||
depends_on:
|
depends_on:
|
||||||
- install
|
- install
|
||||||
@@ -64,6 +55,7 @@ steps:
|
|||||||
environment:
|
environment:
|
||||||
SKIP_ENV_VALIDATION: 'true'
|
SKIP_ENV_VALIDATION: 'true'
|
||||||
commands:
|
commands:
|
||||||
|
- *install_deps
|
||||||
- npm run test -- --coverage --coverageThreshold='{"global":{"branches":80,"functions":80,"lines":80,"statements":80}}'
|
- npm run test -- --coverage --coverageThreshold='{"global":{"branches":80,"functions":80,"lines":80,"statements":80}}'
|
||||||
depends_on:
|
depends_on:
|
||||||
- install
|
- install
|
||||||
@@ -74,6 +66,7 @@ steps:
|
|||||||
SKIP_ENV_VALIDATION: 'true'
|
SKIP_ENV_VALIDATION: 'true'
|
||||||
NODE_ENV: 'production'
|
NODE_ENV: 'production'
|
||||||
commands:
|
commands:
|
||||||
|
- *install_deps
|
||||||
- npm run build
|
- npm run build
|
||||||
depends_on:
|
depends_on:
|
||||||
- lint
|
- lint
|
||||||
|
|||||||
@@ -12,10 +12,6 @@
|
|||||||
# ambiguity about lanes or origin. Recipients replying should FLIP the
|
# ambiguity about lanes or origin. Recipients replying should FLIP the
|
||||||
# preamble: [<dst> -> <src>] ... (this tool sends; it does not auto-reply).
|
# preamble: [<dst> -> <src>] ... (this tool sends; it does not auto-reply).
|
||||||
#
|
#
|
||||||
# Optionally tags the message with a TRIAGE CLASS (see -C / --class) so a
|
|
||||||
# comms daemon can route it (deliver-to-agent vs log-and-drop) from an exact
|
|
||||||
# field instead of re-deriving intent from the body.
|
|
||||||
#
|
|
||||||
# WHY A WRAPPER
|
# WHY A WRAPPER
|
||||||
# Reliable submission into an interactive REPL (Claude Code / Codex) is fiddly:
|
# Reliable submission into an interactive REPL (Claude Code / Codex) is fiddly:
|
||||||
# a trailing Enter is often swallowed and the message sits as an unsubmitted
|
# a trailing Enter is often swallowed and the message sits as an unsubmitted
|
||||||
@@ -30,7 +26,6 @@
|
|||||||
# agent-send.sh [-L socket] -s <dst_session> -m "message" # local target
|
# agent-send.sh [-L socket] -s <dst_session> -m "message" # local target
|
||||||
# agent-send.sh [-L socket] -H user@host -s <dst_session> -m "message" # remote target
|
# agent-send.sh [-L socket] -H user@host -s <dst_session> -m "message" # remote target
|
||||||
# agent-send.sh [-L socket] -H user@host -n <dst_hostname> -s <sess> -f msg.txt
|
# agent-send.sh [-L socket] -H user@host -n <dst_hostname> -s <sess> -f msg.txt
|
||||||
# agent-send.sh -s mos-claude --class terminal-log -m "ACK — received"
|
|
||||||
# echo "msg" | agent-send.sh [-L socket] -H user@host -s <dst_session>
|
# echo "msg" | agent-send.sh [-L socket] -H user@host -s <dst_session>
|
||||||
#
|
#
|
||||||
# OPTIONS
|
# OPTIONS
|
||||||
@@ -41,61 +36,27 @@
|
|||||||
# Default: local hostname, or (remote) resolved via one ssh.
|
# Default: local hostname, or (remote) resolved via one ssh.
|
||||||
# -m MESSAGE message text (single- or multi-line)
|
# -m MESSAGE message text (single- or multi-line)
|
||||||
# -f FILE read message from FILE instead of -m
|
# -f FILE read message from FILE instead of -m
|
||||||
# -C CLASS triage class for a comms daemon. One of:
|
|
||||||
# terminal-log log-only; never needs the agent's attention
|
|
||||||
# actionable carries a decision/blocker/gate — deliver
|
|
||||||
# human from a human operator — deliver
|
|
||||||
# reaction an emoji/ack reaction
|
|
||||||
# Long form: --class CLASS (or --class=CLASS). When SET, the
|
|
||||||
# preamble carries a ` class=<CLASS>` token INSIDE the bracket:
|
|
||||||
# [<src> -> <dst> class=terminal-log] <message>
|
|
||||||
# When OMITTED, NO token is emitted and the preamble is
|
|
||||||
# byte-for-byte identical to the classic format. Consumers MUST
|
|
||||||
# treat an absent class as 'actionable' (fail-safe: agent sees it).
|
|
||||||
# -S SRC_LABEL override source label "<host>:<session>" (default: auto)
|
# -S SRC_LABEL override source label "<host>:<session>" (default: auto)
|
||||||
# -r N Enter-flush attempts passed through (default 2)
|
# -r N Enter-flush attempts passed through (default 2)
|
||||||
# -v verbose: print pane tail after delivery
|
# -v verbose: print pane tail after delivery
|
||||||
# -h help
|
# -h help
|
||||||
#
|
#
|
||||||
# PREAMBLE GRAMMAR (for consumers / daemons mirroring this producer)
|
|
||||||
# ^\[(\S+) -> (\S+?)(?: class=(terminal-log|actionable|human|reaction))?\] (.*)$
|
|
||||||
# group 1 = src label group 2 = dst host:session
|
|
||||||
# group 3 = class (absent => actionable) group 4 = message body
|
|
||||||
#
|
|
||||||
# EXIT CODES (passed through from send-message.sh)
|
# EXIT CODES (passed through from send-message.sh)
|
||||||
# 0 delivered/queued · 1 target not found · 2 still draft · 3 usage error
|
# 0 delivered/queued · 1 target not found · 2 still draft · 3 usage error
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
SELF_DIR=$(cd -- "$(dirname -- "$0")" && pwd)
|
SELF_DIR=$(cd -- "$(dirname -- "$0")" && pwd)
|
||||||
# Sender is overridable via env purely for testing (inject a capture stub). The
|
SENDER="$SELF_DIR/send-message.sh"
|
||||||
# default is the canonical send-message.sh beside this script; production callers
|
|
||||||
# never set AGENT_SEND_SENDER, so behavior is unchanged.
|
|
||||||
SENDER="${AGENT_SEND_SENDER:-$SELF_DIR/send-message.sh}"
|
|
||||||
|
|
||||||
# Translate the long option --class[=value] into "-C value" so getopts (which is
|
|
||||||
# short-option-only) can parse it. Every other argument passes through untouched,
|
|
||||||
# so callers that never use --class hit the exact original getopts path.
|
|
||||||
args=()
|
|
||||||
while [ $# -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--class) [ $# -ge 2 ] || { echo "ERROR: --class requires a value" >&2; exit 3; }
|
|
||||||
args+=(-C "$2"); shift 2 ;;
|
|
||||||
--class=*) args+=(-C "${1#*=}"); shift ;;
|
|
||||||
*) args+=("$1"); shift ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
set -- ${args[@]+"${args[@]}"}
|
|
||||||
|
|
||||||
DST_SESSION=""; SSH_TARGET=""; DST_HOST=""; MSG=""; FILE=""; SOCKET_NAME=""
|
DST_SESSION=""; SSH_TARGET=""; DST_HOST=""; MSG=""; FILE=""; SOCKET_NAME=""
|
||||||
SRC_LABEL=""; RETRIES=2; VERBOSE=0; CLASS=""
|
SRC_LABEL=""; RETRIES=2; VERBOSE=0
|
||||||
usage() { sed -n '2,/^set -uo pipefail/{/^set -uo pipefail/d;p}' "$0"; exit "${1:-3}"; }
|
usage() { sed -n '2,44p' "$0"; exit "${1:-3}"; }
|
||||||
|
|
||||||
while getopts "L:s:H:n:m:f:S:r:C:vh" o; do
|
while getopts "L:s:H:n:m:f:S:r:vh" o; do
|
||||||
case "$o" in
|
case "$o" in
|
||||||
L) SOCKET_NAME=$OPTARG ;;
|
L) SOCKET_NAME=$OPTARG ;;
|
||||||
s) DST_SESSION=$OPTARG ;; H) SSH_TARGET=$OPTARG ;; n) DST_HOST=$OPTARG ;;
|
s) DST_SESSION=$OPTARG ;; H) SSH_TARGET=$OPTARG ;; n) DST_HOST=$OPTARG ;;
|
||||||
m) MSG=$OPTARG ;; f) FILE=$OPTARG ;; S) SRC_LABEL=$OPTARG ;;
|
m) MSG=$OPTARG ;; f) FILE=$OPTARG ;; S) SRC_LABEL=$OPTARG ;;
|
||||||
C) CLASS=$OPTARG ;;
|
|
||||||
r) RETRIES=$OPTARG ;; v) VERBOSE=1 ;; h) usage 0 ;; *) usage 3 ;;
|
r) RETRIES=$OPTARG ;; v) VERBOSE=1 ;; h) usage 0 ;; *) usage 3 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
@@ -103,17 +64,6 @@ done
|
|||||||
[ -n "$DST_SESSION" ] || { echo "ERROR: -s DST_SESSION is required" >&2; usage 3; }
|
[ -n "$DST_SESSION" ] || { echo "ERROR: -s DST_SESSION is required" >&2; usage 3; }
|
||||||
[ -x "$SENDER" ] || { echo "ERROR: send-message.sh not found beside this script" >&2; exit 3; }
|
[ -x "$SENDER" ] || { echo "ERROR: send-message.sh not found beside this script" >&2; exit 3; }
|
||||||
|
|
||||||
# Validate the triage class only when one was given. An absent class emits NO
|
|
||||||
# token (preamble byte-identical to the classic format); the consumer defaults
|
|
||||||
# absent => actionable.
|
|
||||||
CLASS_TOKEN=""
|
|
||||||
if [ -n "$CLASS" ]; then
|
|
||||||
case "$CLASS" in
|
|
||||||
terminal-log|actionable|human|reaction) CLASS_TOKEN=" class=${CLASS}" ;;
|
|
||||||
*) echo "ERROR: invalid --class '$CLASS' (allowed: terminal-log, actionable, human, reaction)" >&2; exit 3 ;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Message body from -f / -m / stdin.
|
# Message body from -f / -m / stdin.
|
||||||
if [ -n "$FILE" ]; then [ -r "$FILE" ] || { echo "ERROR: cannot read $FILE" >&2; exit 3; }; MSG=$(cat -- "$FILE")
|
if [ -n "$FILE" ]; then [ -r "$FILE" ] || { echo "ERROR: cannot read $FILE" >&2; exit 3; }; MSG=$(cat -- "$FILE")
|
||||||
elif [ -z "$MSG" ] && [ ! -t 0 ]; then MSG=$(cat)
|
elif [ -z "$MSG" ] && [ ! -t 0 ]; then MSG=$(cat)
|
||||||
@@ -140,7 +90,7 @@ if [ -z "$DST_HOST" ]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
PREAMBLE="[${SRC_LABEL} -> ${DST_HOST}:${DST_SESSION}${CLASS_TOKEN}]"
|
PREAMBLE="[${SRC_LABEL} -> ${DST_HOST}:${DST_SESSION}]"
|
||||||
FULL="${PREAMBLE} ${MSG}"
|
FULL="${PREAMBLE} ${MSG}"
|
||||||
B64=$(printf '%s' "$FULL" | base64 -w0)
|
B64=$(printf '%s' "$FULL" | base64 -w0)
|
||||||
|
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# agent-send.test.sh — regression + grammar lock for agent-send.sh --class.
|
|
||||||
#
|
|
||||||
# Strategy: inject a capture stub via AGENT_SEND_SENDER that decodes the -b
|
|
||||||
# base64 payload and prints the FULL message (preamble + body) so we can assert
|
|
||||||
# the exact bytes on the wire. Local path only (no ssh), -n pins the dst host so
|
|
||||||
# the preamble is deterministic across machines.
|
|
||||||
#
|
|
||||||
# Guarantees locked here:
|
|
||||||
# 1. REGRESSION BAR — no --class => preamble byte-for-byte identical to classic.
|
|
||||||
# 2. --class <c> => ` class=<c>` token emitted inside the bracket.
|
|
||||||
# 3. --class=<c> (equals form) parses identically to the space form.
|
|
||||||
# 4. -C <c> short form parses identically.
|
|
||||||
# 5. invalid class => exit 3, nothing sent.
|
|
||||||
# 6. --class with no value => exit 3.
|
|
||||||
# 7. the documented consumer regex parses producer output for every class.
|
|
||||||
set -uo pipefail
|
|
||||||
|
|
||||||
HERE=$(cd -- "$(dirname -- "$0")" && pwd)
|
|
||||||
TOOL="$HERE/agent-send.sh"
|
|
||||||
|
|
||||||
# Capture stub: stands in for send-message.sh. Decodes -b and prints the payload.
|
|
||||||
STUB=$(mktemp)
|
|
||||||
trap 'rm -f "$STUB"' EXIT
|
|
||||||
cat >"$STUB" <<'STUB_EOF'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -uo pipefail
|
|
||||||
b64=""
|
|
||||||
while getopts "t:b:r:v" o; do case "$o" in b) b64=$OPTARG ;; *) : ;; esac; done
|
|
||||||
printf '%s' "$b64" | base64 -d
|
|
||||||
STUB_EOF
|
|
||||||
chmod +x "$STUB"
|
|
||||||
|
|
||||||
PASS=0; FAIL=0
|
|
||||||
ok() { PASS=$((PASS+1)); printf 'ok %s\n' "$1"; }
|
|
||||||
no() { FAIL=$((FAIL+1)); printf 'FAIL %s\n %s\n' "$1" "$2"; }
|
|
||||||
|
|
||||||
# Run the tool with the stub injected; echoes captured payload on stdout.
|
|
||||||
run() { AGENT_SEND_SENDER="$STUB" bash "$TOOL" -S a:src -n dsthost "$@"; }
|
|
||||||
|
|
||||||
# Documented consumer grammar — the daemon will mirror exactly this.
|
|
||||||
GRAMMAR='^\[(\S+) -> (\S+) class=(terminal-log|actionable|human|reaction)\] (.*)$'
|
|
||||||
GRAMMAR_NOCLASS='^\[(\S+) -> (\S+)\] (.*)$'
|
|
||||||
|
|
||||||
# 1. REGRESSION BAR: classic preamble, byte-for-byte.
|
|
||||||
got=$(run -s mos -m "hello world")
|
|
||||||
want='[a:src -> dsthost:mos] hello world'
|
|
||||||
[ "$got" = "$want" ] && ok "regression: no --class is byte-identical" \
|
|
||||||
|| no "regression: no --class is byte-identical" "got=[$got] want=[$want]"
|
|
||||||
|
|
||||||
# 2. --class space form emits the token.
|
|
||||||
got=$(run -s mos --class terminal-log -m "ACK")
|
|
||||||
want='[a:src -> dsthost:mos class=terminal-log] ACK'
|
|
||||||
[ "$got" = "$want" ] && ok "--class terminal-log emits token" \
|
|
||||||
|| no "--class terminal-log emits token" "got=[$got] want=[$want]"
|
|
||||||
|
|
||||||
# 3. --class=value equals form.
|
|
||||||
got=$(run -s mos --class=actionable -m "decide X")
|
|
||||||
want='[a:src -> dsthost:mos class=actionable] decide X'
|
|
||||||
[ "$got" = "$want" ] && ok "--class=actionable (equals form)" \
|
|
||||||
|| no "--class=actionable (equals form)" "got=[$got] want=[$want]"
|
|
||||||
|
|
||||||
# 4. -C short form.
|
|
||||||
got=$(run -s mos -C human -m "from a person")
|
|
||||||
want='[a:src -> dsthost:mos class=human] from a person'
|
|
||||||
[ "$got" = "$want" ] && ok "-C human (short form)" \
|
|
||||||
|| no "-C human (short form)" "got=[$got] want=[$want]"
|
|
||||||
|
|
||||||
# 5. invalid class => exit 3, no send.
|
|
||||||
if out=$(run -s mos --class bogus -m "x" 2>/dev/null); then
|
|
||||||
no "invalid class rejected" "expected non-zero exit, got 0 (out=[$out])"
|
|
||||||
else
|
|
||||||
rc=$?
|
|
||||||
[ "$rc" = 3 ] && [ -z "$out" ] && ok "invalid class => exit 3, nothing sent" \
|
|
||||||
|| no "invalid class => exit 3, nothing sent" "rc=$rc out=[$out]"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 6. --class with no value => exit 3.
|
|
||||||
if run -s mos -m "x" --class 2>/dev/null; then
|
|
||||||
no "--class with no value rejected" "expected non-zero exit, got 0"
|
|
||||||
else
|
|
||||||
[ "$?" = 3 ] && ok "--class with no value => exit 3" || no "--class with no value => exit 3" "wrong rc"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 7. consumer grammar parses every class + classic line.
|
|
||||||
for c in terminal-log actionable human reaction; do
|
|
||||||
line=$(run -s mos --class "$c" -m "body $c")
|
|
||||||
[[ "$line" =~ $GRAMMAR ]] && [ "${BASH_REMATCH[3]}" = "$c" ] && [ "${BASH_REMATCH[4]}" = "body $c" ] \
|
|
||||||
&& ok "grammar parses class=$c" || no "grammar parses class=$c" "line=[$line]"
|
|
||||||
done
|
|
||||||
classic=$(run -s mos -m "plain body")
|
|
||||||
[[ "$classic" =~ $GRAMMAR_NOCLASS ]] && [ "${BASH_REMATCH[3]}" = "plain body" ] \
|
|
||||||
&& ok "grammar (no-class) parses classic line" || no "grammar (no-class) parses classic line" "line=[$classic]"
|
|
||||||
|
|
||||||
echo "---"
|
|
||||||
echo "PASS=$PASS FAIL=$FAIL"
|
|
||||||
[ "$FAIL" -eq 0 ]
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mosaicstack/mosaic",
|
"name": "@mosaicstack/mosaic",
|
||||||
"version": "0.0.41",
|
"version": "0.0.40",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
||||||
|
|||||||
Reference in New Issue
Block a user