Files
nexu-io-open-design/apps/daemon/tests/memory-annotations.test.ts
Tom Huang d19c53d274 Agent-driven brand extraction + two-loop memory overhaul (#4260)
* Implement brand management routes and CLI support

- Added `brand-routes.ts` to handle HTTP endpoints for brand operations: listing, extracting, retrieving details, deleting, and serving logos.
- Introduced `brands-cli-help.ts` for CLI commands related to brand management, including usage instructions for listing, creating, retrieving, and deleting brands.
- Updated `cli.ts` to integrate brand commands into the existing CLI structure, allowing headless management of brands via the command line.
- Created supporting files for brand metadata handling, including `design-md.ts` for rendering brand information in markdown format and `index.ts` for the brand engine API.
- Implemented `prefetch.ts` to fetch and process brand material from specified URLs, ensuring a streamlined extraction process.
- Enhanced server setup in `server.ts` to register brand routes and manage brand-related data effectively.

This commit establishes a comprehensive framework for managing brands within the application, facilitating both HTTP and CLI interactions.

* Enhance memory management and onboarding experience

- Introduced canonical profile labels to ensure consistent handling of user input in profile forms, preventing duplicate entries.
- Updated the `parseProfileBody` and `captureProfileFromForm` functions to utilize the new canonical label matching.
- Added a memory callout section in the onboarding view to highlight the benefits of memory usage, including personalized responses and reduced setup questions.
- Implemented new UI elements in the onboarding view to improve user engagement with memory features.
- Expanded i18n support for new onboarding messages related to memory benefits across multiple languages.

* Refactor onboarding flow and enhance design system integration

- Updated the onboarding process to include a new brand extraction step, replacing the previous newsletter step.
- Adjusted the tracking logic to reflect the new onboarding steps, ensuring accurate analytics for user progress.
- Improved the UI for the onboarding view, including new input fields for email collection during the brand extraction phase.
- Refined the EntryShell component to remove outdated comments and clarify the onboarding renderer's purpose.
- Enhanced CSS styles for the onboarding steps to improve layout and user experience.
- Updated internationalization strings across multiple languages to reflect changes in the onboarding flow and brand extraction messaging.

* Add brand management features and enhance font handling

- Introduced new modules for managing brand assets, including `chrome.ts` for headless Chrome operations and `fonts.ts` for self-hosting web fonts.
- Implemented `prefetch.ts` to streamline the brand material extraction process, allowing for efficient harvesting of colors, fonts, and logos.
- Enhanced the brand system with new schema definitions in `schema.ts` to support brand color and font management.
- Developed the `engine` module to integrate brand building and rendering processes, including token derivation and artifact generation.
- Improved the overall structure and organization of brand-related files for better maintainability and scalability.

* Enhance brand extraction and project management features

- Updated `brand-routes.ts` to include new dependencies for project management, allowing for the registration of brand-related projects.
- Modified the `extractBrand` function to support project ID and system files, improving the brand extraction process.
- Enhanced the CLI commands in `cli.ts` to handle project IDs during brand creation, enabling better tracking of brand projects.
- Updated the server setup in `server.ts` to register new project-related routes.
- Improved the UI components to display project information associated with brands, including buttons for opening projects in the `BrandDetailView` and `BrandsTab`.
- Added new metadata fields in the contracts to support project tracking and management for brands.

This commit establishes a more robust framework for managing brand projects, enhancing both backend and frontend functionalities.

* Enhance onboarding profile management and memory persistence

- Added new canonical profile labels for 'Organization size', 'Use cases', and 'Discovery source' to improve user input consistency.
- Introduced `OnboardingProfileState` type to manage onboarding profile data more effectively.
- Implemented functions to build and persist the onboarding profile body to memory, ensuring user selections are saved accurately.
- Updated the `OnboardingView` component to handle profile persistence during navigation and submission steps.
- Enhanced tests to verify that user selections are correctly persisted to the memory profile.

This commit improves the onboarding experience by ensuring that user inputs are consistently captured and stored, enhancing overall user engagement with the application.

* Reflow brand extraction into an agent-driven, live flow

Replace the deterministic SSE prefetch/preview/system pipeline with an
agent-driven extraction: POST /api/brands now reserves the brand and stands
up a backing project with the target site open in an in-app browser tab plus
a seeded prompt, so the agent measures, synthesizes brand.json incrementally,
and the user can clear anti-bot walls by hand. New /preview and /finalize
routes let the agent render the kit page live and register the resulting
user:<id> design system, so extracted brand facts persist as a structured,
reusable brand kit instead of a one-shot deterministic guess.

Adds the brand-extract skill (SKILL.md + brand-kit.html template), kit-render
engine, brand-extraction-engine tests, brand project covers in the Designs
tab, onboarding extract handoff, and the matching od brand extract/preview/
finalize CLI subcommands and contract updates.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Sediment finalized brands into structured memory

Reflow a finalized brand into the memory store (brandToMemoryEntries +
reflowBrandToMemory) so future chats can ground vague requests in the
brand's palette, type, voice and rules. finalizeBrand now wires through
the runtime dataDir and best-effort persists the brand, MemoryChangeEvent
gains a 'brand' source, and the brand kit render hardens its inline JSON
escaping. Adds brand.previewEmpty / brand.viewDetails across all locales.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Implement logo fallback and imagery support in brand extraction

- Introduced a deterministic logo fallback mechanism to ensure that brand extraction processes can retrieve and save site logos, even when the agent fails to do so.
- Enhanced the `startBrandExtraction` and `finalizeBrand` functions to utilize the new logo fallback, allowing for better handling of logo assets.
- Added support for imagery samples in brand validation, enabling the inclusion of representative images in the brand kit.
- Updated the brand kit rendering to include self-hosted fonts and imagery, improving the overall presentation of brand assets.

This commit strengthens the brand extraction workflow by ensuring that logos and imagery are reliably captured and displayed, enhancing the user experience in brand management.

* Enhance memory management with rule proposal and verification features

- Introduced new functionality for distilling annotations into rule proposals, allowing users to suggest rules based on in-canvas annotations through the `od memory rule suggest` command.
- Implemented a verification system that programmatically enforces compliance with active rules during artifact generation, ensuring that all active rules are covered in the self-verify scorecard.
- Added endpoints for managing verification outcomes, including listing, removing, and clearing verification records, enhancing the transparency of the verification process.
- Updated the memory management system to support the retrieval of active rule entries, ensuring that only linked rules are considered during verification.
- Enhanced tests for both rule proposal generation and verification processes to ensure reliability and correctness.

This commit strengthens the memory management capabilities by integrating rule proposals and verification, improving the overall user experience in managing design rules and ensuring compliance.

* Distill review annotations into memory and enforce self-verify scorecard

Add distillAnnotationsToMemory to mine inline preview comments/highlights/
marks into durable feedback + rule memory via a dedicated distiller prompt,
threaded through the existing extract pipeline with an 'annotation' change
source. Tighten the self-verify prompt (daemon + contracts) to state the
daemon programmatically checks the scorecard, so a missing or uncovered
scorecard on an artifact turn is an enforcement failure. Cover the rule
suggest and verification-history routes with tests.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Apply brand design system through web config on "Use in new chat"

Thread onApplyDesignSystem from the entry shell into BrandsTab so the brand's
registered design system is applied via the web config channel instead of a
bare daemon PATCH that left the Home composer stale. Add a transient
home-intent latch + event so the Brands tab can request the Prototype chip on
the already-mounted HomeView, which consumes it once the plugin catalog loads.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Wire annotation distillation into background memory extraction

Add a background distill pass that mines inline review annotations
(comments / highlights / drawn marks) from a turn into durable memory
alongside the general LLM extraction, surface an `annotation` memory
toast source in the web UI, and cover the flow with a unit test.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix brand design system not applying to composer on "Use in new chat"

Selecting a brand's "Use in new chat" applies the brand's design system as
the default and fires the Prototype chip intent in the same synchronous click
handler. HomeView consumed that intent inside the event listener, so `pickChip`
ran before React committed the config change and seeded the composer's
design-system field from the stale (empty) default — the composer showed
"No design system" instead of the brand until a reload.

Split the intent handling: the listener now only bumps a tick, and a separate
effect consumes the chip after the re-render lands, so the seeded design system
reflects the freshly-applied brand. Add the previously-untracked home-intent
latch test coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(web): rework Brands into Brand Kit and add Home create entry

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(brands): harvest real cover/hero images for the Images module

The brand kit's Images gallery only populated when the extraction agent
remembered to save imagery — so a forgetful or bot-blocked agent (and the
pre-imagery "Open Design" brand) left it empty. Add a deterministic,
server-side imagery fallback (imagery-fallback.ts), mirroring the logo
fallback: it parses og:image/twitter:image, large <img> (highest-res
srcset/<picture>), <link rel=preload as=image>, and CSS background-image
hero blocks, fetches candidates with browser-shaped headers, decodes
PNG/GIF/JPEG/WebP dimensions to keep only big representative images
(dropping icons/sprites/logos/tracking pixels), dedupes by content hash,
and saves up to 8 of the largest into imagery/ with labeled samples.

finalizeBrand runs it as a timeout-bounded, failure-tolerant safety net
(injectable so tests stay offline) when the agent captured too few
samples, first adopting any on-disk images. The extraction prompt and
brand-extract SKILL now explicitly direct the agent to harvest the site's
large/cover/hero images, filtered by rendered size.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(qa): implement deck layout validation and safety checks

Add a new QA module for validating the layout of generated brand decks to ensure robustness against clipping and truncation issues. The `analyseDeckLayout` function checks for critical layout invariants, including the presence of `.slide` sections, correct container types, and necessary runtime layers. Introduce `assertDeckLayoutSafe` to enforce these checks during brand system rebuilds, preventing the deployment of decks that fail validation. Additionally, create comprehensive tests to verify the functionality of the new layout validation features.

* fix(brands): apply deck shrink-to-fit synchronously so slides never clip

The no-clip runtime scheduled its fit pass through requestAnimationFrame,
whose callbacks are throttled while the deck is offscreen or occluded. A
slide could therefore stay unscaled — and clip its content — until first
paint. Fit synchronously on resize/load/fonts-ready with a trailing
setTimeout settle pass for late reflow, removing the rAF dependency.

Verified at the previously-broken 1024x620 viewport: container-type:size,
zero truncations, runtime auto-applies scale (Problem 0.71, ASK 0.87,
Product 0.97, Competition 0.97) and frame clip count drops to 0.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(web): let New Brand modal embed scrollable brand reference picker

Add a fillHeight mode to BrandReferencePicker so the heading, quick-pick
row and controls stay pinned while only the gallery scrolls inside a
bounded-height parent. Wire it into NewBrandModal with a stable, spacious
dialog and refresh the related newBrand/brandPicker copy across all 18
locales.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(brands): enhance brand extraction with deterministic seed harvesting

Introduce a new `seed-fallback` module to provide a server-side deterministic palette and typography seed during brand extraction. This ensures that the brand kit's initial display includes a harvested logo, an approximate color palette, and font families, improving the user experience by reducing the all-skeleton appearance during the first paint. Update the `startBrandExtraction` function to utilize this new module, allowing for a more seamless and visually appealing brand extraction process.

Additionally, enhance the `BrandReferencePicker` component to reflect loading states and errors during brand extraction, ensuring users receive immediate feedback on their actions. Update related tests to verify the idempotency of the `finalizeBrand` function, ensuring that re-finalizing a brand correctly reuses the existing design system without duplication.

* feat(brand-extract): enhance BrandReferencePicker and localization updates

Updated the BrandReferencePicker component to reflect loading states and errors during brand extraction, improving user feedback. Added a new localization key for the brand extraction process and updated existing translations in English, Simplified Chinese, and Traditional Chinese to enhance clarity and user experience. Additionally, introduced new styles for better interaction with brand assets in the brand kit template.

* feat(brands): wire in-page lightbox/masonry/asset preview + refine seed

Brand-kit preview improvements for the live extraction kit:
- brand-kit.html: add in-page overlay system (sandboxed iframe has no
  top-nav) — clickable image lightbox with prev/next, a "view all"
  masonry modal, and a full-page asset preview modal that loads
  system/artifacts/<kind>.html in an iframe. Defer auto-reload while an
  overlay is open so it never yanks the modal out mid-interaction.
- seed-fallback.ts: prefer vivid mid-luminance hues for the seeded
  accent/accent-secondary, and drop icon/symbol faces (Remix Icon etc.)
  from the typography seed so specimens never render glyph soup.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(brands): wire in-page lightbox/masonry/asset preview + refine seed

Brand-kit preview improvements for the live extraction kit:
- brand-kit.html: add in-page overlay system (sandboxed iframe has no
  top-nav) — clickable image lightbox with prev/next, a "view all"
  masonry modal, and a full-page asset preview modal that loads
  system/artifacts/<kind>.html in an iframe. Defer auto-reload while an
  overlay is open so it never yanks the modal out mid-interaction.
- seed-fallback.ts: prefer vivid mid-luminance hues for the seeded
  accent/accent-secondary, and drop icon/symbol faces (Remix Icon etc.)
  from the typography seed so specimens never render glyph soup.

Co-authored-by: Cursor <cursoragent@cursor.com>

* i18n(web): add brandPicker.opening across remaining locales + picker test

Completes the brand-reference picker i18n key that was committed only for
en/zh-CN/zh-TW, so every locale satisfies the typed Dict, and lands the
BrandReferencePicker extraction-feedback test left untracked by the
concurrent worker.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(EntryShell): enhance AMR cloud card visibility post-detection

Updated the EntryShell component to ensure the AMR cloud card remains visible after detection settles, even when the AMR runtime is unavailable. This change prevents the card from disappearing and allows it to degrade gracefully to fallback content and sign-in flow. Additionally, added tests to verify the new behavior, ensuring a better user experience during onboarding.

* feat(brands): programmatic-first design system extraction + rename

Make brand extraction two-phase so a usable design system is ready the
moment the user enters a URL — the instant "aha" — instead of waiting on
the AI agent:

1. PROGRAMMATIC-FIRST (synchronous): startBrandExtraction now harvests the
   site deterministically (logo, palette, typography, one-line description,
   cover imagery, source URL) via prefetchBrand, synthesizes a valid design
   system with brandFromMaterial (no LLM), and finalizes + registers it
   before returning. finalizeBrand is refactored into a reusable
   finalizeBrandCore shared by both the programmatic path and the agent path.
2. ASYNC AI ENRICHMENT: the seeded agent prompt is reframed to enrich the
   already-usable design system and re-finalize in place (same user:<id>),
   updating every artifact/template.

Bounded + best-effort: a blocked/unreachable origin skips phase 1 and stays
`extracting` for the agent to drive. Gated on userDesignSystemsRoot so the
legacy agent-only path stays intact for tests.

Also rename the user-facing "Brand Kit" surface to "Design System" across
en + zh-CN strings, project names, and the enrichment prompt.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(brands): restore design-systems nav entry + reconcile BrandsTab on re-activation

Address review feedback on PR #4260:

1. EntryNavRail dropped the only control that reached view==='design-systems'
   when Brands replaced it in the rail, leaving the still-rendered/routed
   design-system manager deep-link only (the entry-nav-design-systems e2e
   specs assert this). Restore a reachable rail entry (blocks icon, existing
   navDesignSystems key) alongside Brands.

2. BrandsTab only fetched once on mount, but EntryShell keeps sub-views
   mounted and toggles visibility, so a brand finishing extraction in its
   backing project never reconciled until a full reload. Refresh whenever the
   Brands view becomes active again, and poll while any brand is extracting
   (torn down once settled / when hidden).

Red spec: tests/components/BrandsTab.refresh.test.tsx (fails pre-fix:
fetchBrands called once, not twice).

* fix(web): surface missing backing projects

* fix(web): re-enable brand actions after use

* fix(daemon): serve brand logos from data roots

* fix(brands): reconcile failed extractions

* fix(brands): non-blocking extraction start + lifecycle-accurate status (#4470)

Acceptance fixes for the Brand Kit surface (PR #4260, even-ferry),
addressing five reported issues:

- Perf: `startBrandExtraction` no longer blocks the POST /api/brands
  response on the synchronous programmatic harvest+finalize. It now waits
  only a short budget (~1.2s) so fast origins still land on a ready,
  applyable design system (the instant "aha"), while slow/blocked origins
  return immediately on the "Extracting…" skeleton and finalize in the
  background. Measured: example.com POST dropped from a ~19s block to
  ~1.2s, then finalized to `ready` in the background.

- Brand Kit stays stuck on "Extracting…" after a manual stop: the backing
  run's canceled/failed status now reconciles the brand to `failed` with a
  reason (this extends the existing reconcile; adds a regression spec).

- Brand Kit ignores a backing run that is awaiting user input
  (anti-bot wall / question form): new reversible `needs_input` brand
  status, derived on read (never persisted) so it flips back once the user
  answers. Surfaced as a "Needs input" badge + hint in the card and a
  distinct status dot in the list; the list keeps polling while paused.

- Home → Recent projects showed brand-extraction projects as
  "Not started": /api/projects now falls back to the brand lifecycle for
  brand projects whose run-derived status is `not_started`
  (extracting→running, needs_input→awaiting_input, ready→succeeded,
  failed→failed), so a live extraction is never shown as not started.

- "Use in new chat" produced no visible result: it now carries a
  confirmation notice through the Home intent channel so Home shows a
  "Using <brand>" toast, making the navigate+apply verifiable (the
  composer already preselects the brand's design system).

Tests: daemon brand-routes (canceled→failed, needs_input transient) and
brand-extraction-engine (fast return + background finalize) specs; web
BrandPreviewCard specs (needs_input badge/hint, Use-in-new-chat notice).
#4 validated end-to-end against a real daemon. guard + typecheck green.

* [codex] Fix brand extraction incomplete states (#4486)

* fix brand extraction incomplete states

* fix web memory tests for advanced modal

Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex)

* fix onboarding P0 brand-step navigation

Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex)

* fix daemon brand extraction test timeout

Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex)

* fix brand extraction fallback prompt state

Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex)

---------

Co-authored-by: Siri-Ray <2667192167@qq.com>
Co-authored-by: Looper <looper@noreply.github.com>

* [codex] Fix brand extraction terminal status reconciliation (#4532)

* Fix brand extraction terminal status reconciliation

* Keep prefetched brand pages previewable

* fix(brands): preserve ready extraction status

Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex)

* fix(brands): persist terminal extraction signal

Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex)

