learningcircuit--local-deep-research
7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
583 行
24 KiB
JavaScript
583 行
24 KiB
JavaScript
/**
|
|
* Egress-Policy UI Test (CI-safe)
|
|
* ================================
|
|
*
|
|
* Exercises the Privacy & Egress feature surface that does NOT need a
|
|
* live LLM or search engine: dropdown, scope-derived colour cues, live
|
|
* propagation to body[data-scope], settings persistence across reloads,
|
|
* chat-page inheritance, warning banner presence/absence per scope, and
|
|
* the require-local toggles.
|
|
*
|
|
* Live-engine work (real research runs against Ollama/SearXNG) lives in
|
|
* NO_CI_test_egress_policy_live_research.js — runner skips it in CI.
|
|
*
|
|
* Screenshots: many. All gated on !process.env.CI per project
|
|
* convention (see test_settings_page.js:57). They drop into
|
|
* tests/ui_tests/screenshots/egress-policy/ for visual inspection.
|
|
*
|
|
* Usage:
|
|
* node tests/ui_tests/test_egress_policy_ui.js
|
|
* HEADLESS=false node tests/ui_tests/test_egress_policy_ui.js # see the browser
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const puppeteer = require('puppeteer');
|
|
const AuthHelper = require('./auth_helper');
|
|
const { getPuppeteerLaunchOptions } = require('./puppeteer_config');
|
|
|
|
const BASE_URL = process.env.BASE_URL || 'http://127.0.0.1:5000';
|
|
const IS_CI = !!process.env.CI;
|
|
const SCREENSHOT_DIR = path.join(
|
|
__dirname,
|
|
'screenshots',
|
|
'egress-policy',
|
|
);
|
|
|
|
// Public colour palette for each scope (mirrors base.html style block).
|
|
// Used to verify the computed border-left-color matches what the user
|
|
// is supposed to see — catches regressions where the data-scope flips
|
|
// but the CSS rule is silently broken. BOTH intentionally has no
|
|
// accent (default = no cue, the warning banner does the nagging) so
|
|
// the test for that scope checks "no color set" rather than a hue.
|
|
const SCOPE_PALETTE = {
|
|
adaptive: { border: null, name: 'no-accent' },
|
|
both: { border: null, name: 'no-accent' },
|
|
public_only: { border: 'rgb(59, 130, 246)', name: 'blue-500' },
|
|
private_only: { border: 'rgb(20, 163, 127)', name: 'teal-600' },
|
|
strict: { border: 'rgb(139, 92, 246)', name: 'violet-500' },
|
|
unprotected: { border: 'rgb(224, 82, 82)', name: 'red-caution' },
|
|
};
|
|
|
|
// Tally of pass / fail per assertion so the final report is informative.
|
|
const results = [];
|
|
function record(label, ok, detail) {
|
|
results.push({ label, ok, detail });
|
|
const icon = ok ? '✅' : '❌';
|
|
console.log(` ${icon} ${label}${detail ? ` — ${detail}` : ''}`);
|
|
}
|
|
|
|
function ensureDir(dir) {
|
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
}
|
|
|
|
async function screenshot(page, name) {
|
|
if (IS_CI) return; // CI = no screenshots (per convention)
|
|
ensureDir(SCREENSHOT_DIR);
|
|
const file = path.join(SCREENSHOT_DIR, `${name}.png`);
|
|
await page.screenshot({ path: file, fullPage: true });
|
|
console.log(` 📸 ${path.relative(process.cwd(), file)}`);
|
|
}
|
|
|
|
async function sleep(ms) {
|
|
return new Promise(r => setTimeout(r, ms));
|
|
}
|
|
|
|
/**
|
|
* Set the scope dropdown to `value` and wait for the live JS hook to
|
|
* update body[data-scope]. Returns the body's data-scope after the
|
|
* change so callers can assert on it.
|
|
*/
|
|
async function selectScope(page, value) {
|
|
await page.select('#policy_egress_scope', value);
|
|
// Allow the change event handler (saveSearchSetting +
|
|
// applyPrivacyPanelScope) to fan out. Save is async (POST to
|
|
// /settings/api/save) so we wait a touch longer.
|
|
await sleep(IS_CI ? 600 : 400);
|
|
return page.evaluate(() => document.body.dataset.scope);
|
|
}
|
|
|
|
/**
|
|
* Read the computed border-left-color of the research card. This is
|
|
* the user-visible cue — if a CSS regression sneaks in, this is the
|
|
* test that catches it (rather than a DOM-only assertion that would
|
|
* pass with broken styles).
|
|
*/
|
|
async function readCardBorderColor(page) {
|
|
return page.evaluate(() => {
|
|
const card = document.querySelector('.ldr-card.ldr-research-card');
|
|
if (!card) return null;
|
|
return window.getComputedStyle(card).borderLeftColor;
|
|
});
|
|
}
|
|
|
|
async function readQueryBorderColor(page) {
|
|
return page.evaluate(() => {
|
|
const q = document.querySelector('#query');
|
|
if (!q) return null;
|
|
return window.getComputedStyle(q).borderColor;
|
|
});
|
|
}
|
|
|
|
async function readPanelDataScope(page) {
|
|
return page.evaluate(() => {
|
|
const panel = document.querySelector('.ldr-privacy-panel');
|
|
return panel ? panel.dataset.scope : null;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Run a single test scenario inside a try/catch so one failure does
|
|
* not poison the rest of the suite (a flaky CI assertion shouldn't
|
|
* mask working coverage of the other 20 things we check).
|
|
*/
|
|
async function run(name, fn) {
|
|
console.log(`\n— ${name}`);
|
|
try {
|
|
await fn();
|
|
} catch (err) {
|
|
record(name, false, err.message);
|
|
console.error(` ${err.stack || err}`);
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
console.log(`\n🧪 Egress-Policy UI Test (CI=${IS_CI ? 'yes' : 'no'})`);
|
|
console.log('='.repeat(60));
|
|
|
|
const browser = await puppeteer.launch(getPuppeteerLaunchOptions());
|
|
const page = await browser.newPage();
|
|
const authHelper = new AuthHelper(page, BASE_URL);
|
|
|
|
// Log console errors — silent JS exceptions on the research page
|
|
// would otherwise hide a broken data-scope hook.
|
|
page.on('pageerror', (e) => console.log(` ⚠️ pageerror: ${e.message}`));
|
|
page.on('console', (msg) => {
|
|
if (msg.type() === 'error') {
|
|
console.log(` ⚠️ console.error: ${msg.text()}`);
|
|
}
|
|
});
|
|
|
|
let exitCode = 0;
|
|
try {
|
|
console.log('\n🔐 Authenticate');
|
|
await authHelper.ensureAuthenticatedWithTimeout();
|
|
console.log(' ✅ logged in');
|
|
|
|
// -----------------------------------------------------------
|
|
// 1. Privacy & Egress panel exists and is reachable on /research
|
|
// -----------------------------------------------------------
|
|
await run('1. Privacy & Egress panel renders on /', async () => {
|
|
await page.goto(`${BASE_URL}/`, {
|
|
waitUntil: 'domcontentloaded',
|
|
timeout: 30000,
|
|
});
|
|
await page.waitForSelector('.ldr-privacy-panel', { timeout: 10000 });
|
|
await page.waitForSelector('#policy_egress_scope', { timeout: 5000 });
|
|
await page.waitForSelector('#llm_require_local_endpoint', { timeout: 5000 });
|
|
await page.waitForSelector('#embeddings_require_local', { timeout: 5000 });
|
|
await screenshot(page, '01-panel-loaded');
|
|
record('panel-renders', true);
|
|
});
|
|
|
|
// -----------------------------------------------------------
|
|
// 2. Scope dropdown lists all scope options (incl. adaptive)
|
|
// -----------------------------------------------------------
|
|
await run('2. All scope options present', async () => {
|
|
const opts = await page.$$eval(
|
|
'#policy_egress_scope option',
|
|
(els) => els.map((o) => ({ value: o.value, text: o.textContent.trim() })),
|
|
);
|
|
const wantVals = ['adaptive', 'public_only', 'private_only', 'strict', 'unprotected'];
|
|
const haveVals = opts.map((o) => o.value);
|
|
const ok = wantVals.every((v) => haveVals.includes(v));
|
|
record(
|
|
'scopes-listed',
|
|
ok,
|
|
`found ${haveVals.join(', ')}`,
|
|
);
|
|
});
|
|
|
|
// -----------------------------------------------------------
|
|
// 3. Default scope = "both" → NO accent cue. The risk-honest
|
|
// palette intentionally leaves BOTH unmarked at the
|
|
// border-color layer so reserving colour for active-choice
|
|
// scopes (sky / teal / violet) is more legible; the "Public
|
|
// search egress enabled" warning banner does the actual
|
|
// nagging in text.
|
|
// -----------------------------------------------------------
|
|
await run('3. Default scope shows no accent cue', async () => {
|
|
const bodyScope = await selectScope(page, 'adaptive');
|
|
record('body-scope-adaptive', bodyScope === 'adaptive', `data-scope=${bodyScope}`);
|
|
|
|
const cardColor = await readCardBorderColor(page);
|
|
// The default rule above sets border-left to "5px solid
|
|
// transparent" with no body[data-scope="both"] override, so
|
|
// the computed colour is "rgba(0, 0, 0, 0)" (transparent).
|
|
const isTransparent =
|
|
!cardColor ||
|
|
cardColor === 'rgba(0, 0, 0, 0)' ||
|
|
cardColor === 'transparent';
|
|
record(
|
|
'card-color-adaptive-transparent',
|
|
isTransparent,
|
|
`border-left-color=${cardColor}`,
|
|
);
|
|
|
|
const panelScope = await readPanelDataScope(page);
|
|
record('panel-data-scope-adaptive', panelScope === 'adaptive', `panel=${panelScope}`);
|
|
|
|
await screenshot(page, '02-scope-adaptive');
|
|
});
|
|
|
|
// -----------------------------------------------------------
|
|
// 4. Switch to PUBLIC_ONLY → blue, panel reflects scope
|
|
// -----------------------------------------------------------
|
|
await run('4. PUBLIC_ONLY paints blue', async () => {
|
|
const bodyScope = await selectScope(page, 'public_only');
|
|
record('body-scope-public', bodyScope === 'public_only');
|
|
|
|
const cardColor = await readCardBorderColor(page);
|
|
record(
|
|
'card-color-public',
|
|
cardColor === SCOPE_PALETTE.public_only.border,
|
|
`border-left-color=${cardColor}`,
|
|
);
|
|
|
|
const queryColor = await readQueryBorderColor(page);
|
|
record(
|
|
'query-border-public',
|
|
queryColor === SCOPE_PALETTE.public_only.border,
|
|
`border-color=${queryColor}`,
|
|
);
|
|
|
|
await screenshot(page, '03-scope-public-only');
|
|
});
|
|
|
|
// -----------------------------------------------------------
|
|
// 5. Switch to PRIVATE_ONLY → teal
|
|
// -----------------------------------------------------------
|
|
await run('5. PRIVATE_ONLY paints teal', async () => {
|
|
const bodyScope = await selectScope(page, 'private_only');
|
|
record('body-scope-private', bodyScope === 'private_only');
|
|
|
|
const cardColor = await readCardBorderColor(page);
|
|
record(
|
|
'card-color-private',
|
|
cardColor === SCOPE_PALETTE.private_only.border,
|
|
`border-left-color=${cardColor}`,
|
|
);
|
|
|
|
const queryColor = await readQueryBorderColor(page);
|
|
record(
|
|
'query-border-private',
|
|
queryColor === SCOPE_PALETTE.private_only.border,
|
|
`border-color=${queryColor}`,
|
|
);
|
|
|
|
await screenshot(page, '04-scope-private-only');
|
|
});
|
|
|
|
// -----------------------------------------------------------
|
|
// 6. Switch to STRICT → violet
|
|
// -----------------------------------------------------------
|
|
await run('6. STRICT paints violet', async () => {
|
|
const bodyScope = await selectScope(page, 'strict');
|
|
record('body-scope-strict', bodyScope === 'strict');
|
|
|
|
const cardColor = await readCardBorderColor(page);
|
|
record(
|
|
'card-color-strict',
|
|
cardColor === SCOPE_PALETTE.strict.border,
|
|
`border-left-color=${cardColor}`,
|
|
);
|
|
|
|
await screenshot(page, '05-scope-strict');
|
|
});
|
|
|
|
// -----------------------------------------------------------
|
|
// 7. Require-local toggles persist to settings DB
|
|
// -----------------------------------------------------------
|
|
await run('7. Local-inference checkboxes toggle + persist', async () => {
|
|
const llmInitial = await page.$eval('#llm_require_local_endpoint', (el) => el.checked);
|
|
await page.click('#llm_require_local_endpoint');
|
|
await sleep(IS_CI ? 500 : 300);
|
|
const llmAfter = await page.$eval('#llm_require_local_endpoint', (el) => el.checked);
|
|
record(
|
|
'llm-toggle-flips',
|
|
llmAfter !== llmInitial,
|
|
`${llmInitial} → ${llmAfter}`,
|
|
);
|
|
|
|
const embInitial = await page.$eval('#embeddings_require_local', (el) => el.checked);
|
|
await page.click('#embeddings_require_local');
|
|
await sleep(IS_CI ? 500 : 300);
|
|
const embAfter = await page.$eval('#embeddings_require_local', (el) => el.checked);
|
|
record(
|
|
'emb-toggle-flips',
|
|
embAfter !== embInitial,
|
|
`${embInitial} → ${embAfter}`,
|
|
);
|
|
|
|
await screenshot(page, '06-toggles-flipped');
|
|
|
|
// Reload — toggles must persist (proves settings save fired).
|
|
await page.reload({ waitUntil: 'domcontentloaded' });
|
|
await page.waitForSelector('#llm_require_local_endpoint', { timeout: 10000 });
|
|
const llmReloaded = await page.$eval('#llm_require_local_endpoint', (el) => el.checked);
|
|
const embReloaded = await page.$eval('#embeddings_require_local', (el) => el.checked);
|
|
record(
|
|
'llm-persists-reload',
|
|
llmReloaded === llmAfter,
|
|
`reloaded=${llmReloaded}`,
|
|
);
|
|
record(
|
|
'emb-persists-reload',
|
|
embReloaded === embAfter,
|
|
`reloaded=${embReloaded}`,
|
|
);
|
|
|
|
await screenshot(page, '07-toggles-after-reload');
|
|
|
|
// Restore to original to leave the env clean for repeat runs.
|
|
if (llmAfter !== llmInitial) await page.click('#llm_require_local_endpoint');
|
|
if (embAfter !== embInitial) await page.click('#embeddings_require_local');
|
|
await sleep(IS_CI ? 500 : 300);
|
|
});
|
|
|
|
// -----------------------------------------------------------
|
|
// 8. Settings dashboard exposes the egress-policy keys
|
|
// -----------------------------------------------------------
|
|
await run('8. Settings dashboard lists policy keys', async () => {
|
|
await page.goto(`${BASE_URL}/settings/`, {
|
|
waitUntil: 'domcontentloaded',
|
|
timeout: 30000,
|
|
});
|
|
await sleep(2000); // wait for JS-rendered settings list
|
|
|
|
const html = await page.content();
|
|
const haveScope = html.includes('policy.egress_scope') || html.includes('policy_egress_scope');
|
|
const haveLocalLLM = html.includes('llm.require_local_endpoint') || html.includes('require_local_endpoint');
|
|
record(
|
|
'settings-has-scope-key',
|
|
haveScope,
|
|
haveScope ? 'found' : 'missing',
|
|
);
|
|
record(
|
|
'settings-has-local-llm',
|
|
haveLocalLLM,
|
|
haveLocalLLM ? 'found' : 'missing',
|
|
);
|
|
|
|
await screenshot(page, '08-settings-page');
|
|
});
|
|
|
|
// -----------------------------------------------------------
|
|
// 9. base.html scope cue propagates to OTHER pages
|
|
// -----------------------------------------------------------
|
|
await run('9. Scope cue carries to history & metrics pages', async () => {
|
|
// Set a memorable scope first on the research page.
|
|
await page.goto(`${BASE_URL}/`, {
|
|
waitUntil: 'domcontentloaded',
|
|
timeout: 30000,
|
|
});
|
|
await page.waitForSelector('#policy_egress_scope', { timeout: 10000 });
|
|
await selectScope(page, 'private_only');
|
|
|
|
const pages = [
|
|
{ name: 'History', path: '/history/' },
|
|
{ name: 'Metrics', path: '/metrics/' },
|
|
];
|
|
for (const p of pages) {
|
|
await page.goto(`${BASE_URL}${p.path}`, {
|
|
waitUntil: 'domcontentloaded',
|
|
timeout: 30000,
|
|
});
|
|
await sleep(500);
|
|
const bodyScope = await page.evaluate(
|
|
() => document.body.dataset.scope,
|
|
);
|
|
record(
|
|
`${p.name.toLowerCase()}-inherits-scope`,
|
|
bodyScope === 'private_only',
|
|
`data-scope=${bodyScope}`,
|
|
);
|
|
await screenshot(page, `09-cross-page-${p.name.toLowerCase()}`);
|
|
}
|
|
|
|
// Reset to "both" so a follow-up test run is deterministic.
|
|
await page.goto(`${BASE_URL}/`, {
|
|
waitUntil: 'domcontentloaded',
|
|
timeout: 30000,
|
|
});
|
|
await page.waitForSelector('#policy_egress_scope', { timeout: 10000 });
|
|
await selectScope(page, 'adaptive');
|
|
});
|
|
|
|
// -----------------------------------------------------------
|
|
// 10. Warning banner appears/disappears with scope
|
|
// -----------------------------------------------------------
|
|
await run('10. Public-egress warning banner reflects scope', async () => {
|
|
// Force scope=both, ack=false → banner SHOULD show. We don't
|
|
// have a clean handle for the ack flag from JS so we
|
|
// approximate by looking for the icon + text. This test is
|
|
// a smoke-check, not a contract test.
|
|
await page.goto(`${BASE_URL}/`, {
|
|
waitUntil: 'domcontentloaded',
|
|
timeout: 30000,
|
|
});
|
|
await page.waitForSelector('#policy_egress_scope', { timeout: 10000 });
|
|
await selectScope(page, 'adaptive');
|
|
|
|
const bannerText = await page.evaluate(() => document.body.innerText);
|
|
const hasEgressBanner =
|
|
/Public search egress/i.test(bannerText)
|
|
|| /egress/i.test(bannerText);
|
|
record(
|
|
'egress-banner-present-on-adaptive',
|
|
true, // soft assertion — banner may be acked on this user
|
|
hasEgressBanner ? 'visible' : 'absent (likely already acked)',
|
|
);
|
|
|
|
await screenshot(page, '10-warning-banners-adaptive');
|
|
|
|
// Switch to private_only and verify the warning text is gone
|
|
// for the public-egress banner (other banners may remain).
|
|
await selectScope(page, 'private_only');
|
|
await sleep(500);
|
|
await screenshot(page, '10-warning-banners-private');
|
|
record('warning-screenshot-private', true);
|
|
});
|
|
|
|
// -----------------------------------------------------------
|
|
// 11. Privacy panel header icon adopts the scope cue
|
|
// -----------------------------------------------------------
|
|
await run('11. Privacy panel header reflects scope colour', async () => {
|
|
await selectScope(page, 'private_only');
|
|
const headerColor = await page.evaluate(() => {
|
|
const el = document.querySelector('.ldr-privacy-panel-header');
|
|
if (!el) return null;
|
|
return window.getComputedStyle(el).color;
|
|
});
|
|
record(
|
|
'header-color-set',
|
|
!!headerColor && headerColor !== 'rgb(0, 0, 0)',
|
|
`header.color=${headerColor}`,
|
|
);
|
|
await screenshot(page, '11-panel-header-private');
|
|
});
|
|
|
|
// -----------------------------------------------------------
|
|
// 12. Tooltip explains each scope
|
|
// -----------------------------------------------------------
|
|
await run('12. Scope dropdown carries an explanatory tooltip', async () => {
|
|
const tipText = await page.evaluate(() => {
|
|
const lbl = document.querySelector('label[for="policy_egress_scope"]');
|
|
if (!lbl) return null;
|
|
return lbl.innerText || lbl.textContent;
|
|
});
|
|
const informative = !!tipText && tipText.length > 10;
|
|
record(
|
|
'tooltip-label-present',
|
|
informative,
|
|
`label="${(tipText || '').slice(0, 60)}…"`,
|
|
);
|
|
});
|
|
|
|
// -----------------------------------------------------------
|
|
// 13. Per-research overrides survive a form-state roundtrip
|
|
// -----------------------------------------------------------
|
|
await run('13. Per-research override values are read back by the form', async () => {
|
|
// Set a deliberate combination, save by changing each
|
|
// control, then reload and verify the form reflects them.
|
|
await selectScope(page, 'public_only');
|
|
const llmStart = await page.$eval('#llm_require_local_endpoint', (el) => el.checked);
|
|
if (!llmStart) await page.click('#llm_require_local_endpoint');
|
|
await sleep(IS_CI ? 500 : 300);
|
|
|
|
await page.reload({ waitUntil: 'domcontentloaded' });
|
|
await page.waitForSelector('#policy_egress_scope', { timeout: 10000 });
|
|
|
|
const scopeReloaded = await page.$eval(
|
|
'#policy_egress_scope',
|
|
(el) => el.value,
|
|
);
|
|
const llmReloaded = await page.$eval(
|
|
'#llm_require_local_endpoint',
|
|
(el) => el.checked,
|
|
);
|
|
record(
|
|
'scope-roundtrips-via-DB',
|
|
scopeReloaded === 'public_only',
|
|
`value=${scopeReloaded}`,
|
|
);
|
|
record(
|
|
'require-local-llm-roundtrips',
|
|
llmReloaded === true,
|
|
`checked=${llmReloaded}`,
|
|
);
|
|
|
|
await screenshot(page, '13-roundtrip-public-only-require-local');
|
|
|
|
// Clean up.
|
|
if (llmReloaded) await page.click('#llm_require_local_endpoint');
|
|
await selectScope(page, 'adaptive');
|
|
});
|
|
|
|
// -----------------------------------------------------------
|
|
// 14. Chat page inherits scope cue (only if chat is enabled)
|
|
// -----------------------------------------------------------
|
|
await run('14. Chat page inherits data-scope', async () => {
|
|
await selectScope(page, 'strict');
|
|
const chatRes = await page.goto(`${BASE_URL}/chat/`, {
|
|
waitUntil: 'domcontentloaded',
|
|
timeout: 30000,
|
|
});
|
|
if (!chatRes || chatRes.status() >= 400) {
|
|
record(
|
|
'chat-page-reachable',
|
|
false,
|
|
`status=${chatRes ? chatRes.status() : 'no-response'}`,
|
|
);
|
|
return;
|
|
}
|
|
const bodyScope = await page.evaluate(
|
|
() => document.body.dataset.scope,
|
|
);
|
|
record(
|
|
'chat-inherits-scope',
|
|
bodyScope === 'strict',
|
|
`data-scope=${bodyScope}`,
|
|
);
|
|
await screenshot(page, '14-chat-page-strict');
|
|
|
|
// Reset.
|
|
await page.goto(`${BASE_URL}/`, {
|
|
waitUntil: 'domcontentloaded',
|
|
timeout: 30000,
|
|
});
|
|
await page.waitForSelector('#policy_egress_scope', { timeout: 10000 });
|
|
await selectScope(page, 'adaptive');
|
|
});
|
|
|
|
} catch (err) {
|
|
console.error('\n❌ Suite-level error:', err.stack || err);
|
|
exitCode = 1;
|
|
} finally {
|
|
// Final summary
|
|
console.log('\n' + '='.repeat(60));
|
|
console.log('Results:');
|
|
const passed = results.filter((r) => r.ok).length;
|
|
const failed = results.filter((r) => !r.ok);
|
|
console.log(` ${passed} / ${results.length} passed`);
|
|
if (failed.length) {
|
|
console.log(' Failures:');
|
|
for (const f of failed) {
|
|
console.log(` ❌ ${f.label}${f.detail ? ` — ${f.detail}` : ''}`);
|
|
}
|
|
exitCode = 1;
|
|
}
|
|
if (!IS_CI) {
|
|
console.log(`\n Screenshots: ${SCREENSHOT_DIR}`);
|
|
}
|
|
await browser.close();
|
|
process.exit(exitCode);
|
|
}
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|