Compare commits

...

2 Commits

Author SHA1 Message Date
roshan
94ebe9e221 fix(craft): fix default dockerfile outputs_template_path and venv_template_path (#8102) 2026-02-02 14:28:08 -08:00
Justin Tahara
99c9c378cd chore(no-auth): Clean up Playwright (#8109) 2026-02-02 12:19:54 -08:00
9 changed files with 19 additions and 320 deletions

View File

@@ -249,7 +249,7 @@ jobs:
strategy:
fail-fast: false
matrix:
project: [admin, no-auth, exclusive]
project: [admin, exclusive]
steps:
- uses: runs-on/action@cd2b598b0515d39d78c38a02d529db87d2196d1e # ratchet:runs-on/action@v2
@@ -299,9 +299,6 @@ jobs:
ONYX_MODEL_SERVER_IMAGE=${ECR_CACHE}:playwright-test-model-server-${RUN_ID}
ONYX_WEB_SERVER_IMAGE=${ECR_CACHE}:playwright-test-web-${RUN_ID}
EOF
if [ "${{ matrix.project }}" = "no-auth" ]; then
echo "PLAYWRIGHT_FORCE_EMPTY_LLM_PROVIDERS=true" >> deployment/docker_compose/.env
fi
# needed for pulling Vespa, Redis, Postgres, and Minio images
# otherwise, we hit the "Unauthenticated users" limit
@@ -430,9 +427,6 @@ jobs:
run: |
# Create test-results directory to ensure it exists for artifact upload
mkdir -p test-results
if [ "${PROJECT}" = "no-auth" ]; then
export PLAYWRIGHT_FORCE_EMPTY_LLM_PROVIDERS=true
fi
npx playwright test --project ${PROJECT}
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f

View File

@@ -149,6 +149,11 @@ RUN if [ "$ENABLE_CRAFT" = "true" ]; then \
ENABLE_CRAFT=true /app/scripts/setup_craft_templates.sh; \
fi
# Set Craft template paths to the in-image locations
# These match the paths where setup_craft_templates.sh creates the templates
ENV OUTPUTS_TEMPLATE_PATH=/app/onyx/server/features/build/sandbox/kubernetes/docker/templates/outputs
ENV VENV_TEMPLATE_PATH=/app/onyx/server/features/build/sandbox/kubernetes/docker/templates/venv
# Put logo in assets
COPY --chown=onyx:onyx ./assets /app/assets

View File

@@ -77,7 +77,7 @@ with SKIP_AUTH=true. This is convenient but slightly different from what happens
in CI so tests might pass locally and fail in CI.
```cd web
SKIP_AUTH=true npx playwright test create_and_edit_assistant.spec.ts --project=no-auth
SKIP_AUTH=true npx playwright test create_and_edit_assistant.spec.ts --project=admin
```
2. Run playwright

45
web/package-lock.json generated
View File

@@ -71,7 +71,6 @@
"react-loader-spinner": "^8.0.0",
"react-markdown": "^9.0.1",
"react-select": "^5.8.0",
"react-truncate-markup": "^5.1.2",
"recharts": "^2.13.1",
"rehype-highlight": "^7.0.2",
"rehype-katex": "^7.0.1",
@@ -8561,11 +8560,6 @@
"version": "1.0.1",
"license": "MIT"
},
"node_modules/computed-style": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/computed-style/-/computed-style-0.1.4.tgz",
"integrity": "sha512-WpAmaKbMNmS3OProfHIdJiNleNJdgUrJfbKArXua28QF7+0CoZjlLn0lp6vlc+dl5r2/X9GQiQRQQU4BzSa69w=="
},
"node_modules/concat-map": {
"version": "0.0.1",
"dev": true,
@@ -12854,18 +12848,6 @@
"url": "https://github.com/sponsors/antonk52"
}
},
"node_modules/line-height": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/line-height/-/line-height-0.3.1.tgz",
"integrity": "sha512-YExecgqPwnp5gplD2+Y8e8A5+jKpr25+DzMbFdI1/1UAr0FJrTFv4VkHLf8/6B590i1wUPJWMKKldkd/bdQ//w==",
"license": "MIT",
"dependencies": {
"computed-style": "~0.1.3"
},
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/lines-and-columns": {
"version": "1.2.4",
"license": "MIT"
@@ -15907,27 +15889,6 @@
"react-dom": ">=16.6.0"
}
},
"node_modules/react-truncate-markup": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/react-truncate-markup/-/react-truncate-markup-5.1.2.tgz",
"integrity": "sha512-eEq6T8Rs+wz98cRYzQECGFNBfXwRYraLg/kz52f6DRBKmzxqB+GYLeDkVe/zrC+2vh5AEwM6nSYFvDWEBljd0w==",
"license": "Apache-2.0",
"dependencies": {
"line-height": "0.3.1",
"memoize-one": "^5.1.1",
"prop-types": "^15.6.0",
"resize-observer-polyfill": "1.5.x"
},
"peerDependencies": {
"react": ">=16.3"
}
},
"node_modules/react-truncate-markup/node_modules/memoize-one": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
"license": "MIT"
},
"node_modules/read-cache": {
"version": "1.0.0",
"license": "MIT",
@@ -16234,12 +16195,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
"license": "MIT"
},
"node_modules/resolve-cwd": {
"version": "3.0.0",
"dev": true,

View File

@@ -87,7 +87,6 @@
"react-loader-spinner": "^8.0.0",
"react-markdown": "^9.0.1",
"react-select": "^5.8.0",
"react-truncate-markup": "^5.1.2",
"recharts": "^2.13.1",
"rehype-highlight": "^7.0.2",
"rehype-katex": "^7.0.1",

View File

@@ -48,14 +48,6 @@ export default defineConfig({
},
grepInvert: /@exclusive/,
},
{
name: "no-auth",
use: {
...devices["Desktop Chrome"],
viewport: { width: 1280, height: 720 },
},
grepInvert: /@exclusive/,
},
{
// this suite runs independently and serially + slower
// we should be cautious about bloating this suite

View File

@@ -2,10 +2,6 @@ import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces"
import { fetchSS } from "../utilsSS";
export async function fetchLLMProvidersSS() {
// Test helper: allow Playwright runs to force an empty provider list so onboarding appears.
if (process.env.PLAYWRIGHT_FORCE_EMPTY_LLM_PROVIDERS === "true") {
return [];
}
const response = await fetchSS("/llm/provider");
if (response.ok) {
return (await response.json()) as LLMProviderDescriptor[];

View File

@@ -2,7 +2,6 @@
import { useState, useMemo, useRef, useLayoutEffect, useEffect } from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import TruncateMarkup from "react-truncate-markup";
import Modal from "@/refresh-components/Modal";
import IconButton from "@/refresh-components/buttons/IconButton";
import CopyIconButton from "@/refresh-components/buttons/CopyIconButton";
@@ -101,8 +100,7 @@ export default function ExpandableTextDisplay({
const contentSize = useMemo(() => getContentSize(content), [content]);
const displaySubtitle = subtitle ?? contentSize;
// Detect truncation for renderContent mode and streaming
// (TruncateMarkup's onTruncate handles plain text static mode)
// Detect truncation for renderContent mode, streaming, and plain text static
useLayoutEffect(() => {
if (renderContent && scrollRef.current) {
// For renderContent mode (streaming or static), use scroll-based detection
@@ -115,8 +113,12 @@ export default function ExpandableTextDisplay({
const textToCheck = displayContent ?? content;
const lineCount = getLineCount(textToCheck);
setIsTruncated(lineCount > maxLines);
} else if (scrollRef.current) {
// For plain text static, use scroll-based detection with line-clamp
setIsTruncated(
scrollRef.current.scrollHeight > scrollRef.current.clientHeight
);
}
// Plain text static mode is handled by TruncateMarkup's onTruncate
}, [isStreaming, renderContent, content, displayContent, maxLines]);
// Scroll to bottom during streaming for renderContent mode
@@ -132,11 +134,6 @@ export default function ExpandableTextDisplay({
prevIsStreamingRef.current = isStreaming;
}, [isStreaming]);
// Handle truncation callback from TruncateMarkup (static mode only)
const handleTruncate = (wasTruncated: boolean) => {
setIsTruncated(wasTruncated);
};
const handleDownload = () => {
const sanitizedTitle = title.replace(/[^a-z0-9]/gi, "_").toLowerCase();
downloadAsTxt(content, sanitizedTitle);
@@ -214,15 +211,13 @@ export default function ExpandableTextDisplay({
);
};
// Render plain text static (TruncateMarkup for reliable truncation)
// Render plain text static (CSS line-clamp + scroll-based truncation detection)
const renderPlainTextStatic = () => (
<TruncateMarkup lines={maxLines} ellipsis="…" onTruncate={handleTruncate}>
<div className="whitespace-pre-wrap">
<Text as="p" mainUiMuted text03>
{displayContent ?? content}
</Text>
</div>
</TruncateMarkup>
<div ref={scrollRef} className={cn("overflow-hidden", lineClampClass)}>
<Text as="span" mainUiMuted text03 className="whitespace-pre-wrap">
{displayContent ?? content}
</Text>
</div>
);
return (

View File

@@ -1,237 +0,0 @@
import { test, expect } from "@chromatic-com/playwright";
import { Route } from "@playwright/test";
import { loginAs } from "@tests/e2e/utils/auth";
test.describe("First user onboarding flow", () => {
test("completes onboarding wizard and unlocks chat input", async ({
page,
}, testInfo) => {
test.skip(
testInfo.project.name !== "no-auth",
"Onboarding flow requires a clean session without preset auth state"
);
// Track whether we've "created" a provider for this run.
let providerCreated = false;
// Force an empty provider list at first so onboarding shows, then return
// a stub provider after the Connect flow completes.
const providerListResponder = async (route: Route) => {
if (route.request().method() !== "GET") {
await route.continue();
return;
}
const body = providerCreated
? JSON.stringify([
{
id: 1,
name: "OpenAI",
provider: "openai",
is_default_provider: true,
default_model_name: "gpt-4o",
model_configurations: [{ name: "gpt-4o", is_visible: true }],
},
])
: "[]";
await route.fulfill({
status: 200,
contentType: "application/json",
body,
});
};
await page.route("**/api/llm/provider", providerListResponder);
await page.route("**/llm/provider", providerListResponder);
// Mock provider creation/update endpoints so fake keys still succeed.
await page.route(
"**/api/admin/llm/provider?is_creation=true",
async (route) => {
if (route.request().method() === "PUT") {
providerCreated = true;
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
id: 1,
name: "OpenAI",
provider: "openai",
is_default_provider: true,
default_model_name: "gpt-4o",
model_configurations: [{ name: "gpt-4o", is_visible: true }],
}),
});
return;
}
await route.continue();
}
);
await page.route("**/api/admin/llm/provider/*/default", async (route) => {
if (route.request().method() === "POST") {
await route.fulfill({
status: 200,
contentType: "application/json",
body: "{}",
});
return;
}
await route.continue();
});
await page.route(
(url) => url.pathname.endsWith("/api/admin/llm/test"),
async (route) => {
if (route.request().method() === "POST") {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ ok: true }),
});
} else {
await route.continue();
}
}
);
await page.context().clearCookies();
await loginAs(page, "admin");
// Reset the admin user's personalization to ensure onboarding starts from step 1
await page.evaluate(async () => {
await fetch("/api/user/personalization", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ name: "" }),
});
});
await page.goto("/app");
await page.waitForLoadState("networkidle");
const dismissNewTeamModal = async () => {
const continueButton = page
.getByRole("button", { name: /Continue with new team/i })
.first();
if ((await continueButton.count()) > 0) {
await continueButton.click();
return true;
}
const tryOnyxButton = page
.getByRole("button", { name: /Try Onyx while waiting/i })
.first();
if ((await tryOnyxButton.count()) > 0) {
await tryOnyxButton.click();
return true;
}
return false;
};
for (let attempt = 0; attempt < 3; attempt++) {
const dismissed = await dismissNewTeamModal();
if (dismissed) {
break;
}
await page.waitForTimeout(250);
}
const onboardingTitle = page
.getByText("Let's take a moment to get you set up.")
.first();
await expect(onboardingTitle).toBeVisible({ timeout: 20000 });
const letsGoButton = page.getByRole("button", { name: "Let's Go" });
await expect(letsGoButton).toBeEnabled();
await letsGoButton.click();
await expect(page.getByText("Step 1 of 3")).toBeVisible();
await expect(page.getByText("What should Onyx call you?")).toBeVisible();
const nameInput = page.getByPlaceholder("Your name").first();
await nameInput.fill("Playwright Tester");
await expect(nameInput).toHaveValue("Playwright Tester");
const nextButton = page.getByRole("button", { name: "Next", exact: true });
await expect(nextButton).toBeEnabled();
await nextButton.click();
await expect(
page
.getByText("Almost there! Connect your models to start chatting.")
.first()
).toBeVisible();
await expect(page.getByText("Step 2 of 3")).toBeVisible();
const providerCards = [
{ title: "GPT", subtitle: "OpenAI" },
{ title: "Claude", subtitle: "Anthropic" },
{ title: "Azure OpenAI", subtitle: "Microsoft Azure Cloud" },
{ title: "Amazon Bedrock", subtitle: "AWS" },
{ title: "Gemini", subtitle: "Google Cloud Vertex AI" },
{ title: "OpenRouter", subtitle: "OpenRouter" },
{ title: "Ollama", subtitle: "Ollama" },
{ title: "Custom LLM Provider", subtitle: "LiteLLM Compatible APIs" },
];
for (const provider of providerCards) {
await expect(
page
.getByRole("button", {
name: new RegExp(`${provider.title}.*${provider.subtitle}`, "i"),
})
.first()
).toBeVisible({ timeout: 20000 });
}
const openaiCard = page
.getByRole("button", { name: /GPT.*OpenAI/i })
.first();
await openaiCard.click();
const providerModal = page.getByRole("dialog", { name: /Set up GPT/i });
await expect(providerModal).toBeVisible({ timeout: 15000 });
const apiKeyInput = page
.getByLabel("API Key", { exact: false })
.or(page.locator('input[type="password"]').first());
await apiKeyInput.fill("sk-onboarding-test-key");
await page.getByRole("button", { name: "Connect" }).click();
await expect(providerModal).toBeHidden({ timeout: 15000 });
await expect(nextButton).toBeEnabled({ timeout: 10000 });
await nextButton.click();
const completionHeading = page
.getByText(
"You're all set, review the optional settings or click Finish Setup"
)
.first();
await expect(completionHeading).toBeVisible();
await expect(page.getByText("Step 3 of 3")).toBeVisible();
const checklistItems = [
"Select web search provider",
"Enable image generation",
"Invite your team",
];
for (const item of checklistItems) {
await expect(page.getByText(item).first()).toBeVisible();
}
const finishSetupButton = page.getByRole("button", {
name: "Finish Setup",
});
await finishSetupButton.click();
await expect(finishSetupButton).toBeHidden({ timeout: 5000 });
await expect(page.getByText("Connect your LLM models")).toHaveCount(0);
const chatInput = page.locator("#onyx-chat-input-textarea");
await chatInput.waitFor({ state: "visible", timeout: 10000 });
await chatInput.fill("Hello from onboarding");
await expect(chatInput).toHaveValue("Hello from onboarding");
await chatInput.fill("");
});
});