Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
407f451
Eval: require a getComputedStyle assertion in stories to prove CSS is…
kasperpeulen Apr 19, 2026
720d018
Eval prompt: reword end-state sentence to avoid biasing toward render()
kasperpeulen Apr 19, 2026
88dffdb
Eval grade: scope CSS-assertion check to added lines in story files
kasperpeulen Apr 20, 2026
fd48b03
Eval: name the CSS-check story `CssCheck` so telemetry can find it
kasperpeulen Apr 20, 2026
28fea35
Build: Source eval prompts from the CLI via EVAL_SETUP_PROMPT
kasperpeulen Apr 20, 2026
9412695
CLI: Port pattern-copy-play prompt improvements from #34596
kasperpeulen Apr 20, 2026
5a85358
Merge remote-tracking branch 'origin/kasper/eval-prompts-from-cli' in…
kasperpeulen Apr 20, 2026
741237e
chore: oxfmt scripts/eval/eval.ts (fix CI format-check)
kasperpeulen Apr 20, 2026
5be3f0b
CLI: Capture real ai-setup markdown in trial records and normalize pr…
kasperpeulen Apr 21, 2026
42f4ed9
CLI: Address ai-setup eval review findings
kasperpeulen Apr 21, 2026
d86161f
Docs: Drop stray eslint-plugin.mdx quote-style change
kasperpeulen Apr 21, 2026
86ae333
Merge remote-tracking branch 'origin/kasper/eval-prompts-from-cli' in…
kasperpeulen Apr 21, 2026
9140f6c
Merge remote-tracking branch 'origin/kasper/eval-sync-storybook-versi…
kasperpeulen Apr 22, 2026
62f2fa6
Merge remote-tracking branch 'origin/kasper/eval-sync-storybook-versi…
kasperpeulen Apr 22, 2026
a6aaf4f
CI: type sync-storybook-version env as NodeJS.ProcessEnv
kasperpeulen Apr 22, 2026
d9db8a8
Eval: Type-annotate env literal in sync-storybook-version
kasperpeulen Apr 22, 2026
01068aa
Merge remote-tracking branch 'origin/kasper/eval-prompts-from-cli' in…
kasperpeulen Apr 22, 2026
99ade86
Merge branch 'project/sb-agentic-setup' into kasper/eval-prompts-from…
yannbf Apr 28, 2026
9729a8b
Merge branch 'kasper/eval-prompts-from-cli' into cursor/eval-css-load…
yannbf Apr 28, 2026
14d97d2
simplify css check
yannbf Apr 28, 2026
96b782c
Merge branch 'project/sb-agentic-setup' into kasper/eval-prompts-from…
yannbf Apr 29, 2026
1557244
Merge branch 'kasper/eval-prompts-from-cli' into cursor/eval-css-load…
yannbf Apr 29, 2026
fd6a2b0
add csscheck to pr body
yannbf Apr 29, 2026
3b67e50
Merge pull request #34595 from storybookjs/cursor/eval-css-loaded-pro…
yannbf Apr 29, 2026
8e7c397
Fix inconsistencies in eval README
Sidnioulz Apr 29, 2026
af1479a
Account for PR feedback
yannbf Apr 29, 2026
b35ff84
fix test
yannbf Apr 29, 2026
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
96 changes: 96 additions & 0 deletions code/lib/cli-storybook/src/ai/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

vi.mock('storybook/internal/common', async () => {
const actual = await vi.importActual<typeof import('storybook/internal/common')>(
'storybook/internal/common'
);
return {
...actual,
cache: { set: vi.fn(), get: vi.fn(), remove: vi.fn() },
};
});

vi.mock('storybook/internal/telemetry', () => ({
telemetry: vi.fn(),
getSessionId: vi.fn().mockResolvedValue('session-xyz'),
snapshotPreviewFile: vi
.fn()
.mockResolvedValue({ previewPath: '/proj/.storybook/preview.ts', previewHash: 'abc' }),
isTelemetryModuleEnabled: vi.fn(() => true),
}));

