Add deterministic announcement demo workspace
@@ -0,0 +1,44 @@
|
||||
# Buzz announcement demo
|
||||
|
||||
This branch includes a deterministic, relay-free workspace for recording the
|
||||
Buzz announcement film.
|
||||
|
||||
## Launch
|
||||
|
||||
From this worktree, run:
|
||||
|
||||
```bash
|
||||
just announcement-staging
|
||||
```
|
||||
|
||||
The command launches the native Tauri staging app on this worktree's isolated
|
||||
port with announcement mode enabled. The app uses the deterministic mock
|
||||
workspace rather than reading or writing real staging-relay data. Reloading the
|
||||
app restores the same clean demo workspace.
|
||||
|
||||
The local demo relay also includes a running agent named **Scout**. Configure
|
||||
OpenAI or Anthropic in the Agents screen, then `@mention` Scout in
|
||||
`#flight-path`, `#design`, `#marketing`, or
|
||||
`#queen-bee-launch`. Scout shows a typing indicator and replies using the
|
||||
selected provider with the recent conversation as context. Direct messages to
|
||||
Scout work too. Agent settings are retained for reloads during the current app
|
||||
session; provider keys are never written into the repository or logged by the
|
||||
local provider bridge.
|
||||
|
||||
For a browser-only preview, use `just announcement-demo`.
|
||||
|
||||
## Demo workspace
|
||||
|
||||
- Workspace: **Honeycomb Studios**
|
||||
- People: Alex Rivera (Product Lead) and nine fictional teammates across
|
||||
engineering, design, marketing, research, QA, data, support, video, and
|
||||
community
|
||||
- Sections: **The Hive**, **Product**, and **Launch Swarm**
|
||||
- Channels include `#announcements`, `#general`, `#flight-path`, `#design`,
|
||||
`#mobile`, `#product-ideas`, `#marketing`, and
|
||||
`#queen-bee-launch`
|
||||
- Projects: `flight-path`, `nectar`, `comb-kit`, and `swarm-launch`
|
||||
- Direct messages: Maya Chen, Jordan Brooks, and Priya Shah
|
||||
|
||||
All portraits are generated fictional people. Scout uses the default generated
|
||||
identity until the announcement's final agent avatar is ready.
|
||||
@@ -488,6 +488,31 @@ desktop-dev:
|
||||
echo "Starting frontend dev server on Vite port ${BUZZ_VITE_PORT}, relay ${BUZZ_RELAY_URL}"
|
||||
pnpm exec vite --port "${BUZZ_VITE_PORT}" --strictPort
|
||||
|
||||
# Open the deterministic, relay-free announcement workspace in a browser
|
||||
announcement-demo:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd {{desktop_dir}}
|
||||
[[ -d node_modules ]] || pnpm install
|
||||
source ../scripts/instance-env.sh
|
||||
demo_url="http://127.0.0.1:${BUZZ_VITE_PORT}/?demo=announcement"
|
||||
pnpm exec vite --port "${BUZZ_VITE_PORT}" --strictPort &
|
||||
vite_pid=$!
|
||||
trap 'kill "$vite_pid" 2>/dev/null || true' EXIT
|
||||
for _ in $(seq 1 40); do
|
||||
if curl -sf "http://127.0.0.1:${BUZZ_VITE_PORT}/" >/dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 0.25
|
||||
done
|
||||
echo "Opening the Buzz announcement demo at ${demo_url}"
|
||||
open "${demo_url}"
|
||||
wait "$vite_pid"
|
||||
|
||||
# Open the deterministic announcement workspace in the native staging shell
|
||||
announcement-staging *ARGS:
|
||||
VITE_ANNOUNCEMENT_DEMO=1 just staging {{ARGS}}
|
||||
|
||||
# ─── Web ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
# Run the web frontend dev server (port derived from worktree to avoid collisions)
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
import type { Plugin } from "vite";
|
||||
|
||||
const AGENT_RESPONSE_PATH = "/__announcement-demo/agent-response";
|
||||
const MAX_REQUEST_BYTES = 64 * 1024;
|
||||
const REQUEST_TIMEOUT_MS = 45_000;
|
||||
|
||||
type AnnouncementDemoAgentMessage = {
|
||||
role: "assistant" | "user";
|
||||
content: string;
|
||||
};
|
||||
|
||||
type AnnouncementDemoAgentRequest = {
|
||||
provider: "anthropic" | "openai";
|
||||
apiKey: string;
|
||||
model: string;
|
||||
systemPrompt: string;
|
||||
messages: AnnouncementDemoAgentMessage[];
|
||||
};
|
||||
|
||||
type ProviderErrorBody = {
|
||||
error?: string | { message?: string };
|
||||
message?: string;
|
||||
};
|
||||
|
||||
function isAgentMessage(value: unknown): value is AnnouncementDemoAgentMessage {
|
||||
if (typeof value !== "object" || value === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const candidate = value as Record<string, unknown>;
|
||||
return (
|
||||
(candidate.role === "assistant" || candidate.role === "user") &&
|
||||
typeof candidate.content === "string" &&
|
||||
candidate.content.trim().length > 0
|
||||
);
|
||||
}
|
||||
|
||||
function parseAgentRequest(value: unknown): AnnouncementDemoAgentRequest {
|
||||
if (typeof value !== "object" || value === null) {
|
||||
throw new Error("The agent request was not valid JSON.");
|
||||
}
|
||||
|
||||
const candidate = value as Record<string, unknown>;
|
||||
if (candidate.provider !== "anthropic" && candidate.provider !== "openai") {
|
||||
throw new Error("Choose Anthropic or OpenAI as the agent provider.");
|
||||
}
|
||||
if (typeof candidate.apiKey !== "string" || !candidate.apiKey.trim()) {
|
||||
throw new Error("Add an API key in the agent settings first.");
|
||||
}
|
||||
if (typeof candidate.model !== "string" || !candidate.model.trim()) {
|
||||
throw new Error("Choose a model in the agent settings first.");
|
||||
}
|
||||
if (
|
||||
typeof candidate.systemPrompt !== "string" ||
|
||||
!Array.isArray(candidate.messages) ||
|
||||
!candidate.messages.every(isAgentMessage)
|
||||
) {
|
||||
throw new Error("The agent conversation context was incomplete.");
|
||||
}
|
||||
|
||||
return {
|
||||
provider: candidate.provider,
|
||||
apiKey: candidate.apiKey.trim(),
|
||||
model: candidate.model.trim(),
|
||||
systemPrompt: candidate.systemPrompt,
|
||||
messages: candidate.messages,
|
||||
};
|
||||
}
|
||||
|
||||
function readRequestBody(request: NodeJS.ReadableStream): Promise<unknown> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
let bytesRead = 0;
|
||||
|
||||
request.on("data", (chunk: Buffer) => {
|
||||
bytesRead += chunk.length;
|
||||
if (bytesRead > MAX_REQUEST_BYTES) {
|
||||
reject(new Error("The agent conversation was too large to send."));
|
||||
return;
|
||||
}
|
||||
chunks.push(chunk);
|
||||
});
|
||||
request.on("end", () => {
|
||||
try {
|
||||
resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
||||
} catch {
|
||||
reject(new Error("The agent request was not valid JSON."));
|
||||
}
|
||||
});
|
||||
request.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeAnthropicModel(model: string) {
|
||||
if (model === "goose-claude-4-6-sonnet") {
|
||||
return "claude-sonnet-4-6";
|
||||
}
|
||||
if (model === "goose-claude-4-6-opus") {
|
||||
return "claude-opus-4-6";
|
||||
}
|
||||
return model.replace(/^anthropic\//, "");
|
||||
}
|
||||
|
||||
function extractProviderError(body: ProviderErrorBody, status: number) {
|
||||
const nestedMessage =
|
||||
typeof body.error === "object" ? body.error.message : undefined;
|
||||
const message = nestedMessage ?? body.message ?? body.error;
|
||||
if (typeof message === "string" && message.trim()) {
|
||||
return `Provider request failed (${status}): ${message.trim()}`;
|
||||
}
|
||||
return `Provider request failed with status ${status}.`;
|
||||
}
|
||||
|
||||
function extractOpenAiText(body: unknown) {
|
||||
if (typeof body !== "object" || body === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const response = body as {
|
||||
output_text?: unknown;
|
||||
output?: Array<{
|
||||
content?: Array<{ type?: unknown; text?: unknown }>;
|
||||
}>;
|
||||
};
|
||||
if (typeof response.output_text === "string" && response.output_text.trim()) {
|
||||
return response.output_text.trim();
|
||||
}
|
||||
|
||||
const text = (response.output ?? [])
|
||||
.flatMap((item) => item.content ?? [])
|
||||
.filter((part) => part.type === "output_text")
|
||||
.map((part) => (typeof part.text === "string" ? part.text : ""))
|
||||
.join("")
|
||||
.trim();
|
||||
return text || null;
|
||||
}
|
||||
|
||||
function extractAnthropicText(body: unknown) {
|
||||
if (typeof body !== "object" || body === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const response = body as {
|
||||
content?: Array<{ type?: unknown; text?: unknown }>;
|
||||
};
|
||||
const text = (response.content ?? [])
|
||||
.filter((part) => part.type === "text")
|
||||
.map((part) => (typeof part.text === "string" ? part.text : ""))
|
||||
.join("")
|
||||
.trim();
|
||||
return text || null;
|
||||
}
|
||||
|
||||
async function requestOpenAiResponse(input: AnnouncementDemoAgentRequest) {
|
||||
const response = await fetch("https://api.openai.com/v1/responses", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${input.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: input.model,
|
||||
instructions: input.systemPrompt,
|
||||
input: input.messages,
|
||||
max_output_tokens: 350,
|
||||
store: false,
|
||||
}),
|
||||
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
||||
});
|
||||
const body = (await response.json()) as unknown;
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
extractProviderError(body as ProviderErrorBody, response.status),
|
||||
);
|
||||
}
|
||||
|
||||
const text = extractOpenAiText(body);
|
||||
if (!text) {
|
||||
throw new Error("OpenAI returned a response without any text.");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
async function requestAnthropicResponse(input: AnnouncementDemoAgentRequest) {
|
||||
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"anthropic-version": "2023-06-01",
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": input.apiKey,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: normalizeAnthropicModel(input.model),
|
||||
max_tokens: 350,
|
||||
system: input.systemPrompt,
|
||||
messages: input.messages,
|
||||
}),
|
||||
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
||||
});
|
||||
const body = (await response.json()) as unknown;
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
extractProviderError(body as ProviderErrorBody, response.status),
|
||||
);
|
||||
}
|
||||
|
||||
const text = extractAnthropicText(body);
|
||||
if (!text) {
|
||||
throw new Error("Anthropic returned a response without any text.");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function friendlyError(error: unknown) {
|
||||
if (error instanceof Error && error.name === "TimeoutError") {
|
||||
return "The model took too long to respond. Please try again.";
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return "The model could not respond just now.";
|
||||
}
|
||||
|
||||
/**
|
||||
* Local-only provider proxy for the announcement demo. Keeping the API request
|
||||
* in Vite means provider credentials are never built into the frontend bundle
|
||||
* or written to source, and no Docker-backed relay is required.
|
||||
*/
|
||||
export function announcementDemoAgentPlugin(): Plugin {
|
||||
return {
|
||||
name: "buzz-announcement-demo-agent",
|
||||
configureServer(server) {
|
||||
server.middlewares.use(AGENT_RESPONSE_PATH, async (request, response) => {
|
||||
response.setHeader("Content-Type", "application/json");
|
||||
response.setHeader("Cache-Control", "no-store");
|
||||
|
||||
if (request.method !== "POST") {
|
||||
response.statusCode = 405;
|
||||
response.end(JSON.stringify({ error: "Method not allowed." }));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const input = parseAgentRequest(await readRequestBody(request));
|
||||
const text =
|
||||
input.provider === "openai"
|
||||
? await requestOpenAiResponse(input)
|
||||
: await requestAnthropicResponse(input);
|
||||
response.statusCode = 200;
|
||||
response.end(JSON.stringify({ text }));
|
||||
} catch (error) {
|
||||
response.statusCode = 502;
|
||||
response.end(JSON.stringify({ error: friendlyError(error) }));
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
const previewPort = process.env.BUZZ_E2E_PREVIEW_PORT ?? "4173";
|
||||
const previewUrl = `http://127.0.0.1:${previewPort}`;
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests/e2e",
|
||||
timeout: 30_000,
|
||||
@@ -10,7 +13,7 @@ export default defineConfig({
|
||||
["html", { open: "never", outputFolder: "playwright-report" }],
|
||||
],
|
||||
use: {
|
||||
baseURL: "http://127.0.0.1:4173",
|
||||
baseURL: previewUrl,
|
||||
screenshot: "only-on-failure",
|
||||
trace: "on-first-retry",
|
||||
video: "retain-on-failure",
|
||||
@@ -21,6 +24,7 @@ export default defineConfig({
|
||||
testMatch: [
|
||||
"**/smoke.spec.ts",
|
||||
"**/onboarding-docked-cta-screenshots.spec.ts",
|
||||
"**/announcement-demo.spec.ts",
|
||||
"**/navigation.spec.ts",
|
||||
"**/channels.spec.ts",
|
||||
"**/channel-shared-header-backdrop.spec.ts",
|
||||
@@ -140,9 +144,9 @@ export default defineConfig({
|
||||
},
|
||||
],
|
||||
webServer: {
|
||||
command: "python3 -m http.server 4173 -d dist",
|
||||
command: `python3 -m http.server ${previewPort} -d dist`,
|
||||
cwd: ".",
|
||||
reuseExistingServer: !process.env.CI,
|
||||
url: "http://127.0.0.1:4173",
|
||||
url: previewUrl,
|
||||
},
|
||||
});
|
||||
|
||||
|
After Width: | Height: | Size: 260 KiB |
|
After Width: | Height: | Size: 437 KiB |
|
After Width: | Height: | Size: 429 KiB |
|
After Width: | Height: | Size: 363 KiB |
|
After Width: | Height: | Size: 444 KiB |
|
After Width: | Height: | Size: 446 KiB |
|
After Width: | Height: | Size: 438 KiB |
|
After Width: | Height: | Size: 399 KiB |
|
After Width: | Height: | Size: 379 KiB |
|
After Width: | Height: | Size: 507 KiB |
|
After Width: | Height: | Size: 408 KiB |
@@ -8,6 +8,12 @@ import { UpdaterProvider } from "@/features/settings/hooks/UpdaterProvider";
|
||||
import { migrateLegacyCommunityStorageBeforeRender } from "@/features/communities/legacyCommunityStorage";
|
||||
import { CommunitiesProvider } from "@/features/communities/useCommunities";
|
||||
import { CommunityOnboardingProvider } from "@/features/onboarding/communityOnboarding";
|
||||
import {
|
||||
ANNOUNCEMENT_DEMO_AGENT,
|
||||
ANNOUNCEMENT_DEMO_COMMUNITY_NAME,
|
||||
ANNOUNCEMENT_DEMO_PEOPLE,
|
||||
ANNOUNCEMENT_DEMO_SECTION_STORE,
|
||||
} from "@/testing/announcementDemoFixtures";
|
||||
import { ThemeProvider } from "@/shared/theme/ThemeProvider";
|
||||
import { EmojiBurstProvider } from "@/shared/ui/EmojiBurstProvider";
|
||||
import { PoofBurstProvider } from "@/shared/ui/PoofBurstProvider";
|
||||
@@ -22,6 +28,9 @@ const E2E_DEFAULT_PUBKEY = "deadbeef".repeat(8);
|
||||
const E2E_COMMUNITY_ID = "e2e-default-community";
|
||||
const ONBOARDING_COMPLETION_STORAGE_KEY_PREFIX = "buzz-onboarding-complete.v1:";
|
||||
const DEV_STATE_RESET_PARAM = "resetDevState";
|
||||
const ANNOUNCEMENT_DEMO_QUERY_VALUE = "announcement";
|
||||
const CHANNEL_SECTIONS_STORAGE_KEY_PREFIX = "buzz-channel-sections.v1";
|
||||
const SELF_PROFILE_STORAGE_KEY_PREFIX = "buzz-self-profile.v1";
|
||||
|
||||
function resetDevWebviewStateFromUrl() {
|
||||
if (!import.meta.env.DEV) {
|
||||
@@ -42,23 +51,44 @@ function resetDevWebviewStateFromUrl() {
|
||||
window.history.replaceState(window.history.state, "", url);
|
||||
}
|
||||
|
||||
function configureDevE2eBridgeFromUrl() {
|
||||
if (!import.meta.env.DEV) {
|
||||
return;
|
||||
}
|
||||
|
||||
function configureMockBridgeFromUrl() {
|
||||
const url = new URL(window.location.href);
|
||||
if (url.searchParams.get("e2e") !== "mock") {
|
||||
const isDevE2eMock =
|
||||
import.meta.env.DEV && url.searchParams.get("e2e") === "mock";
|
||||
const isAnnouncementDemo =
|
||||
url.searchParams.get("demo") === ANNOUNCEMENT_DEMO_QUERY_VALUE ||
|
||||
import.meta.env.VITE_ANNOUNCEMENT_DEMO === "1";
|
||||
|
||||
if (!isDevE2eMock && !isAnnouncementDemo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const e2eWindow = window as E2eWindow;
|
||||
e2eWindow.__BUZZ_E2E__ ??= { mode: "mock" };
|
||||
if (isAnnouncementDemo) {
|
||||
e2eWindow.__BUZZ_E2E__ = {
|
||||
mode: "mock",
|
||||
mock: {
|
||||
announcementDemo: true,
|
||||
managedAgents: [
|
||||
{
|
||||
pubkey: ANNOUNCEMENT_DEMO_AGENT.pubkey,
|
||||
name: ANNOUNCEMENT_DEMO_AGENT.name,
|
||||
systemPrompt: ANNOUNCEMENT_DEMO_AGENT.systemPrompt,
|
||||
status: "running",
|
||||
channelNames: [...ANNOUNCEMENT_DEMO_AGENT.channelNames],
|
||||
respondTo: "owner-only",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
} else {
|
||||
e2eWindow.__BUZZ_E2E__ ??= { mode: "mock" };
|
||||
}
|
||||
|
||||
const community = {
|
||||
addedAt: new Date().toISOString(),
|
||||
id: E2E_COMMUNITY_ID,
|
||||
name: "E2E Test",
|
||||
name: isAnnouncementDemo ? ANNOUNCEMENT_DEMO_COMMUNITY_NAME : "E2E Test",
|
||||
relayUrl: "ws://localhost:3000",
|
||||
};
|
||||
window.localStorage.setItem("buzz-communities", JSON.stringify([community]));
|
||||
@@ -67,6 +97,25 @@ function configureDevE2eBridgeFromUrl() {
|
||||
`${ONBOARDING_COMPLETION_STORAGE_KEY_PREFIX}${E2E_DEFAULT_PUBKEY}`,
|
||||
"true",
|
||||
);
|
||||
|
||||
if (isAnnouncementDemo) {
|
||||
const relayStorageScope = encodeURIComponent("ws://localhost:3000");
|
||||
window.localStorage.setItem(
|
||||
`${CHANNEL_SECTIONS_STORAGE_KEY_PREFIX}:${E2E_DEFAULT_PUBKEY}:${relayStorageScope}`,
|
||||
JSON.stringify(ANNOUNCEMENT_DEMO_SECTION_STORE),
|
||||
);
|
||||
window.localStorage.setItem(
|
||||
`${SELF_PROFILE_STORAGE_KEY_PREFIX}:ws://localhost:3000:${E2E_DEFAULT_PUBKEY}`,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
displayName: ANNOUNCEMENT_DEMO_PEOPLE.viewer.displayName,
|
||||
avatarUrl: ANNOUNCEMENT_DEMO_PEOPLE.viewer.avatarUrl,
|
||||
avatarDataUrl: null,
|
||||
updatedAt: Date.now(),
|
||||
hasProfileEvent: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderApp() {
|
||||
@@ -106,7 +155,7 @@ async function installE2eBridgeIfConfigured() {
|
||||
|
||||
async function bootstrap() {
|
||||
resetDevWebviewStateFromUrl();
|
||||
configureDevE2eBridgeFromUrl();
|
||||
configureMockBridgeFromUrl();
|
||||
await installE2eBridgeIfConfigured();
|
||||
await migrateLegacyCommunityStorageBeforeRender();
|
||||
renderApp();
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
invoke as nativeInvoke,
|
||||
type InvokeArgs,
|
||||
type InvokeOptions,
|
||||
} from "../../node_modules/@tauri-apps/api/core.js";
|
||||
|
||||
export type {
|
||||
InvokeArgs,
|
||||
InvokeOptions,
|
||||
} from "../../node_modules/@tauri-apps/api/core.js";
|
||||
export {
|
||||
addPluginListener,
|
||||
Channel,
|
||||
checkPermissions,
|
||||
convertFileSrc,
|
||||
isTauri,
|
||||
PluginListener,
|
||||
requestPermissions,
|
||||
Resource,
|
||||
SERIALIZE_TO_IPC_FN,
|
||||
transformCallback,
|
||||
} from "../../node_modules/@tauri-apps/api/core.js";
|
||||
|
||||
type AnnouncementDemoWindow = Window & {
|
||||
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: (
|
||||
command: string,
|
||||
payload?: unknown,
|
||||
) => unknown;
|
||||
};
|
||||
|
||||
/** Route native-shell commands through the deterministic announcement bridge. */
|
||||
export function invoke<T>(
|
||||
command: string,
|
||||
payload?: InvokeArgs,
|
||||
options?: InvokeOptions,
|
||||
): Promise<T> {
|
||||
const mockInvoke = (window as AnnouncementDemoWindow)
|
||||
.__BUZZ_E2E_INVOKE_MOCK_COMMAND__;
|
||||
if (mockInvoke) {
|
||||
return Promise.resolve(mockInvoke(command, payload)) as Promise<T>;
|
||||
}
|
||||
return nativeInvoke<T>(command, payload, options);
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("announcement demo loads its workspace, people, and projects", async ({
|
||||
page,
|
||||
}) => {
|
||||
const agentReply =
|
||||
"I’d lead with the handoff moment, then land on the shared launch room. That gives the story a clear before-and-after.";
|
||||
await page.route("**/__announcement-demo/agent-response", async (route) => {
|
||||
await route.fulfill({ json: { text: agentReply } });
|
||||
});
|
||||
await page.goto("/?demo=announcement");
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
typeof (
|
||||
window as Window & {
|
||||
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: unknown;
|
||||
}
|
||||
).__BUZZ_E2E_INVOKE_MOCK_COMMAND__ === "function",
|
||||
);
|
||||
|
||||
await page.evaluate(async () => {
|
||||
const invoke = (
|
||||
window as Window & {
|
||||
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: (
|
||||
command: string,
|
||||
payload?: unknown,
|
||||
) => Promise<unknown>;
|
||||
}
|
||||
).__BUZZ_E2E_INVOKE_MOCK_COMMAND__;
|
||||
if (!invoke) {
|
||||
throw new Error("Announcement demo command bridge is unavailable.");
|
||||
}
|
||||
await invoke("set_global_agent_config", {
|
||||
config: {
|
||||
env_vars: { OPENAI_COMPAT_API_KEY: "e2e-demo-key" },
|
||||
provider: "openai",
|
||||
model: "gpt-5.4-mini",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await expect(page.getByText("The Hive", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("Product", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("Launch Swarm", { exact: true })).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("Honeycomb Studios", { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("Alex Rivera", { exact: true }).last(),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByTestId("channel-DM").filter({ hasText: "Maya Chen" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByTestId("channel-DM").filter({ hasText: "Jordan Brooks" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByTestId("channel-DM").filter({ hasText: "Priya Shah" }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId("channel-DM")).toHaveCount(3);
|
||||
|
||||
await page.getByTestId("channel-flight-path").click();
|
||||
const channelTimeline = page.getByTestId("message-timeline");
|
||||
await expect(channelTimeline).toContainText("Marcus Reed");
|
||||
await expect(channelTimeline).toContainText("Elena Torres");
|
||||
await expect(channelTimeline).toContainText("Perfect. That’s the move.");
|
||||
const demoBuildRow = page
|
||||
.getByTestId("message-row")
|
||||
.filter({ hasText: "Demo build is running" })
|
||||
.last();
|
||||
await expect(
|
||||
demoBuildRow.locator('[data-link-preview="github-pull-request"]'),
|
||||
).toBeVisible();
|
||||
await expect(demoBuildRow.getByTestId("message-reactions")).toContainText(
|
||||
"✅",
|
||||
);
|
||||
await expect(
|
||||
channelTimeline.locator('[data-link-preview="linear-issue"]').last(),
|
||||
).toBeVisible();
|
||||
|
||||
const channelMessage = `The recording pass is ready ${Date.now()}`;
|
||||
await page.getByTestId("message-input").fill(channelMessage);
|
||||
await page.getByTestId("send-message").click();
|
||||
await expect(channelTimeline).toContainText(channelMessage);
|
||||
|
||||
const messageInput = page.getByTestId("message-input");
|
||||
await messageInput.fill("Could ");
|
||||
await messageInput.pressSequentially("@Sco");
|
||||
const agentMention = page
|
||||
.getByTestId("message-composer")
|
||||
.getByTestId("mention-autocomplete")
|
||||
.locator("button", { hasText: "Scout" });
|
||||
await expect(agentMention).toBeVisible();
|
||||
await agentMention.click();
|
||||
await messageInput.pressSequentially(" suggest the strongest story beat?");
|
||||
await page.getByTestId("send-message").click();
|
||||
await expect(channelTimeline).toContainText(agentReply, { timeout: 10_000 });
|
||||
|
||||
const populatedChannels = [
|
||||
["announcements", "Final smoke pass is clean"],
|
||||
["general", "Please nobody breathe on main"],
|
||||
["design", "Looks great on camera"],
|
||||
["mobile", "The draft follows you now"],
|
||||
["marketing", "No copy-paste script"],
|
||||
["queen-bee-launch", "Sound mix is approved"],
|
||||
] as const;
|
||||
for (const [channel, excerpt] of populatedChannels) {
|
||||
await page.getByTestId(`channel-${channel}`).click();
|
||||
await expect(channelTimeline).toContainText(excerpt);
|
||||
}
|
||||
|
||||
await page.getByTestId("channel-design").click();
|
||||
await expect(channelTimeline.getByAltText("image").last()).toBeVisible();
|
||||
await expect(
|
||||
channelTimeline.locator('[data-link-preview="google-docs-document"]'),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByTestId("channel-marketing").click();
|
||||
await expect(
|
||||
channelTimeline
|
||||
.getByTestId("file-card")
|
||||
.filter({ hasText: "launch-social-crops.zip" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
channelTimeline.locator('[data-link-preview="google-sheets-spreadsheet"]'),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByTestId("channel-DM").filter({ hasText: "Maya Chen" }).click();
|
||||
const dmMessage = `Can you join the capture review? ${Date.now()}`;
|
||||
await page.getByTestId("message-input").fill(dmMessage);
|
||||
await page.getByTestId("send-message").click();
|
||||
await expect(page.getByTestId("message-timeline")).toContainText(dmMessage);
|
||||
|
||||
await page.goto("/?demo=announcement#/projects");
|
||||
await page.locator('button[aria-label="Repositories"]').click();
|
||||
for (const project of ["flight-path", "nectar", "comb-kit", "swarm-launch"]) {
|
||||
await expect(
|
||||
page.locator(
|
||||
`[data-testid="project-card-${project}"], [data-testid="project-row-${project}"]`,
|
||||
),
|
||||
).toBeVisible();
|
||||
}
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import path from "node:path";
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { tanstackRouter } from "@tanstack/router-plugin/vite";
|
||||
import { announcementDemoAgentPlugin } from "./announcementDemoAgentPlugin";
|
||||
|
||||
// @ts-expect-error process is a nodejs global
|
||||
const host = process.env.TAURI_DEV_HOST;
|
||||
@@ -21,11 +22,20 @@ export default defineConfig(async () => ({
|
||||
],
|
||||
}),
|
||||
react(),
|
||||
announcementDemoAgentPlugin(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": "/src",
|
||||
"@features-manifest": path.resolve(__dirname, "../preview-features.json"),
|
||||
...(process.env.VITE_ANNOUNCEMENT_DEMO === "1"
|
||||
? {
|
||||
"@tauri-apps/api/core": path.resolve(
|
||||
__dirname,
|
||||
"src/testing/announcementDemoTauriCore.ts",
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
|
||||
|
||||