Replace legacy edge fixtures with production assertions

Signed-off-by: 千乘妍 (Xiaoyaner) <258399167+xiaoyaner0201@users.noreply.github.com>
This commit is contained in:
千乘妍 (Xiaoyaner) 2026-11-03 00:59:18 +08:00
parent 335b692a05
commit facd54575f
3 changed files with 10 additions and 157 deletions

View file

@ -15,41 +15,19 @@ afterEach(() => { document.head.replaceChildren(); vi.useRealTimers(); vi.restor
const website = '4398de8e-40df-4483-9eea-ed213643525f'
const intent = { id: 'skill.example.open', event: 'click' }
describe('optional edge deployment defaults', () => {
const nginx = readFileSync('src/features/analytics/fixtures/pr1-nginx.conf', 'utf8')
const compose = readFileSync('src/features/analytics/fixtures/pr1-compose.yaml', 'utf8')
it('inherits the standard 100 MiB limit on all proxied application routes', () => {
// Tokenize quoted strings as units so braces/semicolons in responses do not
// affect scope. Track blocks instead of accepting a location-only limit.
const tokens = nginx.replace(/#.*$/gm, '').match(/"[^"\n]*"|'[^'\n]*'|[{};]|[^\s{};]+/g) ?? []
const blocks: string[] = [], statement: string[] = []
const limits: Array<{ scope: string[]; value: string }> = []
for (const token of tokens) {
if (token === '{') { blocks.push(statement.join(' ')); statement.length = 0 }
else if (token === '}') { blocks.pop(); statement.length = 0 }
else if (token === ';') {
if (statement[0] === 'client_max_body_size') limits.push({ scope: [...blocks], value: statement[1] })
statement.length = 0
} else statement.push(token)
}
expect(limits).toContainEqual({ scope: ['server'], value: '100M' })
// A collector-only restriction is allowed; application locations must inherit.
expect(limits.filter(limit => limit.scope.some(scope => scope.startsWith('location') && !scope.includes('/_skillhub-umami/')))).toEqual([])
expect(limits.filter(limit => limit.scope.length === 1)).toEqual([{ scope: ['server'], value: '100M' }])
describe('production deployment defaults', () => {
it('keeps the 100 MiB application limit at server scope', () => {
const nginx = readFileSync('nginx.conf.template', 'utf8')
expect(nginx).toMatch(/server\s*\{(?:(?!location)[\s\S])*?client_max_body_size 100M;/)
})
it('defaults to the release Web entry, not the development port', () => {
it('keeps the release Web entry and independent collector networking', () => {
const release = readFileSync('../compose.release.yml', 'utf8')
const overlay = readFileSync('../compose.release.analytics.yml', 'utf8')
const collector = readFileSync('../compose.analytics-umami.yml', 'utf8')
expect(release).toContain('${WEB_PORT:-80}:80')
expect(compose).toContain('SKILLHUB_WEB_UPSTREAM: ${SKILLHUB_WEB_UPSTREAM:-host.docker.internal:80}')
expect(compose).not.toContain('host.docker.internal:8080')
})
it('maps the host gateway on native Linux for the edge service', () => {
const edge = compose.split('\n edge:\n')[1]
expect(edge).toBeDefined()
expect(edge).toContain('extra_hosts: ["host.docker.internal:host-gateway"]')
expect(overlay).toContain(' web:\n networks:\n - default\n - analytics')
expect(collector).toContain(' external: true')
expect(collector).toContain('analytics_postgres_data')
})
})

View file

@ -1,40 +0,0 @@
# LOCAL DEMO ONLY. Umami starts with admin / umami: change it immediately.
# The DB password and app secret below are public demo values, NOT production secrets.
# SkillHub is an existing upstream, not built or configured by this demo.
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: umami
POSTGRES_USER: umami
POSTGRES_PASSWORD: demo-only-change-me
healthcheck:
test: [CMD-SHELL, pg_isready -U umami -d umami]
interval: 5s
timeout: 3s
retries: 20
umami:
image: ghcr.io/umami-software/umami:3.4.1
environment:
DATABASE_URL: postgresql://umami:demo-only-change-me@postgres:5432/umami
APP_SECRET: demo-only-change-this-before-use
depends_on:
postgres:
condition: service_healthy
# Direct admin access is LOCAL setup only; never proxy the admin through edge.
ports: ["127.0.0.1:8300:3000"]
edge:
image: nginx:1.30.6-alpine
# Docker Desktop/OrbStack provide this name; native Linux needs this mapping.
extra_hosts: ["host.docker.internal:host-gateway"]
environment:
SKILLHUB_WEB_UPSTREAM: ${SKILLHUB_WEB_UPSTREAM:-host.docker.internal:80}
UMAMI_UPSTREAM: umami:3000
NGINX_ENVSUBST_FILTER: ^(SKILLHUB_WEB_UPSTREAM|UMAMI_UPSTREAM)$$
volumes:
- ./nginx.conf:/etc/nginx/templates/default.conf.template:ro
- ./observe/v1/observer.mjs:/srv/_skillhub-observe/v1/observer.mjs:ro
- ./observe/v1/skillhub-intents.mjs:/srv/_skillhub-observe/v1/skillhub-intents.mjs:ro
- ./observe/v1/umami-adapter.mjs:/srv/_skillhub-observe/v1/umami-adapter.mjs:ro
ports: ["127.0.0.1:8081:8081"]
depends_on: [umami]

View file

@ -1,85 +0,0 @@
# OPTIONAL deployment-owned edge template. Not included by SkillHub.
# Demo HTTP listener: publish on loopback only, behind your existing HTTPS edge.
# Substitute only SKILLHUB_WEB_UPSTREAM and UMAMI_UPSTREAM (host:port).
# Merge existing TLS, CSP, authentication, host validation and security headers.
upstream skillhub_analytics_web { server ${SKILLHUB_WEB_UPSTREAM}; }
upstream skillhub_analytics_umami { server ${UMAMI_UPSTREAM}; }
server {
listen 8081;
server_name localhost;
# Preserve the standard Web upload limit on every application proxy route.
client_max_body_size 100M;
# Install ONLY the three .mjs files here, not declarations or other files.
# A single versioned prefix serves the entry AND its relative imports.
location ^~ /_skillhub-observe/v1/ {
root /srv;
try_files $uri =404;
error_page 404 = @module_missing;
autoindex off;
types { application/javascript mjs; }
default_type application/javascript;
add_header X-Content-Type-Options nosniff always;
add_header Cache-Control "public, max-age=31536000, immutable";
}
location @module_missing {
default_type application/javascript;
add_header X-Content-Type-Options nosniff always;
add_header Cache-Control "no-store" always;
return 404 '// module not found\n';
}
location /_skillhub-observe/ { return 404; }
# Expose only two collector endpoints, NEVER Umami's dashboard/admin API.
location = /_skillhub-umami/script.js {
limit_except GET { deny all; }
proxy_pass http://skillhub_analytics_umami/script.js;
proxy_set_header Cookie "";
proxy_set_header Authorization "";
proxy_set_header Referer "";
proxy_hide_header Set-Cookie;
add_header X-Content-Type-Options nosniff always;
}
location = /_skillhub-umami/api/send {
limit_except POST { deny all; }
proxy_pass http://skillhub_analytics_umami/api/send;
proxy_set_header Cookie "";
proxy_set_header Authorization "";
proxy_set_header Referer "";
proxy_hide_header Set-Cookie;
proxy_cache off;
add_header Cache-Control "no-store" always;
}
location /_skillhub-umami/ { return 404; }
# No filtering or cache-policy changes for API, auth, assets, runtime config,
# registry guides/downloads or health. Preserve original URI (no trailing /).
location ~ ^/(api/|oauth2/|login/oauth2/|\.well-known/|assets/|registry/|runtime-config\.js$|nginx-health) {
proxy_pass http://skillhub_analytics_web;
proxy_set_header Host $http_host;
}
# Only UI HTML is eligible; sub_filter's default MIME is text/html.
location / {
proxy_pass http://skillhub_analytics_web;
proxy_set_header Host $http_host;
proxy_set_header Accept-Encoding "";
proxy_set_header If-None-Match "";
proxy_set_header If-Modified-Since "";
proxy_cache off;
proxy_hide_header ETag;
proxy_hide_header Last-Modified;
proxy_hide_header Content-MD5;
proxy_hide_header Cache-Control;
proxy_hide_header Expires;
etag off;
if_modified_since off;
add_header Cache-Control "no-store" always;
# Also prevent the browser's HTTP Referer from leaking page paths/queries.
proxy_hide_header Referrer-Policy;
add_header Referrer-Policy "no-referrer" always;
sub_filter_once on;
sub_filter_last_modified off;
sub_filter '</head>' '<script type="module" src="/_skillhub-observe/v1/observer.mjs"></script></head>';
}
}