vi.mock('storybook/internal/node-logger', () => ({
logger: { log: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn() },
}));

vi.mock('../../../create-storybook/src/services/ProjectTypeService.ts', () => ({
ProjectTypeService: class {
async detectLanguage() {
return 'ts';
}
},
}));

vi.mock('../automigrate/helpers/mainConfigFile.ts', () => ({
getStorybookData: vi.fn().mockResolvedValue({
versionInstalled: '10.4.0',
frameworkPackage: '@storybook/react-vite',
rendererPackage: '@storybook/react',
renderer: 'react',
builderPackage: '@storybook/builder-vite',
addons: [],
configDir: '/proj/.storybook',
storiesPaths: [],
hasCsfFactoryPreview: false,
packageManager: {},
}),
}));
Comment on lines +3 to +47

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify mocking-pattern compliance in code/lib/cli-storybook/src/ai/index.test.ts
FILE="code/lib/cli-storybook/src/ai/index.test.ts"

echo "== vi.mock declarations =="
rg -n '^\s*vi\.mock\(' "$FILE"

echo
echo "== spy:true usage (expected for package/file mocks) =="
rg -n 'spy\s*:\s*true' "$FILE" || true

echo
echo "== Top-level mock behavior declarations (should generally move to beforeEach) =="
rg -n 'mockResolvedValue|mockReturnValue|vi\.fn\(' "$FILE"

echo
echo "== beforeEach block =="
rg -n -A20 -B2 '^\s*beforeEach\s*\(' "$FILE"

Repository: storybookjs/storybook

Length of output: 1856


Refactor mocks to use spy: true and move behavior setup to beforeEach.

All five module-level vi.mock() calls (lines 3, 13, 21, 25, 33) lack the { spy: true } option and configure mock behaviors (.mockResolvedValue(), vi.fn()) at the module scope. Per repository spy-mocking rules, add { spy: true } to each mock declaration, move all behavior configurations into the beforeEach block via vi.mocked(), and keep module-level mocks pure factory functions.

Example refactor pattern:
// ❌ Current: mock with behavior at module level
vi.mock('storybook/internal/telemetry', () => ({
  telemetry: vi.fn(),
  getSessionId: vi.fn().mockResolvedValue('session-xyz'),
}));

// ✅ Correct: mock with spy: true, behavior in beforeEach
vi.mock('storybook/internal/telemetry', { spy: true });

