From 9dee6a9fb0a8cce89ad277337a2c10aad2ef5e07 Mon Sep 17 00:00:00 2001 From: jinjunnn Date: Wed, 22 Jul 2026 10:58:00 -0400 Subject: [PATCH] feat(ui-mac): release alpha session without spike gates Flip the session release default to Alpha, remove the localStorage double-gate, and unmount the REQ-087 spike host for issue 500. --- .../ui-mac/src/main/alpha-surfaces.test.ts | 10 ++++-- .../req087-characterization.test.ts | 12 +++++-- .../alpha-session-workspace.test.ts | 33 ++++++++++++++++--- .../alpha-session-workspace.tsx | 16 ++++----- .../renderer/dev/surface-map-model.test.ts | 6 ++-- packages/ui-mac/src/renderer/index.tsx | 9 +---- packages/ui-mac/src/shared/alpha-surfaces.ts | 2 +- 7 files changed, 57 insertions(+), 31 deletions(-) diff --git a/packages/ui-mac/src/main/alpha-surfaces.test.ts b/packages/ui-mac/src/main/alpha-surfaces.test.ts index 5d3a2415d31e..85ffe037130d 100644 --- a/packages/ui-mac/src/main/alpha-surfaces.test.ts +++ b/packages/ui-mac/src/main/alpha-surfaces.test.ts @@ -31,7 +31,7 @@ describe("resolveSurfaces — Alpha-only composition", () => { const defaults = resolveSurfaces({ env: {}, file: {}, appVersion: "1.0.0" }) expect(defaults.home).toEqual({ mode: "alpha", reason: "release-default" }) expect(defaults.newSession).toEqual({ mode: "alpha", reason: "release-default" }) - expect(defaults.session).toEqual({ mode: "legacy", reason: "release-default" }) + expect(defaults.session).toEqual({ mode: "alpha", reason: "release-default" }) const pin = resolveSurfaces({ env: {}, file: { pins: { session: "alpha" } }, appVersion: "1.0.0" }) expect(pin.session).toEqual({ mode: "alpha", reason: "pin" }) @@ -41,6 +41,12 @@ describe("resolveSurfaces — Alpha-only composition", () => { appVersion: "1.0.0", }) expect(env.home).toEqual({ mode: "legacy", reason: "env-override" }) + const sessionEnv = resolveSurfaces({ + env: { ALPHA_SURFACE_SESSION: "legacy" }, + file: { pins: { session: "alpha" } }, + appVersion: "1.0.0", + }) + expect(sessionEnv.session).toEqual({ mode: "legacy", reason: "env-override" }) }) test("a current or historical crash record never changes an Alpha surface to legacy", () => { @@ -63,7 +69,7 @@ describe("resolveSurfaces — Alpha-only composition", () => { file: { pins: { session: "auto-fallback" as never } }, appVersion: "1.0.0", }) - expect(resolved.session).toEqual({ mode: "legacy", reason: "release-default" }) + expect(resolved.session).toEqual({ mode: "alpha", reason: "release-default" }) }) }) diff --git a/packages/ui-mac/src/renderer/alpha-ui/session-spike/req087-characterization.test.ts b/packages/ui-mac/src/renderer/alpha-ui/session-spike/req087-characterization.test.ts index 1793da4d67fa..1c638cd61858 100644 --- a/packages/ui-mac/src/renderer/alpha-ui/session-spike/req087-characterization.test.ts +++ b/packages/ui-mac/src/renderer/alpha-ui/session-spike/req087-characterization.test.ts @@ -133,8 +133,16 @@ describe("REQ-087 §5 timeline 关键机制锚点", () => { }) describe("REQ-087 §6 通道结论锁定(REQ-088 C1 合法窄通道,不可静默漂移)", () => { - test("session surface 发布默认仍是 legacy(REQ-088 未交付,flags-off 零变化)", () => { - expect(SURFACE_RELEASE_STATES.session).toBe("legacy") + test("REQ-088 T5:session defaults to alpha and the spike host/localStorage gate are no longer mounted", () => { + expect(SURFACE_RELEASE_STATES.session).toBe("alpha") + const rendererIndex = readFileSync(join(import.meta.dir, "../../index.tsx"), "utf8") + const workspace = readFileSync( + join(import.meta.dir, "../session-workspace/alpha-session-workspace.tsx"), + "utf8", + ) + expect(rendererIndex).not.toContain("SessionSpikeHost") + expect(workspace).not.toContain("isSessionSpikeEnabled") + expect(workspace).not.toContain("ALPHA_SESSION_SPIKE") }) test("@opencode-ai/app exports map = 既有六条 + ./surface/session 窄导出(C1,ADR-027 修订)", () => { const pkg = JSON.parse(readFileSync(join(APP, "package.json"), "utf8")) as { exports: Record } diff --git a/packages/ui-mac/src/renderer/alpha-ui/session-workspace/alpha-session-workspace.test.ts b/packages/ui-mac/src/renderer/alpha-ui/session-workspace/alpha-session-workspace.test.ts index 1b4a3148c038..d890c4ffd873 100644 --- a/packages/ui-mac/src/renderer/alpha-ui/session-workspace/alpha-session-workspace.test.ts +++ b/packages/ui-mac/src/renderer/alpha-ui/session-workspace/alpha-session-workspace.test.ts @@ -6,7 +6,7 @@ // R2 chrome 一律 alpha 命名空间(a-swk-* 类 + data-alpha-* 属性),不与上游锚点同名; // R7 三个 takeover 不移进叶/workspace 内(挂载通道零耦合,另有 takeover-adapter-coexistence // 测试①钉 children 通道;此处钉 workspace 侧不 import)。 -// 另钉:双闸工厂形态、SurfaceBoundary 组合、跨 server 引导的 rethrow 纪律、窄导出唯一消费、 +// 另钉:单一 release-state seam、SurfaceBoundary 组合、跨 server 引导的 rethrow 纪律、窄导出唯一消费、 // C4 携带项③ 的侧栏预热接线。断言红 = 红线破坏,回 T6 审计 §3 重评,不得只改测试。 import { describe, expect, test } from "bun:test" @@ -88,15 +88,38 @@ describe("R7 / Stage C-1:takeover 不进 workspace;本期不自渲染 AlphaCompo }) }) -describe("双闸 + seam 契约(发布态本期保持 legacy,T5 才升级)", () => { - test("工厂:localStorage 闸关 ⇒ undefined(= surfaces.session 未注入,seam 走上游默认叶)", () => { - expect(tsx).toContain("if (!isSessionSpikeEnabled()) return undefined") +describe("single release-state seam contract", () => { + test("factory returns a preloadable component without localStorage", () => { + const result = Bun.spawnSync({ + cmd: [ + process.execPath, + "--eval", + `import { mock } from "bun:test"; +mock.module("react/jsx-dev-runtime", () => ({ Fragment: Symbol(), jsxDEV: () => ({}) })); +mock.module("@solidjs/router", () => ({ useLocation: () => ({ pathname: "" }), useNavigate: () => () => {} })); +const module = await import("./src/renderer/alpha-ui/session-workspace/alpha-session-workspace.tsx"); +const component = module.alphaSessionWorkspaceSurface(); +console.log(JSON.stringify({ localStorage: typeof globalThis.localStorage, component: typeof component, preload: typeof component.preload }));`, + ], + cwd: join(here, "../../../.."), + env: process.env, + }) + expect(result.stderr.toString()).toBe("") + expect(JSON.parse(result.stdout.toString())).toEqual({ + localStorage: "undefined", + component: "function", + preload: "function", + }) + expect(tsx).not.toContain("isSessionSpikeEnabled") + expect(tsx).not.toContain("ALPHA_SESSION_SPIKE") }) - test("env-override 闸:index.tsx 只在 resolved.session.mode === alpha 时调用工厂", () => { + test("index.tsx calls the factory only when resolved.session.mode is alpha", () => { expect(rendererIndex).toContain(`if (resolved?.session.mode !== "alpha") return undefined`) expect(rendererIndex).toContain(`return alphaSessionWorkspaceSurface()`) expect(rendererIndex).toContain(`const session = productionRoutes.session.mount(resolved)`) + expect(rendererIndex.match(/return alphaSessionWorkspaceSurface\(\)/g)).toHaveLength(1) + expect(rendererIndex).not.toContain("SessionSpikeHost") }) test("SurfaceBoundary 语义保持(C4 真机实证链路):workspace 最外层即 surface 边界", () => { diff --git a/packages/ui-mac/src/renderer/alpha-ui/session-workspace/alpha-session-workspace.tsx b/packages/ui-mac/src/renderer/alpha-ui/session-workspace/alpha-session-workspace.tsx index 4276bca8c1b9..4dcbf366a752 100644 --- a/packages/ui-mac/src/renderer/alpha-ui/session-workspace/alpha-session-workspace.tsx +++ b/packages/ui-mac/src/renderer/alpha-ui/session-workspace/alpha-session-workspace.tsx @@ -1,6 +1,6 @@ // AlphaSessionWorkspace — REQ-088 T2(Issue #181):session surface 的正式 Alpha 外框。 // REQ-087 spike 的 surface 侧原型(session-spike-host.tsx 旧 sessionSpikeSurface)在此转正; -// 容器侧探针仍留在 session-spike/(T7 统一清理)。 +// 未挂载的容器侧探针模块仍留在 session-spike/(T7 统一清理)。 // // 结构与纪律(变更任何一条先回 spike 报告 §6 / T6 审计 §3 重评): // - 叶经 C1 合法窄导出(app 包 exports 的 ./surface/session 子路径,ADR-027 修订、 @@ -9,8 +9,8 @@ // 全部保持上游默认生命周期,外框零 upstream context(只读版本化路由 ABI)。 // - surface override 与默认叶 XOR(app.tsx `props.surfaces?.session ?? Session`),结构上不存在 // 双挂载;SurfaceBoundary 兜致命 render 错误 → 留在 Alpha 区域并进入 Recovery。 -// - 双闸:主进程 env-override `ALPHA_SURFACE_SESSION=alpha` + localStorage 闸(spike-flag), -// 任一闸关 ⇒ 工厂返回 undefined = seam 走上游默认叶,零变化。发布态本期保持 legacy(T5 才升级)。 +// - 单一挂载条件:调用方仅在 resolved.session.mode === "alpha" 时注入本工厂返回的组件; +// legacy 解析结果不注入,由 seam 走上游默认叶。 // - 宿主红线(T6 审计 §3;alpha-session-workspace.test.ts 钉死): // R1 活叶包裹保持普通流(flex:1 + min-height:0,不隐藏、不脱流)——takeover 的 // offsetParent 可见性口径依赖于此; @@ -30,7 +30,6 @@ import { useLocation, useNavigate } from "@solidjs/router" import type { MaybePreloadableComponent } from "@opencode-ai/app" import { hrefFor, parseRoute } from "../../../shared/route-manifest" import { SurfaceBoundary } from "../surface-boundary" -import { isSessionSpikeEnabled } from "../session-spike/spike-flag" import { isCrossServerSessionError, workspaceContextOf } from "./session-workspace-core" import { t } from "../../i18n" import "./session-workspace.css" @@ -129,13 +128,10 @@ export function AlphaSessionWorkspace() { } /** - * surface 工厂:双闸(localStorage 闸 + 主进程 ALPHA_SURFACE_SESSION=alpha,后者由调用方的 - * resolved.session.mode === "alpha" 前置保证)全开才返回组件,否则 undefined ⇒ surfaces.session - * 维持未注入,seam 走上游默认叶(严格零变化)。localStorage 闸沿用 spike-flag(键 - * ALPHA_SESSION_SPIKE)——T5 发布态阶梯前不改口径,T7 统一裁决其去留。 + * surface 工厂无条件返回 Alpha 组件。唯一挂载条件由调用方的 + * resolved.session.mode === "alpha" 保证;legacy 解析结果保持上游默认叶。 */ -export function alphaSessionWorkspaceSurface(): MaybePreloadableComponent | undefined { - if (!isSessionSpikeEnabled()) return undefined +export function alphaSessionWorkspaceSurface(): MaybePreloadableComponent { const Comp: MaybePreloadableComponent = () => // 与 seam 的 preload 契约对齐(app.tsx `preload: () => Leaf.preload?.()`)。 Comp.preload = preloadSessionLeaf diff --git a/packages/ui-mac/src/renderer/dev/surface-map-model.test.ts b/packages/ui-mac/src/renderer/dev/surface-map-model.test.ts index a970b4604fc3..0fd1651fa94f 100644 --- a/packages/ui-mac/src/renderer/dev/surface-map-model.test.ts +++ b/packages/ui-mac/src/renderer/dev/surface-map-model.test.ts @@ -19,7 +19,7 @@ describe("surface map model", () => { const resolved = { home: { mode: "alpha", reason: "release-default" }, newSession: { mode: "alpha", reason: "release-default" }, - session: { mode: "legacy", reason: "release-default" }, + session: { mode: "alpha", reason: "release-default" }, } as const expect(surfaceRuntimeState(home, resolved)).toEqual({ @@ -28,9 +28,9 @@ describe("surface map model", () => { release: "alpha", }) expect(surfaceRuntimeState(session, resolved)).toEqual({ - mode: "Legacy", + mode: "Alpha", detail: "release-default", - release: "legacy", + release: "alpha", }) expect(surfaceRuntimeState(home, null).detail).toBe("resolver-error fallback") }) diff --git a/packages/ui-mac/src/renderer/index.tsx b/packages/ui-mac/src/renderer/index.tsx index 833f4a2b726f..3e9ccc49e388 100644 --- a/packages/ui-mac/src/renderer/index.tsx +++ b/packages/ui-mac/src/renderer/index.tsx @@ -49,7 +49,6 @@ import { SurfaceBoundary } from "./alpha-ui/surface-boundary" import { RuntimeRecoveryHost } from "./alpha-ui/RuntimeRecoveryHost" import { UpstreamDialogHost } from "./alpha-ui/UpstreamDialogHost" import type { ResolvedSurfaces } from "../shared/alpha-surfaces" -import { SessionSpikeHost } from "./alpha-ui/session-spike/session-spike-host" // REQ-087 spike 探针(默认恒 off,T7 清理) import { alphaSessionWorkspaceSurface } from "./alpha-ui/session-workspace/alpha-session-workspace" // REQ-088 T2 import { AlphaOnboarding } from "./alpha-ui/AlphaOnboarding" import { AlphaSettings } from "./alpha-ui/settings" @@ -497,9 +496,7 @@ render(() => { const session = productionRoutes.session.mount(resolved) if (home) surfaces[productionRoutes.home.surface] = home if (newSession) surfaces[productionRoutes["new-session"].surface] = newSession - // REQ-088 T2:session 叶经 AlphaSessionWorkspace 正式外框注入(SurfaceBoundary + 窄导出叶)。 - // 发布态本期仍 legacy(T5 才升级);双闸(ALPHA_SURFACE_SESSION=alpha env-override + - // localStorage ALPHA_SESSION_SPIKE)未全开时返回 undefined = 上游默认叶,零变化。 + // REQ-088 T5:resolved session mode 是唯一挂载条件;legacy 保持上游默认叶。 if (session) surfaces[productionRoutes.session.surface] = session return surfaces }) @@ -557,10 +554,6 @@ render(() => { - {/* REQ-087 spike 容器侧探针:flag off ⇒ 恒 null(session-spike/spike-flag.ts) */} - - - )} diff --git a/packages/ui-mac/src/shared/alpha-surfaces.ts b/packages/ui-mac/src/shared/alpha-surfaces.ts index a772151e50e7..1be016785dfd 100644 --- a/packages/ui-mac/src/shared/alpha-surfaces.ts +++ b/packages/ui-mac/src/shared/alpha-surfaces.ts @@ -20,7 +20,7 @@ export type ResolvedSurfaces = Record export const SURFACE_RELEASE_STATES: Record = { home: "alpha", newSession: "alpha", - session: "legacy", // REQ-088 not yet delivered + session: "alpha", } export const SURFACE_ABI_VERSION = 1