v0.31.8
This commit is contained in:
1 parent
68ada6680d
commit
180a0e4877
56 files changed
+9866
-205
No files matched your search
@@ -24,7 +24,7 @@ def read_docker_agent_source() -> str:
|
||||
|
||||
preflight(root)
|
||||
manifest = tomllib.loads((root / "Cargo.toml").read_text())
|
||||
assert manifest["package"]["version"] == "0.31.7"
|
||||
assert manifest["package"]["version"] == "0.31.8"
|
||||
assert manifest["dependencies"]["quinn"]["version"] == "0.11.12"
|
||||
h3dep = manifest["dependencies"]["h3"]
|
||||
assert h3dep["package"] == "hpx-h3" and h3dep["version"] == "=2.5.20" and "quinn" in h3dep["features"]
|
||||
@@ -32,14 +32,14 @@ assert "h3-quinn" not in manifest["dependencies"]
|
||||
lock_path = root / "Cargo.lock"
|
||||
if lock_path.exists():
|
||||
lock = lock_path.read_text()
|
||||
assert 'name = "rust-proxy"\nversion = "0.31.7"' in lock
|
||||
assert 'name = "rust-proxy"\nversion = "0.31.8"' in lock
|
||||
assert 'name = "systemstat"' in lock and 'name = "metrics-process"' in lock and 'name = "sysinfo"' in lock, "Cargo.lock must be resolver-generated after adding cross-platform statistics libraries"
|
||||
for release_doc, marker in {
|
||||
"README.md": "# RustProxy 0.31.7",
|
||||
"CHANGELOG.md": "## 0.31.7",
|
||||
"ROADMAP.md": "## 0.31.7",
|
||||
"AGENTS.md": "Current source version: 0.31.7.",
|
||||
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.31.7",
|
||||
"README.md": "# RustProxy 0.31.8",
|
||||
"CHANGELOG.md": "## 0.31.8",
|
||||
"ROADMAP.md": "## 0.31.8",
|
||||
"AGENTS.md": "Current source version: 0.31.8.",
|
||||
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.31.8",
|
||||
}.items():
|
||||
assert marker in (root / release_doc).read_text(), f"stale release identity in {release_doc}"
|
||||
assert tomllib.loads((root / "rust-toolchain.toml").read_text())["toolchain"]["channel"] == "stable"
|
||||
@@ -285,7 +285,7 @@ assert 'Opt-in health decisions based on ordinary user traffic. Defaults to disa
|
||||
assert '"force_ssl"' in openapi and '"forward_client_ip"' in openapi
|
||||
assert '"backend_header"' in openapi and '"backend_timing_header"' in openapi
|
||||
assert '"StreamMetrics"' in openapi and '"streams"' in openapi
|
||||
for marker in ['"version": "0.31.7"', '"/api/traffic"', '"capture_user_agent"', '"capture_country"', '"hot_auto_trim_enabled"', '"hot_auto_trim_after_seconds"', '"hot_auto_trim_target_percent"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']:
|
||||
for marker in ['"version": "0.31.8"', '"/api/traffic"', '"capture_user_agent"', '"capture_country"', '"hot_auto_trim_enabled"', '"hot_auto_trim_after_seconds"', '"hot_auto_trim_target_percent"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']:
|
||||
assert marker in openapi, marker
|
||||
for marker in ['"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
|
||||
assert marker in openapi, marker
|
||||
@@ -1179,7 +1179,8 @@ for memory_marker_0230 in [
|
||||
]:
|
||||
assert memory_marker_0230 in server_source or memory_marker_0230 in metrics_source, memory_marker_0230
|
||||
cache_fill_0230 = rust_source(root, 'src/cache/fill.rs')
|
||||
assert 'let body_allocation = self.data.capacity().max(self.data.len());' in cache_fill_0230
|
||||
assert 'let allocated = self.data.capacity().max(self.data.len());' in cache_fill_0230
|
||||
assert 'shared_allocated' in cache_fill_0230 and 'try_into_mut()' in cache_fill_0230
|
||||
assert 'let cost = hot_entry_cost(' in cache_fill_0230
|
||||
assert 'fn hot_entry_cost(' in cache_source
|
||||
for marker_0221 in [
|
||||
@@ -1394,7 +1395,7 @@ assert '"error_log" => {' not in admin_0241 and 'request("/api/config/error_log"
|
||||
assert '"error_log"' in openapi and 'logging, error_log, forwarding' not in openapi
|
||||
assert 'logging_section_persists_error_log_settings_atomically' in admin_0241 and 'error_log_is_not_a_standalone_configuration_section' in admin_0241
|
||||
assert 'location.pathname === "/logging"' in settings_js_0241
|
||||
assert '"RuntimeInformation"' in openapi and '"version": "0.31.7"' in openapi
|
||||
assert '"RuntimeInformation"' in openapi and '"version": "0.31.8"' in openapi
|
||||
|
||||
# 0.24.1 compiler and hostname-cardinality regressions.
|
||||
tls_0241 = rust_source(root, "src/tls.rs")
|
||||
@@ -1633,5 +1634,5 @@ for marker_0282 in [
|
||||
assert 'Zewnętrzne ścieżki plików' in (root / 'langs/pl_pl.json').read_text()
|
||||
assert 'setfacl -m u:rustproxy:--x /etc/ssl/' in (root / 'docs/INSTALL.md').read_text()
|
||||
|
||||
print("PASS: 0.31.7 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, external-packaging/Grafana/Prometheus/404-host/custom-location/path/cache/dashboard/Nagios UI/source contracts, stopped legacy-renewal removal, session/admin/transport contracts, deterministic package source and embedded SQLite schema-v9 migrations.")
|
||||
print("PASS: 0.31.8 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, external-packaging/Grafana/Prometheus/404-host/custom-location/path/cache/dashboard/Nagios UI/source contracts, stopped legacy-renewal removal, session/admin/transport contracts, deterministic package source and embedded SQLite schema-v9 migrations.")
|
||||
print("NOT VERIFIED: Rust parsing, types, compilation, linking or runtime behavior. Run ./dev.sh test.")
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check the 0.31.7 dependency/source migration; optionally require a resolved lock.
|
||||
"""Check the 0.31.8 dependency/source migration; optionally require a resolved lock.
|
||||
|
||||
This is not a substitute for rustc, cargo metadata or runtime tests.
|
||||
"""
|
||||
@@ -14,7 +14,7 @@ parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--require-lock', action='store_true', help='Require the post-upgrade Cargo.lock produced by cargo update')
|
||||
args = parser.parse_args()
|
||||
manifest = tomllib.loads((ROOT / 'Cargo.toml').read_text())
|
||||
assert manifest['package']['version'] == '0.31.7'
|
||||
assert manifest['package']['version'] == '0.31.8'
|
||||
assert manifest['package']['rust-version'] == '1.99'
|
||||
deps = manifest['dependencies']
|
||||
expected_requirements = {
|
||||
@@ -78,4 +78,4 @@ if missing:
|
||||
print(f'PENDING: lockfile still requires cargo update ({details})')
|
||||
else:
|
||||
print('PASS: pinned key packages found in post-upgrade lock; cargo --locked verification is still required')
|
||||
print('PASS: 0.31.7 source dependency APIs, manifest, TLS features, toolchain images and Swagger pin (static only)')
|
||||
print('PASS: 0.31.8 source dependency APIs, manifest, TLS features, toolchain images and Swagger pin (static only)')
|
||||
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env node
|
||||
// Runs the real Docker VHosts dashboard module against a minimal DOM + API fixture.
|
||||
// This is a JS-only regression, not a compiled RustProxy or live agent test.
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
|
||||
const dockerSource = readFileSync('src/admin/web/docker.js', 'utf8');
|
||||
const coreSource = readFileSync('src/admin/web/app/core.js', 'utf8');
|
||||
const english = JSON.parse(readFileSync('langs/en_US.json', 'utf8')).translations;
|
||||
const polish = JSON.parse(readFileSync('langs/pl_pl.json', 'utf8')).translations;
|
||||
for (const translations of [english, polish]) {
|
||||
assert.ok(translations.dockerNoObservation);
|
||||
assert.ok(translations.dockerAgentConnectionFailed);
|
||||
assert.ok(translations.dockerUnknownPollError);
|
||||
assert.doesNotMatch(translations.dockerNoObservation.toLowerCase(), /refresh|odśwież/);
|
||||
}
|
||||
|
||||
class FakeNode {
|
||||
constructor(tag, text = '', cls = '') {
|
||||
this.tagName = tag;
|
||||
this.content = String(text ?? '');
|
||||
this.className = cls;
|
||||
this.children = [];
|
||||
this.dataset = {};
|
||||
this.attrs = {};
|
||||
this.events = {};
|
||||
this.hidden = false;
|
||||
this.open = false;
|
||||
this.classList = {
|
||||
toggle: (name, value) => {
|
||||
const parts = new Set(this.className.split(' ').filter(Boolean));
|
||||
if (value) parts.add(name); else parts.delete(name);
|
||||
this.className = [...parts].join(' ');
|
||||
},
|
||||
add: name => { this.className += ` ${name}`; },
|
||||
remove: name => { this.className = this.className.split(' ').filter(part => part !== name).join(' '); },
|
||||
};
|
||||
}
|
||||
get textContent() { return this.content + this.children.map(child => child.textContent || '').join(''); }
|
||||
set textContent(value) { this.content = String(value ?? ''); this.children = []; }
|
||||
append(...nodes) { this.children.push(...nodes.filter(Boolean)); }
|
||||
replaceChildren(...nodes) { this.children = nodes.filter(Boolean); }
|
||||
setAttribute(name, value) { this.attrs[name] = value; }
|
||||
addEventListener(name, fn) { this.events[name] = fn; }
|
||||
descendants() { return this.children.flatMap(child => [child, ...child.descendants()]); }
|
||||
querySelectorAll(selector) {
|
||||
const part = selector.split(' ').at(-1);
|
||||
if (part.startsWith('.')) return this.descendants().filter(child => child.className.split(' ').includes(part.slice(1)));
|
||||
return [];
|
||||
}
|
||||
querySelector(selector) { return this.querySelectorAll(selector)[0] || null; }
|
||||
}
|
||||
const node = (tag, text, cls) => new FakeNode(tag, text, cls);
|
||||
const destination = node('div');
|
||||
const elements = new Map([
|
||||
['docker-discovery-list', destination],
|
||||
['overview-refresh', node('button')],
|
||||
['page-label', node('span')],
|
||||
['page-title', node('h1')],
|
||||
['page-description', node('p')]
|
||||
]);
|
||||
const agent = {
|
||||
id: 'docker1', name: 'Docker One', enabled: true,
|
||||
base_domain: 'apps.example.test', endpoint: 'https://192.0.2.2:9876',
|
||||
tls_fingerprint_sha256: 'A'.repeat(64), poll_interval_seconds: 30,
|
||||
assignments: []
|
||||
};
|
||||
const config = { docker_agents: { agents: [agent] }, listeners: { endpoints: [] }, tls: { certificates: [] } };
|
||||
const storage = new Map();
|
||||
const requests = [];
|
||||
let nextResult = { observations: [], pools: [], pool_routes: [] };
|
||||
let refreshSpec;
|
||||
let syncs = 0, manuals = 0;
|
||||
const coordinator = {
|
||||
register(id, spec) { assert.equal(id, 'docker'); refreshSpec = spec; },
|
||||
sync() { syncs++; },
|
||||
manual(id) { assert.equal(id, 'docker'); manuals++; return Promise.resolve(); },
|
||||
setPanel(name) { this.panel = name; },
|
||||
};
|
||||
const ctx = {
|
||||
el: node,
|
||||
$: name => elements.get(name) || null,
|
||||
t: name => english[name] || name,
|
||||
document: {
|
||||
activeElement: null,
|
||||
querySelectorAll(selector) {
|
||||
if (selector === '#docker-discovery-list .docker-agent-card') return destination.children;
|
||||
return [];
|
||||
}
|
||||
},
|
||||
window: { rustProxyRefresh: coordinator, rustProxySettings: {} },
|
||||
rustProxyRefresh: coordinator,
|
||||
localStorage: { getItem: key => storage.get(key) || null, setItem: (key, value) => storage.set(key, value) },
|
||||
authenticated: true, authEpoch: 1,
|
||||
currentPanel: 'docker-vhosts', envelope: { config },
|
||||
settingsDirty: new Set(), busy: false,
|
||||
api: async (path, args = {}) => { requests.push({ path, args }); return nextResult; },
|
||||
toast(message) { throw new Error(`Unexpected toast: ${message}`); },
|
||||
console,
|
||||
};
|
||||
vm.createContext(ctx);
|
||||
vm.runInContext(dockerSource, ctx, { filename: 'src/admin/web/docker.js' });
|
||||
assert(refreshSpec, 'register Docker refresh with central scheduler');
|
||||
assert.equal(refreshSpec.interval(), 5);
|
||||
ctx.window.rustProxyDocker.render();
|
||||
assert.equal(manuals, 1, 'deep-link bootstrap fetches a snapshot after configuration loads');
|
||||
assert.equal(syncs, 1, 'deep-link bootstrap syncs waiting-state scheduler');
|
||||
assert.equal(refreshSpec.active(), true, 'bootstrap retries cached status without requiring local Live view');
|
||||
assert.match(destination.textContent, /WAITING/);
|
||||
assert.match(destination.textContent, /Waiting for the first agent check/);
|
||||
assert.doesNotMatch(destination.textContent, /Last Docker agent check failed/);
|
||||
assert.doesNotMatch(destination.textContent, /Agent connection failed/);
|
||||
assert.equal(destination.querySelector('.docker-agent-error'), null, 'pending must not be styled as a failure');
|
||||
let tlsPill = destination.querySelectorAll('.pill').find(item => item.textContent === 'TLS PINNED');
|
||||
assert.ok(tlsPill);
|
||||
assert.ok(!tlsPill.className.split(' ').includes('live'), 'configured fingerprint does not prove successful handshake');
|
||||
|
||||
await ctx.window.rustProxyDocker.refresh(false);
|
||||
assert.equal(requests.at(-1).path, '/api/docker');
|
||||
assert.notEqual(requests.at(-1).args.method, 'POST', 'automatic status fetch never polls the agent');
|
||||
assert.equal(refreshSpec.active(), true, 'retry while the initial server-side poll is not complete');
|
||||
|
||||
nextResult = { observations: [{ agent_id: 'docker1', ok: false, error: 'connection refused', checked_at_unix_ms: 1, inventory: null }], pools: [], pool_routes: [] };
|
||||
await ctx.window.rustProxyDocker.refresh(false);
|
||||
assert.match(destination.textContent, /Last Docker agent check failed/);
|
||||
assert.match(destination.textContent, /connection refused/);
|
||||
assert.doesNotMatch(destination.textContent, /TLS certificate SHA-256:/, 'pinned fingerprint is configuration, not the reason for an arbitrary failure');
|
||||
assert.ok(destination.querySelector('.docker-agent-error'), 'a genuine failed observation must be visible');
|
||||
assert.equal(refreshSpec.active(), false, 'local Live-off stops automatic refresh after first status, even if it is a failure');
|
||||
|
||||
nextResult = { observations: [{ agent_id: 'docker1', ok: true, error: null, checked_at_unix_ms: Date.now(), inventory: {
|
||||
agent_version: '0.22.3', hostname: 'docker1', addresses: [], services: [], public_ip: '192.0.2.2', certificate_sha256: 'A'.repeat(64)
|
||||
} }], pools: [], pool_routes: [] };
|
||||
await ctx.window.rustProxyDocker.refresh(false);
|
||||
assert.match(destination.textContent, /CONNECTED/);
|
||||
assert.doesNotMatch(destination.textContent, /Last Docker agent check failed/);
|
||||
assert.equal(refreshSpec.active(), false, 'local Live-off remains off with a successful observation');
|
||||
tlsPill = destination.querySelectorAll('.pill').find(item => item.textContent === 'TLS PINNED');
|
||||
assert.ok(tlsPill.className.split(' ').includes('live'), 'pin may be marked verified after a successful poll');
|
||||
storage.set('rustproxy.docker.live-view', '1');
|
||||
assert.equal(refreshSpec.active(), true, 'Live view still polls normally');
|
||||
storage.delete('rustproxy.docker.live-view');
|
||||
|
||||
// Execute the real panel-navigation function with side effects disabled.
|
||||
const from = coreSource.indexOf('function setPanel(name, navigate = true) {');
|
||||
const to = coreSource.indexOf('function bytes(value)', from);
|
||||
assert(from >= 0 && to > from);
|
||||
ctx.PANEL_PATHS = { overview: '/', 'docker-vhosts': '/docker-vhosts' };
|
||||
vm.runInContext(coreSource.slice(from, to), ctx);
|
||||
ctx.currentPanel = 'overview';
|
||||
ctx.setPanel('docker-vhosts', false);
|
||||
assert.equal(manuals, 2, 'opening Docker VHosts requests a new cached snapshot even with Live view off');
|
||||
ctx.setPanel('docker-vhosts', false);
|
||||
assert.equal(manuals, 2, 'rendering the same panel does not duplicate one-shot requests');
|
||||
ctx.setPanel('overview', false);
|
||||
ctx.setPanel('docker-vhosts', false);
|
||||
assert.equal(manuals, 3, 're-entering the page reads the newest cached status');
|
||||
|
||||
ctx.currentPanel = 'overview';
|
||||
assert.equal(refreshSpec.active(), false, 'no bootstrap activity outside Docker VHosts');
|
||||
ctx.currentPanel = 'docker-vhosts';
|
||||
config.docker_agents.agents = [{ ...agent, enabled: false }];
|
||||
ctx.window.rustProxyDocker.render();
|
||||
assert.equal(refreshSpec.active(), false, 'disabled agents do not cause polling');
|
||||
assert.doesNotMatch(destination.textContent, /Last Docker agent check failed/);
|
||||
console.log('PASS: 0.31.8 Docker startup awaiting/poll failure/success, pinned TLS semantics, Live-off bootstrap and return-navigation refresh (real JS, simulated DOM/API).');
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rust-independent documentation/Reader consistency checks for the 0.31.7 release."""
|
||||
"""Rust-independent documentation/Reader consistency checks for the 0.31.8 release."""
|
||||
from pathlib import Path
|
||||
import re
|
||||
from rust_source import rust_source
|
||||
@@ -51,7 +51,7 @@ assert '"/docs/content.json"' in admin
|
||||
readme = (ROOT / "README.md").read_text(encoding="utf-8")
|
||||
roadmap = (ROOT / "ROADMAP.md").read_text(encoding="utf-8")
|
||||
changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
|
||||
assert readme.startswith("# RustProxy 0.31.7")
|
||||
assert readme.startswith("# RustProxy 0.31.8")
|
||||
assert "docs/WEB_SERVER.md" in readme and "docs/README.md" in readme
|
||||
assert readme.count("## What changed in ") == 1, "README duplicates historical release notes; use CHANGELOG/archive"
|
||||
assert "## Release history" in readme and "docs/ROADMAP_HISTORY.md" in readme
|
||||
|
||||
@@ -4,7 +4,7 @@ from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
html = (ROOT / 'src/admin/web/index.html').read_text()
|
||||
assert '0.31.7' in (ROOT / "Cargo.toml").read_text()
|
||||
assert '0.31.8' in (ROOT / "Cargo.toml").read_text()
|
||||
assert '<a class="brand" href="/"' in html, 'logo must remain a true full-page navigation'
|
||||
assert 'addEventListener("click"' not in html.split('<a class="brand" href="/"',1)[1].split('</a>',1)[0]
|
||||
body = (ROOT / 'src/proxy/cache_helpers.rs').read_text()
|
||||
|
||||
@@ -168,7 +168,7 @@ reset = invalidation.index("store.key_generations.clear();", barrier)
|
||||
assert "store.key_generations.capacity() > 256" in invalidation[barrier:reset]
|
||||
matcher = invalidation.index("let affected = self.purge_matcher(&matcher);", reset)
|
||||
assert start < barrier < reset < matcher
|
||||
require("src/cache/tests/v022_cache_tests/part_02.rs", "many_distinct_selective_purges_do_not_retain_tombstones_forever")
|
||||
require("src/cache/tests/v022_cache_tests.rs", "many_distinct_selective_purges_do_not_retain_tombstones_forever")
|
||||
|
||||
# Repeated hot-path analytics labels should use borrowed lookups instead of
|
||||
# allocating a String for every request in an already populated top table.
|
||||
|
||||
@@ -76,13 +76,13 @@ def check(root: Path, repair_known: bool = False) -> list[str]:
|
||||
raise ValueError(f"Conflicting Rust modules: {flat.relative_to(root)} and {module.relative_to(root)}")
|
||||
for path in ["src/auth/mod.rs", "src/auth/store.rs", "src/backup/snapshot.rs", "src/upstream.rs", "src/tls.rs", "src/config/internal_routing.rs", "src/proxy/websocket.rs", "src/proxy/error_fallback.rs", "src/cache/mod.rs", "src/cache/model.rs", "src/cache/lifecycle.rs", "src/cache/cold.rs", "src/cache/dashboard.rs", "src/cache/tickets.rs", "src/cache/lookup.rs", "src/cache/policy.rs", "src/cache/fill.rs", "src/cache/tests.rs", "src/settings.rs", "src/compression.rs", "src/diagnostics.rs", "src/traffic.rs", "src/system_stats/mod.rs", "src/system_stats/linux.rs", "src/vhost.rs", "src/vhost/error_fallback.rs", "src/web_server/mod.rs", "src/web_server/config.rs", "src/web_server/filesystem.rs", "src/web_server/security.rs", "src/web_server/response.rs", "src/web_server/request.rs", "src/web_server/routing.rs", "src/web_server/fastcgi.rs", "src/web_server/htaccess.rs", "src/web_server/serve.rs", "src/web_server/tests.rs", "src/balance.rs", "src/security.rs", "src/admin/web/vhosts/core.js", "src/admin/web/vhosts/rates.js", "src/admin/web/vhosts/health.js", "src/admin/web/vhosts/editor.js", "src/admin/web/vhosts/render.js", "src/admin/web/web-server.js", "src/admin/web/streams.js", "src/admin/web/host404.js", "src/admin/web/app/core.js", "src/admin/web/app/session-resume.js", "src/admin/web/app/preferences.js", "src/admin/web/app/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/management.js", "src/admin/web/drafts.js", "src/pages.rs", "tools/dev/mock_server.py", "tools/dev/mock_auth.py", "src/forwarding.rs", "src/tinyauth.rs", "src/http3.rs", "src/stream.rs", "src/admin/web/integrations.js", "src/admin/web/traffic-panel.js", "src/prometheus.rs", "src/admin/web/settings.js", "src/admin/web/docker.js", "src/docker.rs", "docker-agent/main.go", "docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go", "docker-agent/certificate.go", "docker-agent/server.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/install.sh", "docs/DOCKER_DISCOVERY.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/README.md", "docs/TODO.md", "docs/CHANGELOG_TECHNICAL_ARCHIVE.md", "docs/ROADMAP_HISTORY.md", "tools/checks/docs_check.py", "tools/checks/web_server_contracts.py", "tools/checks/rust_source.py", "tools/checks/rust_module_contracts.py", "tools/checks/source_module_contracts.py", "scripts/runtime/acme_provider.py", "src/basic_auth.rs", "scripts/deploy/deploy.py", "scripts/deploy/deploy_core.py", "scripts/deploy/deploy_runtime.py", "scripts/migration/migrate_npm.py", "scripts/migration/migrate_npm_core.py", "scripts/migration/migrate_npm_api.py", "docs/NPM_MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/build/swagger_assets.py", "src/admin/documentation.rs", "src/admin/openapi.json", "langs/en_US.json", "langs/pl_pl.json", "Dockerfile", ".dockerignore", "docs/CONTAINER.md", "docs/PROMETHEUS.md", "docs/NOT_FOUND_HOSTS.md", "grafana/rustproxy-overview.json"]:
|
||||
if not (root / path).is_file():
|
||||
raise ValueError(f"Incomplete RustProxy 0.31.7 source tree: missing {path}")
|
||||
raise ValueError(f"Incomplete RustProxy 0.31.8 source tree: missing {path}")
|
||||
# The Reader uses compile-time include_str! for its bundled Markdown. A
|
||||
# dangling registration is a real compiler error, unlike an omitted old
|
||||
# release audit. Match both reader_doc! and the expanded include_str! form.
|
||||
registry = root / "src/admin/documentation/documents.rs"
|
||||
if not registry.is_file():
|
||||
raise ValueError("Incomplete RustProxy 0.31.7 source tree: missing src/admin/documentation/documents.rs")
|
||||
raise ValueError("Incomplete RustProxy 0.31.8 source tree: missing src/admin/documentation/documents.rs")
|
||||
embedded_docs = set(re.findall(
|
||||
r'"(docs/[A-Za-z0-9_.-]+\.md)"', registry.read_text(encoding="utf-8")
|
||||
))
|
||||
@@ -91,7 +91,7 @@ def check(root: Path, repair_known: bool = False) -> list[str]:
|
||||
raise ValueError(f"Missing embedded Reader document: {doc_path}; restore the file or remove its Reader registration")
|
||||
text = (root / "src/lib.rs").read_text(encoding="utf-8")
|
||||
if '#[path = "auth/mod.rs"]' not in text:
|
||||
raise ValueError("src/lib.rs is not the 0.31.7 entry point; extract the complete package, not individual files.")
|
||||
raise ValueError("src/lib.rs is not the 0.31.8 entry point; extract the complete package, not individual files.")
|
||||
return messages
|
||||
|
||||
|
||||
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
"""0.31.8 source-only regression; does NOT claim Rust compilation or memory profiling."""
|
||||
from pathlib import Path
|
||||
import json
|
||||
import re
|
||||
import tomllib
|
||||
from html.parser import HTMLParser
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
def read(rel): return (ROOT / rel).read_text(encoding='utf-8')
|
||||
assert tomllib.loads(read('Cargo.toml'))['package']['version'] == '0.31.8'
|
||||
assert 'name = "rust-proxy"\nversion = "0.31.8"' in read('Cargo.lock')
|
||||
assert json.loads(read('src/admin/openapi.json'))['info']['version'] == '0.31.8'
|
||||
|
||||
pages = read('src/pages.rs')
|
||||
for code in (400, 401, 403, 404, 408, 410, 413, 429, 500, 502, 503, 504):
|
||||
file = f'src/admin/web/error_{code}.html'
|
||||
markup = read(file)
|
||||
assert markup.lstrip().startswith('<!doctype html>'), file
|
||||
assert 'background: #131615' in markup and 'max-width: 620px' in markup, file
|
||||
assert re.search(rf'<h1>{code} - [^<]+</h1>', markup), file
|
||||
assert f'include_str!("admin/web/error_{code}.html")' in pages, file
|
||||
assert 'status_200.html' in pages and '<h1>200 - OK</h1>' in read('src/admin/web/status_200.html')
|
||||
assert 'RustProxy generated this response.' in pages and 'background: #131615' in pages
|
||||
assert 'matches!(status, StatusCode::NO_CONTENT | StatusCode::RESET_CONTENT | StatusCode::NOT_MODIFIED)' in pages
|
||||
|
||||
geoip = read('src/geoip.rs')
|
||||
assert 'ipv4: Box<[V4Range]>, ipv6: Box<[V6Range]>' in geoip
|
||||
assert 'ipv4.into_boxed_slice()' in geoip and 'ipv6.into_boxed_slice()' in geoip
|
||||
assert 'start: [u8; 16]' in geoip and 'end: [u8; 16]' in geoip
|
||||
assert 'u128::from(ip).to_be_bytes()' in geoip and 'ip <= item.end' in geoip
|
||||
assert 'size_of::<V6Range>(), 34' in read('src/geoip/tests.rs')
|
||||
|
||||
cached = read('src/cache/fill.rs') + read('src/cache/lookup/tiered.rs')
|
||||
for marker in ('bytes.try_into_mut()', 'mutable.capacity()', 'candidate.shared = Some(bytes.clone())',
|
||||
'shared_allocated', 'candidate.fill_gauge.grow(allocation', 'if transformed { 0 }',
|
||||
'candidate.data.extend_from_slice(data)'):
|
||||
assert marker in cached, marker
|
||||
|
||||
q = read('src/admin/file_logs.rs') + read('src/admin/file_logs_query.rs')
|
||||
for marker in ('sort_by: Option<String>', 'sort_descending: Option<bool>', 'valid_raw_field_key(field)',
|
||||
'fn sort_file_log_records(', 'stored_log_sort_key(&record, field)',
|
||||
'file_log_field(record, field)', 'None, Some(_)', 'sort_file_log_records(&mut records'):
|
||||
assert marker in q, marker
|
||||
assert q.index('sort_file_log_records(&mut records') < q.index('let page = records')
|
||||
print('PASS: 0.31.8 versions, 13 matching HTML pages, GeoIP packed/boxed sources, cache ownership/accounting and bounded server sorting (static only).')
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env node
|
||||
// JS-only regression of the Stored Logs sorting interaction (no Rust server).
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
const read = name => fs.readFileSync(name, 'utf8');
|
||||
const ui = read('src/admin/web/file-logs.js');
|
||||
const css = read('src/admin/web/style.css');
|
||||
const api = JSON.parse(read('src/admin/openapi.json'));
|
||||
const params = api.paths['/api/file-logs/query'].get.parameters.map(param => param.name);
|
||||
assert.ok(params.includes('sort_by') && params.includes('sort_descending'));
|
||||
assert.ok(api.components.schemas.AdminPreferences.properties.file_log_sort);
|
||||
assert.ok(api.components.schemas.AdminPreferences.properties.file_log_descending);
|
||||
assert.match(ui, /params\.set\("sort_by", sortBy\)/);
|
||||
assert.match(ui, /params\.set\("sort_descending", String\(sortDescending\)\)/);
|
||||
assert.match(css, /file-log-sort-button/);
|
||||
|
||||
class FakeNode {
|
||||
constructor(tag, text = '', cls = '') { this.tag = tag; this.textContent = text; this.className = cls; this.children = []; this.dataset = {}; this.attrs = {}; this.events = {}; }
|
||||
setAttribute(name, value) { this.attrs[name] = value; }
|
||||
append(...nodes) { this.children.push(...nodes); }
|
||||
replaceChildren(...nodes) { this.children = nodes; }
|
||||
addEventListener(name, handler) { this.events[name] = handler; }
|
||||
click() { this.events.click?.(); }
|
||||
}
|
||||
const row = new FakeNode('tr');
|
||||
const sort = ui.slice(ui.indexOf(' function syncHeaders() {'), ui.indexOf(' function syncColumnPicker()'));
|
||||
assert.ok(sort.startsWith(' function syncHeaders() {') && sort.includes('file-log-sort-button'));
|
||||
const ctx = {
|
||||
el: (tag, text, cls) => new FakeNode(tag, text, cls),
|
||||
$: () => ({querySelector: () => row}),
|
||||
orderedColumns: () => ['time', 'status'],
|
||||
visibleColumns: new Set(['time', 'status']),
|
||||
specialLabels: new Map([['time', 'time'], ['status', 'status']]),
|
||||
columnLabel: id => id,
|
||||
t: key => key,
|
||||
sortBy: 'time', sortDescending: true, selectedIndex: 3, offset: 100,
|
||||
saved: 0, refreshed: 0,
|
||||
savePreferences() { this.saved++; },
|
||||
refresh() { this.refreshed++; },
|
||||
};
|
||||
// Methods capture explicit counters rather than relying on vm's `this` binding.
|
||||
ctx.savePreferences = () => { ctx.saved++; };
|
||||
ctx.refresh = reset => { if (reset) ctx.refreshed++; };
|
||||
vm.createContext(ctx);
|
||||
vm.runInContext(`${sort}\nglobalThis.draw = syncHeaders;`, ctx);
|
||||
ctx.draw();
|
||||
assert.equal(row.children.length, 2);
|
||||
assert.equal(row.children[0].attrs['aria-sort'], 'descending');
|
||||
assert.equal(row.children[1].attrs['aria-sort'], 'none');
|
||||
row.children[1].children[0].click();
|
||||
assert.equal(ctx.sortBy, 'status');
|
||||
assert.equal(ctx.sortDescending, false);
|
||||
assert.equal(ctx.offset, 0);
|
||||
assert.equal(ctx.saved, 1);
|
||||
assert.equal(ctx.refreshed, 1);
|
||||
assert.equal(row.children[1].attrs['aria-sort'], 'ascending');
|
||||
row.children[1].children[0].click();
|
||||
assert.equal(ctx.sortDescending, true);
|
||||
assert.equal(row.children[1].attrs['aria-sort'], 'descending');
|
||||
assert.equal(ctx.saved, 2);
|
||||
assert.equal(ctx.refreshed, 2);
|
||||
console.log('PASS: 0.31.8 Stored Logs headers sort by column, toggle direction, reset pagination, persist preferences and request server sorting.');
|
||||
@@ -135,7 +135,7 @@ def upgrade_smoke(tmp: Path) -> None:
|
||||
assert backup.is_dir()
|
||||
assert (backup / "src/obsolete.rs").is_file()
|
||||
assert not (target / "src/obsolete.rs").exists()
|
||||
assert 'version = "0.31.7"' in (target / "Cargo.toml").read_text(encoding="utf-8")
|
||||
assert 'version = "0.31.8"' in (target / "Cargo.toml").read_text(encoding="utf-8")
|
||||
assert (target / "data/keep.txt").read_text(encoding="utf-8") == "preserve\n"
|
||||
assert (target / ".env").read_text(encoding="utf-8") == "RUST_PROXY_DEMO=0\n"
|
||||
assert (target / "langs/site.json").is_file()
|
||||
@@ -146,7 +146,7 @@ def upgrade_smoke(tmp: Path) -> None:
|
||||
assert (target / "docker-agent/certificate.go").is_file()
|
||||
assert (target / "docker-agent/server.go").is_file()
|
||||
manifest = json.loads((backup / "upgrade.json").read_text(encoding="utf-8"))
|
||||
assert manifest["to_version"] == "0.31.7"
|
||||
assert manifest["to_version"] == "0.31.8"
|
||||
assert manifest["removed_obsolete_runtime_state"] is True
|
||||
assert not legacy.exists()
|
||||
assert not (backup / "legacy-data").exists()
|
||||
@@ -617,7 +617,7 @@ def source_contract_smoke() -> None:
|
||||
assert marker in admin, marker
|
||||
anonymous_block = admin.split("fn anonymous_allowed", 1)[1].split("const CERTIFICATE_API_KEY_HEADER", 1)[0]
|
||||
assert '"/api/status"' not in anonymous_block
|
||||
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.31.7"' in openapi_0241
|
||||
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.31.8"' in openapi_0241
|
||||
assert 'id="panel-logging"' not in html
|
||||
for marker in ['id="settings-tab-logging"', 'id="settings-page-logging"', 'id="logging-form"', 'id="error-log-form"']:
|
||||
assert marker in html, marker
|
||||
@@ -1102,7 +1102,7 @@ def source_contract_smoke() -> None:
|
||||
assert '.managed-update' in main and "flush_hot_to_cold_for_update" in main
|
||||
openapi = (ROOT / "src/admin/openapi.json").read_text(encoding="utf-8")
|
||||
assert '"/api/config/history/restore"' in openapi
|
||||
for marker in ['"version": "0.31.7"', '"memory_preset"', '"cold_preset"', '"memory_budget_bytes"', '"max_inflight"', '"preset"', '"session_days"', '"session_lifetime_seconds"', '"accept_local_listener_risk"', '"backend_header"', '"backend_timing_header"', '"cold_on_age"', '"match_type"', '"regex_insensitive"', '"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
|
||||
for marker in ['"version": "0.31.8"', '"memory_preset"', '"cold_preset"', '"memory_budget_bytes"', '"max_inflight"', '"preset"', '"session_days"', '"session_lifetime_seconds"', '"accept_local_listener_risk"', '"backend_header"', '"backend_timing_header"', '"cold_on_age"', '"match_type"', '"regex_insensitive"', '"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
|
||||
assert marker in openapi, marker
|
||||
assert '"software_update"' in openapi and '"update_available"' in openapi
|
||||
for marker in ['"/api/certificates/deploy"', '"CertificateDeploy"', '"CertificateDeployResult"', '"/api/certificates/reload"']:
|
||||
@@ -1216,7 +1216,15 @@ def reproducible_archive_smoke(tmp: Path) -> None:
|
||||
assert "RustProxy/tools/checks/source_module_contracts.py" in names
|
||||
for name in names:
|
||||
if name.startswith("RustProxy/") and name.endswith(".rs"):
|
||||
assert len(archive.read(name).decode("utf-8").splitlines()) <= 500, name
|
||||
relative = Path(name).parts[1:]
|
||||
is_test = relative[0] == "tests" or (
|
||||
relative[0] == "src" and (
|
||||
relative[-1] == "tests.rs" or relative[-1].endswith("_tests.rs")
|
||||
or "tests" in relative[1:-1]
|
||||
)
|
||||
)
|
||||
if not is_test:
|
||||
assert len(archive.read(name).decode("utf-8").splitlines()) <= 500, name
|
||||
if name.startswith("RustProxy/src/admin/web/") and name.endswith(".js"):
|
||||
assert len(archive.read(name).decode("utf-8").splitlines()) <= 500, name
|
||||
if name.startswith("RustProxy/docker-agent/") and name.endswith(".go"):
|
||||
@@ -1307,7 +1315,7 @@ def main() -> None:
|
||||
source_contract_smoke()
|
||||
container_contract_smoke()
|
||||
grafana_contract_smoke()
|
||||
print("PASS: 0.31.7 SQLite/schema-9/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, external Debian-packaging separation, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.")
|
||||
print("PASS: 0.31.8 SQLite/schema-9/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, external Debian-packaging separation, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"""Static structure contracts for the split Rust source tree.
|
||||
|
||||
These checks are intentionally compiler-independent. They prevent the module split
|
||||
from regressing into large monoliths, keep unit tests out of implementation files,
|
||||
from regressing into large implementation monoliths, allow full-sized test files,
|
||||
and catch the class of dangling-attribute/broken-include mistakes that can otherwise
|
||||
surface only at cargo check time.
|
||||
"""
|
||||
@@ -19,8 +19,12 @@ MAX_LINES = 500
|
||||
|
||||
|
||||
def is_test_source(path: Path) -> bool:
|
||||
relative = path.relative_to(SRC)
|
||||
return path.name == "tests.rs" or path.stem.endswith("_tests") or "tests" in relative.parts
|
||||
relative = path.relative_to(ROOT)
|
||||
if relative.parts[0] == "tests":
|
||||
return True
|
||||
return relative.parts[0] == "src" and (
|
||||
path.name == "tests.rs" or path.stem.endswith("_tests") or "tests" in relative.parts[1:]
|
||||
)
|
||||
|
||||
|
||||
include_re = re.compile(r'include!\(\s*"([^"]+\.rs)"\s*\)')
|
||||
@@ -89,10 +93,12 @@ def delimiters_balanced(text: str) -> bool:
|
||||
return not stack and state in {"code", "line"}
|
||||
|
||||
|
||||
# The 500-line rule applies to every Rust source in the repository, including integration tests.
|
||||
# The 500-line rule applies to production Rust sources, not unit/integration tests.
|
||||
for path in sorted(ROOT.rglob("*.rs")):
|
||||
if any(part in {"target", ".git"} for part in path.parts):
|
||||
continue
|
||||
if is_test_source(path):
|
||||
continue
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
assert len(lines) <= MAX_LINES, f"{path.relative_to(ROOT)} has {len(lines)} lines (max {MAX_LINES})"
|
||||
|
||||
@@ -126,6 +132,22 @@ for path in sorted(SRC.rglob("*.rs")):
|
||||
f"test cfg in implementation must target a separate test file: {path.relative_to(ROOT)}"
|
||||
)
|
||||
|
||||
# Unit tests are intentionally consolidated rather than mechanically numbered.
|
||||
for folder in (
|
||||
"admin/tests", "balance/tests", "config/tests", "web_server/tests",
|
||||
"cache/tests/tests", "cache/tests/v022_cache_tests", "cache/tests/cold_tier_tests",
|
||||
):
|
||||
assert not (SRC / folder).exists(), f"obsolete split-test directory: {folder}"
|
||||
for relative in (
|
||||
"admin/tests.rs", "balance/tests.rs", "config/tests.rs", "web_server/tests.rs",
|
||||
"cache/tests/tests.rs", "cache/tests/v022_cache_tests.rs", "cache/tests/cold_tier_tests.rs",
|
||||
):
|
||||
assert (SRC / relative).is_file(), f"missing consolidated test file: {relative}"
|
||||
assert 'include_str!("web/login-keys.json")' in (SRC / "admin/tests.rs").read_text(), (
|
||||
"admin test asset path must be relative to the consolidated test file"
|
||||
)
|
||||
assert (SRC / "admin/web/login-keys.json").is_file()
|
||||
|
||||
# Explicit regressions from the first Web Server split.
|
||||
response = (SRC / "web_server/response.rs").read_text(encoding="utf-8")
|
||||
assert not response.rstrip().endswith("#[derive(Debug, Clone)]"), "dangling derive returned in Web Server response module"
|
||||
@@ -150,16 +172,21 @@ server_tests = (SRC / "server/serve/tests.rs").read_text(encoding="utf-8")
|
||||
assert server_tests.count("serve_inner(") == server_tests.count("Http2Connections::default()"), (
|
||||
"direct serve_inner test calls must pass the Http2Connections argument"
|
||||
)
|
||||
cache_policy_tests = (SRC / "cache/tests/tests/part_03.rs").read_text(encoding="utf-8")
|
||||
assert "let plan = plan(" not in cache_policy_tests, "cache policy test shadows the plan() function"
|
||||
cold_tests = (SRC / "cache/tests/cold_tier_tests/part_02.rs").read_text(encoding="utf-8")
|
||||
cache_policy_tests = (SRC / "cache/tests/tests.rs").read_text(encoding="utf-8")
|
||||
# Older cache tests legitimately use `let plan = plan(...)`. The regression
|
||||
# applies to the Vary/revalidation group formerly stored in part_03.rs.
|
||||
revalidation_tests = cache_policy_tests.split(
|
||||
"async fn vary_selects_variants_without_fragmenting_the_primary_hash", 1
|
||||
)[1]
|
||||
assert "let plan = plan(" not in revalidation_tests, "cache policy test shadows the plan() function"
|
||||
cold_tests = (SRC / "cache/tests/cold_tier_tests.rs").read_text(encoding="utf-8")
|
||||
for field in ("scheme:", "authority:", "target:", "tags:", "created_at:"):
|
||||
assert field in cold_tests, f"ColdEntry test fixture is missing current field {field}"
|
||||
admin_tests = (SRC / "admin/tests/part_02.rs").read_text(encoding="utf-8")
|
||||
admin_tests = (SRC / "admin/tests.rs").read_text(encoding="utf-8")
|
||||
assert "let configuration = router\n .clone()\n .oneshot(" in admin_tests, (
|
||||
"admin persistence test must retain Router before Tower::oneshot consumes a clone"
|
||||
)
|
||||
web_tests_1 = (SRC / "web_server/tests/part_01.rs").read_text(encoding="utf-8")
|
||||
web_tests_1 = (SRC / "web_server/tests.rs").read_text(encoding="utf-8")
|
||||
assert "ETAG, EXPIRES," not in web_tests_1, "unused EXPIRES import returned in Web Server tests"
|
||||
|
||||
print(f"PASS: Rust modules are <= {MAX_LINES} lines, test code is isolated, include! targets resolve, and split-module/all-target regressions are guarded.")
|
||||
print(f"PASS: production Rust modules are <= {MAX_LINES} lines (tests exempt), numbered test fragments are absent, include! targets resolve, and module regressions are guarded.")
|
||||
@@ -21,7 +21,7 @@ const openapi = json('src/admin/openapi.json');
|
||||
const en = json('langs/en_US.json').translations;
|
||||
const pl = json('langs/pl_pl.json').translations;
|
||||
|
||||
assert.match(manifest, /version = "0\.31\.7"/);
|
||||
assert.match(manifest, /version = "0\.31\.8"/);
|
||||
assert.match(lib, /pub mod user_agent;/);
|
||||
assert.match(model, /user_agent_blocking: crate::user_agent::Policy/);
|
||||
assert.match(vhost, /user_agent_blocking: crate::user_agent::Policy/);
|
||||
@@ -52,7 +52,7 @@ assert.match(fileLogs, /localStorage\.setItem\(CONSOLIDATION_STORAGE_KEY/);
|
||||
assert.match(fileLogs, /aggregate_min_count/);
|
||||
assert.match(fileLogs, /file-log-aggregate-copy/);
|
||||
|
||||
assert.equal(openapi.info.version, '0.31.7');
|
||||
assert.equal(openapi.info.version, '0.31.8');
|
||||
assert.ok(openapi.components.schemas.Config.properties.user_agent_blocking);
|
||||
assert.ok(openapi.components.schemas.Vhost.properties.user_agent_blocking);
|
||||
assert.ok(openapi.components.schemas.FileLogSource.properties.group_id);
|
||||
|
||||
@@ -43,7 +43,7 @@ const code = js.slice(js.indexOf(' const runtime = latestStatus?.runtime || {
|
||||
const elements = new Map();
|
||||
const dollar = id => { if (!elements.has(id)) elements.set(id, { textContent: '' }); return elements.get(id); };
|
||||
const status = {
|
||||
version: '0.31.7', uptime_seconds: 4201,
|
||||
version: '0.31.8', uptime_seconds: 4201,
|
||||
runtime: {
|
||||
platform: 'Linux', operating_system: 'Debian GNU/Linux 13', kernel_version: '6.12.0', architecture: 'x86_64',
|
||||
statistics_source: 'systemstat+sysinfo', data_directory: '/var/lib/rustproxy', database_path: '/var/lib/rustproxy/config.sqlite3',
|
||||
@@ -63,7 +63,7 @@ const context = {
|
||||
vm.runInNewContext(code, context);
|
||||
assert.equal(dollar('runtime-operating-system').textContent, 'Debian GNU/Linux 13');
|
||||
assert.equal(dollar('runtime-kernel-version').textContent, '6.12.0');
|
||||
assert.equal(dollar('runtime-version').textContent, '0.31.7');
|
||||
assert.equal(dollar('runtime-version').textContent, '0.31.8');
|
||||
assert.equal(dollar('runtime-logical-cpus').textContent, '8');
|
||||
assert.equal(dollar('runtime-cpu-usage').textContent, '17.2%');
|
||||
assert.equal(dollar('runtime-python').textContent, 'Available · 3.13.5');
|
||||
@@ -124,4 +124,4 @@ for (const key of ['python_version','pip_version','certbot_version']) {
|
||||
assert.ok(runtimeSchema.properties.certificate_runtime.properties[key]);
|
||||
assert.equal(runtimeSchema.properties.certificate_runtime.properties[key].nullable, true);
|
||||
}
|
||||
console.log('PASS: 0.31.7 legend palette, About cleanup, system tab/navigation, read-only API schema and simulated status rendering.');
|
||||
console.log('PASS: 0.31.8 legend palette, About cleanup, system tab/navigation, read-only API schema and simulated status rendering.');
|
||||
Reference in new issue
Block a user