chore(platform): add local smoke script
This commit is contained in:
parent
dc5e79b1d5
commit
a2053a70f1
10
README.md
10
README.md
@ -39,6 +39,16 @@ curl -sf http://localhost:4016/health
|
||||
curl -sf http://localhost:4016/api/bootstrap
|
||||
```
|
||||
|
||||
Local production-readiness smoke:
|
||||
|
||||
```bash
|
||||
pnpm run smoke:local
|
||||
|
||||
# Use an already-running backend or skip shared service checks when isolating product behavior.
|
||||
SMOKE_START_BACKEND=0 pnpm run smoke:local -- --no-start
|
||||
pnpm run smoke:local -- --skip-platform
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
| Surface | Stack | Port |
|
||||
|
||||
@ -172,6 +172,28 @@ Expected:
|
||||
|
||||
## 7. NoteLett Backend Checks
|
||||
|
||||
The automated local smoke entrypoint is:
|
||||
|
||||
```bash
|
||||
pnpm run smoke:local
|
||||
```
|
||||
|
||||
It follows the common-platform self-test convention of generating a short-lived local test JWT, then verifies:
|
||||
|
||||
- `GET /health`
|
||||
- `GET /api/bootstrap`
|
||||
- platform-service, extraction-service, and mcp-server health
|
||||
- authenticated workspace create
|
||||
- authenticated note create/read
|
||||
- best-effort workspace/note cleanup
|
||||
|
||||
Useful variants:
|
||||
|
||||
```bash
|
||||
pnpm run smoke:local -- --no-start
|
||||
pnpm run smoke:local -- --skip-platform
|
||||
```
|
||||
|
||||
Start backend in memory mode for local product smoke:
|
||||
|
||||
```bash
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
"typecheck": "pnpm --filter @notelett/backend run typecheck && pnpm --filter @notelett/web run typecheck && pnpm --filter @notelett/mobile run typecheck",
|
||||
"test": "pnpm --filter @notelett/backend run test && pnpm --filter @notelett/web run test && pnpm --filter @notelett/mobile run test",
|
||||
"build": "pnpm --filter @notelett/backend run build && pnpm --filter @notelett/web run build",
|
||||
"smoke:local": "bash scripts/local-smoke.sh",
|
||||
"verify": "pnpm run typecheck && pnpm run test && pnpm run build",
|
||||
"prepare": "husky"
|
||||
},
|
||||
|
||||
267
scripts/local-smoke.sh
Executable file
267
scripts/local-smoke.sh
Executable file
@ -0,0 +1,267 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
COMMON_PLAT_DIR="${COMMON_PLAT_DIR:-${ROOT_DIR}/../learning_ai/learning_ai_common_plat}"
|
||||
|
||||
NOTELETT_URL="${NOTELETT_URL:-http://localhost:4016}"
|
||||
NOTELETT_API_URL="${NOTELETT_API_URL:-${NOTELETT_URL}/api}"
|
||||
PLATFORM_URL="${PLATFORM_URL:-http://localhost:4003}"
|
||||
EXTRACTION_URL="${EXTRACTION_URL:-http://localhost:4005}"
|
||||
MCP_ORIGIN="${MCP_ORIGIN:-http://localhost:4007}"
|
||||
JWT_SECRET="${JWT_SECRET:-dev-secret-do-not-use-in-prod}"
|
||||
PRODUCT_ID="${PRODUCT_ID:-notelett}"
|
||||
SMOKE_USER_ID="${SMOKE_USER_ID:-notelett-smoke-user}"
|
||||
SMOKE_START_BACKEND="${SMOKE_START_BACKEND:-1}"
|
||||
SMOKE_SKIP_PLATFORM="${SMOKE_SKIP_PLATFORM:-0}"
|
||||
|
||||
BACKEND_PID=""
|
||||
BACKEND_LOG="${TMPDIR:-/tmp}/notelett-local-smoke-backend.log"
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: scripts/local-smoke.sh [--no-start] [--skip-platform] [--help]
|
||||
|
||||
Checks:
|
||||
- NoteLett GET /health
|
||||
- NoteLett GET /api/bootstrap
|
||||
- platform-service, extraction-service, and mcp-server /health
|
||||
- authenticated workspace + note create/read/delete flow in DB_PROVIDER=memory
|
||||
|
||||
Environment:
|
||||
NOTELETT_URL=http://localhost:4016
|
||||
PLATFORM_URL=http://localhost:4003
|
||||
EXTRACTION_URL=http://localhost:4005
|
||||
MCP_ORIGIN=http://localhost:4007
|
||||
JWT_SECRET=dev-secret-do-not-use-in-prod
|
||||
SMOKE_START_BACKEND=1
|
||||
SMOKE_SKIP_PLATFORM=0
|
||||
COMMON_PLAT_DIR=../learning_ai/learning_ai_common_plat
|
||||
USAGE
|
||||
}
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--)
|
||||
;;
|
||||
--no-start)
|
||||
SMOKE_START_BACKEND=0
|
||||
;;
|
||||
--skip-platform)
|
||||
SMOKE_SKIP_PLATFORM=1
|
||||
;;
|
||||
--help|-h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $arg" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "$BACKEND_PID" ]] && kill -0 "$BACKEND_PID" >/dev/null 2>&1; then
|
||||
kill "$BACKEND_PID" >/dev/null 2>&1 || true
|
||||
wait "$BACKEND_PID" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
curl_json() {
|
||||
local method="$1"
|
||||
local url="$2"
|
||||
local body="${3:-}"
|
||||
shift 3 || true
|
||||
|
||||
if [[ -n "$body" ]]; then
|
||||
curl -fsS -X "$method" "$url" \
|
||||
-H 'content-type: application/json' \
|
||||
"$@" \
|
||||
--data "$body"
|
||||
else
|
||||
curl -fsS -X "$method" "$url" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
healthy() {
|
||||
curl -fsS "$NOTELETT_URL/health" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
ensure_common_platform_builds() {
|
||||
if [[ ! -d "$COMMON_PLAT_DIR" ]]; then
|
||||
echo "error: missing common-platform checkout at $COMMON_PLAT_DIR" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local palace_dist="$COMMON_PLAT_DIR/packages/palace/dist/index.js"
|
||||
if [[ ! -f "$palace_dist" ]]; then
|
||||
echo "info: building linked common-platform @bytelyst/palace package"
|
||||
GITEA_NPM_TOKEN="${GITEA_NPM_TOKEN:-dummy}" \
|
||||
pnpm -C "$COMMON_PLAT_DIR" --filter @bytelyst/palace run build
|
||||
fi
|
||||
|
||||
local llm_dist="$COMMON_PLAT_DIR/packages/llm/dist/index.js"
|
||||
if [[ ! -f "$llm_dist" ]] || ! grep -q 'buildVisionMessage' "$llm_dist"; then
|
||||
echo "info: building linked common-platform @bytelyst/llm package"
|
||||
GITEA_NPM_TOKEN="${GITEA_NPM_TOKEN:-dummy}" \
|
||||
pnpm -C "$COMMON_PLAT_DIR" --filter @bytelyst/llm run build
|
||||
fi
|
||||
}
|
||||
|
||||
start_backend_if_needed() {
|
||||
if healthy; then
|
||||
echo "ok: NoteLett backend already healthy at $NOTELETT_URL"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "$SMOKE_START_BACKEND" != "1" ]]; then
|
||||
echo "error: NoteLett backend is not healthy at $NOTELETT_URL and SMOKE_START_BACKEND=0" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "info: starting NoteLett backend in memory mode"
|
||||
ensure_common_platform_builds
|
||||
(
|
||||
cd "$ROOT_DIR"
|
||||
DB_PROVIDER=memory \
|
||||
NODE_ENV=development \
|
||||
JWT_SECRET="$JWT_SECRET" \
|
||||
PRODUCT_ID="$PRODUCT_ID" \
|
||||
pnpm --filter @notelett/backend run dev
|
||||
) >"$BACKEND_LOG" 2>&1 &
|
||||
BACKEND_PID="$!"
|
||||
|
||||
for _ in {1..60}; do
|
||||
if healthy; then
|
||||
echo "ok: NoteLett backend started at $NOTELETT_URL"
|
||||
return
|
||||
fi
|
||||
if ! kill -0 "$BACKEND_PID" >/dev/null 2>&1; then
|
||||
echo "error: backend exited before becoming healthy; log follows" >&2
|
||||
tail -n 80 "$BACKEND_LOG" >&2 || true
|
||||
return 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "error: backend did not become healthy within 60 seconds; log follows" >&2
|
||||
tail -n 80 "$BACKEND_LOG" >&2 || true
|
||||
return 1
|
||||
}
|
||||
|
||||
generate_token() {
|
||||
(
|
||||
cd "$ROOT_DIR/backend"
|
||||
JWT_SECRET="$JWT_SECRET" PRODUCT_ID="$PRODUCT_ID" SMOKE_USER_ID="$SMOKE_USER_ID" node --input-type=module <<'NODE'
|
||||
import { SignJWT } from 'jose';
|
||||
|
||||
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
|
||||
const productId = process.env.PRODUCT_ID ?? 'notelett';
|
||||
const sub = process.env.SMOKE_USER_ID ?? 'notelett-smoke-user';
|
||||
|
||||
const token = await new SignJWT({
|
||||
email: 'smoke@notelett.local',
|
||||
role: 'admin',
|
||||
productId,
|
||||
type: 'access',
|
||||
})
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setSubject(sub)
|
||||
.setIssuer('bytelyst-platform')
|
||||
.setIssuedAt()
|
||||
.setExpirationTime('15m')
|
||||
.sign(secret);
|
||||
|
||||
process.stdout.write(token);
|
||||
NODE
|
||||
)
|
||||
}
|
||||
|
||||
json_field_equals() {
|
||||
local json="$1"
|
||||
local field="$2"
|
||||
local expected="$3"
|
||||
|
||||
JSON_INPUT="$json" FIELD="$field" EXPECTED="$expected" node --input-type=module <<'NODE'
|
||||
const body = JSON.parse(process.env.JSON_INPUT ?? '{}');
|
||||
const actual = body[process.env.FIELD];
|
||||
if (actual !== process.env.EXPECTED) {
|
||||
console.error(`Expected ${process.env.FIELD}=${process.env.EXPECTED}, got ${String(actual)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
NODE
|
||||
}
|
||||
|
||||
check_platform_dependencies() {
|
||||
if [[ "$SMOKE_SKIP_PLATFORM" == "1" ]]; then
|
||||
echo "skip: platform dependency checks disabled"
|
||||
return
|
||||
fi
|
||||
|
||||
curl -fsS "$PLATFORM_URL/health" >/dev/null
|
||||
echo "ok: platform-service health"
|
||||
|
||||
curl -fsS "$EXTRACTION_URL/health" >/dev/null
|
||||
echo "ok: extraction-service health"
|
||||
|
||||
curl -fsS "$MCP_ORIGIN/health" >/dev/null
|
||||
echo "ok: mcp-server health"
|
||||
}
|
||||
|
||||
run_product_flow() {
|
||||
local token="$1"
|
||||
local run_id
|
||||
run_id="smoke-$(date +%s)-$$"
|
||||
local workspace_id="ws-$run_id"
|
||||
local note_id="note-$run_id"
|
||||
local auth_header="authorization: Bearer $token"
|
||||
|
||||
local workspace_body
|
||||
workspace_body="{\"id\":\"$workspace_id\",\"name\":\"Smoke Workspace\",\"members\":[]}"
|
||||
local workspace
|
||||
workspace="$(curl_json POST "$NOTELETT_API_URL/workspaces" "$workspace_body" -H "$auth_header")"
|
||||
json_field_equals "$workspace" id "$workspace_id"
|
||||
echo "ok: authenticated workspace create"
|
||||
|
||||
local note_body
|
||||
note_body="{\"id\":\"$note_id\",\"workspaceId\":\"$workspace_id\",\"title\":\"Smoke Note\",\"body\":\"<p>Local smoke note body.</p>\",\"tags\":[\"smoke\"],\"links\":[]}"
|
||||
local note
|
||||
note="$(curl_json POST "$NOTELETT_API_URL/notes" "$note_body" -H "$auth_header")"
|
||||
json_field_equals "$note" id "$note_id"
|
||||
echo "ok: authenticated note create"
|
||||
|
||||
local fetched
|
||||
fetched="$(curl_json GET "$NOTELETT_API_URL/notes/$note_id?workspaceId=$workspace_id" "" -H "$auth_header")"
|
||||
json_field_equals "$fetched" productId "$PRODUCT_ID"
|
||||
json_field_equals "$fetched" id "$note_id"
|
||||
echo "ok: authenticated note read"
|
||||
|
||||
curl_json DELETE "$NOTELETT_API_URL/notes/$note_id?workspaceId=$workspace_id" "" -H "$auth_header" >/dev/null 2>&1 || true
|
||||
curl_json DELETE "$NOTELETT_API_URL/workspaces/$workspace_id" "" -H "$auth_header" >/dev/null 2>&1 || true
|
||||
echo "ok: smoke cleanup attempted"
|
||||
}
|
||||
|
||||
main() {
|
||||
start_backend_if_needed
|
||||
|
||||
curl -fsS "$NOTELETT_URL/health" >/dev/null
|
||||
echo "ok: NoteLett health"
|
||||
|
||||
local bootstrap
|
||||
bootstrap="$(curl_json GET "$NOTELETT_API_URL/bootstrap" "")"
|
||||
json_field_equals "$bootstrap" productId "$PRODUCT_ID"
|
||||
echo "ok: NoteLett bootstrap"
|
||||
|
||||
check_platform_dependencies
|
||||
|
||||
local token
|
||||
token="$(generate_token)"
|
||||
run_product_flow "$token"
|
||||
|
||||
echo "ok: local production-readiness smoke passed"
|
||||
}
|
||||
|
||||
main
|
||||
Loading…
Reference in New Issue
Block a user