mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
Merge branch 'main' into upstream/traffic-layer-stale
This commit is contained in:
27 files changed
+457
-19
No files matched your search
@@ -39,6 +39,11 @@ COPY --chown=node:node . .
|
|||||||
ENV HOST=0.0.0.0
|
ENV HOST=0.0.0.0
|
||||||
ENV PORT=4173
|
ENV PORT=4173
|
||||||
EXPOSE 4173
|
EXPOSE 4173
|
||||||
|
# node:slim has no curl, so probe with Node's built-in fetch. 127.0.0.1 is
|
||||||
|
# always on the Host allowlist, and the generous start period covers Vite's
|
||||||
|
# first-run dependency optimisation.
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
||||||
|
CMD ["node", "-e", "fetch('http://127.0.0.1:'+(Number.parseInt(process.env.PORT,10)||4173)+'/').then((r)=>process.exit(r.ok?0:1),()=>process.exit(1))"]
|
||||||
# The launcher is `npm run dev` plus one step: it trusts the container's
|
# The launcher is `npm run dev` plus one step: it trusts the container's
|
||||||
# gateway address for Provider Settings, because that is where Docker delivers
|
# gateway address for Provider Settings, because that is where Docker delivers
|
||||||
# the host's own browser from (never 127.0.0.1). See scripts/docker-start.mjs.
|
# the host's own browser from (never 127.0.0.1). See scripts/docker-start.mjs.
|
||||||
|
|||||||
@@ -111,13 +111,15 @@ app's **POWER UP** panel; [Keys & Costs](#-api-keys) explains the options.
|
|||||||
|
|
||||||
Available on **Windows, macOS, and Linux**. The Pinokio maintainer reports
|
Available on **Windows, macOS, and Linux**. The Pinokio maintainer reports
|
||||||
cross-platform testing of the fixed installer. The launcher installs the
|
cross-platform testing of the fixed installer. The launcher installs the
|
||||||
locked dependencies, finds a free local port, and opens the app.
|
locked dependencies, finds a free local port, and opens the app in your system
|
||||||
|
browser. The Server menu item keeps the launcher console available.
|
||||||
|
|
||||||
**Tried before and installation failed?** Update Pinokio and try again.
|
**Tried before and installation failed?** Update Pinokio and try again.
|
||||||
Version 8.2 fixes the launcher installation issue;
|
Version 8.2 fixes the launcher installation issue;
|
||||||
[details from the Pinokio maintainer](https://pinokio.co/posts/01m1m4p9xxm3qw7dnnpj2wr93g).
|
[details from the Pinokio maintainer](https://pinokio.co/posts/01m1m4p9xxm3qw7dnnpj2wr93g).
|
||||||
|
|
||||||
**Pinokio opens but the globe is very choppy?** Before treating that as a GPU
|
**Using an older launcher or opening the embedded view manually?** If the globe
|
||||||
|
is very choppy, before treating that as a GPU
|
||||||
compatibility or minimum-hardware problem, copy the local server URL shown by
|
compatibility or minimum-hardware problem, copy the local server URL shown by
|
||||||
Pinokio and open the same URL in your normal hardware-accelerated browser. One
|
Pinokio and open the same URL in your normal hardware-accelerated browser. One
|
||||||
Pinokio 8.2.0 user reported severe stutter in Pinokio's embedded view that went
|
Pinokio 8.2.0 user reported severe stutter in Pinokio's embedded view that went
|
||||||
@@ -207,6 +209,8 @@ env UID=1026 GID=100 docker compose up -d
|
|||||||
|
|
||||||
Keep the `env`: bash treats `UID` as read-only, so a plain `UID=1026` in front of a command is silently ignored. If the container can't write the checkout, it says so at start. Docker Desktop on macOS and Windows maps ownership for you.
|
Keep the `env`: bash treats `UID` as read-only, so a plain `UID=1026` in front of a command is silently ignored. If the container can't write the checkout, it says so at start. Docker Desktop on macOS and Windows maps ownership for you.
|
||||||
|
|
||||||
|
On SELinux hosts (Fedora, RHEL), compose mounts the checkout with `:z` so the container may read it; Docker may relabel the checkout for container access, which can take a moment on a large checkout. Without that, the start fails with `Cannot find module '/app/scripts/docker-start.mjs'`.
|
||||||
|
|
||||||
Behind a reverse proxy, put your keys in `.env` or pass them as environment variables - Provider Settings refuses proxied requests on purpose - and add the hostname you serve it on, for example `GEV_ALLOWED_HOSTS=globe.example.lan` (see [Sharing an instance](#-sharing-an-instance)).
|
Behind a reverse proxy, put your keys in `.env` or pass them as environment variables - Provider Settings refuses proxied requests on purpose - and add the hostname you serve it on, for example `GEV_ALLOWED_HOSTS=globe.example.lan` (see [Sharing an instance](#-sharing-an-instance)).
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
|
|
||||||
God's Eye View is a local-first client for **public** data. It is built for exploration, demos, and learning — not as a hardened production service. This document explains the security model so you can run it safely and report issues responsibly.
|
God's Eye View is a local-first client for **public** data. It is built for exploration, demos, and learning — not as a hardened production service. This document explains the security model so you can run it safely and report issues responsibly.
|
||||||
|
|
||||||
|
Vite's development file server also denies `.gev-logs/`, `.gev-cache/`, and
|
||||||
|
the package checkout's root `dist/` directory. Dependency `dist/` directories
|
||||||
|
remain available. This filesystem deny list does not filter `public/` assets or
|
||||||
|
preview output; never put private artifacts in a public asset directory.
|
||||||
|
|
||||||
## Reporting a vulnerability
|
## Reporting a vulnerability
|
||||||
|
|
||||||
Please report security issues **privately** — do not open a public issue for anything exploitable.
|
Please report security issues **privately** — do not open a public issue for anything exploitable.
|
||||||
|
|||||||
+33
-1
@@ -1,9 +1,29 @@
|
|||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
import { applicationHtmlPlugin } from './application-html.js';
|
import { applicationHtmlPlugin } from './application-html.js';
|
||||||
import cesium from 'vite-plugin-cesium';
|
import cesium from 'vite-plugin-cesium';
|
||||||
import { DEFAULT_ALLOWED_HOSTS, hostCheckPlugin } from './allowedHosts.js';
|
import { DEFAULT_ALLOWED_HOSTS, hostCheckPlugin } from './allowedHosts.js';
|
||||||
import { embedFramingPlugin } from './embed-framing.js';
|
import { embedFramingPlugin } from './embed-framing.js';
|
||||||
import { panelBuildPlugin } from './panel.js';
|
import { panelBuildPlugin } from './panel.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The repository root (this file lives at `<root>/build/vite.js`), as a
|
||||||
|
* forward-slash path for glob matching. Vite's `fs.deny` patterns are
|
||||||
|
* matched against the full absolute file path, and a bare `**\/dist/**`
|
||||||
|
* matches a `dist` directory at *any* depth — including dependencies that
|
||||||
|
* ship their own, such as `node_modules/vite/dist/client/client.mjs` (the
|
||||||
|
* dev client itself). Anchoring to this repository's root denies only the
|
||||||
|
* project's own build output.
|
||||||
|
*/
|
||||||
|
const REPO_ROOT = fileURLToPath(new URL('..', import.meta.url))
|
||||||
|
.replace(/\\/g, '/')
|
||||||
|
.replace(/\/$/, '');
|
||||||
|
|
||||||
|
export function escapeFsDenyPath(path) {
|
||||||
|
return path.replace(/[()[\]{}*?!+@|]/g, '\\$&');
|
||||||
|
}
|
||||||
|
|
||||||
|
const REPO_ROOT_GLOB = escapeFsDenyPath(REPO_ROOT);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Content-Security-Policy for every document the dev/preview server serves.
|
* Content-Security-Policy for every document the dev/preview server serves.
|
||||||
* No inline script is permitted, and the only foreign script origins are the
|
* No inline script is permitted, and the only foreign script origins are the
|
||||||
@@ -165,7 +185,19 @@ export function createBrowserViteConfig({
|
|||||||
// build/allowedHosts.js); IP addresses are always accepted.
|
// build/allowedHosts.js); IP addresses are always accepted.
|
||||||
allowedHosts: [...allowedHosts],
|
allowedHosts: [...allowedHosts],
|
||||||
fs: {
|
fs: {
|
||||||
deny: ['.env', '.env.*', '*.{crt,pem}', '**/.git/**', '**/ENVIRONMENT'],
|
deny: [
|
||||||
|
'.env',
|
||||||
|
'.env.*',
|
||||||
|
'*.{crt,pem}',
|
||||||
|
'**/.git/**',
|
||||||
|
'**/ENVIRONMENT',
|
||||||
|
'**/.gev-logs/**',
|
||||||
|
'**/.gev-cache/**',
|
||||||
|
// Root-anchored: a bare '**/dist/**' would also deny every
|
||||||
|
// dependency's own dist/ (e.g. node_modules/vite/dist/client),
|
||||||
|
// breaking /@vite/client and other dev-client assets.
|
||||||
|
`${REPO_ROOT_GLOB}/dist/**`,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
// These headers protect the document containing Provider Settings and
|
// These headers protect the document containing Provider Settings and
|
||||||
// give the whole page a real Content-Security-Policy (BROWSER_CSP).
|
// give the whole page a real Content-Security-Policy (BROWSER_CSP).
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Keyless standalone startup answers an unavailable Google tile-token endpoint
|
||||||
|
with uncached 204 instead of a browser-visible 404. Existing providers take
|
||||||
|
precedence, and unrelated API routes behave as before. (dvilrgamerz, #1066)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Open God's Eye View from Pinokio in the system browser using the supported
|
||||||
|
`popout` menu option. The application's framing policy is unchanged
|
||||||
|
(NukeThemAII, #1222).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Carry HUD LAT/LON seconds that round up to 60.00" into the minutes and
|
||||||
|
degrees, so longitude -122.35 reads `122°21'00.00"W` instead of
|
||||||
|
`122°20'60.00"W` (rudycelekli, #1339).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Wake staged weather imagery on a 150 ms timer while waiting for readiness,
|
||||||
|
avoiding a continuous render loop and cancelling the wake when work ends
|
||||||
|
(@nu-stats, #1345).
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
- Start the Docker install on SELinux hosts such as Fedora and RHEL. Compose now
|
||||||
|
mounts the checkout with `:z`; without it the container could not read `/app`
|
||||||
|
and stopped with "Cannot find module '/app/scripts/docker-start.mjs'"
|
||||||
|
(Anıl Güvenç, @anilguvenc, #1391).
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
- Report Docker container health with a local HTTP check using Node, with the
|
||||||
|
same port parsing as the launcher (John Rogers, @14traits, #1394).
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
- Harden Vite's filesystem deny list for local-only artifacts: repo `dist/`,
|
||||||
|
`.gev-logs/`, and `.gev-cache/` are denied without blocking dependency
|
||||||
|
`dist/` directories, and checkout paths containing glob metacharacters are
|
||||||
|
escaped before building the root-anchored pattern (blowing-a-raspberry, #948).
|
||||||
+4
-1
@@ -22,5 +22,8 @@ services:
|
|||||||
# UI land in the .env on your host instead of vanishing with the
|
# UI land in the .env on your host instead of vanishing with the
|
||||||
# container. node_modules stays in the image: sharp and friends are
|
# container. node_modules stays in the image: sharp and friends are
|
||||||
# compiled for Linux and must not be shadowed by a host install.
|
# compiled for Linux and must not be shadowed by a host install.
|
||||||
- .:/app
|
# `:z` relabels the checkout for containers on SELinux hosts (Fedora,
|
||||||
|
# RHEL); without it the container cannot read /app at all and Node
|
||||||
|
# reports the launcher as "Cannot find module". Elsewhere it is ignored.
|
||||||
|
- .:/app:z
|
||||||
- /app/node_modules
|
- /app/node_modules
|
||||||
+40
-1
@@ -1,5 +1,15 @@
|
|||||||
# God's Eye View Current State
|
# God's Eye View Current State
|
||||||
|
|
||||||
|
Docker Compose mounts the checkout with the shared SELinux `:z` label so
|
||||||
|
containers can read it on enforcing SELinux hosts. The separate image-owned
|
||||||
|
`node_modules` volume and loopback-only port publishing remain unchanged.
|
||||||
|
|
||||||
|
The HUD's LAT/LON degrees-minutes-seconds readout rounds the coordinate once, to
|
||||||
|
hundredths of a second, and then splits it. A value within 0.005" of the next
|
||||||
|
whole minute carries into the minutes and degrees, so `-122.35` reads
|
||||||
|
`122°21'00.00"W` rather than `122°20'60.00"W`. The field widths and hemisphere
|
||||||
|
letters are unchanged.
|
||||||
|
|
||||||
Aircraft detection candidates reuse normalized display text until callsign or
|
Aircraft detection candidates reuse normalized display text until callsign or
|
||||||
registration changes, including in-place enrichment. Each candidate resolves its
|
registration changes, including in-place enrichment. Each candidate resolves its
|
||||||
fleet model once per collection. Positions, model ownership and tracked-aircraft
|
fleet model once per collection. Positions, model ownership and tracked-aircraft
|
||||||
@@ -296,6 +306,10 @@ catalog construction owns each instance.
|
|||||||
Wind animates one forecast without advancing forecast time. Separate Weather
|
Wind animates one forecast without advancing forecast time. Separate Weather
|
||||||
observation layers provide radar and satellite history; none claims measured cloud volume.
|
observation layers provide radar and satellite history; none claims measured cloud volume.
|
||||||
Weather imagery releases its camera move listener on disable and registers it once on re-enable. Disabling also clears active imagery work.
|
Weather imagery releases its camera move listener on disable and registers it once on re-enable. Disabling also clears active imagery work.
|
||||||
|
A staged weather frame with loaded tiles and no pending requests rechecks
|
||||||
|
readiness on a single 150 ms wake timer instead of requesting every render.
|
||||||
|
The existing readiness conditions and two settled frames still govern promotion;
|
||||||
|
completion, cancellation, replacement and teardown clear the wake timer.
|
||||||
Weather imagery drapes onto the globe basemap. On photorealistic 3D Tiles each
|
Weather imagery drapes onto the globe basemap. On photorealistic 3D Tiles each
|
||||||
observed product is instead a raised, translucent shell: one rectangle over the
|
observed product is instead a raised, translucent shell: one rectangle over the
|
||||||
product bounds at a fixed height (global infrared 5.5 km, regional infrared
|
product bounds at a fixed height (global infrared 5.5 km, regional infrared
|
||||||
@@ -1839,6 +1853,12 @@ supported. `.env.example` and `pinokio/_ENVIRONMENT` document both entries.
|
|||||||
The Street View headings tool uses the same server-first selection after
|
The Street View headings tool uses the same server-first selection after
|
||||||
resolving environment overrides per variable; its explicit `--key` wins.
|
resolving environment overrides per variable; its explicit `--key` wins.
|
||||||
|
|
||||||
|
In standalone development and preview servers, a missing Google tile-token
|
||||||
|
provider returns an uncached `204 No Content` for `GET /api/google/tiles-token`
|
||||||
|
(and HEAD) instead of a browser-visible 404. The fallback is registered after
|
||||||
|
real token providers and before the unknown-API handler, so existing Google
|
||||||
|
browser keys, provider-issued tokens, and unrelated API routes remain unchanged.
|
||||||
|
|
||||||
## Infrastructure marker visibility
|
## Infrastructure marker visibility
|
||||||
|
|
||||||
Datacenter/dam registration uses fresh reusable factories with the application's
|
Datacenter/dam registration uses fresh reusable factories with the application's
|
||||||
@@ -5375,11 +5395,21 @@ pins and share links already supply complete boxes.
|
|||||||
|
|
||||||
## Auth + Launch
|
## Auth + Launch
|
||||||
|
|
||||||
|
Vite's development filesystem deny list excludes local `.gev-logs/` and
|
||||||
|
`.gev-cache/` contents and the package checkout's root `dist/`, while allowing
|
||||||
|
dependency `dist/` assets. Checkout glob characters are escaped. This does not
|
||||||
|
apply to `public/` assets or built-preview serving.
|
||||||
|
|
||||||
|
|
||||||
- Recommended launcher: `./scripts/dev-fresh.sh` (also: `dev-secure.sh` for stricter bindings, `dev-cctv.sh` for CCTV source-pack tuning)
|
- Recommended launcher: `./scripts/dev-fresh.sh` (also: `dev-secure.sh` for stricter bindings, `dev-cctv.sh` for CCTV source-pack tuning)
|
||||||
- A successful Pinokio install writes the owner-only `pinokio/.installed`
|
- A successful Pinokio install writes the owner-only `pinokio/.installed`
|
||||||
marker. The nested launcher menu resolves that marker from its own directory:
|
marker. The nested launcher menu resolves that marker from its own directory:
|
||||||
an absent marker exposes Install, a present marker exposes Start, and a
|
an absent marker exposes Install, a present marker exposes Start, and a
|
||||||
running server with a captured ready URL exposes Open God's Eye View.
|
running server with a captured ready URL exposes Open God's Eye View. That
|
||||||
|
default item uses Pinokio's `popout` option to open the system browser; the
|
||||||
|
Server item retains the launcher console. The app's framing headers stay
|
||||||
|
unchanged. This does not assume all Pinokio desktop versions enforce those
|
||||||
|
headers in their embedded view.
|
||||||
- Build gate: `npm run build`
|
- Build gate: `npm run build`
|
||||||
- Network access: local-only by default (`HOST=localhost` in dev-fresh.sh); LAN is an explicit opt-in via `HOST=0.0.0.0` (launcher prints a key-exposure warning + LAN URL; see SECURITY.md)
|
- Network access: local-only by default (`HOST=localhost` in dev-fresh.sh); LAN is an explicit opt-in via `HOST=0.0.0.0` (launcher prints a key-exposure warning + LAN URL; see SECURITY.md)
|
||||||
- OpenSky default mode: OAuth (`OPENSKY_AUTH_MODE=oauth`; `anon` works without credentials)
|
- OpenSky default mode: OAuth (`OPENSKY_AUTH_MODE=oauth`; `anon` works without credentials)
|
||||||
@@ -6231,3 +6261,12 @@ when their footprints touch or contain one another, never across a seam toleranc
|
|||||||
- `scripts/qa-form-fields.mjs` exercises native selections, number validity,
|
- `scripts/qa-form-fields.mjs` exercises native selections, number validity,
|
||||||
calibration commit/cancel and stale-camera/disposal guards against an installed
|
calibration commit/cancel and stale-camera/disposal guards against an installed
|
||||||
controller with fixture ports, plus default/Cyber and narrow translated layouts.
|
controller with fixture ports, plus default/Cyber and narrow translated layouts.
|
||||||
|
|
||||||
|
## Docker health reporting
|
||||||
|
|
||||||
|
The Docker image probes its local HTTP root with Node every 30 seconds, using
|
||||||
|
the launcher's parsed `PORT` (default 4173). A successful HTTP response marks
|
||||||
|
the container healthy. Checks time out after 10 seconds, with a 30-second
|
||||||
|
startup grace period and three consecutive failures before reporting unhealthy.
|
||||||
|
This reports app HTTP availability, not individual provider health, and does not
|
||||||
|
configure automatic restarts.
|
||||||
+3
-1
@@ -22,8 +22,10 @@ module.exports = {
|
|||||||
if (starting) {
|
if (starting) {
|
||||||
const local = info.local('start.js');
|
const local = info.local('start.js');
|
||||||
if (local?.url) {
|
if (local?.url) {
|
||||||
|
// Open in the system browser without changing the app's framing policy.
|
||||||
|
// This also avoids relying on Pinokio's embedded browser behavior.
|
||||||
return [
|
return [
|
||||||
{ default: true, icon: 'fa-solid fa-earth-americas', text: 'Open God\'s Eye View', href: local.url },
|
{ default: true, icon: 'fa-solid fa-earth-americas', text: 'Open God\'s Eye View', href: local.url, popout: true },
|
||||||
{ icon: 'fa-solid fa-terminal', text: 'Server', href: 'start.js' },
|
{ icon: 'fa-solid fa-terminal', text: 'Server', href: 'start.js' },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1087,5 +1087,6 @@
|
|||||||
"scripts/qa-status-badges.mjs",
|
"scripts/qa-status-badges.mjs",
|
||||||
"src/dockerTrustedPeers.test.mjs",
|
"src/dockerTrustedPeers.test.mjs",
|
||||||
"src/cameraGlobeRotation.test.mjs",
|
"src/cameraGlobeRotation.test.mjs",
|
||||||
"src/voice/globeResetConfirmation.test.mjs"
|
"src/voice/globeResetConfirmation.test.mjs",
|
||||||
|
"src/hudDms.test.mjs"
|
||||||
]
|
]
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Treat an unavailable Google Maps tile-token endpoint as a supported, silent
|
||||||
|
* outcome. Install after real token providers and before the unknown-API 404.
|
||||||
|
*/
|
||||||
|
export function googleTokenUnavailablePlugin() {
|
||||||
|
const install = (server) => {
|
||||||
|
server.middlewares.use((req, res, next) => {
|
||||||
|
const pathname = String(req.url || '').split('?')[0];
|
||||||
|
if (
|
||||||
|
(req.method !== 'GET' && req.method !== 'HEAD') ||
|
||||||
|
pathname !== '/api/google/tiles-token'
|
||||||
|
) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(204, { 'Cache-Control': 'no-store' });
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'google-token-unavailable',
|
||||||
|
configureServer: install,
|
||||||
|
configurePreviewServer: install,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { createBrowserViteConfig } from '../../build/vite.js';
|
|||||||
import { localProviderPlugins } from '../providers/local.js';
|
import { localProviderPlugins } from '../providers/local.js';
|
||||||
import { localMcpPlugin } from '../mcp/plugin.js';
|
import { localMcpPlugin } from '../mcp/plugin.js';
|
||||||
import { apiNotFoundPlugin } from './api-not-found.js';
|
import { apiNotFoundPlugin } from './api-not-found.js';
|
||||||
|
import { googleTokenUnavailablePlugin } from './google-token-unavailable.js';
|
||||||
import { standaloneVoiceTools } from './voiceTools.js';
|
import { standaloneVoiceTools } from './voiceTools.js';
|
||||||
import { unwatchedRuntimeDirsPlugin } from './unwatched-cache.js';
|
import { unwatchedRuntimeDirsPlugin } from './unwatched-cache.js';
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ export default defineConfig(({ command, mode }) => {
|
|||||||
plugins: [
|
plugins: [
|
||||||
...localProviderPlugins({ realtime: { tools }, gemini: { tools } }),
|
...localProviderPlugins({ realtime: { tools }, gemini: { tools } }),
|
||||||
localMcpPlugin(),
|
localMcpPlugin(),
|
||||||
|
googleTokenUnavailablePlugin(),
|
||||||
apiNotFoundPlugin(),
|
apiNotFoundPlugin(),
|
||||||
unwatchedRuntimeDirsPlugin(),
|
unwatchedRuntimeDirsPlugin(),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -75,4 +75,5 @@ test('the Docker install path keeps its loopback-only, launcher-owned contract',
|
|||||||
assert.match(compose, /^\s+- GEV_KEY_SETUP_TRUSTED_PEERS$/m, 'the operator policy reaches the launcher without a hard-coded address');
|
assert.match(compose, /^\s+- GEV_KEY_SETUP_TRUSTED_PEERS$/m, 'the operator policy reaches the launcher without a hard-coded address');
|
||||||
assert.match(compose, /^\s+- GEV_ALLOWED_HOSTS$/m, 'the host allowlist reaches Vite');
|
assert.match(compose, /^\s+- GEV_ALLOWED_HOSTS$/m, 'the host allowlist reaches Vite');
|
||||||
assert.doesNotMatch(compose, /network_mode/, 'the container keeps its own network namespace');
|
assert.doesNotMatch(compose, /network_mode/, 'the container keeps its own network namespace');
|
||||||
|
assert.match(compose, /^\s+- \.:\/app:z$/m, 'SELinux hosts can read the bind-mounted checkout');
|
||||||
});
|
});
|
||||||
+6
-5
@@ -574,11 +574,12 @@ export class IntelHUD {
|
|||||||
* @returns {string} Formatted DMS string, e.g. `"38°53'23.10"N"`.
|
* @returns {string} Formatted DMS string, e.g. `"38°53'23.10"N"`.
|
||||||
*/
|
*/
|
||||||
_toDMS(decimal, type) {
|
_toDMS(decimal, type) {
|
||||||
const abs = Math.abs(decimal);
|
// Round once, in hundredths of a second, so a value that rounds up to
|
||||||
const deg = Math.floor(abs);
|
// 60.00" carries into the minutes and degrees instead of printing 60.
|
||||||
const minFloat = (abs - deg) * 60;
|
const hundredths = Math.round(Math.abs(decimal) * 360000);
|
||||||
const min = Math.floor(minFloat);
|
const deg = Math.floor(hundredths / 360000);
|
||||||
const sec = ((minFloat - min) * 60).toFixed(2);
|
const min = Math.floor(hundredths / 6000) % 60;
|
||||||
|
const sec = ((hundredths % 6000) / 100).toFixed(2);
|
||||||
|
|
||||||
let dir;
|
let dir;
|
||||||
if (type === 'lat') dir = decimal >= 0 ? 'N' : 'S';
|
if (type === 'lat') dir = decimal >= 0 ? 'N' : 'S';
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
// The HUD's LAT/LON readout in degrees-minutes-seconds. Seconds are printed to
|
||||||
|
// two decimals, so a value within 0.005" of the next whole minute rounds up.
|
||||||
|
// That carry must reach the minutes (and degrees): `122°20'60.00"W` is not a
|
||||||
|
// coordinate. Ordinary decimal inputs land there through binary floating
|
||||||
|
// point: 122.35 - 122 is 0.34999999999999432, so -122.35 read 20'60.00".
|
||||||
|
//
|
||||||
|
// hud.js imports `mgrs`, a CommonJS package whose named exports Node's ESM
|
||||||
|
// loader cannot see, so this installs the same one-specifier stub as
|
||||||
|
// hudAltitudeDatum.test.mjs before importing it.
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { registerHooks } from 'node:module';
|
||||||
|
|
||||||
|
const MGRS_STUB_URL = 'gev-test-stub:mgrs';
|
||||||
|
registerHooks({
|
||||||
|
resolve(specifier, context, next) {
|
||||||
|
if (specifier === 'mgrs') return { url: MGRS_STUB_URL, shortCircuit: true };
|
||||||
|
return next(specifier, context);
|
||||||
|
},
|
||||||
|
load(url, context, next) {
|
||||||
|
if (url === MGRS_STUB_URL) {
|
||||||
|
return {
|
||||||
|
format: 'module',
|
||||||
|
shortCircuit: true,
|
||||||
|
source:
|
||||||
|
'export function forward() { return "10SEG55776339"; }\nexport default { forward };\n',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return next(url, context);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { IntelHUD } = await import('./hud.js');
|
||||||
|
|
||||||
|
const toDMS = (decimal, type) => IntelHUD.prototype._toDMS(decimal, type);
|
||||||
|
|
||||||
|
test('a rounded-up second carries into the minutes', () => {
|
||||||
|
assert.equal(toDMS(-122.35, 'lon'), `122°21'00.00"W`);
|
||||||
|
assert.equal(toDMS(139.7, 'lon'), `139°42'00.00"E`);
|
||||||
|
assert.equal(toDMS(37.0166666, 'lat'), `37°01'00.00"N`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a carry through 59 minutes reaches the degrees', () => {
|
||||||
|
assert.equal(toDMS(38.999999, 'lat'), `39°00'00.00"N`);
|
||||||
|
assert.equal(toDMS(-0.999999, 'lat'), `01°00'00.00"S`);
|
||||||
|
assert.equal(toDMS(179.999999, 'lon'), `180°00'00.00"E`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ordinary values keep their existing format', () => {
|
||||||
|
assert.equal(toDMS(38.889722, 'lat'), `38°53'23.00"N`);
|
||||||
|
assert.equal(toDMS(-77.035278, 'lon'), `077°02'07.00"W`);
|
||||||
|
assert.equal(toDMS(51.5, 'lat'), `51°30'00.00"N`);
|
||||||
|
assert.equal(toDMS(-33.8568, 'lat'), `33°51'24.48"S`);
|
||||||
|
assert.equal(toDMS(0, 'lon'), `000°00'00.00"E`);
|
||||||
|
assert.equal(toDMS(90, 'lat'), `90°00'00.00"N`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the telemetry tick paints the carried value into both readouts', () => {
|
||||||
|
const elements = new Map(
|
||||||
|
[
|
||||||
|
'hud-alt',
|
||||||
|
'hud-summary',
|
||||||
|
'hud-mgrs',
|
||||||
|
'hud-latlon',
|
||||||
|
'hud-bottom-line',
|
||||||
|
'hud-gsd',
|
||||||
|
'hud-coll',
|
||||||
|
'hud-ona',
|
||||||
|
'hud-mode',
|
||||||
|
].map((id) => [id, { textContent: '' }]),
|
||||||
|
);
|
||||||
|
const previousDocument = globalThis.document;
|
||||||
|
globalThis.document = {
|
||||||
|
getElementById: (id) => elements.get(id) ?? null,
|
||||||
|
querySelectorAll: () => [],
|
||||||
|
addEventListener() {},
|
||||||
|
removeEventListener() {},
|
||||||
|
};
|
||||||
|
const viewer = {
|
||||||
|
camera: {
|
||||||
|
pitch: -Math.PI / 2,
|
||||||
|
positionCartographic: {
|
||||||
|
latitude: (37.75 * Math.PI) / 180,
|
||||||
|
longitude: (-122.35 * Math.PI) / 180,
|
||||||
|
height: 1500,
|
||||||
|
},
|
||||||
|
computeViewRectangle: () => undefined,
|
||||||
|
moveStart: { addEventListener() {}, removeEventListener() {} },
|
||||||
|
moveEnd: { addEventListener() {}, removeEventListener() {} },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let hud;
|
||||||
|
try {
|
||||||
|
hud = new IntelHUD(viewer);
|
||||||
|
hud._updateCameraData();
|
||||||
|
const latlon = elements.get('hud-latlon').textContent;
|
||||||
|
const bottom = elements.get('hud-bottom-line').textContent;
|
||||||
|
assert.equal(latlon, `37°45'00.00"N 122°21'00.00"W`);
|
||||||
|
assert.match(bottom, /LON: 122°21'00\.00"W/);
|
||||||
|
assert.doesNotMatch(`${latlon} ${bottom}`, /60\.00"/);
|
||||||
|
} finally {
|
||||||
|
hud?.destroy();
|
||||||
|
if (previousDocument === undefined) delete globalThis.document;
|
||||||
|
else globalThis.document = previousDocument;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -14,6 +14,8 @@ const GLOBAL_TILE_MAXIMUM_LEVEL = 3;
|
|||||||
// Six decoded 2048×1024 canvases (8 MiB each) bound the cache below 50 MiB.
|
// Six decoded 2048×1024 canvases (8 MiB each) bound the cache below 50 MiB.
|
||||||
const MAX_MOSAICS = 6;
|
const MAX_MOSAICS = 6;
|
||||||
const MAX_PREFETCH_TILES = 8;
|
const MAX_PREFETCH_TILES = 8;
|
||||||
|
// How often a staged frame waiting out its quiet interval rechecks readiness.
|
||||||
|
export const READY_WAKE_MS = 150;
|
||||||
|
|
||||||
/** Globe imagery: own at most a displayed and a staging frame. Use native Cesium
|
/** Globe imagery: own at most a displayed and a staging frame. Use native Cesium
|
||||||
* tile scheduling, projection and texture disposal; the application clock is never touched. */
|
* tile scheduling, projection and texture disposal; the application clock is never touched. */
|
||||||
@@ -126,6 +128,7 @@ function createGlobeRendering({
|
|||||||
frame.offRender?.();
|
frame.offRender?.();
|
||||||
frame.offCamera?.();
|
frame.offCamera?.();
|
||||||
clearTimeout(frame.timeout);
|
clearTimeout(frame.timeout);
|
||||||
|
clearTimeout(frame.wake);
|
||||||
for (const request of frame.requests) request.cancel?.();
|
for (const request of frame.requests) request.cancel?.();
|
||||||
frame.requests.clear();
|
frame.requests.clear();
|
||||||
frame.retries.clear();
|
frame.retries.clear();
|
||||||
@@ -277,6 +280,8 @@ function createGlobeRendering({
|
|||||||
frame.offCamera?.();
|
frame.offCamera?.();
|
||||||
frame.offCamera = null;
|
frame.offCamera = null;
|
||||||
clearTimeout(frame.timeout);
|
clearTimeout(frame.timeout);
|
||||||
|
clearTimeout(frame.wake);
|
||||||
|
frame.wake = null;
|
||||||
frame.offAbort?.();
|
frame.offAbort?.();
|
||||||
if (ok) {
|
if (ok) {
|
||||||
lastError = null;
|
lastError = null;
|
||||||
@@ -450,8 +455,20 @@ function createGlobeRendering({
|
|||||||
if (++settled >= 2) finish(true);
|
if (++settled >= 2) finish(true);
|
||||||
else viewer.scene.requestRender();
|
else viewer.scene.requestRender();
|
||||||
} else settled = 0;
|
} else settled = 0;
|
||||||
if (incoming === frame && frame.pending === 0 && frame.loaded > 0)
|
// Waiting out the quiet interval needs another readiness check
|
||||||
|
// later, not a render every frame: wake on a timer so the render
|
||||||
|
// governor's frame cap still holds while a frame is staged.
|
||||||
|
if (
|
||||||
|
incoming === frame &&
|
||||||
|
frame.pending === 0 &&
|
||||||
|
frame.loaded > 0 &&
|
||||||
|
!frame.wake
|
||||||
|
)
|
||||||
|
frame.wake = setTimeout(() => {
|
||||||
|
frame.wake = null;
|
||||||
|
if (!frame.closed && incoming === frame)
|
||||||
viewer.scene.requestRender();
|
viewer.scene.requestRender();
|
||||||
|
}, READY_WAKE_MS);
|
||||||
});
|
});
|
||||||
viewer.scene.requestRender();
|
viewer.scene.requestRender();
|
||||||
onChange();
|
onChange();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { createWeatherRendering } from './rendering.js';
|
import { createWeatherRendering, READY_WAKE_MS } from './rendering.js';
|
||||||
import { createWeatherLayer } from './index.js';
|
import { createWeatherLayer } from './index.js';
|
||||||
import { createWeatherClock } from './clock.js';
|
import { createWeatherClock } from './clock.js';
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
@@ -242,6 +242,50 @@ test('a ready weather frame does not wait for unrelated terrain, but requires su
|
|||||||
h.rendering.clear();
|
h.rendering.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a staged frame waiting out its quiet interval wakes on a timer, not every render', async (t) => {
|
||||||
|
t.mock.timers.enable({ apis: ['setTimeout'] });
|
||||||
|
let now = 0;
|
||||||
|
const h = renderingHarness({ now: () => now });
|
||||||
|
h.viewer.scene.globe.tilesLoaded = false;
|
||||||
|
const loaded = h.rendering.setFrame(snapshot, times[0]);
|
||||||
|
const tile = deferred();
|
||||||
|
h.providers[0].response = tile;
|
||||||
|
const request = h.providers[0].requestImage(0, 0, 0, {});
|
||||||
|
tile.resolve({});
|
||||||
|
await request;
|
||||||
|
const before = h.renders();
|
||||||
|
for (let i = 0; i < 10; i++) h.postRender.emit();
|
||||||
|
assert.equal(
|
||||||
|
h.renders(),
|
||||||
|
before,
|
||||||
|
'renders while waiting do not request further renders',
|
||||||
|
);
|
||||||
|
t.mock.timers.tick(READY_WAKE_MS - 1);
|
||||||
|
assert.equal(h.renders(), before);
|
||||||
|
t.mock.timers.tick(1);
|
||||||
|
assert.equal(h.renders(), before + 1, 'one wake per interval');
|
||||||
|
now = 200;
|
||||||
|
h.postRender.emit();
|
||||||
|
h.postRender.emit();
|
||||||
|
assert.equal(await loaded, true);
|
||||||
|
const settledRenders = h.renders();
|
||||||
|
t.mock.timers.tick(READY_WAKE_MS * 4);
|
||||||
|
assert.equal(h.renders(), settledRenders, 'commit clears the wake timer');
|
||||||
|
|
||||||
|
const abandoned = h.rendering.setFrame(snapshot, times[1]);
|
||||||
|
const second = deferred();
|
||||||
|
h.providers.at(-1).response = second;
|
||||||
|
const secondRequest = h.providers.at(-1).requestImage(0, 0, 0, {});
|
||||||
|
second.resolve({});
|
||||||
|
await secondRequest;
|
||||||
|
h.postRender.emit();
|
||||||
|
h.rendering.clear();
|
||||||
|
assert.equal(await abandoned, false);
|
||||||
|
const clearedRenders = h.renders();
|
||||||
|
t.mock.timers.tick(READY_WAKE_MS * 4);
|
||||||
|
assert.equal(h.renders(), clearedRenders, 'clear cancels the wake timer');
|
||||||
|
});
|
||||||
|
|
||||||
test('camera movement drops abandoned deferred tiles but still waits for admitted requests', async () => {
|
test('camera movement drops abandoned deferred tiles but still waits for admitted requests', async () => {
|
||||||
let now = 0;
|
let now = 0;
|
||||||
const h = renderingHarness({ now: () => now });
|
const h = renderingHarness({ now: () => now });
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export function createGoogleTokenSource({
|
|||||||
headers: { Accept: 'application/json' },
|
headers: { Accept: 'application/json' },
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
});
|
});
|
||||||
|
if (response.status === 204) return null;
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
await response.body?.cancel();
|
await response.body?.cancel();
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -79,3 +79,12 @@ test('a server that does not offer tokens yields none', async () => {
|
|||||||
assert.equal(await createGoogleTokenSource({ fetchImpl }).token(), null);
|
assert.equal(await createGoogleTokenSource({ fetchImpl }).token(), null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('an empty 204 response explicitly means no Google token is available', async () => {
|
||||||
|
const { calls, fetchImpl } = server([
|
||||||
|
new Response(null, { status: 204, headers: { 'Cache-Control': 'no-store' } }),
|
||||||
|
]);
|
||||||
|
const tokens = createGoogleTokenSource({ fetchImpl });
|
||||||
|
assert.equal(await tokens.token(), null);
|
||||||
|
assert.deepEqual(calls, [GOOGLE_TOKEN_PATH]);
|
||||||
|
});
|
||||||
@@ -113,6 +113,9 @@ test('Pinokio menu resolves the nested install marker and exposes each lifecycle
|
|||||||
runtime.url = url;
|
runtime.url = url;
|
||||||
const items = await menu(kernel, info);
|
const items = await menu(kernel, info);
|
||||||
assert.equal(items.filter((item) => item.default).length, 1);
|
assert.equal(items.filter((item) => item.default).length, 1);
|
||||||
|
const ready = installed && running === 'start.js' && Boolean(url);
|
||||||
|
assert.equal(items[0].popout, ready ? true : undefined);
|
||||||
|
assert.ok(items.slice(1).every((item) => item.popout === undefined));
|
||||||
return items.map(({ text, href, default: isDefault = false }) => ({ text, href, default: isDefault }));
|
return items.map(({ text, href, default: isDefault = false }) => ({ text, href, default: isDefault }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { createServer } from 'node:http';
|
||||||
|
import { once } from 'node:events';
|
||||||
|
import { createGoogleTokenSource } from '../maps/googleTokens.js';
|
||||||
|
import { googleTokenUnavailablePlugin } from '../../server/standalone/google-token-unavailable.js';
|
||||||
|
|
||||||
|
async function withServer({ beforeFallback } = {}, check) {
|
||||||
|
const middlewares = [];
|
||||||
|
const app = {
|
||||||
|
middlewares: {
|
||||||
|
use: (handler) => middlewares.push(handler),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (beforeFallback) app.middlewares.use(beforeFallback);
|
||||||
|
googleTokenUnavailablePlugin().configureServer(app);
|
||||||
|
app.middlewares.use((_req, res) => {
|
||||||
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: 'Unknown API route' }));
|
||||||
|
});
|
||||||
|
|
||||||
|
const server = createServer((req, res) => {
|
||||||
|
let index = 0;
|
||||||
|
const next = () => middlewares[index++]?.(req, res, next);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
server.listen(0, '127.0.0.1');
|
||||||
|
await once(server, 'listening');
|
||||||
|
const address = server.address();
|
||||||
|
const url = 'http://127.0.0.1:' + address.port;
|
||||||
|
try {
|
||||||
|
await check(url);
|
||||||
|
} finally {
|
||||||
|
server.close();
|
||||||
|
await once(server, 'close');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('keyless GET and HEAD return uncached 204 instead of 404', async () => {
|
||||||
|
await withServer({}, async (url) => {
|
||||||
|
for (const method of ['GET', 'HEAD']) {
|
||||||
|
const response = await fetch(url + '/api/google/tiles-token?probe=1', {
|
||||||
|
method,
|
||||||
|
});
|
||||||
|
assert.equal(response.status, 204);
|
||||||
|
assert.equal(response.headers.get('cache-control'), 'no-store');
|
||||||
|
assert.equal(await response.text(), '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the existing client treats unavailable tokens as null', async () => {
|
||||||
|
await withServer({}, async (url) => {
|
||||||
|
const source = createGoogleTokenSource({
|
||||||
|
fetchImpl: (path, options) => fetch(new URL(path, url), options),
|
||||||
|
});
|
||||||
|
assert.equal(await source.token(), null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unrelated API paths and unsupported methods remain 404', async () => {
|
||||||
|
await withServer({}, async (url) => {
|
||||||
|
for (const [path, method] of [
|
||||||
|
['/api/google/tiles-token/extra', 'GET'],
|
||||||
|
['/api/not-a-token', 'GET'],
|
||||||
|
['/api/google/tiles-token', 'POST'],
|
||||||
|
]) {
|
||||||
|
const response = await fetch(new URL(path, url), { method });
|
||||||
|
assert.equal(response.status, 404);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a real token provider registered first still wins', async () => {
|
||||||
|
await withServer(
|
||||||
|
{
|
||||||
|
beforeFallback: (req, res, next) => {
|
||||||
|
if (req.url !== '/api/google/tiles-token') return next();
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(
|
||||||
|
JSON.stringify({
|
||||||
|
accessToken: 'server-token',
|
||||||
|
expiresAt: 10000000000000,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async (url) => {
|
||||||
|
const response = await fetch(url + '/api/google/tiles-token');
|
||||||
|
assert.equal(response.status, 200);
|
||||||
|
assert.equal((await response.json()).accessToken, 'server-token');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
@@ -5,7 +6,11 @@ import { rm, writeFile } from 'node:fs/promises';
|
|||||||
import { request } from 'node:http';
|
import { request } from 'node:http';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { build, createServer, preview } from 'vite';
|
import { build, createServer, preview } from 'vite';
|
||||||
import { BROWSER_CSP, createBrowserViteConfig } from '../../build/vite.js';
|
import {
|
||||||
|
BROWSER_CSP,
|
||||||
|
createBrowserViteConfig,
|
||||||
|
escapeFsDenyPath,
|
||||||
|
} from '../../build/vite.js';
|
||||||
import standaloneConfig, * as compatibility from '../../vite.config.js';
|
import standaloneConfig, * as compatibility from '../../vite.config.js';
|
||||||
import * as providers from '../../server/providers/local.js';
|
import * as providers from '../../server/providers/local.js';
|
||||||
import { apiNotFoundPlugin } from '../../server/standalone/api-not-found.js';
|
import { apiNotFoundPlugin } from '../../server/standalone/api-not-found.js';
|
||||||
@@ -30,6 +35,20 @@ test('explicit build inputs preserve browser-only defines, plugin order and loop
|
|||||||
assert.deepEqual(config.server.allowedHosts, ['localhost', '127.0.0.1']);
|
assert.deepEqual(config.server.allowedHosts, ['localhost', '127.0.0.1']);
|
||||||
assert.ok(config.server.fs.deny.includes('**/ENVIRONMENT'));
|
assert.ok(config.server.fs.deny.includes('**/ENVIRONMENT'));
|
||||||
assert.ok(config.server.fs.deny.includes('.env.*'));
|
assert.ok(config.server.fs.deny.includes('.env.*'));
|
||||||
|
assert.ok(config.server.fs.deny.includes('**/.gev-logs/**'));
|
||||||
|
assert.ok(config.server.fs.deny.includes('**/.gev-cache/**'));
|
||||||
|
const root = fileURLToPath(new URL('../../', import.meta.url))
|
||||||
|
.replace(/\\/g, '/')
|
||||||
|
.replace(/\/$/, '');
|
||||||
|
assert.deepEqual(
|
||||||
|
config.server.fs.deny.filter((pattern) => pattern.endsWith('/dist/**')),
|
||||||
|
[`${escapeFsDenyPath(root)}/dist/**`],
|
||||||
|
'only project build output is denied, not dependency dist directories',
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
escapeFsDenyPath('C:/Users/a/project (1) [test]{copy}'),
|
||||||
|
'C:/Users/a/project \\(1\\) \\[test\\]\\{copy\\}',
|
||||||
|
);
|
||||||
assert.equal(config.server.headers['X-Frame-Options'], 'DENY');
|
assert.equal(config.server.headers['X-Frame-Options'], 'DENY');
|
||||||
assert.equal(config.server.headers['X-Content-Type-Options'], 'nosniff');
|
assert.equal(config.server.headers['X-Content-Type-Options'], 'nosniff');
|
||||||
const csp = config.server.headers['Content-Security-Policy'];
|
const csp = config.server.headers['Content-Security-Policy'];
|
||||||
@@ -98,7 +117,7 @@ test('root config retains existing named exports and standalone provider order',
|
|||||||
assert.equal(compatibility[name], value, name);
|
assert.equal(compatibility[name], value, name);
|
||||||
const config = standaloneConfig({ mode: 'test' });
|
const config = standaloneConfig({ mode: 'test' });
|
||||||
const standaloneProviders = config.plugins
|
const standaloneProviders = config.plugins
|
||||||
.slice(4, -5)
|
.slice(4, -6)
|
||||||
.map((plugin) => plugin.name);
|
.map((plugin) => plugin.name);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
standaloneProviders.filter((name) => name === 'gemini-live-proxy').length,
|
standaloneProviders.filter((name) => name === 'gemini-live-proxy').length,
|
||||||
@@ -112,9 +131,10 @@ test('root config retains existing named exports and standalone provider order',
|
|||||||
standaloneProviders.filter((name) => name !== 'gemini-live-proxy'),
|
standaloneProviders.filter((name) => name !== 'gemini-live-proxy'),
|
||||||
providers.localProviderPlugins().map((plugin) => plugin.name),
|
providers.localProviderPlugins().map((plugin) => plugin.name),
|
||||||
);
|
);
|
||||||
assert.equal(config.plugins.at(-6).name, 'gev-key-setup');
|
assert.equal(config.plugins.at(-7).name, 'gev-key-setup');
|
||||||
// The local MCP route follows every provider and precedes the API fallback.
|
// Token fallback follows real providers and local MCP before the API 404.
|
||||||
assert.equal(config.plugins.at(-5).name, 'local-mcp');
|
assert.equal(config.plugins.at(-6).name, 'local-mcp');
|
||||||
|
assert.equal(config.plugins.at(-5).name, 'google-token-unavailable');
|
||||||
assert.equal(config.plugins.at(-4).name, 'api-not-found');
|
assert.equal(config.plugins.at(-4).name, 'api-not-found');
|
||||||
// Config-only, so it sits after the provider slice rather than in it.
|
// Config-only, so it sits after the provider slice rather than in it.
|
||||||
assert.equal(config.plugins.at(-3).name, 'gev-unwatched-runtime-dirs');
|
assert.equal(config.plugins.at(-3).name, 'gev-unwatched-runtime-dirs');
|
||||||
|
|||||||
Reference in new issue
Block a user