beforeEach(() => {
  vi.mocked(getSessionId).mockResolvedValue('session-xyz');
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/lib/cli-storybook/src/ai/index.test.ts` around lines 3 - 46, The
module-level mocks for 'storybook/internal/common',
'storybook/internal/telemetry', 'storybook/internal/node-logger',
'../../../create-storybook/src/services/ProjectTypeService.ts', and
'../automigrate/helpers/mainConfigFile.ts' must be pure factories and use { spy:
true }; move all behavioral setup into beforeEach using vi.mocked(...). For each
mocked symbol (e.g., cache.set/get/remove from the common mock, telemetry,
getSessionId, snapshotPreviewFile from telemetry, logger methods from
node-logger, ProjectTypeService.detectLanguage, and getStorybookData) change the
vi.mock call to include { spy: true } and remove any
.mockResolvedValue/.mockImplementation/vi.fn() wiring from module scope, then in
beforeEach call vi.mocked(<symbol>).mockResolvedValue(...) or
.mockImplementation(...) to restore the behaviors (for class methods use
vi.mocked(ProjectTypeService.prototype).mockImplementationOnce or
mockResolvedValue on detectLanguage as needed).


import { cache } from 'storybook/internal/common';
import {
isTelemetryModuleEnabled,
snapshotPreviewFile,
telemetry,
} from 'storybook/internal/telemetry';

import { aiSetup } from './index.ts';

beforeEach(() => {
vi.mocked(cache.set).mockClear();
vi.mocked(snapshotPreviewFile).mockClear();
vi.mocked(telemetry).mockClear();
});

describe('aiSetup telemetry gating', () => {
it('records ai-setup-pending + preview snapshot when telemetry is enabled', async () => {
await aiSetup({ configDir: '/proj/.storybook', disableTelemetry: false });

expect(vi.mocked(snapshotPreviewFile)).toHaveBeenCalledTimes(1);
expect(vi.mocked(cache.set)).toHaveBeenCalledWith(
'ai-setup-pending',
expect.objectContaining({
configDir: expect.stringContaining('.storybook'),
sessionId: 'session-xyz',
previewPath: '/proj/.storybook/preview.ts',
previewHash: 'abc',
})
);
expect(vi.mocked(telemetry)).toHaveBeenCalledWith('ai-setup', expect.any(Object));
});

it('skips snapshot + cache write when telemetry is disabled', async () => {
vi.mocked(isTelemetryModuleEnabled).mockReturnValueOnce(false);

await aiSetup({ configDir: '/proj/.storybook', disableTelemetry: true });

expect(vi.mocked(snapshotPreviewFile)).not.toHaveBeenCalled();
expect(vi.mocked(cache.set)).not.toHaveBeenCalled();
});

it('treats missing disableTelemetry as enabled (backwards compatible default)', async () => {
await aiSetup({ configDir: '/proj/.storybook' });

expect(vi.mocked(snapshotPreviewFile)).toHaveBeenCalledTimes(1);
expect(vi.mocked(cache.set)).toHaveBeenCalledWith('ai-setup-pending', expect.any(Object));
});
});
29 changes: 17 additions & 12 deletions code/lib/cli-storybook/src/ai/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { cache } from 'storybook/internal/common';
import { logger } from 'storybook/internal/node-logger';
import {
getSessionId,
isTelemetryModuleEnabled,
snapshotPreviewFile,
telemetry,
type AiSetupPendingRecord,
Expand Down Expand Up @@ -79,7 +80,7 @@ export async function aiSetup(options: AiSetupOptions): Promise<void> {
return;
}

const result = generateMarkdownOutput(projectInfo);
const result = await generateMarkdownOutput(projectInfo);
const markdownOutput = result.markdown;

await telemetry('ai-setup', {
Expand All @@ -99,17 +100,21 @@ export async function aiSetup(options: AiSetupOptions): Promise<void> {

// Snapshot the preview file baseline and cache the pending setup record.
// Subsequent CLI entry points (dev, build, doctor, etc.) read this to
// collect evidence of what the agent accomplished.
const resolvedConfigDir = resolve(projectInfo.configDir);
const previewSnapshot = await snapshotPreviewFile(resolvedConfigDir);
const sessionId = await getSessionId();
const pendingRecord: AiSetupPendingRecord = {
timestamp: Date.now(),
sessionId,
configDir: resolvedConfigDir,
...previewSnapshot,
};
await cache.set('ai-setup-pending', pendingRecord);
// collect evidence of what the agent accomplished — but only via telemetry
// (the `ai-setup-evidence` event). Skip the snapshot + cache write when
// telemetry is disabled so there's nobody to read it.
if (isTelemetryModuleEnabled()) {
const resolvedConfigDir = resolve(projectInfo.configDir);
const previewSnapshot = await snapshotPreviewFile(resolvedConfigDir);
const sessionId = await getSessionId();
const pendingRecord: AiSetupPendingRecord = {
timestamp: Date.now(),
sessionId,
configDir: resolvedConfigDir,
...previewSnapshot,
};
await cache.set('ai-setup-pending', pendingRecord);
}
Comment on lines +103 to +117

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ValentinFunk what do you think of this? Did you plan a new API for these use cases or is this still the "canonical" way to do things?

I think Kasper is doing the right thing here as we wanna avoid useless compute, but this happens earlier than when we actually can use the telemetry higher order function.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm so sorry Valentin, I need to stop pinging you by accident 😭


if (output) {
const outputPath = resolve(output);
Expand Down
Loading
Loading