Files
open-design/e2e/lib
8b3ec59cfe feat(campaign): DeepSeek V4 Flash free-week surfaces (landing + web + attribution) (#6507)
* feat(campaign): port DeepSeek V4 Flash free-week surfaces from #6450

1:1 port of the demo net diff (base feat/workspace-team) onto main.
Landing home banner, landing pricing benefits, cloud-team-demo review
page, in-app home modal, workbench badge, model-switcher benefit badge,
and the campaign attribution/analytics pipeline. No behavior changes in
this commit; agreed behavior fixes land separately.

* fix(campaign): show the DeepSeek V4 Flash modal only on the active home view

The dialog portals to document.body while EntryShell hides inactive entry
views with display:none, so the campaign escaped the home view and
interrupted projects/tasks/plugins routes. Thread EntryShell's existing
isActive signal through HomeView into a new `active` prop: when inactive
the modal neither opens nor renders (and closes without being marked seen,
so returning to home while still unseen re-opens it).

Requirement: 弹窗只在 #/home 展示,不在编辑器或其他工作流中打断用户。

* fix(campaign): fail closed when frequency-control storage is unreadable

hasSeenCampaign() answered "unseen" when localStorage threw (private
mode / storage disabled), so the modal re-opened on every Home mount even
though markCampaignSeen() could never persist the dismissal. The campaign
promise is one appearance per activity window — an unreadable store now
counts as seen.

* feat(campaign): make the paid 立即使用 CTA switch to DeepSeek V4 Flash for real

产品拍板 D5 + Demo 表格「点击立即使用模型切换为DSV4」: the paid modal CTA
previously only clicked the composer chip open and highlighted it. Now it
performs the actual switch — agent to Cloud (amr), model to
deepseek-v4-flash — through EntryShell's onAgentChange/onAgentModelChange
persistence pair (the same sink InlineModelSwitcher writes through),
threaded HomeView → DeepSeekV4FlashCampaign as onUseCampaignModel. The
chip keeps the data-campaign-highlight pulse as feedback but no longer
opens the picker; the use_now analytics element is unchanged.

* fix(campaign): carry metricsConsent + device id on the unpaid modal upgrade

The unpaid modal's recordAmrEntry was the only campaign touchpoint that
dropped metricsConsent — the workbench badge and the model-switcher
upgrade already forward it — so the consent-gated AMR analytics mirror
never fired and the plans URL missed od_device_id. Thread
config.telemetry.metrics and config.installationId from EntryShell through
HomeView into the modal, and stamp the URL via
amrHandoffDeviceId + attributedAmrUrl(plansUrl, attribution, deviceId).

* fix(campaign): make the campaign/campaignAudience/campaignUsage review params production-inert

产品拍板 D7 + 需求文档「该入口仅用于本地 Demo,不作为线上产品规则」: the
review URL fixtures forced campaign surfaces for real users on released
builds.

Web: new isCampaignReviewAllowed() gate (NODE_ENV !== 'production',
inlined at build time) now fronts isDeepSeekV4FlashCampaignReview, the
campaignAudience override, the modal's shouldForceCampaignReview, and the
switcher's campaignUsage read.

Landing (Astro): inline scripts cannot see build-time constants, so the
home banner, pricing page, and cloud-team-demo inject the verdict as
data-campaign-review-allowed (__OD_LANDING_NOINDEX__: staging/PR previews
true, production false) and preview activation now requires it. The
[locale] wrappers re-render the same components, so they inherit the gate.

* fix(landing): stamp campaign attribution only inside the activity window

Pricing subscribe CTAs and the cloud-team-demo upgrade/pricing clicks
minted __odRecordCampaignEntry with campaignId and stamped
campaign_id on analytics props unconditionally, so clicks outside the
fixed Aug 6-13 window were counted toward the campaign. Both surfaces now
pass the campaign id and the campaign_id prop only while campaignActive
is true; od_entry_* attribution itself is still recorded outside the
window, just without the campaign stamp. Inbound first-touch
od_campaign_id preservation in posthog-analytics.ts is untouched.

* test(campaign): use undefined-safe access for window.open mock calls

Strict TS (tsc -b) flags open.mock.calls[0][0] as possibly undefined in
the modal spec added for the unpaid-upgrade consent behavior; go through
String(...[0]?.[0]) instead. No behavioral change.

* chore(campaign): drop demo-only artifacts before shipping

Remove the cloud-team-demo static review page (fake team cards and
prices, noindex but publicly reachable — review-only material that must
not ship), its locale wrapper and test, and five unreferenced campaign
poster PNGs left over from demo iterations. The team-modal campaign
surfaces the page previewed live in the real Cloud plan-selection
component (powerformer/vela#1379).

* fix(campaign): make review URLs a pure UI projection with zero config writes

Product boundary ruling: the client-side campaign change is a badge next
to DeepSeek V4 Flash (auto-hides after the window) plus the modal CTA
reusing the existing model-selection callbacks — selection behavior must
not change. The demo's review fixtures violated that: an effect force-
switched the persisted agent to AMR under ?campaignAudience=, and the
model write-back effect persisted the review projection into the user's
config. Both writes are removed; review URLs now project AMR + DeepSeek
purely in the UI and leave persisted state untouched.

This also roots out the CI heap OOM in
InlineModelSwitcher.compact-model-click.test.tsx (run 31076791463):
the stateful test wrapper re-rendered on every persisted write, looping
the unconditional write-back forever. With review write-back gone the
file completes in ~10s. The review-URL test now asserts the projection
and that onAgentChange/onAgentModelChange are never called.

* refactor(campaign): remove DeepSeek V4 Flash URL review backdoors

Product decision: the ?campaign= / ?campaignAudience= / ?campaignUsage=
review parameter mechanisms are deleted entirely. Campaign visibility is
now decided only by the real activity window plus the real subscription
audience, and pre-launch acceptance switches to a temporary startAt
constant override instead of URL fixtures.

web:
- campaigns/deepseek-v4-flash.ts: drop review param constants, review
  gate, review detection, and the audience URL override; visibility and
  audience resolution take only `now` (plus plan/loggedIn); restricted
  presentation kept as a reserved branch for the backend usage-limit
  signal (no trigger wired yet)
- DeepSeekV4FlashCampaign.tsx: drop the forced review open and the
  synthetic openedAt+7d countdown; the countdown always runs against the
  real endAtExclusive boundary
- InlineModelSwitcher.tsx: drop campaignUsage URL read, audience
  override, review agent/model fixtures and the currentAgent/model
  review projection (incl. data-campaign-review); 已暂停 and 升级可用
  badge branches stay as reserved presentation flags with no trigger
- EntryShell.tsx / use-deepseek-v4-flash-campaign.ts: no more
  location.search plumbing; home-hero.css review-width rule removed

landing-page:
- index.astro / pricing/index.astro: preview branch removed (review
  params, review-allowed attribute, synthetic preview countdown);
  campaignActive follows the fixed window only
- _lib/deepseek-v4-flash-campaign.ts: reviewParam field removed

tests: review-only specs deleted; convertible specs rewritten as pure
time-window assertions (clock pinned inside/outside the real window);
new negative specs pin that no campaign surface reads URL parameters.

* fix(campaign): device id on badge handoff + impressions on window-open transition

Two review findings from PerishCode, both verified real:

- EntryShell workbench-badge handoff called attributedAmrUrl without the
  device id, so opted-in clicks omitted od_device_id (confirmed in live
  browser acceptance: the modal URL carried it, the badge URL did not).
  Now derives it via amrHandoffDeviceId like the other two touchpoints;
  the source-contract assertion pins the three-arg call.
- The pricing page emitted campaign impressions only during initial
  script execution, so a tab opened before startAt showed the surfaces
  at launch without any surface_view. Impressions now share the reveal
  loop's cadence with a one-time flag, covering both load-time and
  mid-session window-open transitions.

* temp!: open the campaign window early for pre-launch acceptance — REVERT BEFORE MERGE

startAt 2026-08-06T20:00 → 00:00 (+08:00) in both front-end constants so
reviewers can see the real campaign state (true countdown, no review
backdoors) on local runs and the PR preview today. endAtExclusive is
untouched. This commit MUST be reverted before this PR enters the merge
queue — merging it would launch the landing surfaces ahead of 20:00.

* test(campaign): derive the start-instant countdown from the window constants

Pins the assertion to endAtExclusive - startAt instead of a literal
'7天 00:00:00', so the temporary pre-launch start-time override (and any
future window change) cannot silently break the suite.

* Revert "temp!: open the campaign window early for pre-launch acceptance — REVERT BEFORE MERGE"

This reverts commit f123289c0f.

* Reapply "temp!: open the campaign window early for pre-launch acceptance — REVERT BEFORE MERGE"

This reverts commit e472cc81d2.

* test(e2e): seed campaign-modal dismissal into every Playwright context

The DeepSeek V4 Flash home modal fires once per fresh browser profile the
moment the campaign window is open — which is every CI run from launch
onward. It steals focus and covers the elements functional specs are
about to drive, surfacing as 14-minute waitForResponse timeouts in the
UI P0 groups. Seed the one-shot dismissal (a legitimate user state the
product already persists) through a shared context fixture on both test
and clusterTest, so specs stay deterministic inside and outside the
window. One storage entry per campaign; keys stay in sync with the
component's SEEN_KEY.

* test(e2e): seed campaign dismissals into collab cluster contexts

createCollabCluster allocates its own BrowserContext via browser.newContext(),
so the suite-level context fixture never runs for multi-client specs. That left
the DeepSeek V4 Flash home modal free to open and block ensureRailOpen clicks
(UI P0 project-collab / project-workspace-editor). Share the dismissal seeder
and apply it before each cluster client's first page.

* test(e2e): move project collab scenarios to P1

* test(ci): remove project collab from P0 matrix

* test(e2e): mock campaign init scripts in collab cluster

* fix(e2e): restore project-collab P0 gate and collab mock seeding

PerishCode correctly blocked the follow-up that retagged the multi-client
convergence specs as P1 and dropped project-collab from the merge-gated UI P0
matrix: those two cases are the only browser coverage for live two-client
content/presence/unshare and authority-revocation convergence.

- Restore [P0] tags, the project-collab group/matrix/coverage entries, and the
  six-domain / four-domain shadow contracts (matches main).
- Teach collab-cluster unit mocks about addInitScript so the campaign-
  dismissal seed on independently created BrowserContexts does not break E2E
  Vitest.

* fix(campaign): close paid-CTA mode switch and attribution gaps

Address Siri-Ray's blocking review on head 61de050:

1. Paid 立即使用 now flips execution mode to daemon before writing amr +
   deepseek-v4-flash, so a BYOK (mode=api) user does not keep the BYOK
   provider after the modal CTA.
2. Landing campaign helpers preserve od_device_id across Pricing → Cloud,
   treat campaign membership as explicit (no stale inbound od_campaign_id
   inheritance), and Pricing only re-stamps campaign id while active.
3. Daemon AmrEntryAnalyticsPayload/parser validate and forward campaignId +
   conversionSource so the consented AMR mirror no longer strips them.

---------

Co-authored-by: mrcfps <mrc@powerformer.com>
Co-authored-by: Amy <1184569493@qq.com>
2026-08-06 19:51:33 +08:00
..