* fix(brands): clear stale terminal retry markers

Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex)

---------

Co-authored-by: Siri-Ray <2667192167@qq.com>
Co-authored-by: Looper <looper@noreply.github.com>

* Fix brand kit modal intent handling (#4528)

Co-authored-by: Siri-Ray <2667192167@qq.com>

* fix(web): let design system contents scroll in narrow review layout (#4526)

Co-authored-by: Siri-Ray <2667192167@qq.com>

* [codex] Fix Brand Kit preview lifecycle guards (#4527)

* Fix brand kit preview lifecycle guards

* fix(brands): keep detail selected on delete failure

Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex)

* fix(brands): handle delete HTTP failures

Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex)

---------

Co-authored-by: Siri-Ray <2667192167@qq.com>
Co-authored-by: Looper <looper@noreply.github.com>

* ci: retrigger pull_request checks

* test(e2e): stabilize entry settings shard

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: PerishCode <perishcode@gmail.com>
Co-authored-by: lefarcen <935902669@qq.com>
Co-authored-by: PerishFire <39043006+PerishCode@users.noreply.github.com>
Co-authored-by: Siri-Ray <2667192167@qq.com>
Co-authored-by: Looper <looper@noreply.github.com>
2026-06-22 08:34:05 +00:00

158 lines
5.4 KiB
TypeScript

import { promises as fsp } from 'node:fs';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { distillAnnotationsToMemory } from '../src/memory-llm.js';
import {
memoryDir,
readMemoryEntry,
readMemoryIndex,
writeMemoryConfig,
memoryEvents,
} from '../src/memory.js';
import { __resetExtractionsForTests } from '../src/memory-extractions.js';
const dataDir = path.join(
process.env.OD_DATA_DIR ?? process.cwd(),
'memory-annotations-test',
);
const originalFetch = globalThis.fetch;
// Helper: stub the OpenAI chat-completions response the distiller's provider
// path will hit (the extraction override below routes through callOpenAI).
function mockOpenAiEntries(entries: unknown[]): void {
globalThis.fetch = vi.fn(async () =>
new Response(
JSON.stringify({
choices: [{ message: { content: JSON.stringify({ entries }) } }],
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
),
) as typeof fetch;
}
describe('annotation → memory distillation', () => {
beforeEach(async () => {
await fsp.rm(memoryDir(dataDir), { recursive: true, force: true });
__resetExtractionsForTests();
await writeMemoryConfig(dataDir, {
extraction: { provider: 'openai', apiKey: 'sk-test', model: 'gpt-4o-mini' },
});
});
afterEach(() => {
globalThis.fetch = originalFetch;
vi.restoreAllMocks();
});
it('auto-keeps feedback + rule memory from preview comments, no manual Keep', async () => {
mockOpenAiEntries([
{
type: 'rule',
name: 'Primary buttons use brand green',
description: 'Standing colour rule for primary actions',
body: 'Assertion: Primary action buttons use the brand green.\nCheck: Inspect primary buttons and confirm their background is the brand green token.',
},
{
type: 'feedback',
name: 'Minimal decoration',
description: 'Keep visuals calm',
body: 'The user dislikes busy, over-decorated layouts and prefers at most two accent colors.',
},
]);
const events: Array<{ kind?: string; source?: string; count?: number }> = [];
const onChange = (e: { kind?: string; source?: string; count?: number }) =>
events.push(e);
memoryEvents.on('change', onChange);
let written;
try {
written = await distillAnnotationsToMemory(
dataDir,
{
annotations: [
{
comment: '这个按钮太花了,主按钮应该用品牌绿',
label: 'Hero CTA button',
currentText: 'Get started',
selectionKind: 'element',
},
],
userMessage: '调整一下首屏',
},
{ projectRoot: process.cwd() },
);
} finally {
memoryEvents.off('change', onChange);
}
expect(written).toHaveLength(2);
// Both entries land on disk (auto-keep) — no Keep round-trip.
const rule = await readMemoryEntry(dataDir, 'rule_primary_buttons_use_brand_green');
const feedback = await readMemoryEntry(dataDir, 'feedback_minimal_decoration');
expect(rule?.type).toBe('rule');
expect(rule?.body).toContain('Assertion:');
expect(feedback?.type).toBe('feedback');
// Linked into MEMORY.md so they actually get injected / enforced.
const index = await readMemoryIndex(dataDir);
expect(index).toContain('rule_primary_buttons_use_brand_green.md');
expect(index).toContain('feedback_minimal_decoration.md');
// The non-blocking toast signal carries the annotation source.
const extract = events.find((e) => e.kind === 'extract');
expect(extract).toMatchObject({ source: 'annotation', count: 2 });
// The model saw the distiller system prompt and the user's comment.
const fetchMock = vi.mocked(globalThis.fetch);
const [, init] = fetchMock.mock.calls[0]!;
const body = JSON.parse(String((init as RequestInit)?.body));
expect(body.messages[0].content).toContain('memory distiller');
expect(body.messages[1].content).toContain('主按钮应该用品牌绿');
});
it('skips annotations that carry no comment (no provider call)', async () => {
mockOpenAiEntries([]);
const written = await distillAnnotationsToMemory(
dataDir,
{ annotations: [{ comment: '', label: 'A card', selectionKind: 'element' }] },
{ projectRoot: process.cwd() },
);
expect(written).toEqual([]);
expect(globalThis.fetch).not.toHaveBeenCalled();
});
it('drops non-feedback/rule candidates the model returns from a critique', async () => {
mockOpenAiEntries([
{
type: 'project',
name: 'Redesign sprint',
description: 'transient',
body: 'A one-off project note that should not come from a design critique.',
},
{
type: 'feedback',
name: 'Denser hierarchy',
description: 'prefers density',
body: 'The user prefers higher information density with a clear visual hierarchy.',
},
]);
const written = await distillAnnotationsToMemory(
dataDir,
{
annotations: [
{ comment: 'too sparse, pack more in', label: 'Section', selectionKind: 'element' },
],
},
{ projectRoot: process.cwd() },
);
expect(written).toHaveLength(1);
expect(written[0]).toMatchObject({ type: 'feedback', name: 'Denser hierarchy' });
await expect(readMemoryEntry(dataDir, 'project_redesign_sprint')).resolves.toBeNull();
});
});