GDD 批准后补上交付出口:标题栏给出本地路径、查看正文与外部打开
批准之后审批卡按设计整张收掉,从那一刻起用户就再也够不到自己刚批的 GDD:
approvedGddRef 前端没人读,渲染好的 game/fast_gdd.md 也没有任何入口。做方案链路
跑到头是没有交付物的。
不新增卡片——交付行挂在阶段进度条底下,只在 state 为 approved 且不在恢复态时出现:
一行绝对路径,两个按钮「查看 GDD 正文」「打开文件」。批准前后是同一个框,多一行,
视觉连续。恢复态不给出口:那时权威投影还没收敛,磁盘上那份未必是用户批的那版。
正文弹层从审批卡里抽成 GddDetailsDialog 两处共用,内容一字未改,于是批准前后看到
的是同一份正文。路径按项目路径自身的分隔符拼,Windows 下不会混出反斜杠与正斜杠
各半的怪路径。
后端新增 open_local_project_plan_gdd_markdown,走 opener 交给系统默认程序。路径不
由前端拼:命令自己用 resolve_local_project_path 在项目根下解析常量相对路径——那是
项目内路径的唯一安全入口(根校验、归一化、逐段拒绝符号链接),GDD 的渲染侧用的也
是同一个解析器,两边对「项目内的这个文件」必须是同一个判定。再加存在性与普通文件
检查,未渲染时给出明确原因而不是把不存在的路径丢给 shell。
顺带修一条我在 80200e6a3 调高度时漏跑全量而留下的红:project-development 里那条
断言把 clamp 的三个断点钉成了字面量。它要锁的不变量是「自带上限 + 自己滚」,数值
是随排版调整的设计取值;钉死只会让每次调高度都顺带改测试,却挡不住真正的回归。
改成不锁数值,并补一条策划窄条没退回去继承 240px 天花板的断言。
新增测试(前端三条都做过 A/B,关掉交付行即红):
- Rust:路径门四条(正常解析、未渲染、非项目目录、相对路径)。
- 前端:approved 态出现路径与两个按钮且没有批准/修改/退回;点「打开文件」用正确
参数 invoke;recoveryPending 时交付行不出现。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -552,6 +552,104 @@ pub(crate) fn validated_local_project_directory_path(
|
||||
Ok(path.to_path_buf())
|
||||
}
|
||||
|
||||
/// Open the approved Fast GDD Markdown in whatever application the OS has
|
||||
/// registered for it.
|
||||
///
|
||||
/// The GDD is the one product artifact the 立项策划 lane hands back, and it is
|
||||
/// already on disk — `plan.submit_gdd` renders `game/fast_gdd.md` and the
|
||||
/// approval receipt re-renders it with the approved header. This command only
|
||||
/// hands that existing path to the shell; it never creates or rewrites it.
|
||||
#[tauri::command]
|
||||
pub(crate) fn open_local_project_plan_gdd_markdown(
|
||||
app: tauri::AppHandle,
|
||||
project_path: String,
|
||||
) -> Result<(), String> {
|
||||
let path = validated_local_project_plan_gdd_markdown_path(project_path.trim())?;
|
||||
app.opener()
|
||||
.open_path(path.to_string_lossy().into_owned(), None::<&str>)
|
||||
.map_err(|error| format!("打开 Fast GDD 文件失败:{error}"))
|
||||
}
|
||||
|
||||
pub(crate) fn validated_local_project_plan_gdd_markdown_path(
|
||||
project_path: &str,
|
||||
) -> Result<PathBuf, String> {
|
||||
let root = validated_local_project_directory_path(project_path)?;
|
||||
// `resolve_local_project_path` 是项目内路径的唯一安全入口:它做根校验、相对路径
|
||||
// 归一化,并逐段拒绝符号链接。这里的相对路径是常量,但仍然走它——GDD 的渲染侧
|
||||
// (`planning_storage`)用的也是同一个解析器,两边对「项目内的这个文件」必须是
|
||||
// 同一个判定,不能一边解析一边拼字符串。
|
||||
let path = resolve_local_project_path(&root, PLAN_FAST_GDD_PATH)?;
|
||||
match fs::symlink_metadata(&path) {
|
||||
Ok(metadata) if metadata.file_type().is_file() => Ok(path),
|
||||
Ok(_) => Err("Fast GDD 产物不是普通文件".to_string()),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
|
||||
Err("Fast GDD 产物尚未生成,请先完成立项策划审批".to_string())
|
||||
}
|
||||
Err(error) => Err(format!("读取 Fast GDD 产物失败:{error}")),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod plan_gdd_markdown_path_tests {
|
||||
use super::*;
|
||||
|
||||
fn fixture() -> tempfile::TempDir {
|
||||
let temporary = tempfile::tempdir().expect("create GDD path fixture");
|
||||
crate::project::init_local_game_project_at(
|
||||
&temporary.path().join("project"),
|
||||
"gdd-open",
|
||||
"打开 GDD 产物",
|
||||
)
|
||||
.expect("initialize GDD path fixture");
|
||||
temporary
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_the_rendered_markdown_under_the_project_root() {
|
||||
let temporary = fixture();
|
||||
let root = temporary.path().join("project");
|
||||
fs::create_dir_all(root.join("game")).expect("create game directory");
|
||||
fs::write(root.join(PLAN_FAST_GDD_PATH), "# Fast GDD").expect("render markdown");
|
||||
|
||||
let resolved =
|
||||
validated_local_project_plan_gdd_markdown_path(&root.to_string_lossy().into_owned())
|
||||
.expect("resolve rendered markdown");
|
||||
|
||||
assert_eq!(resolved, root.join(PLAN_FAST_GDD_PATH));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refuses_to_open_a_markdown_that_has_not_been_rendered_yet() {
|
||||
// 恢复态下 `plan.submit_gdd` 的 Markdown 渲染可能还没落盘。这时按钮必须给出
|
||||
// 明确原因,而不是把一个不存在的路径丢给 shell 由系统弹一个无从解释的错误。
|
||||
let temporary = fixture();
|
||||
let root = temporary.path().join("project");
|
||||
|
||||
let error =
|
||||
validated_local_project_plan_gdd_markdown_path(&root.to_string_lossy().into_owned())
|
||||
.expect_err("missing markdown must fail closed");
|
||||
|
||||
assert!(error.contains("尚未生成"), "unexpected error: {error}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refuses_a_project_path_that_is_not_an_initialized_project() {
|
||||
let temporary = tempfile::tempdir().expect("create bare fixture");
|
||||
let error = validated_local_project_plan_gdd_markdown_path(
|
||||
&temporary.path().to_string_lossy().into_owned(),
|
||||
)
|
||||
.expect_err("a directory without .agent is not a project root");
|
||||
assert!(!error.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refuses_a_relative_project_path() {
|
||||
let error = validated_local_project_plan_gdd_markdown_path("relative/project")
|
||||
.expect_err("relative project path must fail");
|
||||
assert!(error.contains("绝对路径"), "unexpected error: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn get_local_game_manifest(
|
||||
project_path: String,
|
||||
|
||||
@@ -2364,6 +2364,7 @@ fn main() {
|
||||
pick_local_project_directory,
|
||||
pick_local_file,
|
||||
open_local_project_directory,
|
||||
open_local_project_plan_gdd_markdown,
|
||||
control_agent_run,
|
||||
generate_local_game_draft,
|
||||
chat_with_game_creator_agent,
|
||||
|
||||
@@ -1,10 +1,30 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { resolveTauriInvoke } from '../../app/tauri';
|
||||
import type {
|
||||
PlanGddDecisionAction,
|
||||
PlanGddStateViewV1,
|
||||
} from '../../app/types';
|
||||
|
||||
/** 立项策划唯一的产品产物,由 `plan.submit_gdd` 与审批回执渲染到项目内。 */
|
||||
const PLAN_FAST_GDD_RELATIVE_PATH = 'game/fast_gdd.md';
|
||||
|
||||
/**
|
||||
* 拼出交付文件的绝对路径。
|
||||
*
|
||||
* 用户要拿这行去资源管理器里找文件,所以跟随项目路径本身的分隔符:Windows 下项目
|
||||
* 路径是反斜杠,混排出来的 `C:\...\project/game/fast_gdd.md` 虽然能用,但复制到
|
||||
* 地址栏之外的地方就不像一个路径了。
|
||||
*/
|
||||
function planGddMarkdownDisplayPath(projectPath: string) {
|
||||
const trimmed = projectPath.trim().replace(/[\\/]+$/u, '');
|
||||
if (!trimmed) {
|
||||
return PLAN_FAST_GDD_RELATIVE_PATH;
|
||||
}
|
||||
const separator = trimmed.includes('\\') ? '\\' : '/';
|
||||
return `${trimmed}${separator}${PLAN_FAST_GDD_RELATIVE_PATH.split('/').join(separator)}`;
|
||||
}
|
||||
|
||||
type GddApprovalCardProps = {
|
||||
state: PlanGddStateViewV1 | null;
|
||||
busy: boolean;
|
||||
@@ -48,11 +68,12 @@ function approvalCardVisible(state: PlanGddStateViewV1 | null) {
|
||||
export function PlanGddSurface({
|
||||
state,
|
||||
active = false,
|
||||
projectPath,
|
||||
busy,
|
||||
error,
|
||||
onRefresh,
|
||||
onDecision,
|
||||
}: GddApprovalCardProps & { active?: boolean }) {
|
||||
}: GddApprovalCardProps & { active?: boolean; projectPath: string }) {
|
||||
const showProgress = stageProgressVisible(state, active);
|
||||
const showCard = approvalCardVisible(state);
|
||||
if (!showProgress && !showCard) {
|
||||
@@ -64,7 +85,11 @@ export function PlanGddSurface({
|
||||
aria-label="立项策划"
|
||||
>
|
||||
{showProgress ? (
|
||||
<PlanGddStageProgress state={state} active={active} />
|
||||
<PlanGddStageProgress
|
||||
state={state}
|
||||
active={active}
|
||||
projectPath={projectPath}
|
||||
/>
|
||||
) : null}
|
||||
{showCard ? (
|
||||
<GddApprovalCard
|
||||
@@ -82,10 +107,15 @@ export function PlanGddSurface({
|
||||
export function PlanGddStageProgress({
|
||||
state,
|
||||
active = false,
|
||||
projectPath = '',
|
||||
}: {
|
||||
state: PlanGddStateViewV1 | null;
|
||||
active?: boolean;
|
||||
projectPath?: string;
|
||||
}) {
|
||||
const [detailsOpen, setDetailsOpen] = useState(false);
|
||||
const [openError, setOpenError] = useState('');
|
||||
const [opening, setOpening] = useState(false);
|
||||
if (!state || !stageProgressVisible(state, active)) {
|
||||
return null;
|
||||
}
|
||||
@@ -102,6 +132,14 @@ export function PlanGddStageProgress({
|
||||
const roundLabel = state.session?.awaitingAnswerFor
|
||||
? `第 ${answeredRounds + 1} 轮 / 共 3 轮`
|
||||
: `已完成 ${answeredRounds}/3 轮澄清`;
|
||||
// 批准之后审批卡整张收掉,交付出口就落在这条标题栏上:GDD 的 Markdown 已经在项目
|
||||
// 里(提交时渲染、审批回执重渲染带上 approved 头),这里只是把它指出来并交给系统
|
||||
// 打开。恢复态不给出口——那时权威投影还没收敛,路径上的内容可能不是用户批的那版。
|
||||
const deliveredGdd =
|
||||
state.state === 'approved' && !state.recoveryPending
|
||||
? state.displayGdd
|
||||
: null;
|
||||
const markdownPath = planGddMarkdownDisplayPath(projectPath);
|
||||
return (
|
||||
<section className="plan-gdd-stage-progress" aria-label="立项策划阶段进度">
|
||||
<div className="plan-gdd-stage-progress__header">
|
||||
@@ -116,6 +154,57 @@ export function PlanGddStageProgress({
|
||||
: `当前版本:v${latestVersion}`}
|
||||
</span>
|
||||
</div>
|
||||
{deliveredGdd ? (
|
||||
<div
|
||||
className="plan-gdd-stage-progress__delivery"
|
||||
aria-label="GDD 交付"
|
||||
>
|
||||
<code title={markdownPath}>{markdownPath}</code>
|
||||
<div className="plan-gdd-stage-progress__delivery-actions">
|
||||
<button type="button" onClick={() => setDetailsOpen(true)}>
|
||||
查看 GDD 正文
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={opening || !projectPath.trim()}
|
||||
onClick={() => {
|
||||
const invoke = resolveTauriInvoke();
|
||||
if (!invoke) {
|
||||
setOpenError('当前环境不支持打开本地文件');
|
||||
return;
|
||||
}
|
||||
setOpenError('');
|
||||
setOpening(true);
|
||||
void invoke('open_local_project_plan_gdd_markdown', {
|
||||
projectPath,
|
||||
})
|
||||
.catch((error: unknown) =>
|
||||
setOpenError(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
),
|
||||
)
|
||||
.finally(() => setOpening(false));
|
||||
}}
|
||||
>
|
||||
{opening ? '正在打开' : '打开文件'}
|
||||
</button>
|
||||
</div>
|
||||
{openError ? (
|
||||
<small
|
||||
className="plan-gdd-stage-progress__delivery-error"
|
||||
role="alert"
|
||||
>
|
||||
{openError}
|
||||
</small>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{detailsOpen && deliveredGdd ? (
|
||||
<GddDetailsDialog
|
||||
gdd={deliveredGdd}
|
||||
onClose={() => setDetailsOpen(false)}
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -196,6 +285,63 @@ const visibleGddSections = (
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Fast GDD 的正文视图。
|
||||
*
|
||||
* 审批时挂在审批卡上,批准之后审批卡收掉、改由阶段进度条那条交付行触发——同一份
|
||||
* 弹层两处共用,用户在批准前后看到的是同一个正文。
|
||||
*/
|
||||
export function GddDetailsDialog({
|
||||
gdd,
|
||||
onClose,
|
||||
}: {
|
||||
gdd: NonNullable<PlanGddStateViewV1['displayGdd']>;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="gdd-approval-card__dialog-backdrop"
|
||||
role="presentation"
|
||||
onMouseDown={(event) => {
|
||||
if (event.target === event.currentTarget) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<section
|
||||
className="gdd-approval-card__dialog gdd-approval-card__details"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="gdd-approval-details-title"
|
||||
>
|
||||
<header>
|
||||
<h3 id="gdd-approval-details-title">{gdd.game.title}</h3>
|
||||
<p>{gdd.game.oneLiner}</p>
|
||||
</header>
|
||||
{visibleGddSections(gdd)
|
||||
.filter((section) => section.content)
|
||||
.map((section) => (
|
||||
<article key={section.title}>
|
||||
<strong>{section.title}</strong>
|
||||
<p>{section.content}</p>
|
||||
</article>
|
||||
))}
|
||||
<small
|
||||
className="gdd-approval-card__details-trace"
|
||||
aria-label="GDD 版本与指纹"
|
||||
>
|
||||
{`Fast GDD v${gdd.version} · ${gdd.fingerprint}`}
|
||||
</small>
|
||||
<div className="gdd-approval-card__dialog-actions">
|
||||
<button type="button" onClick={onClose}>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function GddApprovalCard({
|
||||
state,
|
||||
busy,
|
||||
@@ -361,46 +507,7 @@ export function GddApprovalCard({
|
||||
) : null}
|
||||
|
||||
{gddDetailsOpen ? (
|
||||
<div
|
||||
className="gdd-approval-card__dialog-backdrop"
|
||||
role="presentation"
|
||||
onMouseDown={(event) => {
|
||||
if (event.target === event.currentTarget) {
|
||||
setGddDetailsOpen(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<section
|
||||
className="gdd-approval-card__dialog gdd-approval-card__details"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="gdd-approval-details-title"
|
||||
>
|
||||
<header>
|
||||
<h3 id="gdd-approval-details-title">{gdd.game.title}</h3>
|
||||
<p>{gdd.game.oneLiner}</p>
|
||||
</header>
|
||||
{visibleGddSections(gdd)
|
||||
.filter((section) => section.content)
|
||||
.map((section) => (
|
||||
<article key={section.title}>
|
||||
<strong>{section.title}</strong>
|
||||
<p>{section.content}</p>
|
||||
</article>
|
||||
))}
|
||||
<small
|
||||
className="gdd-approval-card__details-trace"
|
||||
aria-label="GDD 版本与指纹"
|
||||
>
|
||||
{`Fast GDD v${gdd.version} · ${gdd.fingerprint}`}
|
||||
</small>
|
||||
<div className="gdd-approval-card__dialog-actions">
|
||||
<button type="button" onClick={() => setGddDetailsOpen(false)}>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<GddDetailsDialog gdd={gdd} onClose={() => setGddDetailsOpen(false)} />
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -108,6 +108,7 @@ export function ProjectSupervisorView({
|
||||
<PlanGddSurface
|
||||
state={planGddState}
|
||||
active={isPlanningLaneRuntime(runtimePanelProps.runtime)}
|
||||
projectPath={projectPath}
|
||||
busy={planGddHydrateBusy}
|
||||
error={planGddError}
|
||||
onRefresh={onPlanGddRefresh}
|
||||
|
||||
@@ -368,6 +368,7 @@ export function ProjectWorkspaceChatPane({
|
||||
<PlanGddSurface
|
||||
state={planGddState}
|
||||
active={isPlanningLaneRuntime(projectSupervisorRuntime)}
|
||||
projectPath={projectPath}
|
||||
busy={planGddHydrateBusy}
|
||||
error={planGddError}
|
||||
onRefresh={onPlanGddRefresh}
|
||||
|
||||
@@ -3502,6 +3502,54 @@ textarea {
|
||||
gap: 8px 14px;
|
||||
}
|
||||
|
||||
/* 批准后的交付行。它是策划阶段唯一的产物出口,所以给一条分隔线把它和上面的状态
|
||||
区分开,而不是混成第三行元信息。 */
|
||||
.plan-gdd-stage-progress__delivery {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
padding-top: 9px;
|
||||
border-top: 1px solid #e6ecf5;
|
||||
}
|
||||
|
||||
.plan-gdd-stage-progress__delivery code {
|
||||
min-width: 0;
|
||||
color: #3c4a5c;
|
||||
font-size: 11px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.plan-gdd-stage-progress__delivery-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.plan-gdd-stage-progress__delivery-actions button {
|
||||
min-height: 30px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid #cfd7e6;
|
||||
border-radius: 6px;
|
||||
color: #27364a;
|
||||
background: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.plan-gdd-stage-progress__delivery-actions button:hover:not(:disabled),
|
||||
.plan-gdd-stage-progress__delivery-actions button:focus-visible {
|
||||
border-color: #1f6feb;
|
||||
color: #1f6feb;
|
||||
}
|
||||
|
||||
.plan-gdd-stage-progress__delivery-actions button:disabled {
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
.plan-gdd-stage-progress__delivery-error {
|
||||
color: #b42323;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.plan-gdd-stage-progress__header strong {
|
||||
color: #27364a;
|
||||
}
|
||||
|
||||
@@ -55,6 +55,30 @@ function draftPlanGddState(
|
||||
});
|
||||
}
|
||||
|
||||
/** 已批准:审批卡整张收掉,只剩标题栏和它下面的交付行。 */
|
||||
function approvedPlanGddState() {
|
||||
const base = createPlanGddStateView();
|
||||
const gddRef = base.versions[0]!.gddRef;
|
||||
return createPlanGddStateView({
|
||||
state: 'approved',
|
||||
pendingApproval: null,
|
||||
approvedGddRef: gddRef,
|
||||
versions: [
|
||||
{
|
||||
...base.versions[0]!,
|
||||
status: 'approved',
|
||||
decision: {
|
||||
action: 'approve' as const,
|
||||
decidedAtUtc: '2026-08-18T01:00:00Z',
|
||||
},
|
||||
},
|
||||
],
|
||||
session: base.session
|
||||
? { ...base.session, phase: 'approved', awaitingAnswerFor: null }
|
||||
: null,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 完整总控面板在策划链路下漏给用户的几块:面板本体、currentAction/计划进度、
|
||||
* 五段式紧凑进度、子 Agent 列表。策划链路的断言统一从这里查。
|
||||
@@ -308,6 +332,66 @@ export function registerPlanGddApprovalTests() {
|
||||
);
|
||||
});
|
||||
|
||||
it('hands the approved GDD back through the stage strip instead of a card', async () => {
|
||||
// 批准之后审批卡按设计整张收掉,此前那一刻起用户就再也够不到 GDD:approvedGddRef
|
||||
// 前端没人读,渲染好的 Markdown 也没有出口。交付行补的就是这个缺口——不额外占一
|
||||
// 张卡,只在标题栏下多一行:文件在哪、看正文、用外部程序打开。
|
||||
const harness = createProjectSupervisorRuntimeHarness();
|
||||
harness.setPlanGddState(approvedPlanGddState());
|
||||
await mountPlanningSurface(harness);
|
||||
|
||||
expect(screen.queryByLabelText('GDD 审批卡')).toBeNull();
|
||||
const delivery = await screen.findByLabelText('GDD 交付');
|
||||
expect(delivery.textContent).toContain(
|
||||
`${harness.projectPath}/game/fast_gdd.md`,
|
||||
);
|
||||
expect(screen.queryByRole('button', { name: /^批准/ })).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: '修改' })).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: '退回重做' })).toBeNull();
|
||||
|
||||
// 正文弹层与审批时是同一份,批准前后看到的内容一致。
|
||||
fireEvent.click(
|
||||
within(delivery).getByRole('button', { name: '查看 GDD 正文' }),
|
||||
);
|
||||
const dialog = screen.getByRole('dialog');
|
||||
expect(within(dialog).getByLabelText('GDD 版本与指纹')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('asks the shell to open the rendered GDD for the current project', async () => {
|
||||
const harness = createProjectSupervisorRuntimeHarness();
|
||||
harness.setPlanGddState(approvedPlanGddState());
|
||||
await mountPlanningSurface(harness);
|
||||
|
||||
const delivery = await screen.findByLabelText('GDD 交付');
|
||||
fireEvent.click(within(delivery).getByRole('button', { name: '打开文件' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
harness.invoke.mock.calls.filter(
|
||||
([command]) => command === 'open_local_project_plan_gdd_markdown',
|
||||
),
|
||||
).toHaveLength(1);
|
||||
});
|
||||
const [, args] = harness.invoke.mock.calls.find(
|
||||
([command]) => command === 'open_local_project_plan_gdd_markdown',
|
||||
)!;
|
||||
// 路径交给后端自己在项目根下解析,前端只报项目——避免两侧各拼一次相对路径。
|
||||
expect(args).toEqual({ projectPath: harness.projectPath });
|
||||
});
|
||||
|
||||
it('keeps the delivery row hidden while the approval projection is still recovering', async () => {
|
||||
// 恢复态下权威投影还没收敛,磁盘上那份 Markdown 未必是用户批的那版。此时给出口
|
||||
// 等于让用户读一份可能已经失效的交付物。
|
||||
const harness = createProjectSupervisorRuntimeHarness();
|
||||
harness.setPlanGddState({
|
||||
...approvedPlanGddState(),
|
||||
recoveryPending: true,
|
||||
});
|
||||
await mountPlanningSurface(harness);
|
||||
|
||||
expect(screen.queryByLabelText('GDD 交付')).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps the supervisor runtime panel off the planning lane while the planner is working', async () => {
|
||||
// 完整面板是给做游戏链路的:十几个专业 Agent、多步计划、逐 Agent 重试。策划链路
|
||||
// 只有一个 project-planning 子 Run、一两步计划,面板画出来的全是 D11 拓扑的内部
|
||||
|
||||
@@ -3498,12 +3498,20 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
expect(styles).toMatch(
|
||||
/\.game-workbench-chat \.project-supervisor-message-list\s*\{[^}]*flex:\s*1 1 auto[^}]*min-height:\s*96px[^}]*overflow-y:\s*auto/s,
|
||||
);
|
||||
// 审批卡自带上限并内部滚动,不能靠挤别人来容纳决定项。
|
||||
// 审批卡与运行状态各自带上限并内部滚动,不能靠挤别人来容纳自己的内容。锁的是
|
||||
// 「有 clamp 上限 + 自己滚」这个不变量,不锁具体数值——三个断点是随排版调整的
|
||||
// 设计取值,钉死它们只会让每次调高度都顺带改一次测试,却挡不住真正的回归(去掉
|
||||
// 上限或去掉内部滚动)。
|
||||
expect(styles).toMatch(
|
||||
/\.game-workbench-chat \.gdd-approval-card\s*\{[^}]*max-height:\s*clamp\(160px, 34dvh, 380px\)[^}]*overflow-y:\s*auto/s,
|
||||
/\.game-workbench-chat \.gdd-approval-card\s*\{[^}]*max-height:\s*clamp\([^)]*\)[^}]*overflow-y:\s*auto/s,
|
||||
);
|
||||
expect(styles).toMatch(
|
||||
/\.game-workbench-chat \.agent-runtime-status\s*\{[^}]*max-height:\s*clamp\(120px, 24dvh, 240px\)[^}]*overflow-y:\s*auto/s,
|
||||
/\.game-workbench-chat \.agent-runtime-status\s*\{[^}]*max-height:\s*clamp\([^)]*\)[^}]*overflow-y:\s*auto/s,
|
||||
);
|
||||
// 策划窄条是 `.agent-runtime-status` 的一种,但它只在需要用户动手时出现,用的是
|
||||
// 自己那条更宽的上限;这条断言保证它没有退回去继承调试面板那个 240px 天花板。
|
||||
expect(styles).toMatch(
|
||||
/\.game-workbench-chat \.planning-lane-runtime-strip\s*\{[^}]*max-height:\s*clamp\([^)]*\)/s,
|
||||
);
|
||||
expect(styles).toMatch(
|
||||
/\.game-workbench-chat \.project-runtime-summary\s*\{[^}]*position:\s*sticky[^}]*top:\s*-10px/s,
|
||||
@@ -9806,7 +9814,8 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
projectPath,
|
||||
});
|
||||
let directTurnUpdateHandler:
|
||||
((event: { payload: Record<string, unknown> }) => void) | null = null;
|
||||
| ((event: { payload: Record<string, unknown> }) => void)
|
||||
| null = null;
|
||||
const listen = vi.fn(
|
||||
async (
|
||||
eventName: string,
|
||||
@@ -10763,7 +10772,8 @@ export function registerProjectAgentStatusTests() {
|
||||
},
|
||||
);
|
||||
let runtimeUpdateHandler:
|
||||
((event: { payload: Record<string, unknown> }) => void) | null = null;
|
||||
| ((event: { payload: Record<string, unknown> }) => void)
|
||||
| null = null;
|
||||
const listen = vi.fn(
|
||||
async (
|
||||
eventName: string,
|
||||
|
||||
Reference in New Issue
Block a user