Skip to content

Commit df9a4fc

Browse files
kulvirgitclaude
andcommitted
feat: add Verdaccio-based sanity suite for real npm install testing
Add a parallel sanity suite that tests the actual `npm install -g` flow using Verdaccio as a local npm registry. This catches publish pipeline bugs that the copy-based Dockerfile cannot detect: - Missing files in package tarball (symlinks not followed by npm pack) - Broken postinstall scripts - Wrong bin field in package.json - Dependency resolution failures at install time - Native binary platform detection failures Pipeline: build → publish to Verdaccio → npm install -g → run sanity tests Files added: - Dockerfile.verdaccio: builds packages, publishes at runtime via entrypoint - docker-compose.verdaccio.yml: Verdaccio + postgres + mongodb services - verdaccio/config.yaml: permissive config (publish: $all, 500mb max body) - verdaccio/entrypoint.sh: publish both packages, npm install -g, run tests - run-verdaccio.sh: convenience wrapper Already found real bug: postinstall fails because bin/altimate-code is a symlink that breaks when npm pack/unpacks the tarball in a different directory. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 86a02c4 commit df9a4fc

6 files changed

Lines changed: 308 additions & 0 deletions

File tree

test/sanity/Dockerfile.verdaccio

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
FROM oven/bun:1.3.10-debian
2+
3+
# System deps (what a real user would have)
4+
RUN apt-get update && apt-get install -y \
5+
git python3 python3-pip python3-venv curl sqlite3 npm \
6+
&& rm -rf /var/lib/apt/lists/*
7+
8+
# dbt in venv (matches real user setup)
9+
RUN python3 -m venv /opt/dbt && \
10+
/opt/dbt/bin/pip install --quiet dbt-core dbt-duckdb dbt-postgres
11+
ENV PATH="/opt/dbt/bin:$PATH"
12+
13+
# Fresh user, clean HOME — simulates new user
14+
ENV HOME=/home/testuser
15+
RUN useradd -m testuser
16+
USER testuser
17+
WORKDIR /home/testuser
18+
19+
# ── Copy build artifacts (pre-built by `bun run build`) ────────
20+
COPY --chown=testuser packages/opencode/dist/ /home/testuser/build/packages/opencode/dist/
21+
COPY --chown=testuser packages/opencode/bin/ /home/testuser/build/packages/opencode/bin/
22+
COPY --chown=testuser packages/opencode/script/postinstall.mjs /home/testuser/build/packages/opencode/script/postinstall.mjs
23+
COPY --chown=testuser packages/opencode/package.json /home/testuser/build/packages/opencode/package.json
24+
COPY --chown=testuser packages/dbt-tools/dist/ /home/testuser/build/packages/dbt-tools/dist/
25+
COPY --chown=testuser packages/dbt-tools/bin/ /home/testuser/build/packages/dbt-tools/bin/
26+
COPY --chown=testuser .opencode/skills/ /home/testuser/build/.opencode/skills/
27+
COPY --chown=testuser LICENSE /home/testuser/build/LICENSE
28+
COPY --chown=testuser CHANGELOG.md /home/testuser/build/CHANGELOG.md
29+
COPY --chown=testuser README.md /home/testuser/build/README.md
30+
31+
# Copy test scripts and entrypoint
32+
COPY --chown=testuser test/sanity/ /home/testuser/sanity/
33+
RUN chmod +x /home/testuser/sanity/run.sh \
34+
/home/testuser/sanity/phases/*.sh \
35+
/home/testuser/sanity/pr-tests/*.sh \
36+
/home/testuser/sanity/lib/*.sh
37+
38+
# Copy the publish-and-install entrypoint
39+
COPY --chown=testuser test/sanity/verdaccio/entrypoint.sh /home/testuser/entrypoint.sh
40+
RUN chmod +x /home/testuser/entrypoint.sh
41+
42+
ENTRYPOINT ["/home/testuser/entrypoint.sh"]
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Verdaccio-based sanity suite: tests the real npm install -g flow
2+
# Usage: docker compose -f test/sanity/docker-compose.verdaccio.yml up --build --abort-on-container-exit --exit-code-from sanity
3+
services:
4+
verdaccio:
5+
image: verdaccio/verdaccio
6+
volumes:
7+
- ./verdaccio/config.yaml:/verdaccio/conf/config.yaml
8+
ports:
9+
- "4873:4873"
10+
healthcheck:
11+
test: ["CMD", "wget", "-q", "-O/dev/null", "http://0.0.0.0:4873/-/ping"]
12+
interval: 3s
13+
retries: 10
14+
15+
sanity:
16+
build:
17+
context: ../..
18+
dockerfile: test/sanity/Dockerfile.verdaccio
19+
args:
20+
REGISTRY_URL: http://verdaccio:4873
21+
environment:
22+
- ANTHROPIC_API_KEY
23+
- ALTIMATE_CODE_CONN_SNOWFLAKE_TEST
24+
- TEST_PG_HOST=postgres
25+
- TEST_PG_PORT=5432
26+
- TEST_PG_PASSWORD=testpass123
27+
- TEST_MONGODB_HOST=mongodb
28+
- TEST_MONGODB_PORT=27017
29+
depends_on:
30+
verdaccio:
31+
condition: service_healthy
32+
postgres:
33+
condition: service_healthy
34+
mongodb:
35+
condition: service_healthy
36+
37+
postgres:
38+
image: postgres:16-alpine
39+
environment:
40+
POSTGRES_PASSWORD: testpass123
41+
ports:
42+
- "15432:5432"
43+
healthcheck:
44+
test: pg_isready
45+
interval: 5s
46+
retries: 10
47+
48+
mongodb:
49+
image: mongo:7
50+
ports:
51+
- "17017:27017"
52+
healthcheck:
53+
test: mongosh --eval "db.adminCommand('ping')" --quiet
54+
interval: 5s
55+
retries: 10

test/sanity/run-verdaccio.sh

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/bin/bash
2+
# Run the Verdaccio-based sanity suite: build → publish to local registry → npm install -g → test
3+
# This tests the EXACT same flow a real user goes through with `npm install -g altimate-code`
4+
set -euo pipefail
5+
6+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
7+
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
8+
9+
echo "========================================"
10+
echo " Verdaccio Sanity Suite"
11+
echo " Tests the real npm install -g flow"
12+
echo " Time: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
13+
echo "========================================"
14+
echo ""
15+
16+
# Ensure binary is built
17+
if [ ! -d "$REPO_ROOT/packages/opencode/dist/@altimateai" ]; then
18+
echo "ERROR: No built binary found at packages/opencode/dist/@altimateai/"
19+
echo " Run: cd packages/opencode && bun run build"
20+
exit 1
21+
fi
22+
23+
# Clean up on exit
24+
cleanup() {
25+
echo ""
26+
echo "Cleaning up Docker containers..."
27+
docker compose -f "$SCRIPT_DIR/docker-compose.verdaccio.yml" down --volumes --remove-orphans 2>/dev/null || true
28+
}
29+
trap cleanup EXIT
30+
31+
# Run the suite
32+
docker compose -f "$SCRIPT_DIR/docker-compose.verdaccio.yml" up \
33+
--build \
34+
--abort-on-container-exit \
35+
--exit-code-from sanity
36+
37+
EXIT_CODE=$?
38+
39+
echo ""
40+
if [ $EXIT_CODE -eq 0 ]; then
41+
echo " VERDACCIO SANITY: ALL PASSED"
42+
else
43+
echo " VERDACCIO SANITY: FAILED (exit $EXIT_CODE)"
44+
fi
45+
46+
exit $EXIT_CODE

test/sanity/verdaccio/config.yaml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Verdaccio config for sanity testing — fully open, no auth required
2+
storage: /verdaccio/storage
3+
auth:
4+
htpasswd:
5+
file: /verdaccio/conf/htpasswd
6+
max_users: -1
7+
uplinks:
8+
npmjs:
9+
url: https://registry.npmjs.org/
10+
packages:
11+
"@altimateai/*":
12+
access: $all
13+
publish: $all
14+
unpublish: $all
15+
proxy: npmjs
16+
"altimate-code":
17+
access: $all
18+
publish: $all
19+
unpublish: $all
20+
"**":
21+
access: $all
22+
proxy: npmjs
23+
max_body_size: 500mb
24+
server:
25+
keepAliveTimeout: 60
26+
log:
27+
type: stdout
28+
format: pretty
29+
level: warn
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
#!/bin/bash
2+
# Verdaccio sanity entrypoint: publish to local registry → npm install -g → run sanity tests
3+
# This runs at container startup (not during build) so Verdaccio is available.
4+
set -euo pipefail
5+
6+
REGISTRY_URL="${REGISTRY_URL:-http://verdaccio:4873}"
7+
BUILD_DIR="/home/testuser/build"
8+
9+
echo "========================================"
10+
echo " Verdaccio Install Pipeline"
11+
echo " Registry: $REGISTRY_URL"
12+
echo "========================================"
13+
14+
# ── Wait for Verdaccio ─────────────────────────────────────────
15+
echo ""
16+
echo "--- Waiting for Verdaccio ---"
17+
for i in $(seq 1 30); do
18+
if curl -sf "$REGISTRY_URL/-/ping" >/dev/null 2>&1; then
19+
echo " Verdaccio is ready"
20+
break
21+
fi
22+
if [ "$i" -eq 30 ]; then
23+
echo " FATAL: Verdaccio not reachable after 30s"
24+
exit 1
25+
fi
26+
sleep 1
27+
done
28+
29+
# ── Configure npm to use Verdaccio ─────────────────────────────
30+
# Write .npmrc directly — npm config set doesn't always work for scoped registries
31+
REGISTRY_HOST=$(echo "$REGISTRY_URL" | sed 's|http://||')
32+
cat > ~/.npmrc <<NPMRCEOF
33+
registry=${REGISTRY_URL}
34+
//${REGISTRY_HOST}/:_authToken=anonymous-sanity-token
35+
always-auth=true
36+
NPMRCEOF
37+
echo " Registry: $REGISTRY_URL"
38+
echo " .npmrc contents:"
39+
cat ~/.npmrc
40+
41+
# ── Step 1: Publish platform binary package ────────────────────
42+
echo ""
43+
echo "--- Publishing platform binary ---"
44+
cd "$BUILD_DIR/packages/opencode"
45+
ARCH=$(uname -m | sed 's/x86_64/x64/' | sed 's/aarch64/arm64/')
46+
BIN_PKG_DIR="dist/@altimateai/altimate-code-linux-${ARCH}"
47+
# Sanitize version: replace invalid chars (e.g., "/" from branch names) for npm semver compliance
48+
RAW_VERSION=$(node -e "console.log(require('./${BIN_PKG_DIR}/package.json').version)")
49+
VERSION=$(echo "$RAW_VERSION" | sed 's|/|-|g')
50+
BIN_NAME="@altimateai/altimate-code-linux-${ARCH}"
51+
BIN_VERSION="$VERSION"
52+
CORE_DEP=$(node -e "console.log(require('./package.json').dependencies['@altimateai/altimate-core'])")
53+
54+
echo " Version: $VERSION"
55+
echo " Binary: $BIN_NAME@$BIN_VERSION"
56+
echo " Core dep: @altimateai/altimate-core@$CORE_DEP"
57+
58+
# Patch version in binary package.json to sanitized version
59+
cd "$BIN_PKG_DIR"
60+
node -e "const p=require('./package.json');p.version='$VERSION';require('fs').writeFileSync('package.json',JSON.stringify(p,null,2)+'\n')"
61+
npm publish --registry "$REGISTRY_URL" 2>&1 || echo " (binary publish failed or already published)"
62+
63+
# ── Step 2: Build and publish main altimate-code package ───────
64+
echo ""
65+
echo "--- Building main package ---"
66+
PUBLISH_DIR=$(mktemp -d /tmp/altimate-publish-XXXXXX)
67+
68+
# Copy exactly what publish.ts copies
69+
cd "$BUILD_DIR/packages/opencode"
70+
cp -r bin "$PUBLISH_DIR/bin"
71+
cp script/postinstall.mjs "$PUBLISH_DIR/postinstall.mjs"
72+
cp -r "$BUILD_DIR/.opencode/skills" "$PUBLISH_DIR/skills"
73+
74+
# Bundle dbt-tools
75+
mkdir -p "$PUBLISH_DIR/dbt-tools/bin" "$PUBLISH_DIR/dbt-tools/dist"
76+
cp "$BUILD_DIR/packages/dbt-tools/bin/altimate-dbt" "$PUBLISH_DIR/dbt-tools/bin/altimate-dbt" 2>/dev/null || true
77+
cp "$BUILD_DIR/packages/dbt-tools/dist/index.js" "$PUBLISH_DIR/dbt-tools/dist/" 2>/dev/null || true
78+
cp "$BUILD_DIR/packages/dbt-tools/dist/node_python_bridge.py" "$PUBLISH_DIR/dbt-tools/dist/" 2>/dev/null || true
79+
echo '{"type":"module"}' > "$PUBLISH_DIR/dbt-tools/package.json"
80+
81+
cp "$BUILD_DIR/LICENSE" "$PUBLISH_DIR/LICENSE"
82+
cp "$BUILD_DIR/CHANGELOG.md" "$PUBLISH_DIR/CHANGELOG.md"
83+
84+
# Write package.json matching what publish.ts produces
85+
node -e "
86+
const pkg = {
87+
name: 'altimate-code',
88+
version: '$VERSION',
89+
license: 'MIT',
90+
bin: { altimate: './bin/altimate', 'altimate-code': './bin/altimate-code' },
91+
scripts: { postinstall: 'bun ./postinstall.mjs || node ./postinstall.mjs' },
92+
dependencies: { '@altimateai/altimate-core': '$CORE_DEP' },
93+
optionalDependencies: { '$BIN_NAME': '$BIN_VERSION' },
94+
peerDependencies: {
95+
pg: '>=8', 'snowflake-sdk': '>=1', '@google-cloud/bigquery': '>=8',
96+
'@databricks/sql': '>=1', mysql2: '>=3', mssql: '>=11',
97+
oracledb: '>=6', duckdb: '>=1', mongodb: '>=6'
98+
},
99+
peerDependenciesMeta: {
100+
pg: {optional:true}, 'snowflake-sdk': {optional:true},
101+
'@google-cloud/bigquery': {optional:true}, '@databricks/sql': {optional:true},
102+
mysql2: {optional:true}, mssql: {optional:true}, oracledb: {optional:true},
103+
duckdb: {optional:true}, mongodb: {optional:true}
104+
}
105+
};
106+
require('fs').writeFileSync('$PUBLISH_DIR/package.json', JSON.stringify(pkg,null,2)+'\n');
107+
"
108+
109+
echo "--- Publishing main package ---"
110+
cd "$PUBLISH_DIR"
111+
npm publish --registry "$REGISTRY_URL" 2>&1
112+
echo " Published altimate-code@$VERSION to $REGISTRY_URL"
113+
114+
# ── Step 3: Clean install as a real user ───────────────────────
115+
echo ""
116+
echo "--- npm install -g altimate-code ---"
117+
# Wipe build artifacts — user starts fresh
118+
rm -rf "$BUILD_DIR" "$PUBLISH_DIR"
119+
cd /home/testuser
120+
121+
# Install to user-local prefix (testuser can't write /usr/local)
122+
mkdir -p /home/testuser/.npm-global
123+
npm config set prefix /home/testuser/.npm-global
124+
export PATH="/home/testuser/.npm-global/bin:$PATH"
125+
npm install -g altimate-code --registry "$REGISTRY_URL" 2>&1
126+
echo ""
127+
echo " Installed: $(which altimate 2>/dev/null || echo 'NOT FOUND')"
128+
echo " Version: $(altimate --version 2>/dev/null || echo 'FAILED')"
129+
130+
# ── Step 4: Run sanity tests ──────────────────────────────────
131+
echo ""
132+
echo "========================================"
133+
echo " Running sanity tests against npm install"
134+
echo "========================================"
135+
exec /home/testuser/sanity/run.sh

test/sanity/verdaccio/htpasswd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
sanity:{SHA}uwvl+vY+h0Xh0mhJ0H5e7QVlrQg=

0 commit comments

Comments
 (0)