Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions apps/web/src/domains/onboarding/onboarding-lifecycle-sync.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ type TestOnboardingRecipe = {

let onboardingCompleted = false;
let prechatOnboardingCondensedFlow = true;
let activationFlowExperiment = false;
let selfIntroGreeting = true;
let isIOSWeb = false;
let isMacOSWeb = false;
Expand Down Expand Up @@ -200,7 +199,6 @@ mock.module("@/stores/client-feature-flag-store", () => ({
useClientFeatureFlagStore: {
use: {
prechatOnboardingCondensedFlow: () => prechatOnboardingCondensedFlow,
experimentActivationFlow20260603: () => activationFlowExperiment,
selfIntroGreeting: () => selfIntroGreeting,
},
},
Expand Down Expand Up @@ -349,7 +347,6 @@ beforeEach(() => {
checkAssistantImpl = async () => {};
onboardingCompleted = false;
prechatOnboardingCondensedFlow = true;
activationFlowExperiment = false;
selfIntroGreeting = true;
isIOSWeb = false;
isMacOSWeb = false;
Expand Down Expand Up @@ -476,8 +473,19 @@ describe("onboarding lifecycle sync", () => {
});
});

test("activation flow flag selects the activation bootstrap after pre-chat", async () => {
activationFlowExperiment = true;
test("activation flow recipe delivers cohort and bootstrap to the onboarding context", async () => {
// The platform delivers cohort + bootstrapTemplate via the recipe endpoint (PR 5)
// for treatment users. The context builder forwards them unchanged — no independent
// flag evaluation. This is the "no second eval" invariant from JARVIS-1102.
fetchOnboardingRecipeImpl = async () => ({
cohort: ACTIVATION_FLOW_COHORT,
bootstrapTemplate: ACTIVATION_RAIL_BOOTSTRAP_TEMPLATE,
initialMessage: "",
tasks: [],
tone: "grounded",
skills: [],
skipPrechat: false,
});

render(<PreChatFlow />);

Expand All @@ -494,6 +502,7 @@ describe("onboarding lifecycle sync", () => {
cohort: ACTIVATION_FLOW_COHORT,
initialMessage: "Hi, I'm Alice. Nice to meet you.",
bootstrapTemplate: ACTIVATION_RAIL_BOOTSTRAP_TEMPLATE,
skills: [],
});
});

Expand Down
3 changes: 0 additions & 3 deletions apps/web/src/domains/onboarding/pages/pre-chat-flow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,6 @@ export function PreChatFlow() {
const showIOSAppStep = isIOSWeb && !readIOSAppDownloaded();
const condensedPrechatFlag =
useClientFeatureFlagStore.use.prechatOnboardingCondensedFlow();
const activationFlowEnabled =
useClientFeatureFlagStore.use.experimentActivationFlow20260603();
const selfIntroGreetingEnabled =
useClientFeatureFlagStore.use.selfIntroGreeting();
const preferredFunnelVariant =
Expand Down Expand Up @@ -330,7 +328,6 @@ export function PreChatFlow() {
userName,
assistantName,
selfIntroGreetingEnabled,
activationFlowEnabled,
googleConnected,
googleScopes,
connectedScopes: args?.connectedScopes,
Expand Down
35 changes: 25 additions & 10 deletions apps/web/src/domains/onboarding/prechat-context.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { describe, expect, test } from "bun:test";

import { DEFAULT_PRECHAT_INITIAL_MESSAGE } from "@/domains/onboarding/prechat";
import {
ACTIVATION_FLOW_COHORT,
ACTIVATION_RAIL_BOOTSTRAP_TEMPLATE,
Expand Down Expand Up @@ -29,32 +28,48 @@ function baseInput(
}

describe("buildPreChatContext — activation rail", () => {
test("selects the activation bootstrap template when the experiment flag is on", () => {
test("selects the activation cohort and bootstrap template from the recipe", () => {
// Treatment users receive a recipe from the platform (PR 5) whose cohort is
// ACTIVATION_FLOW_COHORT and bootstrap_template is ACTIVATION_RAIL_BOOTSTRAP_TEMPLATE.
// The context builder propagates them unchanged via the recipe path.
const context = buildPreChatContext(
baseInput({ activationFlowEnabled: true }),
baseInput({
recipe: {
cohort: ACTIVATION_FLOW_COHORT,
bootstrapTemplate: ACTIVATION_RAIL_BOOTSTRAP_TEMPLATE,
initialMessage: "",
tasks: [],
tone: "",
skills: [],
skipPrechat: false,
},
}),
);

expect(context.cohort).toBe(ACTIVATION_FLOW_COHORT);
expect(context.bootstrapTemplate).toBe(ACTIVATION_RAIL_BOOTSTRAP_TEMPLATE);
});

test("activation template wins over a marketing recipe template", () => {
test("marketing campaign recipe cohort takes precedence over activation when both present", () => {
// The platform delivers the campaign recipe when one matches (PR 5 resolution order:
// campaign > experiment assignment). The context builder forwards the campaign cohort.
const context = buildPreChatContext(
baseInput({
activationFlowEnabled: true,
recipe: {
cohort: "content-automation",
bootstrapTemplate: "BOOTSTRAP-CONTENT-AUTOMATION.md",
initialMessage: "Campaign hello",
tasks: ["writing"],
tone: "grounded",
skills: ["geo-writing"],
} as BuildPreChatContextInput["recipe"],
skipPrechat: true,
},
}),
);

expect(context.cohort).toBe(ACTIVATION_FLOW_COHORT);
expect(context.bootstrapTemplate).toBe(ACTIVATION_RAIL_BOOTSTRAP_TEMPLATE);
expect(context.skills).toEqual(["geo-writing"]);
expect(context.initialMessage).toBe(DEFAULT_PRECHAT_INITIAL_MESSAGE);
expect(context.cohort).toBe("content-automation");
expect(context.bootstrapTemplate).toBe("BOOTSTRAP-CONTENT-AUTOMATION.md");
expect(context.initialMessage).toBe("Campaign hello");
});
});

Expand Down
9 changes: 1 addition & 8 deletions apps/web/src/domains/onboarding/prechat-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@ export interface BuildPreChatContextInput {
userName: string;
assistantName: string;
selfIntroGreetingEnabled: boolean;
/** Selects the activation rail bootstrap template for experiment users. */
activationFlowEnabled?: boolean;
/** Persisted Google connection state from a step the user already passed. */
googleConnected: boolean;
googleScopes: string[];
Expand Down Expand Up @@ -100,11 +98,6 @@ export function buildPreChatContext(
context.skills = recipe.skills;
}

if (input.activationFlowEnabled) {
context.cohort = ACTIVATION_FLOW_COHORT;
context.bootstrapTemplate = ACTIVATION_RAIL_BOOTSTRAP_TEMPLATE;
}

const trimmedUser = input.userName.trim();
if (trimmedUser) context.userName = trimmedUser;
const trimmedAssistant = input.assistantName.trim();
Expand Down Expand Up @@ -134,7 +127,7 @@ export function buildPreChatContext(

context.initialMessage = resolveInitialMessage(
context,
input.activationFlowEnabled ? null : recipe,
recipe,
input.selfIntroGreetingEnabled,
);
return context;
Expand Down
11 changes: 9 additions & 2 deletions apps/web/src/lib/feature-flags/feature-flag-catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,15 @@ describe("feature flag catalog", () => {
expect(ASSISTANT_FLAG_DEFAULTS.selfIntroGreeting).toBe(false);
});

test("exposes the activation flow experiment as a client flag", () => {
expect(CLIENT_FLAG_DEFAULTS.experimentActivationFlow20260603).toBe(false);
test("does not declare the activation flow experiment as a toggleable flag", () => {
// The activation rail is gated by the onboarding recipe cohort (control /
// treatment) assigned vid-keyed on the platform — not by a boolean feature
// flag. Declaring it in the registry would render it as a manual toggle in
// Settings → Feature Flags, letting a hand-flipped override diverge from the
// vid-keyed assignment, so it must not appear in either flag store (JARVIS-1102).
expect(
"experimentActivationFlow20260603" in CLIENT_FLAG_DEFAULTS,
).toBe(false);
expect(
"experimentActivationFlow20260603" in ASSISTANT_FLAG_DEFAULTS,
).toBe(false);
Expand Down
8 changes: 0 additions & 8 deletions apps/web/src/lib/feature-flags/feature-flag-registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,6 @@
"description": "Enable the condensed pre-chat onboarding flow for a standard LaunchDarkly percentage rollout.",
"defaultEnabled": false
},
{
"id": "experiment-activation-flow-2026-06-03",
"scope": "client",
"key": "experiment-activation-flow-2026-06-03",
"label": "Activation Flow Experiment 2026-06-03",
"description": "Route allowlisted users to the activation-rail bootstrap template after pre-chat. Off by default and targeted through LaunchDarkly.",
"defaultEnabled": false
},
{
"id": "local-docker-enabled",
"scope": "client",
Expand Down
8 changes: 0 additions & 8 deletions meta/feature-flags/feature-flag-registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,6 @@
"description": "Enable the condensed pre-chat onboarding flow for a standard LaunchDarkly percentage rollout.",
"defaultEnabled": false
},
{
"id": "experiment-activation-flow-2026-06-03",
"scope": "client",
"key": "experiment-activation-flow-2026-06-03",
"label": "Activation Flow Experiment 2026-06-03",
"description": "Route allowlisted users to the activation-rail bootstrap template after pre-chat. Off by default and targeted through LaunchDarkly.",
"defaultEnabled": false
},
{
"id": "local-docker-enabled",
"scope": "client",
Expand Down
Loading