[fix] 5 bugs: 2 SyntaxErrors in nexus_think.py, Groq model name, server race condition, corrupt public/nexus/
Some checks failed
CI / validate (pull_request) Failing after 5s
Some checks failed
CI / validate (pull_request) Failing after 5s
Bug 1: nexus_think.py line 318 — stray '.' between function call and if-block This is a SyntaxError. The entire consciousness loop cannot import. The Nexus Mind has been dead since this was committed. Bug 2: nexus_think.py line 445 — 'parser.add_.argument()' Another SyntaxError — extra underscore in argparse call. The CLI entrypoint crashes on startup. Bug 3: groq_worker.py — DEFAULT_MODEL = 'groq/llama3-8b-8192' The Groq API expects bare model names. The 'groq/' prefix causes a 404. Fixed to 'llama3-8b-8192'. Bug 4: server.py — clients.remove() in finally block Raises KeyError if the websocket was never added to the set. Fixed to clients.discard() (safe no-op if not present). Also added tracking for disconnected clients during broadcast. Bug 5: public/nexus/ — 3 corrupt duplicate files (28.6 KB wasted) app.js, style.css, and index.html all had identical content (same SHA). These are clearly a broken copy operation. The real files are at repo root. Tests: 6 new, 21/22 total pass. The 1 pre-existing failure is in test_portals_json_uses_expanded_registry_schema (schema mismatch, not related to this PR). Signed-off-by: gemini <gemini@hermes.local>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
test-results/
|
test-results/
|
||||||
nexus/__pycache__/
|
nexus/__pycache__/
|
||||||
|
tests/__pycache__/
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ from typing import Optional
|
|||||||
log = logging.getLogger("nexus")
|
log = logging.getLogger("nexus")
|
||||||
|
|
||||||
GROQ_API_URL = "https://api.groq.com/openai/v1/chat/completions"
|
GROQ_API_URL = "https://api.groq.com/openai/v1/chat/completions"
|
||||||
DEFAULT_MODEL = "groq/llama3-8b-8192"
|
DEFAULT_MODEL = "llama3-8b-8192"
|
||||||
|
|
||||||
class GroqWorker:
|
class GroqWorker:
|
||||||
"""A worker for the Groq API."""
|
"""A worker for the Groq API."""
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ class NexusMind:
|
|||||||
]
|
]
|
||||||
|
|
||||||
summary = self._call_thinker(messages)
|
summary = self._call_thinker(messages)
|
||||||
.
|
|
||||||
if summary:
|
if summary:
|
||||||
self.experience_store.save_summary(
|
self.experience_store.save_summary(
|
||||||
summary=summary,
|
summary=summary,
|
||||||
@@ -442,7 +442,7 @@ def main():
|
|||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Nexus Mind — Embodied consciousness loop"
|
description="Nexus Mind — Embodied consciousness loop"
|
||||||
)
|
)
|
||||||
parser.add_.argument(
|
parser.add_argument(
|
||||||
"--model", default=DEFAULT_MODEL,
|
"--model", default=DEFAULT_MODEL,
|
||||||
help=f"Ollama model name (default: {DEFAULT_MODEL})"
|
help=f"Ollama model name (default: {DEFAULT_MODEL})"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,284 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
|
||||||
<meta http-equiv="Pragma" content="no-cache" />
|
|
||||||
<meta http-equiv="Expires" content="0" />
|
|
||||||
<title>Cookie check</title>
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
color-scheme: light dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Inter', Helvetica, Arial, sans-serif;
|
|
||||||
background: light-dark(#F8F8F7, #191919);
|
|
||||||
color: light-dark(#1f1f1f, #e3e3e3);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
box-sizing: border-box;
|
|
||||||
min-height: 100vh;
|
|
||||||
margin: 0;
|
|
||||||
padding: 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
background: light-dark(#FFFFFF, #1F1F1F);
|
|
||||||
padding: 32px;
|
|
||||||
border-radius: 16px;
|
|
||||||
border: 1px solid light-dark(#E2E3E4, #3E3E3E);
|
|
||||||
max-width: min(80%, 500px);
|
|
||||||
width: 100%;
|
|
||||||
color: light-dark(#2B2D31, #D4D4D4);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-top: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
color: light-dark(#2B2D31, #D4D4D4);
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-size: 14px;
|
|
||||||
color: light-dark(#2B2D31, #D4D4D4);
|
|
||||||
line-height: 21px;
|
|
||||||
margin: 0 0 1.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
line-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
background-color: light-dark(#fff, #323232);
|
|
||||||
color: light-dark(#2B2D31, #FCFCFC);
|
|
||||||
border: 1px solid light-dark(#E2E3E4, #3E3E3E);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 21px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
font-weight: 400;
|
|
||||||
font-family: 'Inter', Helvetica, Arial, sans-serif;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: light-dark(#EAEAEB, #424242);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Loading Spinner Animation */
|
|
||||||
.spinner {
|
|
||||||
margin: 0 auto 1.5rem auto;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border: 4px solid light-dark(#f0f0f0, #262626);
|
|
||||||
border-top: 4px solid light-dark(#076eff, #87a9ff); /* Blue color */
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
border-radius: 10px;
|
|
||||||
display: block;
|
|
||||||
margin: 0 auto 2rem auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<img
|
|
||||||
class="logo"
|
|
||||||
src="https://www.gstatic.com/aistudio/ai_studio_favicon_2_256x256.png"
|
|
||||||
alt="AI Studio Logo"
|
|
||||||
width="256"
|
|
||||||
height="256"
|
|
||||||
/>
|
|
||||||
<div class="spinner"></div>
|
|
||||||
<div id="error-ui" class="hidden">
|
|
||||||
<div class="icon">
|
|
||||||
<svg
|
|
||||||
version="1.1"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="48px"
|
|
||||||
height="48px"
|
|
||||||
fill="#D73A49"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M12,2C6.486,2,2,6.486,2,12s4.486,10,10,10s10-4.486,10-10S17.514,2,12,2z M13,17h-2v-2h2V17z M13,13h-2V7h2V13z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div id="stepOne" class="text-container">
|
|
||||||
<h1>Action required to load your app</h1>
|
|
||||||
<p>
|
|
||||||
It looks like your browser is blocking a required security cookie, which is common on
|
|
||||||
older versions of iOS and Safari.
|
|
||||||
</p>
|
|
||||||
<div class="button-container">
|
|
||||||
<button id="authInSeparateWindowButton" onclick="redirectToReturnUrl(true)">Authenticate in new window</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="stepTwo" class="text-container hidden">
|
|
||||||
<h1>Action required to load your app</h1>
|
|
||||||
<p>
|
|
||||||
It looks like your browser is blocking a required security cookie, which is common on
|
|
||||||
older versions of iOS and Safari.
|
|
||||||
</p>
|
|
||||||
<div class="button-container">
|
|
||||||
<button id="interactButton" onclick="redirectToReturnUrl(false)">Close and continue</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="stepThree" class="text-container hidden">
|
|
||||||
<h1>Almost there!</h1>
|
|
||||||
<p>
|
|
||||||
Grant permission for the required security cookie below.
|
|
||||||
</p>
|
|
||||||
<div class="button-container">
|
|
||||||
<button id="grantPermissionButton" onclick="grantStorageAccess()">Grant permission</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
const AUTH_FLOW_TEST_COOKIE_NAME = '__SECURE-aistudio_auth_flow_may_set_cookies';
|
|
||||||
const COOKIE_VALUE = 'true';
|
|
||||||
|
|
||||||
function getCookie(name) {
|
|
||||||
const cookies = document.cookie.split(';');
|
|
||||||
for (let i = 0; i < cookies.length; i++) {
|
|
||||||
let cookie = cookies[i].trim();
|
|
||||||
if (cookie.startsWith(name + '=')) {
|
|
||||||
return cookie.substring(name.length + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setAuthFlowTestCookie() {
|
|
||||||
// Set the cookie's TTL to 1 minute. This is a short lived cookie because it is only used
|
|
||||||
// when the user does not have an auth token or their auth token needs to be reset.
|
|
||||||
// Making this cookie too long-lived allows the user to get into a state where they can't
|
|
||||||
// mint a new auth token.
|
|
||||||
document.cookie = `${AUTH_FLOW_TEST_COOKIE_NAME}=${COOKIE_VALUE}; Path=/; Secure; SameSite=None; Domain=${window.location.hostname}; Partitioned; Max-Age=60;`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the test cookie is set, false otherwise.
|
|
||||||
*/
|
|
||||||
function authFlowTestCookieIsSet() {
|
|
||||||
return getCookie(AUTH_FLOW_TEST_COOKIE_NAME) === COOKIE_VALUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redirects to the return url. If autoClose is true, then the return url will be opened in a
|
|
||||||
* new window, and it will be closed automatically when the page loads.
|
|
||||||
*/
|
|
||||||
async function redirectToReturnUrl(autoClose) {
|
|
||||||
const initialReturnUrlStr = new URLSearchParams(window.location.search).get('return_url');
|
|
||||||
const returnUrl = initialReturnUrlStr ? new URL(initialReturnUrlStr) : null;
|
|
||||||
|
|
||||||
// Prevent potentially malicious URLs from being used
|
|
||||||
if (returnUrl.protocol.toLowerCase() === 'javascript:') {
|
|
||||||
console.error('Potentially malicious return URL blocked');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (autoClose) {
|
|
||||||
returnUrl.searchParams.set('__auto_close', '1');
|
|
||||||
const url = new URL(window.location.href);
|
|
||||||
url.searchParams.set('return_url', returnUrl.toString());
|
|
||||||
// Land on the cookie check page first, so the user can interact with it before proceeding
|
|
||||||
// to the return url where cookies can be set.
|
|
||||||
window.open(url.toString(), '_blank');
|
|
||||||
const hasAccess = await document.hasStorageAccess();
|
|
||||||
document.querySelector('#stepOne').classList.add('hidden');
|
|
||||||
if (!hasAccess) {
|
|
||||||
document.querySelector('#stepThree').classList.remove('hidden');
|
|
||||||
} else {
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
window.location.href = returnUrl.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Grants the browser permission to set cookies. If successful, then it redirects to the
|
|
||||||
* return url.
|
|
||||||
*/
|
|
||||||
async function grantStorageAccess() {
|
|
||||||
try {
|
|
||||||
await document.requestStorageAccess();
|
|
||||||
redirectToReturnUrl(false);
|
|
||||||
} catch (err) {
|
|
||||||
console.log('error after button click: ', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies that the browser can set cookies. If it can, then it redirects to the return url.
|
|
||||||
* If it can't, then it shows the error UI.
|
|
||||||
*/
|
|
||||||
function verifyCanSetCookies() {
|
|
||||||
setAuthFlowTestCookie();
|
|
||||||
if (authFlowTestCookieIsSet()) {
|
|
||||||
// Check if we are on the auto-close flow, and if so show the interact button.
|
|
||||||
const returnUrl = new URLSearchParams(window.location.search).get('return_url');
|
|
||||||
const autoClose = new URL(returnUrl).searchParams.has('__auto_close');
|
|
||||||
if (autoClose) {
|
|
||||||
document.querySelector('#stepOne').classList.add('hidden');
|
|
||||||
document.querySelector('#stepTwo').classList.remove('hidden');
|
|
||||||
} else {
|
|
||||||
redirectToReturnUrl(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// The cookie could not be set, so initiate the recovery flow.
|
|
||||||
document.querySelector('.logo').classList.add('hidden');
|
|
||||||
document.querySelector('.spinner').classList.add('hidden');
|
|
||||||
document.querySelector('#error-ui').classList.remove('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the cookie verification process.
|
|
||||||
verifyCanSetCookies();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,284 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
|
||||||
<meta http-equiv="Pragma" content="no-cache" />
|
|
||||||
<meta http-equiv="Expires" content="0" />
|
|
||||||
<title>Cookie check</title>
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
color-scheme: light dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Inter', Helvetica, Arial, sans-serif;
|
|
||||||
background: light-dark(#F8F8F7, #191919);
|
|
||||||
color: light-dark(#1f1f1f, #e3e3e3);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
box-sizing: border-box;
|
|
||||||
min-height: 100vh;
|
|
||||||
margin: 0;
|
|
||||||
padding: 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
background: light-dark(#FFFFFF, #1F1F1F);
|
|
||||||
padding: 32px;
|
|
||||||
border-radius: 16px;
|
|
||||||
border: 1px solid light-dark(#E2E3E4, #3E3E3E);
|
|
||||||
max-width: min(80%, 500px);
|
|
||||||
width: 100%;
|
|
||||||
color: light-dark(#2B2D31, #D4D4D4);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-top: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
color: light-dark(#2B2D31, #D4D4D4);
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-size: 14px;
|
|
||||||
color: light-dark(#2B2D31, #D4D4D4);
|
|
||||||
line-height: 21px;
|
|
||||||
margin: 0 0 1.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
line-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
background-color: light-dark(#fff, #323232);
|
|
||||||
color: light-dark(#2B2D31, #FCFCFC);
|
|
||||||
border: 1px solid light-dark(#E2E3E4, #3E3E3E);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 21px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
font-weight: 400;
|
|
||||||
font-family: 'Inter', Helvetica, Arial, sans-serif;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: light-dark(#EAEAEB, #424242);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Loading Spinner Animation */
|
|
||||||
.spinner {
|
|
||||||
margin: 0 auto 1.5rem auto;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border: 4px solid light-dark(#f0f0f0, #262626);
|
|
||||||
border-top: 4px solid light-dark(#076eff, #87a9ff); /* Blue color */
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
border-radius: 10px;
|
|
||||||
display: block;
|
|
||||||
margin: 0 auto 2rem auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<img
|
|
||||||
class="logo"
|
|
||||||
src="https://www.gstatic.com/aistudio/ai_studio_favicon_2_256x256.png"
|
|
||||||
alt="AI Studio Logo"
|
|
||||||
width="256"
|
|
||||||
height="256"
|
|
||||||
/>
|
|
||||||
<div class="spinner"></div>
|
|
||||||
<div id="error-ui" class="hidden">
|
|
||||||
<div class="icon">
|
|
||||||
<svg
|
|
||||||
version="1.1"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="48px"
|
|
||||||
height="48px"
|
|
||||||
fill="#D73A49"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M12,2C6.486,2,2,6.486,2,12s4.486,10,10,10s10-4.486,10-10S17.514,2,12,2z M13,17h-2v-2h2V17z M13,13h-2V7h2V13z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div id="stepOne" class="text-container">
|
|
||||||
<h1>Action required to load your app</h1>
|
|
||||||
<p>
|
|
||||||
It looks like your browser is blocking a required security cookie, which is common on
|
|
||||||
older versions of iOS and Safari.
|
|
||||||
</p>
|
|
||||||
<div class="button-container">
|
|
||||||
<button id="authInSeparateWindowButton" onclick="redirectToReturnUrl(true)">Authenticate in new window</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="stepTwo" class="text-container hidden">
|
|
||||||
<h1>Action required to load your app</h1>
|
|
||||||
<p>
|
|
||||||
It looks like your browser is blocking a required security cookie, which is common on
|
|
||||||
older versions of iOS and Safari.
|
|
||||||
</p>
|
|
||||||
<div class="button-container">
|
|
||||||
<button id="interactButton" onclick="redirectToReturnUrl(false)">Close and continue</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="stepThree" class="text-container hidden">
|
|
||||||
<h1>Almost there!</h1>
|
|
||||||
<p>
|
|
||||||
Grant permission for the required security cookie below.
|
|
||||||
</p>
|
|
||||||
<div class="button-container">
|
|
||||||
<button id="grantPermissionButton" onclick="grantStorageAccess()">Grant permission</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
const AUTH_FLOW_TEST_COOKIE_NAME = '__SECURE-aistudio_auth_flow_may_set_cookies';
|
|
||||||
const COOKIE_VALUE = 'true';
|
|
||||||
|
|
||||||
function getCookie(name) {
|
|
||||||
const cookies = document.cookie.split(';');
|
|
||||||
for (let i = 0; i < cookies.length; i++) {
|
|
||||||
let cookie = cookies[i].trim();
|
|
||||||
if (cookie.startsWith(name + '=')) {
|
|
||||||
return cookie.substring(name.length + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setAuthFlowTestCookie() {
|
|
||||||
// Set the cookie's TTL to 1 minute. This is a short lived cookie because it is only used
|
|
||||||
// when the user does not have an auth token or their auth token needs to be reset.
|
|
||||||
// Making this cookie too long-lived allows the user to get into a state where they can't
|
|
||||||
// mint a new auth token.
|
|
||||||
document.cookie = `${AUTH_FLOW_TEST_COOKIE_NAME}=${COOKIE_VALUE}; Path=/; Secure; SameSite=None; Domain=${window.location.hostname}; Partitioned; Max-Age=60;`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the test cookie is set, false otherwise.
|
|
||||||
*/
|
|
||||||
function authFlowTestCookieIsSet() {
|
|
||||||
return getCookie(AUTH_FLOW_TEST_COOKIE_NAME) === COOKIE_VALUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redirects to the return url. If autoClose is true, then the return url will be opened in a
|
|
||||||
* new window, and it will be closed automatically when the page loads.
|
|
||||||
*/
|
|
||||||
async function redirectToReturnUrl(autoClose) {
|
|
||||||
const initialReturnUrlStr = new URLSearchParams(window.location.search).get('return_url');
|
|
||||||
const returnUrl = initialReturnUrlStr ? new URL(initialReturnUrlStr) : null;
|
|
||||||
|
|
||||||
// Prevent potentially malicious URLs from being used
|
|
||||||
if (returnUrl.protocol.toLowerCase() === 'javascript:') {
|
|
||||||
console.error('Potentially malicious return URL blocked');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (autoClose) {
|
|
||||||
returnUrl.searchParams.set('__auto_close', '1');
|
|
||||||
const url = new URL(window.location.href);
|
|
||||||
url.searchParams.set('return_url', returnUrl.toString());
|
|
||||||
// Land on the cookie check page first, so the user can interact with it before proceeding
|
|
||||||
// to the return url where cookies can be set.
|
|
||||||
window.open(url.toString(), '_blank');
|
|
||||||
const hasAccess = await document.hasStorageAccess();
|
|
||||||
document.querySelector('#stepOne').classList.add('hidden');
|
|
||||||
if (!hasAccess) {
|
|
||||||
document.querySelector('#stepThree').classList.remove('hidden');
|
|
||||||
} else {
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
window.location.href = returnUrl.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Grants the browser permission to set cookies. If successful, then it redirects to the
|
|
||||||
* return url.
|
|
||||||
*/
|
|
||||||
async function grantStorageAccess() {
|
|
||||||
try {
|
|
||||||
await document.requestStorageAccess();
|
|
||||||
redirectToReturnUrl(false);
|
|
||||||
} catch (err) {
|
|
||||||
console.log('error after button click: ', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies that the browser can set cookies. If it can, then it redirects to the return url.
|
|
||||||
* If it can't, then it shows the error UI.
|
|
||||||
*/
|
|
||||||
function verifyCanSetCookies() {
|
|
||||||
setAuthFlowTestCookie();
|
|
||||||
if (authFlowTestCookieIsSet()) {
|
|
||||||
// Check if we are on the auto-close flow, and if so show the interact button.
|
|
||||||
const returnUrl = new URLSearchParams(window.location.search).get('return_url');
|
|
||||||
const autoClose = new URL(returnUrl).searchParams.has('__auto_close');
|
|
||||||
if (autoClose) {
|
|
||||||
document.querySelector('#stepOne').classList.add('hidden');
|
|
||||||
document.querySelector('#stepTwo').classList.remove('hidden');
|
|
||||||
} else {
|
|
||||||
redirectToReturnUrl(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// The cookie could not be set, so initiate the recovery flow.
|
|
||||||
document.querySelector('.logo').classList.add('hidden');
|
|
||||||
document.querySelector('.spinner').classList.add('hidden');
|
|
||||||
document.querySelector('#error-ui').classList.remove('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the cookie verification process.
|
|
||||||
verifyCanSetCookies();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,284 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
|
||||||
<meta http-equiv="Pragma" content="no-cache" />
|
|
||||||
<meta http-equiv="Expires" content="0" />
|
|
||||||
<title>Cookie check</title>
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
color-scheme: light dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Inter', Helvetica, Arial, sans-serif;
|
|
||||||
background: light-dark(#F8F8F7, #191919);
|
|
||||||
color: light-dark(#1f1f1f, #e3e3e3);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
box-sizing: border-box;
|
|
||||||
min-height: 100vh;
|
|
||||||
margin: 0;
|
|
||||||
padding: 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
background: light-dark(#FFFFFF, #1F1F1F);
|
|
||||||
padding: 32px;
|
|
||||||
border-radius: 16px;
|
|
||||||
border: 1px solid light-dark(#E2E3E4, #3E3E3E);
|
|
||||||
max-width: min(80%, 500px);
|
|
||||||
width: 100%;
|
|
||||||
color: light-dark(#2B2D31, #D4D4D4);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-top: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
color: light-dark(#2B2D31, #D4D4D4);
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-size: 14px;
|
|
||||||
color: light-dark(#2B2D31, #D4D4D4);
|
|
||||||
line-height: 21px;
|
|
||||||
margin: 0 0 1.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
line-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
background-color: light-dark(#fff, #323232);
|
|
||||||
color: light-dark(#2B2D31, #FCFCFC);
|
|
||||||
border: 1px solid light-dark(#E2E3E4, #3E3E3E);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 21px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
font-weight: 400;
|
|
||||||
font-family: 'Inter', Helvetica, Arial, sans-serif;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: light-dark(#EAEAEB, #424242);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Loading Spinner Animation */
|
|
||||||
.spinner {
|
|
||||||
margin: 0 auto 1.5rem auto;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border: 4px solid light-dark(#f0f0f0, #262626);
|
|
||||||
border-top: 4px solid light-dark(#076eff, #87a9ff); /* Blue color */
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
border-radius: 10px;
|
|
||||||
display: block;
|
|
||||||
margin: 0 auto 2rem auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<img
|
|
||||||
class="logo"
|
|
||||||
src="https://www.gstatic.com/aistudio/ai_studio_favicon_2_256x256.png"
|
|
||||||
alt="AI Studio Logo"
|
|
||||||
width="256"
|
|
||||||
height="256"
|
|
||||||
/>
|
|
||||||
<div class="spinner"></div>
|
|
||||||
<div id="error-ui" class="hidden">
|
|
||||||
<div class="icon">
|
|
||||||
<svg
|
|
||||||
version="1.1"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="48px"
|
|
||||||
height="48px"
|
|
||||||
fill="#D73A49"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M12,2C6.486,2,2,6.486,2,12s4.486,10,10,10s10-4.486,10-10S17.514,2,12,2z M13,17h-2v-2h2V17z M13,13h-2V7h2V13z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div id="stepOne" class="text-container">
|
|
||||||
<h1>Action required to load your app</h1>
|
|
||||||
<p>
|
|
||||||
It looks like your browser is blocking a required security cookie, which is common on
|
|
||||||
older versions of iOS and Safari.
|
|
||||||
</p>
|
|
||||||
<div class="button-container">
|
|
||||||
<button id="authInSeparateWindowButton" onclick="redirectToReturnUrl(true)">Authenticate in new window</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="stepTwo" class="text-container hidden">
|
|
||||||
<h1>Action required to load your app</h1>
|
|
||||||
<p>
|
|
||||||
It looks like your browser is blocking a required security cookie, which is common on
|
|
||||||
older versions of iOS and Safari.
|
|
||||||
</p>
|
|
||||||
<div class="button-container">
|
|
||||||
<button id="interactButton" onclick="redirectToReturnUrl(false)">Close and continue</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="stepThree" class="text-container hidden">
|
|
||||||
<h1>Almost there!</h1>
|
|
||||||
<p>
|
|
||||||
Grant permission for the required security cookie below.
|
|
||||||
</p>
|
|
||||||
<div class="button-container">
|
|
||||||
<button id="grantPermissionButton" onclick="grantStorageAccess()">Grant permission</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
const AUTH_FLOW_TEST_COOKIE_NAME = '__SECURE-aistudio_auth_flow_may_set_cookies';
|
|
||||||
const COOKIE_VALUE = 'true';
|
|
||||||
|
|
||||||
function getCookie(name) {
|
|
||||||
const cookies = document.cookie.split(';');
|
|
||||||
for (let i = 0; i < cookies.length; i++) {
|
|
||||||
let cookie = cookies[i].trim();
|
|
||||||
if (cookie.startsWith(name + '=')) {
|
|
||||||
return cookie.substring(name.length + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setAuthFlowTestCookie() {
|
|
||||||
// Set the cookie's TTL to 1 minute. This is a short lived cookie because it is only used
|
|
||||||
// when the user does not have an auth token or their auth token needs to be reset.
|
|
||||||
// Making this cookie too long-lived allows the user to get into a state where they can't
|
|
||||||
// mint a new auth token.
|
|
||||||
document.cookie = `${AUTH_FLOW_TEST_COOKIE_NAME}=${COOKIE_VALUE}; Path=/; Secure; SameSite=None; Domain=${window.location.hostname}; Partitioned; Max-Age=60;`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the test cookie is set, false otherwise.
|
|
||||||
*/
|
|
||||||
function authFlowTestCookieIsSet() {
|
|
||||||
return getCookie(AUTH_FLOW_TEST_COOKIE_NAME) === COOKIE_VALUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redirects to the return url. If autoClose is true, then the return url will be opened in a
|
|
||||||
* new window, and it will be closed automatically when the page loads.
|
|
||||||
*/
|
|
||||||
async function redirectToReturnUrl(autoClose) {
|
|
||||||
const initialReturnUrlStr = new URLSearchParams(window.location.search).get('return_url');
|
|
||||||
const returnUrl = initialReturnUrlStr ? new URL(initialReturnUrlStr) : null;
|
|
||||||
|
|
||||||
// Prevent potentially malicious URLs from being used
|
|
||||||
if (returnUrl.protocol.toLowerCase() === 'javascript:') {
|
|
||||||
console.error('Potentially malicious return URL blocked');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (autoClose) {
|
|
||||||
returnUrl.searchParams.set('__auto_close', '1');
|
|
||||||
const url = new URL(window.location.href);
|
|
||||||
url.searchParams.set('return_url', returnUrl.toString());
|
|
||||||
// Land on the cookie check page first, so the user can interact with it before proceeding
|
|
||||||
// to the return url where cookies can be set.
|
|
||||||
window.open(url.toString(), '_blank');
|
|
||||||
const hasAccess = await document.hasStorageAccess();
|
|
||||||
document.querySelector('#stepOne').classList.add('hidden');
|
|
||||||
if (!hasAccess) {
|
|
||||||
document.querySelector('#stepThree').classList.remove('hidden');
|
|
||||||
} else {
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
window.location.href = returnUrl.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Grants the browser permission to set cookies. If successful, then it redirects to the
|
|
||||||
* return url.
|
|
||||||
*/
|
|
||||||
async function grantStorageAccess() {
|
|
||||||
try {
|
|
||||||
await document.requestStorageAccess();
|
|
||||||
redirectToReturnUrl(false);
|
|
||||||
} catch (err) {
|
|
||||||
console.log('error after button click: ', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies that the browser can set cookies. If it can, then it redirects to the return url.
|
|
||||||
* If it can't, then it shows the error UI.
|
|
||||||
*/
|
|
||||||
function verifyCanSetCookies() {
|
|
||||||
setAuthFlowTestCookie();
|
|
||||||
if (authFlowTestCookieIsSet()) {
|
|
||||||
// Check if we are on the auto-close flow, and if so show the interact button.
|
|
||||||
const returnUrl = new URLSearchParams(window.location.search).get('return_url');
|
|
||||||
const autoClose = new URL(returnUrl).searchParams.has('__auto_close');
|
|
||||||
if (autoClose) {
|
|
||||||
document.querySelector('#stepOne').classList.add('hidden');
|
|
||||||
document.querySelector('#stepTwo').classList.remove('hidden');
|
|
||||||
} else {
|
|
||||||
redirectToReturnUrl(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// The cookie could not be set, so initiate the recovery flow.
|
|
||||||
document.querySelector('.logo').classList.add('hidden');
|
|
||||||
document.querySelector('.spinner').classList.add('hidden');
|
|
||||||
document.querySelector('#error-ui').classList.remove('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the cookie verification process.
|
|
||||||
verifyCanSetCookies();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -12,16 +12,19 @@ async def broadcast_handler(websocket):
|
|||||||
try:
|
try:
|
||||||
async for message in websocket:
|
async for message in websocket:
|
||||||
# Broadcast to all OTHER clients
|
# Broadcast to all OTHER clients
|
||||||
|
disconnected = set()
|
||||||
for client in clients:
|
for client in clients:
|
||||||
if client != websocket:
|
if client != websocket:
|
||||||
try:
|
try:
|
||||||
await client.send(message)
|
await client.send(message)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Failed to send to a client: {e}")
|
logging.error(f"Failed to send to a client: {e}")
|
||||||
|
disconnected.add(client)
|
||||||
|
clients.difference_update(disconnected)
|
||||||
except websockets.exceptions.ConnectionClosed:
|
except websockets.exceptions.ConnectionClosed:
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
clients.remove(websocket)
|
clients.discard(websocket) # discard is safe if not present
|
||||||
logging.info(f"Client disconnected. Total clients: {len(clients)}")
|
logging.info(f"Client disconnected. Total clients: {len(clients)}")
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
|
|||||||
111
tests/test_syntax_fixes.py
Normal file
111
tests/test_syntax_fixes.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
"""Tests for syntax and correctness fixes across the-nexus codebase.
|
||||||
|
|
||||||
|
Covers:
|
||||||
|
- nexus_think.py: no stray dots (SyntaxError), no typos in argparse
|
||||||
|
- groq_worker.py: model name has no 'groq/' prefix
|
||||||
|
- server.py: uses discard() not remove() for client cleanup
|
||||||
|
- public/nexus/: corrupt duplicate directory removed
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ast
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
NEXUS_ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
# ── nexus_think.py syntax checks ────────────────────────────────────
|
||||||
|
|
||||||
|
def test_nexus_think_parses_without_syntax_error():
|
||||||
|
"""nexus_think.py must be valid Python.
|
||||||
|
|
||||||
|
Two SyntaxErrors existed:
|
||||||
|
1. Line 318: stray '.' between function call and if-block
|
||||||
|
2. Line 445: 'parser.add_.argument()' (extra underscore)
|
||||||
|
|
||||||
|
If either is present, the entire consciousness loop can't import.
|
||||||
|
"""
|
||||||
|
source = (NEXUS_ROOT / "nexus" / "nexus_think.py").read_text()
|
||||||
|
# ast.parse will raise SyntaxError if the file is invalid
|
||||||
|
try:
|
||||||
|
ast.parse(source, filename="nexus_think.py")
|
||||||
|
except SyntaxError as e:
|
||||||
|
raise AssertionError(
|
||||||
|
f"nexus_think.py has a SyntaxError at line {e.lineno}: {e.msg}"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
|
||||||
|
def test_nexus_think_no_stray_dot():
|
||||||
|
"""There should be no line that is just a dot in nexus_think.py."""
|
||||||
|
source = (NEXUS_ROOT / "nexus" / "nexus_think.py").read_text()
|
||||||
|
for i, line in enumerate(source.splitlines(), 1):
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped == ".":
|
||||||
|
raise AssertionError(
|
||||||
|
f"nexus_think.py has a stray '.' on line {i}. "
|
||||||
|
"This causes a SyntaxError."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_nexus_think_argparse_no_typo():
|
||||||
|
"""parser.add_argument must not be written as parser.add_.argument."""
|
||||||
|
source = (NEXUS_ROOT / "nexus" / "nexus_think.py").read_text()
|
||||||
|
assert "add_.argument" not in source, (
|
||||||
|
"nexus_think.py contains 'add_.argument' — should be 'add_argument'."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── groq_worker.py model name ───────────────────────────────────────
|
||||||
|
|
||||||
|
def test_groq_default_model_has_no_prefix():
|
||||||
|
"""Groq API expects model names without router prefixes.
|
||||||
|
|
||||||
|
Sending 'groq/llama3-8b-8192' returns a 404.
|
||||||
|
The correct name is just 'llama3-8b-8192'.
|
||||||
|
"""
|
||||||
|
source = (NEXUS_ROOT / "nexus" / "groq_worker.py").read_text()
|
||||||
|
for line in source.splitlines():
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped.startswith("DEFAULT_MODEL") and "=" in stripped:
|
||||||
|
assert "groq/" not in stripped, (
|
||||||
|
f"groq_worker.py DEFAULT_MODEL contains 'groq/' prefix: {stripped}. "
|
||||||
|
"The Groq API expects bare model names like 'llama3-8b-8192'."
|
||||||
|
)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# DEFAULT_MODEL not found — that's a different issue, not this test's concern
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# ── server.py client cleanup ────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_server_uses_discard_not_remove():
|
||||||
|
"""server.py must use clients.discard() not clients.remove().
|
||||||
|
|
||||||
|
remove() raises KeyError if the websocket isn't in the set.
|
||||||
|
This happens if an exception occurs before clients.add() runs.
|
||||||
|
discard() is a safe no-op if the element isn't present.
|
||||||
|
"""
|
||||||
|
source = (NEXUS_ROOT / "server.py").read_text()
|
||||||
|
assert "clients.discard(" in source, (
|
||||||
|
"server.py should use clients.discard(websocket) for safe cleanup."
|
||||||
|
)
|
||||||
|
assert "clients.remove(" not in source, (
|
||||||
|
"server.py should NOT use clients.remove(websocket) — "
|
||||||
|
"raises KeyError if websocket wasn't added."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── public/nexus/ corrupt duplicate directory ────────────────────────
|
||||||
|
|
||||||
|
def test_public_nexus_duplicate_removed():
|
||||||
|
"""public/nexus/ contained 3 files with identical content (all 9544 bytes).
|
||||||
|
|
||||||
|
app.js, style.css, and index.html were all the same file — clearly a
|
||||||
|
corrupt copy operation. The canonical files are at the repo root.
|
||||||
|
"""
|
||||||
|
corrupt_dir = NEXUS_ROOT / "public" / "nexus"
|
||||||
|
assert not corrupt_dir.exists(), (
|
||||||
|
"public/nexus/ still exists. These are corrupt duplicates "
|
||||||
|
"(all 3 files have identical content). Remove this directory."
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user