mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
e01efc78f60c85d4f3efd15b65f752d8ff74d837
3135
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e01efc78f6 |
test(e2e): align UI P0 coverage with the current product (#6405)
* test(e2e): preserve useful UI P0 hardening * test(e2e): remove retired Automations coverage * test(e2e): retain Automations smoke coverage Generated-By: looper 0.11.8 (runner=fixer, agent=codex) |
||
|
|
f4b242d66c |
fix(collab): bound background shared-project pulls by manifest entry count (#6558)
* fix(collab): bound background shared-project pulls by manifest entry count Publishing a shared project version made every member daemon materialize the whole tree — hub push lane and catch-up sweeps alike — with no upper bound. In incident #6512 that meant a 7442-file website clone landing on every workspace member's disk, unasked. An authorize-only Vela probe now reads the version's manifest entry count BEFORE any blob transfer, and background lanes defer versions over the threshold (default 2000, OD_COLLAB_BACKGROUND_PULL_MAX_ENTRIES) to the foreground open-project pull, which never consults the guard. Deferrals are remembered per exact scope+version so sweeps never re-authorize. Fail-closed here means PULL AS BEFORE: an older bundled CLI without the probe, a countless or mismatched receipt, and any probe failure all resolve to today's behavior, so a project can never be left permanently unmaterialized. * fix(collab): bind the size probe receipt to the full pull scope The probe compared only projectId, version, and workspaceId, but a workspace holds many principals: a receipt authorized for a different owner or viewer was accepted as counted, and an oversized count there cached a deferral for a scope that was never inspected — suppressing background materialization instead of failing open as the guard promises. Reuse the ordinary pull's receipt validator rather than re-listing fields, so the probe cannot drift from the pull as the receipt contract evolves. Any mismatch now returns uncounted (pull as before). |
||
|
|
4de25d3f1b |
fix(web): 让长演讲者备注在 presenter 视图里滚动而非裁切 (#6271) (#6314)
问题:.presenter 视图里 .notes 这个 aside 作为 body 的 grid item 默认
min-height: auto,不收缩;body 的隐式 grid row 也 auto,跟着内容撑。
当备注文字到几百词时,.notes 把 body row 撑过 viewport,而 body
{ overflow: hidden } 直接切掉底部,.notes-body { overflow: auto }
永远不触发 —— 用户看不到下面的备注,也没有滚动条 (#6271)。
修复:
1. body 加 grid-template-rows: minmax(0, 1fr),把 implicit row 锁到
viewport 高度,row 不再随内容增长。
2. .notes 加 min-height: 0,让 grid item 在 bounded row 里可以收缩,
.notes-body { overflow: auto } 才能真正生效。这跟 .stage 已有的
min-height: 0 模式对称。
回归测试:在 speaker-notes.test.ts 加一个 describe,80 行 ~800 词的
备注塞进 buildSpeakerNotesPresenterHtml,断言:
- body 的 CSS 里包含 grid-template-rows: minmax(0, 1fr)
- .notes 的 CSS 里包含 min-height: 0
- .notes-body 仍然带 overflow: auto
- 完整长备注文本被原样写入 od-presenter-data
锁住这几个不变量,后续 refactor 不会再让长备注被静默裁切。
Co-authored-by: Ray Xi <2667192167@qq.com>
|
||
|
|
7251259334 |
perf(collab): bound vela push fan-out and recover failed publishes (#6564)
* perf(collab): bound vela push fan-out and recover failed publishes Three guardrails for the team-collab publish pipeline: - Merge IGNORED_PROJECT_DIR_NAMES into the vela resource push exclude list (MEMBER_MIRROR_PUSH_EXCLUDED_ENTRIES). Generated/installed trees the project UI already hides from the owner (dist, .next, .venv, coverage, .tmp, __pycache__, ...) no longer fan out to member mirrors; every secret-bearing exclusion is preserved. - Give every vela resource subcommand a wall-clock budget: push 10min (slow-uplink envelope that only reaps truly wedged children), head/shared/remove 60s, pull keeps its existing 30s. A hung CLI now surfaces as an ordinary publish failure instead of pinning the scheduler's in-flight publish forever. - Retry failed publishes on a bounded 5s/15s/45s backoff (max 3 consecutive retries). Success or a fresh author change resets the budget; a change landing mid-publish keeps the existing dirty last-write-wins path without stacking a retry; dispose() clears retry timers. * fix(collab): scope generated-tree exclusions to directories The owner side hides these names as DIRECTORIES only — `collectFiles` consults `shouldSkipDir` inside its `isDirectory()` branch — but they were handed to `vela resource push --exclude`, which matches an entry name whatever its type. A project holding a regular file called `target`, `out` or `dist` therefore had that file silently dropped from every member mirror while the owner kept seeing it, so the two views disagreed with no error anywhere. Found in QA on the previous commit. Send those names in the directory-only trailing-slash form and keep the secret-bearing entries bare, since a credential must stay behind whether it is a file or a directory. The slash doubles as the compatibility seam: a Vela build that does not understand it compares "dist/" against names that never contain a slash, matches nothing, and publishes the full tree — the pre-optimization payload, never a missing file. A new --exclude-dir flag could not degrade that way, because older CLIs reject unknown flags and every publish would fail instead. `.git` and `node_modules` are dropped from the directory-only set: they are already in the bare secret-bearing list, which covers every type. |
||
|
|
cc7d8a5cd5 |
fix(web): recover deck preview after returning to a project tab (#6519)
* fix(web): recover deck preview after returning to a project tab A workspace-scoped deck whose HTML references a relative project asset holds its srcDoc preview empty until GET /api/projects/:id/files answers (the #6142 scoped-asset rewrite hold). The viewer's file-list effect called fetchProjectFiles without an AbortSignal, which pins the shared single-flight entry: a stalled read (queued behind saturated connections; every failure path resolves []) then survives unmount forever and every later mount of the same project + workspace identity rejoins the same dead promise — so leaving and returning to the project showed a permanently white main stage while the thumbnail rail and speaker notes (rendered from source directly) stayed healthy. Two changes: - Abort the file-list read on effect cleanup so a viewer's pending read dies with the viewer and a fresh mount always issues its own read. - Cover the held (empty-srcdoc) preview with the standard artifact-preview-first-load loading indicator instead of showing a bare white iframe, mirroring the URL-load path's cover. * fix(web): remount deck srcdoc preview after a hidden-tab load (#6587) A deck that finishes parsing into its srcdoc iframe while the browser tab is hidden never experiences a real layout pass: Chrome keeps the hidden iframe's child viewport at 0x0, so the deck's one-shot fit resolves to transform: scale(0), and the in-frame recovery loop (chaseFirstLayout, 30 x 50ms resize nudges) is exhausted by background-timer throttling long before the user returns. Neither the host nor the srcdoc bridge listened for visibilitychange, so nothing ever re-ran the fit — the main stage stayed permanently white while the host-rendered thumbnail rail and speaker notes looked healthy. The srcdoc onLoad handler now latches whether the load completed with document.visibilityState === 'hidden'. On the first return to a visible document, the active viewer remounts the srcdoc iframe for a fresh parse (the same mechanism Comment re-activation already relies on) and clears the latch, so visibility round-trips after a healthy visible load never remount (no flicker) and retained viewers (workspaceActive === false) are never touched. Slide position is restored by the existing syncCachedSlideStateToIframe onLoad path. * fix(web): stop pinning the srcDoc loading cover over a loaded empty file The srcDoc first-load cover keyed on srcDoc emptiness alone (!srcDocTransportContent), which cannot distinguish "content still pending" (previewSource === null: the scoped-asset hold or a Reload's synchronous clear) from "loaded, zero bytes" (previewSource === ''), so a legitimate empty HTML file kept a permanent role=status/aria-busy loader with no request in flight. Narrow the condition with an explicit previewSource === null pending check; the redirect-loop-blocked and lazy-transport branches are untouched. * fix(web): run the hidden-load recovery check on viewer activation The visibilitychange recovery listener only exists while the viewer is active. When a retained deck's load completes in a hidden browser tab (latch armed, correctly no action) and the browser returns to visible BEFORE the user clicks back into the project, the event fires with no listener armed and never replays — activation then re-arms the listener but the latch dangles and the 0x0-parsed deck stays white on exactly the switch-back-to-the-project-tab flow. Run one immediate check after addEventListener: the handler's own guards (visible + latch + srcdoc active + deck) make it a one-shot no-op everywhere else, and the latch keeps it idempotent across effect re-runs. |
||
|
|
254d312876 | fix(daemon): allow workspace-less design system deletion (#6591) | ||
|
|
5e826dc1e2 |
fix(daemon): read Qwen Code's configured models instead of two hardcoded ids (#6546)
The Qwen picker was pinned to a static two-entry fallback (qwen3-coder-plus / qwen3-coder-flash) because qwenAgentDef declared fallbackModels with no fetchModels. Qwen Code has no --list-models flag, but it does persist its configured models in ~/.qwen/settings.json under modelProviders.<provider>[].id plus the selected 'model'. On a current install that file lists qwen3.8-max, qwen3.7-max, qwen3.7-plus, qwen3.6-plus and others, none of which the picker could offer — the user had to retype one as a custom model string. Add runtimes/qwen-settings.ts, mirroring mmd-routes.ts (the same problem solved for Claude Code): read the CLI's own config, merge discovered ids ahead of the static fallbacks, and degrade to the fallbacks when the file is missing or malformed. Provider keys are not allowlisted so a Qwen release that adds one keeps working, and only model ids are read — the DASHSCOPE_API_KEY that lives in the same file is never touched. 5 new tests in tests/runtimes/qwen-settings.test.ts, including explicit assertions that no secret from settings.json reaches the model list. tests/runtimes: 694 passed, 1 skipped. tsc --noEmit clean for src+tests. |
||
|
|
34192c6719 |
fix(diagnostics): keep the previous session's daemon log and cut event tails on line boundaries (#6531)
* fix(diagnostics): keep pre-restart daemon log and line-aligned event tails Two gaps made incident support bundles useless after a relaunch: 1. Every packaged launch opened each sidecar's latest.log with mode "w", destroying the previous session's log — the bundle held only the ~70 lines written since the restart while the incident-time daemon log was gone. openLog now rotates the prior file aside as previous.log (exactly one prior session, rename overwrites), and the diagnostics export bundles logs/<app>/previous.log whenever it exists, under the same per-file tail cap. The source entry is existence-conditional so non-rotating launchers (tools-dev appends) do not gain missing-file placeholder noise. 2. The 2MB tail cap on runs/*/events.jsonl cut bytes from mid-file, so the first kept line was half a JSON object and jsonl consumers fell over on line 1. readMaybeTail now advances the cut to the next line boundary (with a guard byte so an already-aligned cut loses nothing); a window with no newline keeps the raw cut. * fix(packaged): keep the prior log when rotation fails instead of truncating it A non-ENOENT rename failure (Windows share-lock on previous.log, read-only or exotic filesystem) still fell through to open(path, "w"), destroying the only copy of the pre-restart log while previous.log stayed unavailable — the exact data loss this rotation exists to prevent. Truncate only when the prior bytes are provably elsewhere: moved aside, or never written (first-launch ENOENT). Otherwise open in append mode, since a merged two-session log is recoverable and an erased one is not. * fix(diagnostics): never export a partial record, and never hide an unreadable log Two holes in the truncation/collection contract: - A single event larger than the whole tail budget left no complete line to align to, and the fallback kept the raw cut — so the export shipped the interior of one JSON record and any consumer streaming the bundle through a jsonl parser died on line 1, exactly what alignment exists to prevent. Signal "no complete line" instead and let the collector record it, so the bundle carries a self-describing placeholder rather than half an object. - The optional-source gate treated EVERY access failure as absence, so a previous.log that exists but is unreachable (EACCES on the log directory, EIO) vanished with no manifest entry and no warning. Only ENOENT may drop the entry now; anything else stays listed and reports the real error. |
||
|
|
0d00eb298a |
feat(daemon): forward skills[] as skillIds[] on start_run MCP tool (#6429)
The MCP bridge exposed only a single skill on start_run; any extra skills[] an agent passed were silently stripped by additionalProperties: false and the handler reading only args.skill. The daemon already composes supplemental skill ids server-side (POST /api/runs spreads the body; composeDaemonSystemPrompt dedupes and stages each skill), so the bridge just needs to serialize them. - McpArgs: add skills?: string[] - start_run inputSchema: add skills (array of strings) - startRun(): forward args.skills as body.skillIds Tests: start_run POSTs skills[] as skillIds; skillIds omitted when skills not provided. Co-authored-by: Newton <newton@mcclean.codes> |
||
|
|
1347ce4e48 |
fix(packaged): fail od:// proxy fast on local resource exhaustion (#6530)
When the machine is out of sockets/file descriptors (net::ERR_INSUFFICIENT_RESOURCES, EMFILE, ENFILE, EADDRNOTAVAIL, ENOBUFS), the proxy's two resilience layers amplified the outage: the undici fallback re-issued every failed net.fetch (7890 doubled requests measured in the incident) and the linear-backoff transient retry stacked another 1334 attempts on top — each consuming exactly the resource the machine had run out of. Classify these errors through one whitelist helper (isLocalResourceExhaustionError, matching both Error.code and the message since Electron net.fetch reports net::ERR_... in the message with no code) and make both layers fail fast: no undici fallback, no retry. Everything outside the whitelist keeps today's behavior — ECONNREFUSED still buys the full retry budget (daemon startup race) and generic net.fetch rejections still fall back to undici (CSS-mask GET rescue). |
||
|
|
3fce756b5e |
docs(landing): publish Open Design 0.17 and 0.18 release notes (#6585)
Co-authored-by: xne998808-ai <276262049+xne998808-ai@users.noreply.github.com> |
||
|
|
19d66e9294 |
docs(release): add 0.18.1 release notes (#6560)
* docs(release): add 0.18.1 release notes * docs(release): credit the desktop sign-in hand-off in 0.18.1 notes * docs(release): correct campaign window, cloud fix scope, and BYOK claim * docs(release): correct cloud fix scope and BYOK claim in 0.18.1 notes |
||
|
|
23c5f421bf | ci(backport): label conflicted backports so the release pre-flight check has a signal (#6574) | ||
|
|
9065a67a5b |
fix(web): stop provisional comment pin numbers colliding after deletion (#6517)
* fix(web): stop provisional comment pin numbers colliding after deletion The provisional pin bubble for a brand-new, not-yet-saved comment was numbered comments.length + 1, but the daemon assigns MAX(pin_seq) + 1 on create. Pin numbers are permanent (a deletion retires its number), so after deleting an earlier comment the count-based guess collided with a pin still on the canvas (e.g. one surviving comment numbered 2 -> new pin also showed 2). Introduce provisionalNextPinNumber() in apps/web/src/comments.ts, which mirrors the daemon's MAX(pin_seq)+1 invariant over the rendered marker numbers, and use it for the active-pin preview in FileViewer. * fix(web): retire non-open comments' pin numbers in the provisional pin Review follow-up (PR #6517): the provisional pin number was computed over creationSortedSideComments, which filters to the current file AND status === 'open' — narrower than the daemon's MAX(pin_seq) scope, which scans every row for the project/file. A resolved/attached/failed comment keeps its pin_seq row, so with a hidden pinSeq of 2 and no open pins the bubble showed 1 while the daemon would assign 3. Compute provisionalNextPinNumber over the file's comments across ALL statuses in FileViewer and pass the result into CommentPreviewOverlays, so the overlay's open-only marker list stays untouched while the provisional number mirrors the daemon's full scope. |
||
|
|
a1995a186a |
test(e2e): harden UI flake waits with real completion signals (#6539)
* test(e2e): harden UI flake waits with real completion signals Replace synthetic /projects routing, blind boot sleeps, and call-site create retries with observable readiness: real navigation, suite daemon health warmup, expect.poll, clock fast-forward for coalesce TTL, and quiescence for AMR refresh traffic. * test(e2e): raise Playwright fixture timeout for daemon warmup The suite fixture now polls daemon /api/health for up to 60s, which consumed the prior 390s budget's bookkeeping margin. Raise the worker fixture timeout to 450s so start + web/daemon warmup + stop still leave a full minute for teardown bookkeeping on slow CI workers. Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build) * test(e2e): fail closed on health body drain and narrow billing mock Require warmPlaywrightDaemonRuntime to fully consume ok health bodies before declaring readiness, drain non-ok probes, and fulfill only the exact /api/workspace/billing path so nested billing routes fall through. Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build) * test(e2e): reopen projects by id in cross-project file-tab restore Avoid relying on chrome workspace tabs surviving leave+create hard navigation; assert per-project file-tab restore via project routes. |
||
|
|
caae6a373e |
feat(web): localize DeepSeek V4 Flash campaign (19 locales) (#6561)
* feat(web): localize DeepSeek V4 Flash campaign across 19 locales Move all campaign user-facing copy into Dict keys so modal, model badges, and the workbench pricing badge follow the active UI language. Also route personal free accounts through amrAccountPlan and portal the campaign badge above workspace-tabs chrome so it stays clickable. * fix(web): fully translate DeepSeek campaign copy for all 19 locales Replace English placeholders in incomplete locale packs and add a matrix test so non-English campaign strings cannot silently equal the EN fallback (with an explicit allowlist for intentional day-unit reuse). * ci: re-trigger validation after runner queue stall * ci: bootstrap Resolve runner profiles on Blacksmith when mode is blacksmith OD_CI_RUNNER_MODE=blacksmith still scheduled the profile-resolution job on nexu-runners-small, so a private ARC stall blocked every trusted PR before workloads could reach Blacksmith. Align bootstrap with the mode contract. * ci: ensure pull_request workflow fires for blacksmith bootstrap * revert: drop Blacksmith CI bootstrap change from campaign PR Keep this PR scoped to DeepSeek campaign i18n + badge/audience fixes. The runner-bootstrap alignment belongs in a separate CI PR with topology coverage, not a backport-labeled product fix. * ci: re-run validation on GitHub-hosted runners (economic mode) * ci: re-fire pull_request checks for Validate workspace |
||
|
|
4e47119edb |
ci: right-size UI P0 runners to medium (#6502)
Measured UI P0 shards fit nexu-runners-medium; drop the blanket xlarge assignment and keep only project-collab on ui_p0_heavy (xlarge) because multi-client collab has hit large/xlarge memory ceilings. |
||
|
|
5580736c1a |
fix(e2e): align packaged onboarding smoke selector (#6538)
* fix(e2e): align packaged onboarding smoke selector * test(e2e): guard packaged onboarding selector drift * fix(e2e): align platform onboarding probes Generated-By: looper 0.11.8 (runner=fixer, agent=codex) * fix(e2e): align packaged smoke with identity gate Generated-By: looper 0.11.8 (runner=fixer, agent=codex) |
||
|
|
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 |
||
|
|
370479e4a7 | fix daemon Langfuse tool status noise (#6457) | ||
|
|
3fb17262bb |
test(daemon): stabilize environment-sensitive baselines (#6371)
Co-authored-by: Ray Xi <2667192167@qq.com> |
||
|
|
b18aad6bbe |
Add Shanghai workshop recap (#6440)
Co-authored-by: koki yanlai xu <koki@kokideMacBook-Air.local> |
||
|
|
1aad2f5ef6 |
fix(release): treat an empty windows smoke mode as core (#6524)
* test(e2e): pin that an empty windows smoke mode means core Two independent holes let an empty value read as a different profile: The workflow-level one — a workflow_call `default:` applies only when an input is OMITTED, so forwarding '' defeats the declared `core` default. The spec-level one — `??` falls back only on null/undefined, so an empty environment variable survives it and `smokeProfile === 'core'` is false. Together they made release/v0.18.1's first prerelease take the `full` path, demand the updater fixture only a genuine `full` request configures, and die before the smoke started — on the branch-cut commit, before anything else had landed there. release/v0.18.0 never showed it because its branch name matched a special case that produced `skip`, so its smoke never ran at all. Red on both layers: the resolver mirrors main's `?? 'core'` verbatim. * fix(release): treat an empty windows smoke mode as core Close both layers that let an empty value read as a different profile. notify-release-feishu now names `core` as its final arm instead of ''. A workflow_call `default:` applies only when an input is omitted, so forwarding an empty string defeated the callee's declared default. win.spec.ts resolves the profile through `resolvePackagedSmokeProfile`, which treats empty, whitespace and unset alike as `core` and rejects anything it does not recognise — a typo must not select the updater path the way '' did. Either fix alone would unblock release/v0.18.1, but they guard different holes: one stops an empty value being produced, the other stops one being misread. |
||
|
|
b014b969c6 |
fix(web): lazy-render design file grids and gate thumbnail fetches (#6513)
* fix(web): lazy-render design file grids and gate thumbnail fetches The design files panel root view intentionally lists every nested file, so a web-clone project can put 4000+ HTML files in one section. Every page card rendered at once, and every HtmlCardThumbnail fetched its content on mount, exhausting local sockets (net::ERR_INSUFFICIENT_RESOURCES) and starving the web<->daemon proxy. - Thumbnail content fetches now start only once a card nears the viewport (IntersectionObserver, 800px bottom rootMargin) and pass through a FIFO pool capped at 6 concurrent fetches; slots return on settle and on unmount, including tasks still queued. - The page-card grid and image masonry render incrementally (48 per batch) behind an invisible end-of-grid sentinel keyed per batch, so the grid reads exactly like a fully rendered one; directory and tab changes reset the revealed count. - Environments without IntersectionObserver keep the previous eager behavior. * fix(web): hold thumbnail fetch slots until the request settles Review follow-up: releasing a slot during effect cleanup freed pool capacity while the underlying fetch was still on the network, so a directory/project switch with six thumbnails pending could start six replacement fetches and push real connection concurrency to twelve. Cleanup now only abandons the reservation: queued tasks are dropped before they start, but a started request keeps its slot until it settles, so the cap bounds actual network concurrency across unmount/remount churn. The regression test encodes the exact switch-while-pending scenario. |
||
|
|
886bcc4db0 |
feat(web): streamline onboarding model source flow (#6475)
* feat(web): streamline onboarding model source flow * test(e2e): align onboarding coverage with cloud-first entry * test(e2e): isolate cloud status in non-auth specs * test(e2e): keep restoration focused on tab state * test(e2e): close cloud-first onboarding gaps * test(e2e): align runtime checks with cloud-first gate * ci(e2e): retire automations from P0 gate --------- Co-authored-by: Amy <1184569493@qq.com> |
||
|
|
99fc8a45bc |
Revert "feat(design-systems): add structured runtime contract (#6456)" (#6520)
This reverts commit
|
||
|
|
ad4f3609ca |
fix(collab): quell the packaged-client cloud 502 storm (heartbeat backoff, upstream error relay, proxy replay) (#6521)
* test(collab): red specs for the packaged-client presence/proxy 502 storm Encode the three client-side halves of the 502 storm as failing specs on main before any fix: - web: a persistently failing presence upstream is heartbeated every 10s forever (62 POSTs in 10 minutes) with no backoff. - daemon: every vela CLI presence failure is flattened into a 502 whose message embeds the spawned command line; upstream 403/404 rejections are indistinguishable from infra outages, every beat re-spawns the CLI, and presence spawns carry no timeout. - web sidecar: an idempotent /api request that dies with a connection reset on a reused pooled daemon socket surfaces as a synthesized 502 instead of being replayed once on a fresh connection. createDaemonProxyHandler is exported as the proxy spec's seam. * fix(collab): stop the packaged-client presence/proxy 502 storm Three client-side halves of one incident (hours of ~2050 DevTools errors: POST /presence/heartbeat and PUT /workspace/billing/interests 502ing every few seconds while the collab upstream rejected one project): - web CollabClient: presence heartbeats move from a fixed 10s interval to a self-scheduling chain with exponential backoff on consecutive failures (10s -> 20s -> 40s -> ... capped at 5min, +/-10% jitter). One success or a new presence identity restores the base cadence, so recovery is untouched. - daemon presence routes: vela CLI failures are classified via the CLI's 'API request failed with status NNN' output. Upstream 401/403 relay as 403, 404 as 404, timeouts as 503; only genuine infrastructure failures keep 502. Response bodies no longer embed the spawned command line or stderr (daemon log keeps the full detail). Repeated consecutive failures for one project open a 20s negative cache that answers without spawning another CLI process, and presence spawns now carry a 10s timeoutMs so a wedged CLI cannot pile up children. - web sidecar proxy: proxied requests use an explicit keep-alive agent whose pooled idle sockets die well before the daemon's 120s keep-alive window, and an idempotent request with a small fully-buffered body that fails with ECONNRESET/EPIPE on a reused pooled socket before any response bytes is replayed once on a fresh connection instead of surfacing a synthesized 502 the daemon never sent. Non-idempotent methods and large bodies keep the streaming path and are never replayed. |
||
|
|
9ce7b3aeec |
fix(byok): restore BYOK model switching in the home and composer pickers (#6515)
* fix(byok): restore BYOK model switching in the home and composer pickers Two #6142 regressions with the same root shape: a model-picker surface stopped deriving its catalogue from the active execution mode. - InlineModelSwitcher: the compact home popover unconditionally rendered the local CLI agent's models, so a BYOK chip (e.g. gpt-4o) opened onto the Open Design cloud catalogue. The popover body now always reflects config.mode; compact only chooses layout density. - AvatarMenu: the project composer popover collapsed the BYOK model area into a read-only box, dropping the previously working model switcher. BYOK mode now offers a selectable provider catalogue (shared cache + local discovery fetch + curated seeds) writing through onApiModelChange, mirroring the daemon-mode list. * test(e2e): expect the BYOK catalogue in the topbar switcher visual case The settings-workspace visual case pinned the #6142 regression as expected behavior: it asserted the compact BYOK popover has no in-place model dropdown and only a Settings hint. With the BYOK catalogue restored, the case now asserts the provider model picker renders in place (current model shown, options selectable from a deterministic mocked catalogue) while the execution-configuration guards stay: no mode segmented control and no provider tabs in the top bar, and the Settings route entry remains. |
||
|
|
2a971e4f0e | ci: add Blacksmith runner mode (#6533) | ||
|
|
66004e0cd7 |
test: raise CI flake budgets for waitFor and wall clocks (#6415)
* test: raise CI flake budgets for waitFor and wall clocks
Web jsdom waitFor was dying on the 1s default under runner contention.
Daemon deploy wall-clock asserts, logo body-timeout margin, and vela
fixed sleeps had zero/thin slack on slow CI CPUs.
* test: scope flake budgets and restore ReDoS guard
Keep the doctype prolog linear-time assert at 500ms so a reintroduced
lazy-body regex (~5s) cannot pass. Drop the global waitFor 5s raise and
apply a 2s budget only to the App.connectors privacy-choice waits that
flaked at 1015–1093ms under CI contention.
Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build)
* test: restore CSS budget and abort-aware logo body mock
Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build)
* test: wait for Vela login close before no-fallback assert
loginInFlight can go idle between child exit and close after cancel,
so the canceled late-fallback test now waits on supervisor settlement
(close/error terminal + pending fallback) instead of status idle.
Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build)
* test: narrow AbortSignal for logo body mock typecheck
Preflight failed because init.signal was still possibly null after the
optional guard. Capture a local signal so tsc can prove it is defined.
Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build)
* test: use global Testing Library asyncUtilTimeout 5s
Raise waitFor/findBy failure budget in the shared jsdom setup instead of
scoping a 2s override to App.connectors privacy cases. Drop no-op deploy
comment churn that did not change the ReDoS wall-clock guards.
* test: scope waitFor budget to privacy waits only
Drop suite-wide Testing Library asyncUtilTimeout 5s so unrelated
waitFor/findBy failures still surface assertion errors quickly.
Restore a 2s timeout only on App.connectors first-run privacy waits
that flaked under CI contention.
Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build)
* Revert "test: scope waitFor budget to privacy waits only"
This reverts commit
|
||
|
|
72b1db761b |
fix(e2e): let the Windows packaged smoke settle on the cloud sign-in landing (#6481)
* test(e2e): pin the Windows packaged app-shell rule (red)
Extract the Windows packaged smoke's app-shell DOM probe and the rule that
reads it into a pure module, and cover both from a platform that can actually
run them. The probe reaches the packaged renderer as a plain string through
`tools-pack inspect --expr`, and `specs/win.spec.ts` only executes on a Windows
runner with a packaged build installed, so today neither is checked anywhere.
Behavior is unchanged: the rule still accepts only `homeVisible === true`, the
same test main's inline `ensureMainAppShell` applies. The two new cases that
describe a first run resting on the cloud sign-in landing therefore fail, which
is the bug — a packaged first run nobody signs in to can never reach home
unattended, so the wait can only time out.
× settles on the cloud sign-in landing when the profile only needs a rendered surface
× still requires home when the profile has to drive the entry rail
AssertionError: expected null to be 'onboarding-landing' // Object.is equality
- Expected: "onboarding-landing"
+ Received: null
Test Files 1 failed (1)
Tests 2 failed | 10 passed (12)
* fix(e2e): accept the packaged Windows onboarding landing as a settled shell
A packaged first run that nobody signs in to comes to rest on the cloud
sign-in landing, and `connectStepRuntimeReady` in EntryShell.tsx will not
advance without a signed-in cloud account, an installed local CLI, or a
verified BYOK key — none of which a release runner has. The core Windows smoke
only screenshots from this point, so the landing is a legitimate terminal
state there; demanding home made the wait time out on a healthy app.
The landing is recognised positively, from the same three affordances the [P0]
onboarding smoke asserts (sign-in CTA plus both runtime links), so a blank
window, a crashed renderer, a boot still on the loader, or an onboarding shell
that mounted and then died all still fail — with a named cause rather than an
opaque timeout.
The full profile goes on to click `entry-nav-updater`, which
`clickUpdaterRailExpression` refuses while onboarding is up, so it keeps
demanding home and now fails here with that stated reason instead of later
with a bare `onboarding-visible`.
Test Files 1 passed (1)
Tests 12 passed (12)
* test(e2e): pin the seeded-onboarding postcondition (red)
PerishCode's review on #6481: the core run seeds onboarding as completed and
then accepts onboarding anyway, so a broken completed-onboarding boot path
passes silently.
Name the decision — `packagedAppShellPolicy` — and cover it. The
implementation still keys off the smoke profile, exactly as the previous commit
left it, so the cases that say a run whose daemon confirms completion must
still require home fail.
× requires home once the daemon confirms onboarding is completed
× keeps a seeded run failing when the app ignores completed onboarding
AssertionError: expected { acceptOnboardingLanding: true } to deeply equal { acceptOnboardingLanding: false }
Test Files 1 failed (1)
Tests 2 failed | 16 passed (18)
* fix(e2e): derive the accepted app shell from the daemon's onboarding config
Setup and expectation now come from one fact instead of two. The smoke reads
`GET /api/app-config` — which serves `readAppConfig(RUNTIME_DATA_DIR)`, the
running daemon's own resolved data root — and the accepted terminal state
follows from it:
- daemon confirms onboarding is completed -> home is the only outcome, so a
renderer that ignores a completed-onboarding config still fails the core
release lane;
- daemon reports a genuine first run -> the cloud sign-in landing is the
correct place to stop, and is accepted only when it actually rendered its
sign-in CTA and both runtime links.
The seed's postcondition is also asserted directly, right after the seeded
`tools-pack win start` where it must hold: that launch rewrites the packaged
config's `namespaceBaseRoot` to the tools-pack runtime root
(tools/pack/src/win/lifecycle.ts) and the runtime derives
`join(namespaceBaseRoot, namespace, 'data')` from it
(apps/packaged/src/paths.ts), which is byte-identical to the path
`seedPackagedOnboardingComplete` writes. A daemon that does not see the seed
there is a real data-root regression and now says so, instead of surfacing
later as an unexplained onboarding screen.
The app-shell read is deliberately taken again rather than reused: the core
profile stops the app and relaunches it through the OS protocol handler, and
that cold start inherits none of this process's environment.
Test Files 1 passed (1)
Tests 18 passed (18)
* test(e2e): pin the daemon onboarding-config probe (red)
PerishCode's second review on #6481: the probe converts an HTTP failure into
`onboardingCompleted: false`, which is the exact branch that permits the
onboarding landing. `status` is returned and never read, and the caller's guard
only rejects a malformed shape — so a 500, a 404, or a daemon that is not
serving yet resolves to "genuine first run" and the core smoke passes without
ever establishing the fact its shell policy depends on.
Move the probe text and its reading into the pure module verbatim, then drive
the shipped text against a fake fetch. Behavior is unchanged, so every failure
mode reads as a valid `false`.
× refuses to answer when the daemon returns a server error
× refuses to answer when the route is missing
× refuses to answer when the daemon is not reachable at all
× refuses to answer when the response carries no config
× names the status so a failure says what could not be established
× never lets a failed probe become permission to accept the landing
AssertionError: expected function to throw an error, but it didn't
Test Files 1 failed (1)
Tests 6 failed | 21 passed (27)
* fix(e2e): fail closed when the daemon onboarding config cannot be read
The probe now reports failure structurally instead of flattening it into a
reading: a transport error, a non-2xx, or a body without `config` each come
back as `{ ok: false, status, error }`, and `packagedOnboardingCompletedFromProbe`
raises `PackagedOnboardingConfigError` for all of them. `status` is load-bearing
— only a 200 that actually carried a config object may produce a reading.
There is deliberately no default and no retry. `waitForHealthyDesktop` has
already proven the same renderer gets 200 from `/api/health`, so a failure here
is a real fault; and the only fallback value available is `false`, which is
exactly the branch that permits the onboarding landing. An unestablished fact
must be an error, never a permission.
Test Files 1 passed (1)
Tests 27 passed (27)
* test(e2e): pin malformed-but-successful onboarding config payloads (red)
PerishCode's third review on #6481, same defect class as rounds 1 and 2:
coercion manufacturing a value that hides the absence of one.
`config.onboardingCompleted === true` turns anything into a valid boolean, so a
200 carrying `{ config: {} }` or `{ config: { onboardingCompleted: "false" } }`
returns `{ ok: true, onboardingCompleted: false, status: 200 }`. The reader's
`typeof ... !== 'boolean'` check then passes, because `=== true` already
synthesized the boolean it was meant to require — the round-3 guarantee that a
reading must "be present" was vacuous as written.
× refuses to answer when a 200 carries no onboardingCompleted field
× refuses to answer when onboardingCompleted is the wrong type
× refuses to answer when config is an array rather than an object
AssertionError: expected { ok: true, …(2) } to match object { ok: false }
Test Files 1 failed (1)
Tests 3 failed | 27 passed (30)
* fix(e2e): require the onboarding reading's type instead of coercing it
The probe now returns `{ ok: false, error, status }` when a 200 carries no
boolean `onboardingCompleted`, so a missing key, a null, or the string "false"
can no longer arrive as a real `false` reading — which was the one value that
permits the onboarding landing. `Array.isArray` also rejects an array `config`,
which `typeof === 'object'` let through.
This makes the round-3 guarantee real rather than vacuous: the reader's
`typeof ... !== 'boolean'` check now runs against a value the probe declined to
synthesize.
Swept the rest of the diff for the same defect class and tightened the two
remaining truthiness reads on the policy path, so the closed direction is
structural rather than dependent on a caller staying honest:
- `packagedAppShellPolicy` requires an explicit `false` before granting
permission; anything else requires home.
- `packagedAppShellSettled` requires `acceptOnboardingLanding === true`.
Everything else on the path already failed closed: the app-shell probe's
`instanceof` flags must all be positively true to settle, `asPackagedAppShellSnapshot`
type-checks every field before use and returns null otherwise, and the reader's
`status`/`ok` guards can only reject.
Test Files 1 passed (1)
Tests 32 passed (32)
* test(e2e): pin that a lost seed is not a genuine first run (red)
PerishCode's fifth review on #6481 — the same family, now at the level of the
scenario rather than a single expression.
The run seeds `onboardingCompleted: true` and asserts the seeded daemon reads
it back. The core profile then stops the app and relaunches it through the OS
protocol handler, which inherits none of the test process's environment. If
that cold launch resolves a different data root the seeded config is gone, the
fresh reading is `false`, and the policy calls it a genuine first run: the
landing is accepted and the smoke passes while blind to the regression it
exists to catch. Recording both readings diagnoses it; it does not fail.
Give the policy the seeded observation and leave the implementation ignoring
it, so the two cases that describe a lost seed fail.
× never downgrades a lost seed to a genuine first run
× keeps a lost seed failing at the shell check
AssertionError: expected { acceptOnboardingLanding: true } to deeply equal { acceptOnboardingLanding: false }
Test Files 1 failed (1)
Tests 2 failed | 32 passed (34)
* fix(e2e): make the seeded onboarding state authoritative across the relaunch
Chose the seed-authoritative shape over making the core scenario unseeded: an
unseeded core run would discard the completed-onboarding boot path, which is
exactly what the first review in this chain said not to lose, and every release
workflow defaults `win_x64_smoke_mode` to `core`, so that path would stop being
exercised anywhere routine.
Once this run has seeded completion and observed the daemon confirm it, a later
`false` is a regression, never new information about the user:
- `assertSeededOnboardingRetained` raises `PackagedOnboardingSeedError` naming
the cold launch and the data root to check;
- `packagedAppShellPolicy` additionally refuses permission for any run whose
seeded observation is not an explicit `false`, so the loss cannot buy the
landing even if the assertion were bypassed.
The spec asserts the seeded observation exists rather than coercing `'skipped'`,
which would have read as "never seeded" and handed the permissive branch the one
input it must not get.
This may surface a real packaged-runtime bug on the next Windows release run:
the protocol cold launch inherits none of the test process's environment, so if
it resolves a different data root the seed is genuinely gone. Failing there is
the correct outcome, not a reason to loosen the check.
Test Files 1 passed (1)
Tests 36 passed (36)
* test(e2e): pin both packaged launch scenarios as transitions (red)
PerishCode's sixth review on #6481: the run seeds unconditionally, requires the
first reading to be true, and round 5 requires that seed to survive — so
`seededObserved` is always true at the only policy call site. The policy can
only return `acceptOnboardingLanding: false`, and the unseeded unit cases
exercise a state the release smoke can never enter. This head produces a better
error and still fails, still needing `win_x64_smoke_mode: skip`.
Extract the settle loop and the phase around it so the smoke's own transition —
read the daemon fact, hold the seeded state across it, derive the policy, settle
— can be driven with only the I/O faked, then cover both scenarios the packaged
app legitimately has. `runPackagedAppShellPhase` still hardcodes the seeded
assumption, mirroring the unconditional seed, so a genuine first run fails with
the wrong error entirely:
× settles a genuine first run on the cloud sign-in landing
× fails a first run whose renderer never painted either surface
PackagedOnboardingSeedError: the relaunched daemon lost the seeded
onboarding state — check that the protocol cold launch still resolves the
tools-pack runtime data root
Test Files 1 failed (1)
Tests 2 failed | 39 passed (41)
* feat(e2e): cover both packaged launch scenarios in the core Windows smoke
Adds the explicitly unseeded first-run phase the smoke never had, so the
landing-accepting path is exercised by a scenario the release lane can actually
enter rather than only by unit fixtures.
Both behaviours are real product behaviour, not a contradiction:
`shouldRouteToFirstRunOnboarding` (apps/web/src/App.tsx) keys purely on
`onboardingCompleted`, and `connectStepRuntimeReady` (EntryShell.tsx) lets
onboarding complete through cloud sign-in *or* a local CLI *or* a verified BYOK
key. So a user who finished setup and is merely signed out belongs on home,
while a run that never completed onboarding belongs on the cloud sign-in
landing.
Chose separately asserted phases over a second `test(...)`: it reuses the single
install rather than adding a second install/uninstall cycle to the release
critical path, and it keeps the two scenarios adjacent so the contrast is
legible. Phase 1 resets the daemon data root and the Electron user-data
partition, asserts the daemon reports `onboardingCompleted: false`, and requires
the landing; it then clears both again so phase 2's seed lands clean. Phase 1 is
core-only because the full profile needs its controlled updater environment from
first launch.
The settle loop and the phase around it moved into the pure module, so the
transition — read the daemon fact, hold the seeded state across it, derive the
policy, settle — is now driven by tests with only the I/O faked, for both
scenarios and their failure modes.
Test Files 1 passed (1)
Tests 41 passed (41)
* test(e2e): pin the real fresh-install app-config response (red)
PerishCode's seventh review on #6481: round 4 made a missing
`onboardingCompleted` fail closed, round 6 added a scenario that produces
exactly that, and they collide.
`resetPackagedRuntimeDataRoot()` deletes `app-config.json`; `readAppConfig`
returns `{}` on ENOENT (app-config.ts:769) and adds only telemetry defaults, so
a fresh install's `/api/app-config` OMITS the key rather than reporting `false`.
The probe classifies that as malformed, the reader throws, and the first-run
phase never reaches the landing it was added to assert. win.spec.ts already
documented the daemon behaviour and the probe treated it as corruption anyway.
Move interpretation off the reader and onto the scenario (the reader now returns
the raw probe outcome), then add a fixture whose body matches the daemon's
actual fresh reply — `config` with telemetry defaults and no
`onboardingCompleted`. Interpretation is still round 4's, so absent stays
collapsed into malformed:
× settles a first run whose daemon never wrote the key
× still fails a completed user whose key went missing
PackagedOnboardingConfigError: packaged windows daemon onboarding config
could not be established: daemon config carried no boolean
onboardingCompleted (got undefined) (status=200)
Test Files 1 failed (1)
Tests 2 failed | 43 passed (45)
* fix(e2e): separate an absent onboarding key from a corrupt one
Chose representing absence as its own outcome over having the first-run phase
write an explicit `onboardingCompleted: false`. Writing one would make the
phase assert against a state a real fresh install never has — the same
"exercises a state the release smoke can never enter" flaw the previous review
just corrected — and would leave the probe unable to tell a fresh install from
a corrupt config anyway.
The probe now returns a discriminated outcome, and interpretation moved off the
reader and onto the declared scenario:
| outcome | first-run | completed-user |
|-------------------|-----------|----------------|
| transport-failure | reject | reject |
| http-error | reject | reject |
| no-config | reject | reject |
| absent | ACCEPT | reject (seed) |
| malformed | reject | reject |
| reading | accept | accept if true |
`absent` is the only asymmetric row, and deliberately so: a key that was never
written is the honest state of a fresh install, while in a seeded run its
disappearance means the seed vanished — which now raises
`PackagedOnboardingSeedError` naming the cold launch rather than a generic
config fault.
The table lives in the module docblock. Seven review rounds each found a defect
of one family — a fault and an absence sharing a representation — so this is
written down as the fix for the family, not for the instance.
Test Files 1 passed (1)
Tests 45 passed (45)
|
||
|
|
52f06da79f |
test: port PR 6229 acceptance coverage to main (#6272)
* test: cover updater floor and manual edit history * test: close daemon and E2E acceptance gaps * test: make daemon restart lifecycle budget explicit Replace replay timing sleep with persisted checkpoint polling and allow the multi-process lifecycle test enough time for startup and cleanup. Generated-By: looper 0.11.3 (runner=fixer, agent=codex) * test: restore onboarding analytics stability assertion Generated-By: looper 0.11.3 (runner=fixer, agent=codex) * test: assert manual edit history preserves preview frame Generated-By: looper 0.11.3 (runner=fixer, agent=codex) * test: make manual edit shortcuts and selection assertions portable Use the platform shortcut modifier for browser copy/paste and target the distinct inserted image before deletion so the structural tests verify the correct node.\n\nGenerated-By: looper 0.11.3 (runner=fixer, agent=codex) * test(e2e): drop superseded manual edit acceptance cases Generated-By: looper 0.11.7 (runner=fixer, agent=codex) * test(e2e): drop duplicate updater floor coverage * test(e2e): assert service-error recovery actions Generated-By: looper 0.11.8 (runner=fixer, agent=codex) * test(daemon): await third-boot reconciliation Generated-By: looper 0.11.8 (runner=fixer, agent=codex) |
||
|
|
2671061c47 |
fix(web): settle project workspace scope when bootstrap arrives late (#6504)
useProjectWorkspaceScope skipped its mount-time scope read whenever the current render could seed from `initialScope`. That answer is recomputed every render, but state is seeded only once, in the useState initializer. App.tsx fills routeProjectSnapshotRef asynchronously, so a route bootstrap that resolves after ProjectView mounted flips `initialScopeCanSeed` on an already-running hook. Because it is an effect dependency, the effect re-ran, aborted the in-flight GET /api/projects/:id/workspace-scope, and returned without fetching or setting state — pinning `loading: true` forever. Nothing could recover it: with no resolved scope the workspace invalidation SSE never subscribes, so no event could bump the revision and retry. The stuck `loading` reaches ProjectView as `workspaceContextReadOnly`, which makes `viewerOnly` true and renders the whole workspace as a read-only shared project — dead composer, disabled create actions — on a project the user owns. Key the skip on whether state was actually seeded at mount, which is what the early return always assumed. A late bootstrap scope is now ignored and the mount-time read settles normally. |
||
|
|
79aeb9977a | fix(landing): lock downloads to stable R2 metadata (#6503) | ||
|
|
2b8aa8c457 |
feat(design-systems): add structured runtime contract (#6456)
* feat(design-systems): add structured runtime contract * chore: retrigger CI for PR 6456 --------- Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com> Co-authored-by: mrcfps <mrc@powerformer.com> |
||
|
|
f2a7568dae |
feat(desktop): accept opendesign://workspace/open focus hand-off deeplink (#6410) (#6499)
* feat(desktop): accept opendesign://workspace/open focus hand-off deeplink The cloud device-activation page will fire this after a client-originated sign-in completes in the browser, so the user lands back in the desktop app instead of staying on the web. The deeplink is payload-free: login state already arrives through the daemon's vela login polling, so handling it only refocuses the client. * fix(desktop): route the deeplink hand-off focus through the runtime `focusPrimaryWindow` grabbed `BrowserWindow.getAllWindows()[0]`, which is the hidden, non-focusable desktop-pet window in a running app (and the splash on a packaged cold start) — never the main window. For `workspace/open` the focus is the entire payload, so the hand-off resolved to a silent no-op while still reporting `ok: true`. Route it through the runtime's own splash-aware `show()` instead, and drop the broken helper. Also tag the focus outcome with `workspace_open_focus` so the completion log stops claiming an invite continuation that never touched the daemon, and pin the accept boundary of `isWorkspaceOpenDeeplink` with a rejection matrix. Generated-By: looper 0.11.8 (runner=fixer, agent=claude-code) * fix(desktop): restore minimized window on hand-off and guard the focus call The revealed path of DesktopRuntime.show() only called window.show() + window.focus(), which leaves a minimized client in the Dock — turning the deeplink hand-off (whose entire payload is the bring-to-front) into a no-op. Route it through ensureWindowVisible, which restores before focusing. The workspace/open branch also called deps.focus outside any guard; a throw during runtime teardown would escape continueInviteFromUrl's documented no-throw contract as an unhandled rejection. Catch it and report a structured focus_failed outcome instead. --------- Co-authored-by: open-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com> Co-authored-by: elifive555555 <296440099+elifive555555@users.noreply.github.com> |
||
|
|
5a2e56101e |
test(e2e): restore workspace-aware P0 coverage (#6445)
* test(e2e): restore workspace-aware P0 coverage * test(e2e): await collaboration unload and event readiness Generated-By: looper 0.11.7 (runner=fixer, agent=codex) * test(e2e): slim multi-client collaboration P0 * ci(e2e): split workspace editor P0 * test(e2e): avoid ambiguous upload assertion |
||
|
|
fef0ddf88e |
ci: auto-rerun once on runner/spot cancel (#6453)
* ci: auto-rerun once when runner/spot cancel kills ci jobs Add rerun.atom.yml as a trusted workflow_run consumer that requests one `gh run rerun --failed` when completed pull_request/merge_group ci runs lose leaf jobs to infra cancel (shutdown signal / cancelled runner), while skipping ordinary assertion failures, stale heads, and ejected merge-queue entries. * fix(ci): grant rerun atom read scopes and refuse mixed failures Add checks/pull-requests read permissions so the helper can fingerprint infra cancels, and require every non-success leaf to be infra-classified before authorizing gh run rerun --failed. Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build) * fix(ci): require open PR for live-head stale gate Stop treating a same-repo branch tip as live-PR evidence. Closed PRs can leave the source branch at the failed run SHA with no open association; query open pulls by repository-qualified head ref instead, and cover the closed-PR + unchanged tip shape in self-check. Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build) * fix(ci): require infra marker for cancel rerun Bare cancelled jobs without shutdown/spot annotations are ordinary (manual cancel) and must not authorize gh run rerun --failed. Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build) * fix(ci): ignore aggregate gate jobs in infra-cancel classify Validate workspace and Runtime summary fail as always() mirrors of leaf outcomes and never carry runner/spot markers. Counting them as ordinary blocked pure infra-cancel reruns whenever the workflow still reached the gate. Exclude those names from classification and cover the shape in self-check. Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build) * fixer: address PR #6453 follow-up items Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build) * fix(ci): refuse mixed ordinary+infra job annotations Jobs that record both a real failure annotation and a later runner shutdown marker must classify as ordinary, not pure infra-cancel, so gh run rerun --failed cannot auto-retry assertion failures. Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build) * fix(ci): paginate check-run annotations before infra classify GitHub defaults annotations to 30 results; a later ordinary failure must still block gh run rerun --failed. Walk pages like paginate_jobs and cover the split with a multi-page request fixture. Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build) * fix(ci): drop tracked pycache from infra-cancel helper PerishCode BLOCKING on #6453: remove the accidental CPython 3.14 bytecode under .github/scripts/__pycache__/ and ignore __pycache__/ plus *.py[cod] so local self-check/compile output stays out of git. * fix(ci): open-PR merge_group freshness; ignore warning annotations Merge-group infra-cancel retries no longer require the synthetic head to still sit in the merge queue — workflow_run completed fires after required checks already ejected it. Freshness is an open PR still associated with the run (commit→pulls or gh-readonly-queue pr-N ref). Warning-level annotations (e.g. scopes.ts full-plan fallback) no longer count as ordinary failures that suppress pure infra-cancel retry. Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build) * fix(ci): match merge_group PR heads and arm rerun e2e Vitest Compare the live open PR head with the PR head SHA encoded in the merge-queue branch before authorizing infra-cancel retry, and enroll the rerun atom surfaces in scopes so PR CI runs the topology self-check. Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build) |
||
|
|
c7dbe83788 | chore(events): append pr_merged | ||
|
|
7a1c34f569 | chore(contributors): +12pts for @lefarcen (PR #6491) | ||
|
|
ace01bcdd6 |
Merge pull request #6491 from nexu-io/backmerge/v0.18.0
chore(release): back-merge release/v0.18.0 into main |
||
|
|
876cb4d054 |
chore(release): back-merge release/v0.18.0 into main
Brings back the commits that landed directly on the release branch and exist nowhere else: #6480 (guard the windows e2e report download so win_x64_smoke_mode: skip is usable on the stable lane), #6452 (retry NSIS install and fail the setup step loudly), #6440 and #6437 (release/v0.18.0 auto-build defaults, both scoped by their own github.ref_name conditions), #6435 (the amr_profile input that enables the Workspace Team transport), plus the release-cut chores. apps/packaged/package.json keeps main's 0.16.2 rather than taking the branch's 0.18.0: this repo advances main's version through its own 'chore(release): bump main to X.Y.Z ahead of stable' PRs (#6000, #5661, #5441), never through a back-merge. |
||
|
|
30c427ce0a |
fix(release): allow a skipped windows smoke on the stable lane (#6480)
release-stable.yml offers win_x64_smoke_mode: skip, but choosing it makes the release impossible to finish. The windows smoke uploads its e2e report with if-no-files-found: warn, so a skipped smoke leaves the report directory empty and publishes no artifact; finalize then downloads that artifact unconditionally and fails — after every platform has already built, signed and notarized, and after the pre-flight tag check has passed. Twenty-five minutes of work is thrown away for a report nothing downstream reads. Guard the download on the same condition that gates the smoke itself, matching how the linux report is guarded by its build result. The mac report stays unconditional because the mac smoke has no skip switch.open-design-v0.18.0 |
||
|
|
0dde0e5043 |
fix(landing): simplify Codex plugin install entrypoint (#6474)
* fix(landing): simplify Codex plugin install entrypoint * fix(landing): handle install guide fetch failures --------- Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com> |
||
|
|
65e1b2a3c5 |
feat(analytics): track recoverable task outcomes (#6411) (#6473)
(cherry picked from commit
|
||
|
|
c670aff42b |
docs(release): add 0.18.0 release notes (#6469) (#6472)
* docs(release): add 0.18.0 release notes
* docs(release): adopt final 0.18.0 product copy for title and intro
(cherry picked from commit
|
||
|
|
63e54e298c |
fix(release): bake the workspace-team gate into stable and preview builds (#6468) (#6471)
release-stable.yml and release-preview.yml never set OPEN_DESIGN_AMR_PROFILE
or OD_VELA_WEB_URL. Neither has a default in tools-pack, so velaWebUrl reached
the packaged config as undefined, workspaceTeamTransportEnv returned an empty
record, and the four vela transports were never injected into the daemon spawn
env. Both channels therefore shipped clients with Workspace Team silently
dormant — the build succeeds, the app installs and starts, and the absence only
surfaces once a package is in a user's hands.
beta and prerelease already inject the pair from their amr_profile input, which
is why prerelease validation looked correct: the artifact under test had the
feature and the stable artifact promoted from it would not have.
Stable and preview are production channels, so the pair is pinned instead of
dispatched — there is no legitimate stable build aimed at the test backend.
Naming prod explicitly is a no-op for AMR itself (the daemon already defaults to
prod in integrations/vela-profile.ts); it exists to satisfy the profile half of
the gate. A fork or PR build without the secret leaves the origin empty, so the
transports stay dormant rather than half-enabled.
(cherry picked from commit
|
||
|
|
9483329ecd |
ci: shard web tests and retain visual failure artifacts (#6416)
* ci: shard web tests and retain visual failure artifacts Split the 6m web workspace job into two vitest shards and keep Playwright trace/screenshot on visual failure for diagnosis. * test(e2e): pin visual-pr-capture retain-on-failure path Assert playwright_visual uploads keep e2e/ui/reports/visual-test-results for both visual-pr-capture and visual-ci artifacts so report-consumed diagnostics cannot silently regress. Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build) * test(e2e): pin web shards and baseline visual diagnostics Lock the two-way web_workspace_tests vitest matrix in the CI topology contract and upload retain-on-failure traces from visual-baseline.yml, the second consumer of playwright.visual.config.ts. Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build) |
||
|
|
d771c1ca5f |
feat(daemon): export execution diagnostics (#6433)
* feat(daemon): export execution diagnostics * fix(daemon): preserve observed execution diagnostics --------- Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com> |
||
|
|
e6aeb38da6 |
feat(analytics): track recoverable task outcomes (#6411)
Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com> |