Skip to content
Draft
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
161 changes: 57 additions & 104 deletions __tests__/features/onboarding/build-prompt.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { basename, resolve } from 'node:path';
import { buildOnboardingPrompt } from '@features/onboarding/index.js';

describe('buildOnboardingPrompt', () => {
Expand Down Expand Up @@ -133,175 +132,129 @@ describe('buildOnboardingPrompt', () => {
});
});

describe('session recordings MCP setup', () => {
describe('session recordings skill reference', () => {
const recordingOpts = {
...baseOpts,
goals: ['session-recordings' as const],
};

it('instructs the agent to create a recording policy and targeting-key rule', () => {
const sut = buildOnboardingPrompt(recordingOpts);
it('delegates to setup-session-recording skill via CLI when installed via CLI', () => {
const sut = buildOnboardingPrompt({
...recordingOpts,
pluginInstallMethod: 'cli',
});

expect(sut).toContain('mcp__confidence-flags__createRecordingPolicy');
expect(sut).toContain('mcp__confidence-flags__addRecordingRule');
expect(sut).toContain('targetingKeySelector');
expect(sut).toContain('`enabled`: true');
expect(sut).toContain('STATUS: Setting up recording policy...');
expect(sut).toContain(
'Invoke the `/confidence:setup-session-recording` skill as a **methodology reference**',
);
expect(sut).not.toContain('Read `.claude/skills/setup-session-recording/SKILL.md`');
});

it('includes Confidence resources in the final change summary', () => {
const sut = buildOnboardingPrompt(recordingOpts);
it('delegates to setup-session-recording skill as file path when installed via download', () => {
const sut = buildOnboardingPrompt({
...recordingOpts,
pluginInstallMethod: 'download',
});

expect(sut).toContain('Created recording policy with targeting key');
expect(sut).toContain(
'Created recording rule (Record all visitors, 100% audience, 100% sessions, enabled)',
'Read `.claude/skills/setup-session-recording/SKILL.md` as a **methodology reference**',
);
});

it('instructs the agent to pass a 100% audience instead of sending 0', () => {
const sut = buildOnboardingPrompt(recordingOpts);
it('uses cursor skills dir for cursor with download method', () => {
const sut = buildOnboardingPrompt({
...recordingOpts,
ide: 'cursor',
pluginInstallMethod: 'download',
});

expect(sut).toContain('`stableAudiencePercentage`: 100');
expect(sut).toContain('`sessionSampleRate`: 1');
expect(sut).toContain('agents often send `0`');
expect(sut).toContain('Read `.cursor/skills/setup-session-recording/SKILL.md`');
});

it('matches recording policies to the client resource name, not display name', () => {
const sut = buildOnboardingPrompt(recordingOpts);
it('uses codex skill invocation for codex with CLI method', () => {
const sut = buildOnboardingPrompt({
...recordingOpts,
ide: 'codex',
pluginInstallMethod: 'cli',
});

expect(sut).toContain('never reuse a policy because its display name looks similar');
expect(sut).toContain(
"Reuse a policy only when its `clients` list contains this client's resource name",
'Invoke the `$setup-session-recording` skill as a **methodology reference**',
);
expect(sut).toContain('pass each non-empty `nextPageToken` back as `pageToken`');
expect(sut).toContain("`clientName` set to this client's resource name");
expect(sut).toContain('Keep the returned resource name (`clients/<id>` from `name:`)');
expect(sut).not.toContain('cannot match an existing policy to a client');
});

it('treats goal selection as confirmation to enable recording', () => {
it('overrides skill formatting with STATUS-line-only output', () => {
const sut = buildOnboardingPrompt(recordingOpts);

expect(sut).toContain('explicit confirmation to start recording');
expect(sut).toContain('enable the rule immediately without asking another question');
expect(sut).toContain('Tell the user afterward that the rule is enabled');
});

it('names the client and policy after the resolved project dir, not the framework', () => {
const sut = buildOnboardingPrompt({ ...recordingOpts, projectDir: '/tmp/checkout-web' });

expect(sut).toContain('with the display name "checkout-web"');
expect(sut).toContain('with `displayName` "checkout-web Session Recording"');
expect(sut).toContain('never after the framework');
expect(sut).not.toContain('react Session Recording');
});

it('resolves --dir . so the client is not named "."', () => {
const sut = buildOnboardingPrompt({ ...recordingOpts, projectDir: '.' });
const name = basename(resolve('.'));

expect(sut).toContain(`with the display name "${name}"`);
expect(sut).not.toContain('with the display name "."');
expect(sut).toContain('Ignore its output formatting entirely');
expect(sut).toContain('no step tracker, no EDUCATE blocks, no AskUserQuestion calls');
expect(sut).toContain('STATUS: Setting up session recording...');
});

it('does not reuse a colliding client from another project', () => {
it('produces a single recording step instead of two', () => {
const sut = buildOnboardingPrompt(recordingOpts);

expect(sut).toContain('Do not reuse a client solely because its display name already exists');
expect(sut).toContain(
'unless the colliding client is the one created earlier in this same run',
);
expect(sut).toContain(
'Never call `mcp__confidence-flags__getClientSecret` for a colliding client',
);
expect(sut).not.toContain(
'If the tool says that display name already exists, keep the resource name',
);
const recordingHeaders = sut.match(/^## \d+\. .*[Rr]ecord/gm) ?? [];
expect(recordingHeaders).toHaveLength(1);
expect(sut).not.toContain('Determine Session Recording SDK');
});

it('refers to targeting key and policy by name instead of numbered steps', () => {
it('includes example STATUS lines for all recording phases', () => {
const sut = buildOnboardingPrompt(recordingOpts);

expect(sut).toContain('from the **Targeting key** item in');
expect(sut).toContain('the resource name from the **Policy** item in');
expect(sut).not.toContain('from step 2');
expect(sut).not.toContain('from step 3');
expect(sut).toContain('STATUS: Checking session recording availability...');
expect(sut).toContain('STATUS: Setting up recording policy...');
expect(sut).toContain('STATUS: Installing session recording SDK...');
expect(sut).toContain('STATUS: Adding session recording provider...');
expect(sut).toContain('STATUS: Configuring privacy and capture settings...');
expect(sut).toContain('STATUS: Verifying project builds...');
});

it('uses the framework public env var so the browser can read the client secret', () => {
it('includes guardrails for framework-specific env vars', () => {
const sut = buildOnboardingPrompt(recordingOpts);

expect(sut).toContain('VITE_CONFIDENCE_CLIENT_SECRET');
expect(sut).toContain('NEXT_PUBLIC_CONFIDENCE_CLIENT_SECRET');
expect(sut).toContain('REACT_APP_CONFIDENCE_CLIENT_SECRET');
expect(sut).toContain('clientSecret: <CLIENT_SECRET_ACCESS>');
expect(sut).toContain('fill `<CLIENT_SECRET_ENV>`');
});

it('gates recording behind an existing consent tool when one is found', () => {
it('includes guardrails for consent-gated recording', () => {
const sut = buildOnboardingPrompt(recordingOpts);

expect(sut).toContain('OneTrust, Cookiebot, Usercentrics, Didomi');
expect(sut).toContain("`mode: 'manual'`");
expect(sut).toContain('only after analytics or recording consent is granted');
expect(sut).toContain('Fill `<RECORDING_CONSENT_STATUS>`');
});

it('tells the agent what recording-rule status to write for each path', () => {
const sut = buildOnboardingPrompt(recordingOpts);

expect(sut).toContain('Fill `<RECORDING_RULE_STATUS>` with "No recording rule was created');
expect(sut).toContain('Fill `<RECORDING_RULE_STATUS>` with "The recording rule is enabled');
expect(sut).toContain(
'Fill `<RECORDING_RULE_STATUS>` with "The existing recording rule records nobody',
);
});

it('flags a reused rule that has no audience segment', () => {
it('includes guardrails for client naming', () => {
const sut = buildOnboardingPrompt(recordingOpts);

expect(sut).toContain('STATUS: Existing recording rule records nobody');
expect(sut).toContain('An audience segment (`segments/<id>`) is the healthy');
expect(sut).toContain('including rules with no targeting conditions');
expect(sut).toContain('records nobody and no MCP tool can repair');
expect(sut).toContain('Name clients after the project, never after the framework');
});

it('keeps the client secret out of output and source', () => {
it('includes guardrails for secret handling', () => {
const sut = buildOnboardingPrompt(recordingOpts);

expect(sut).toContain('write the Frontend client secret to `.env` under that exact name');
expect(sut).toContain('Ensure `.env` is in `.gitignore`');
expect(sut).toContain('`.env`');
expect(sut).toContain('`.gitignore`');
expect(sut).toContain(
'never echo the secret in STATUS lines, the report, or generated source',
);
});

it('picks the targeting key from the context schema like flags', () => {
it('tells the agent to fill report placeholders for recording rule and consent status', () => {
const sut = buildOnboardingPrompt(recordingOpts);

expect(sut).toContain('mcp__confidence-flags__getContextSchema');
expect(sut).toContain('first available entity field');
expect(sut).toContain('Do not assume `user_id` or `targeting_key`');
expect(sut).toContain('`localStorage` only in a browser entrypoint');
expect(sut).not.toContain("inspect the app's auth/session code");
expect(sut).toContain('`<RECORDING_RULE_STATUS>`');
expect(sut).toContain('`<RECORDING_CONSENT_STATUS>`');
});

it('reuses the feature flag identity when both goals are selected', () => {
it('correlates sessions with flag evaluations when both goals are selected', () => {
const sut = buildOnboardingPrompt({
...recordingOpts,
goals: ['feature-flags', 'session-recordings'],
});

expect(sut).toContain('If feature flags were integrated earlier, reuse that entity field');
expect(sut).toContain('reuse the flag identity if present');
});

it('uses Codex MCP tool names for Codex', () => {
const sut = buildOnboardingPrompt({ ...recordingOpts, ide: 'codex' });

expect(sut).toContain('confidence-flags:createRecordingPolicy');
expect(sut).toContain('confidence-flags:addRecordingRule');
expect(sut).not.toContain('mcp__confidence-flags__createRecordingPolicy');
expect(sut).toContain('same identity field in `context` so sessions correlate');
});
});
});
34 changes: 0 additions & 34 deletions __tests__/features/onboarding/recording.test.ts

This file was deleted.

14 changes: 4 additions & 10 deletions src/features/onboarding/build-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { addIf } from '@lib/prompt-utils.js';
import { buildToolVars } from './tool-vars.js';
import { preflight } from './sections/preflight.js';
import { scaffold } from './sections/scaffold.js';
import { integrateViaSkill } from './sections/integrate.js';
import { determineRecordingSDK, integrateRecording } from './sections/recording.js';
import { integrateFeatureFlags } from './sections/feature-flags.js';
import { integrateRecording } from './sections/recording.js';
import { instrumentEvents } from './sections/event-tracking.js';
import { generateReport } from './sections/report.js';
import { summary, rules } from './sections/summary.js';
Expand Down Expand Up @@ -41,17 +41,11 @@ export function buildOnboardingPrompt({
addIf(isEmptyProject, () => scaffold(framework, steps.next())),

addIf(withFlags, () =>
integrateViaSkill(framework, steps.next(), isEmptyProject, ide, pluginInstallMethod),
integrateFeatureFlags(framework, steps.next(), isEmptyProject, ide, pluginInstallMethod),
),

addIf(withRecordings, () => determineRecordingSDK(framework, steps.next(), tools)),
addIf(withRecordings, () =>
integrateRecording({
step: steps.next(),
isEmptyProject,
projectDir,
toolVars: tools,
}),
integrateRecording(framework, steps.next(), isEmptyProject, ide, pluginInstallMethod),
),

addIf(withEventTracking, () =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { IdeId, PluginInstallationMethod } from '@shared-kernel/types.js';
import { loadStep } from '../steps/load.js';
import { referenceInstruction } from '../tool-vars.js';

export function integrateViaSkill(
export function integrateFeatureFlags(
framework: string,
step: number,
isEmptyProject: boolean,
Expand All @@ -11,7 +11,7 @@ export function integrateViaSkill(
): string {
const needsReactGotchas = /react|nextjs|next/i.test(framework);

return loadStep('integrate-via-skill.md', {
return loadStep('integrate-feature-flags.md', {
STEP: step,
FRAMEWORK: framework,
SKILL_READ_INSTRUCTION: referenceInstruction('analyze-project', ide, pluginInstallMethod),
Expand Down
53 changes: 13 additions & 40 deletions src/features/onboarding/sections/recording.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,24 @@
import { basename, dirname, resolve } from 'node:path';
import { CONFIDENCE_DOCS_URL } from '@lib/constants.js';
import type { IdeId, PluginInstallationMethod } from '@shared-kernel/types.js';
import { loadStep } from '../steps/load.js';
import { referenceInstruction } from '../tool-vars.js';

const FALLBACK_PROJECT_NAME = 'project';

type IntegrateRecordingParams = {
step: number;
isEmptyProject: boolean;
projectDir: string;
toolVars: Record<string, string>;
};

export function projectDisplayName(projectDir: string): string {
return basename(resolve(projectDir)) || FALLBACK_PROJECT_NAME;
}

export function projectParentName(projectDir: string): string {
return basename(dirname(resolve(projectDir))) || FALLBACK_PROJECT_NAME;
}

export function determineRecordingSDK(
export function integrateRecording(
framework: string,
step: number,
toolVars: Record<string, string>,
isEmptyProject: boolean,
ide: IdeId,
pluginInstallMethod?: PluginInstallationMethod | null,
): string {
return loadStep('determine-recording-sdk.md', {
STEP: step,
FRAMEWORK: framework,
DOCS_URL: CONFIDENCE_DOCS_URL,
...toolVars,
});
}

export function integrateRecording({
step,
isEmptyProject,
projectDir,
toolVars,
}: IntegrateRecordingParams): string {
return loadStep('integrate-recording.md', {
STEP: step,
PROJECT_NAME: projectDisplayName(projectDir),
PARENT_NAME: projectParentName(projectDir),
DOCS_URL: CONFIDENCE_DOCS_URL,
ANALYSIS_CONTEXT: isEmptyProject
FRAMEWORK: framework,
SKILL_READ_INSTRUCTION: referenceInstruction(
'setup-session-recording',
ide,
pluginInstallMethod,
),
DOMAIN_CONTEXT: isEmptyProject
? "The project was just scaffolded — configure recording on the sample app's main view."
: "Identify the app's entry point or root layout where the session recorder should be initialized.",
...toolVars,
});
}
Loading
Loading