fix: async git in e2e (in-process server deadlock), vitest config, ngrok token resolution
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Showing 4 changed files with +66 and −13
modified
deploy/ecosystem.config.cjs
+3 −1
@@ -41,7 +41,9 @@ module.exports = { | ||
| 41 | 41 | name: 'spbgit-tunnel', |
| 42 | 42 | cwd: APP_DIR, |
| 43 | 43 | script: 'ngrok', |
| 44 | − args: 'start --config deploy/ngrok.yml spbgit', | |
| 44 | + // SPBGIT_NGROK_CONFIG lets the host point at a resolved config that | |
| 45 | + // carries the real authtoken (deploy/ngrok.yml is the tracked template). | |
| 46 | + args: `start --config ${process.env.SPBGIT_NGROK_CONFIG || 'deploy/ngrok.yml'} spbgit`, | |
| 45 | 47 | interpreter: 'none', |
| 46 | 48 | autorestart: true, |
| 47 | 49 | max_restarts: 50, |
modified
deploy/setup-m3u96a.sh
+27 −3
@@ -113,13 +113,37 @@ else | ||
| 113 | 113 | fi |
| 114 | 114 | |
| 115 | 115 | # ── 6. ngrok auth + pm2 ────────────────────────────────────────── |
| 116 | −if [ -z "${NGROK_AUTHTOKEN:-}" ] && ! ngrok config check >/dev/null 2>&1; then | |
| 117 | − printf '\033[1;33m! NGROK_AUTHTOKEN not set and no ngrok config found.\033[0m\n' | |
| 116 | +# Resolve the authtoken: env first, then the agent's default config file. | |
| 117 | +# A resolved config (real token, chmod 600) is written OUTSIDE the repo and | |
| 118 | +# handed to pm2 via SPBGIT_NGROK_CONFIG — deploy/ngrok.yml stays a template. | |
| 119 | +NGROK_DEFAULT_CFG="$HOME/Library/Application Support/ngrok/ngrok.yml" | |
| 120 | +[ -f "$NGROK_DEFAULT_CFG" ] || NGROK_DEFAULT_CFG="$HOME/.config/ngrok/ngrok.yml" | |
| 121 | +AUTHTOKEN="${NGROK_AUTHTOKEN:-}" | |
| 122 | +if [ -z "$AUTHTOKEN" ] && [ -f "$NGROK_DEFAULT_CFG" ]; then | |
| 123 | + AUTHTOKEN="$(grep -E '^[[:space:]]*authtoken:' "$NGROK_DEFAULT_CFG" | head -1 | awk '{print $2}')" | |
| 124 | +fi | |
| 125 | +RESOLVED_CFG="$PREFIX/spbgit/ngrok.resolved.yml" | |
| 126 | +if [ -n "$AUTHTOKEN" ]; then | |
| 127 | + cat > "$RESOLVED_CFG" <<NGROK | |
| 128 | +version: 3 | |
| 129 | +agent: | |
| 130 | + authtoken: $AUTHTOKEN | |
| 131 | +endpoints: | |
| 132 | + - name: spbgit | |
| 133 | + url: https://git.spboucher.ai | |
| 134 | + upstream: | |
| 135 | + url: http://127.0.0.1:7420 | |
| 136 | +NGROK | |
| 137 | + chmod 600 "$RESOLVED_CFG" | |
| 138 | + export SPBGIT_NGROK_CONFIG="$RESOLVED_CFG" | |
| 139 | + ok "ngrok config resolved → $RESOLVED_CFG" | |
| 140 | +else | |
| 141 | + printf '\033[1;33m! No ngrok authtoken found (env or agent config).\033[0m\n' | |
| 118 | 142 | printf ' Run: ngrok config add-authtoken <token> (or export NGROK_AUTHTOKEN)\n' |
| 119 | 143 | fi |
| 120 | 144 | |
| 121 | 145 | say "starting pm2 apps" |
| 122 | −SPBGIT_LOG_DIR="$LOG_DIR" pm2 start deploy/ecosystem.config.cjs --update-env >/dev/null | |
| 146 | +SPBGIT_LOG_DIR="$LOG_DIR" SPBGIT_NGROK_CONFIG="${SPBGIT_NGROK_CONFIG:-}" pm2 start deploy/ecosystem.config.cjs --update-env >/dev/null | |
| 123 | 147 | pm2 save >/dev/null |
| 124 | 148 | ok "pm2 apps started (spbgit-server, spbgit-tunnel) and saved" |
| 125 | 149 | printf ' To survive reboots, run once: \033[1mpm2 startup\033[0m (follow its instructions)\n' |
modified
test/e2e/roundtrip.test.mjs
+14 −9
@@ -14,8 +14,13 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; | ||
| 14 | 14 | import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'node:fs'; |
| 15 | 15 | import { tmpdir } from 'node:os'; |
| 16 | 16 | import { join } from 'node:path'; |
| 17 | −import { execFileSync } from 'node:child_process'; | |
| 17 | +import { execFile } from 'node:child_process'; | |
| 18 | +import { promisify } from 'node:util'; | |
| 18 | 19 | import net from 'node:net'; |
| 20 | + | |
| 21 | +// The server runs inside this very process — git must be spawned async or | |
| 22 | +// the event loop blocks and the push deadlocks against our own server. | |
| 23 | +const execFileAsync = promisify(execFile); | |
| 19 | 24 | import { loadConfig } from '../../src/config.mjs'; |
| 20 | 25 | import { buildServer } from '../../src/server.mjs'; |
| 21 | 26 | import { TokenStore } from '../../src/auth/token.mjs'; |
@@ -116,24 +121,24 @@ describe('full round trip', () => { | ||
| 116 | 121 | expect(res.headers.get('www-authenticate')).toContain('Basic'); |
| 117 | 122 | }); |
| 118 | 123 | |
| 119 | − it('pushes with a real git client and PAT', () => { | |
| 124 | + it('pushes with a real git client and PAT', async () => { | |
| 120 | 125 | const work = join(root, 'work'); |
| 121 | 126 | mkdirSync(work, { recursive: true }); |
| 122 | − execFileSync('git', ['init', '-q', '-b', 'main'], { cwd: work, env: GIT_ENV }); | |
| 127 | + await execFileAsync('git', ['init', '-q', '-b', 'main'], { cwd: work, env: GIT_ENV }); | |
| 123 | 128 | writeFileSync(join(work, 'README.md'), '# E2E\n\n\n\n```js\nconst ok = true;\n```\n'); |
| 124 | 129 | writeFileSync(join(work, 'main.go'), 'package main\n\nfunc main() {}\n'); |
| 125 | − execFileSync('git', ['add', '-A'], { cwd: work, env: GIT_ENV }); | |
| 126 | − execFileSync('git', ['commit', '-qm', 'feat: e2e commit'], { cwd: work, env: GIT_ENV }); | |
| 130 | + await execFileAsync('git', ['add', '-A'], { cwd: work, env: GIT_ENV }); | |
| 131 | + await execFileAsync('git', ['commit', '-qm', 'feat: e2e commit'], { cwd: work, env: GIT_ENV }); | |
| 127 | 132 | const url = new URL(`${base}/e2e-demo.git`); |
| 128 | 133 | url.username = 'spb'; |
| 129 | 134 | url.password = token; |
| 130 | − execFileSync('git', ['push', '-q', url.href, 'main'], { cwd: work, env: GIT_ENV }); | |
| 135 | + await execFileAsync('git', ['push', '-q', url.href, 'main'], { cwd: work, env: GIT_ENV }); | |
| 131 | 136 | }, 60000); |
| 132 | 137 | |
| 133 | − it('anonymous clone works', () => { | |
| 138 | + it('anonymous clone works', async () => { | |
| 134 | 139 | const dest = join(root, 'clone'); |
| 135 | − execFileSync('git', ['clone', '-q', `${base}/e2e-demo.git`, dest], { env: GIT_ENV }); | |
| 136 | − execFileSync('git', ['rev-parse', 'HEAD'], { cwd: dest, env: GIT_ENV }); | |
| 140 | + await execFileAsync('git', ['clone', '-q', `${base}/e2e-demo.git`, dest], { env: GIT_ENV }); | |
| 141 | + await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: dest, env: GIT_ENV }); | |
| 137 | 142 | }, 60000); |
| 138 | 143 | |
| 139 | 144 | it('API reflects the push (commits, language, stats)', async () => { |
added
vitest.config.mjs
+22 −0
@@ -0,0 +1,22 @@ | ||
| 1 | +/** | |
| 2 | + * ───────────────────────────────────────────── | |
| 3 | + * SPB Git — Personal Git Platform | |
| 4 | + * ───────────────────────────────────────────── | |
| 5 | + * Author : Simon-Pierre Boucher | |
| 6 | + * Contact : contact@spboucher.ai | |
| 7 | + * File : vitest.config.mjs | |
| 8 | + * Purpose : Vitest configuration — single fork (e2e binds real ports) | |
| 9 | + * License : MIT © Simon-Pierre Boucher | |
| 10 | + * ───────────────────────────────────────────── | |
| 11 | + */ | |
| 12 | + | |
| 13 | +import { defineConfig } from 'vitest/config'; | |
| 14 | + | |
| 15 | +export default defineConfig({ | |
| 16 | + test: { | |
| 17 | + pool: 'forks', | |
| 18 | + poolOptions: { forks: { singleFork: true } }, | |
| 19 | + testTimeout: 30000, | |
| 20 | + hookTimeout: 120000, | |
| 21 | + }, | |
| 22 | +}); | |
| 23 | ||