diff --git a/README.md b/README.md index 87e2c127..67c329f8 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ An independent, fully open-source terminal project targeting WindTerm 2.7.0 feat ## 项目地图 -新的[自主 C++ / Qt 会话入口](src/qt/README.md)以显式构建开关提供固定 80×24 的最小本地会话;当前默认入口和上面的操作说明仍属于既有 Win32 原型。Qt 工作区、完整文字交互及动态 resize 尚待迁移,不能据此声明完整开发版。 +新的[自主 C++ / Qt 会话入口](src/qt/README.md)以显式构建开关提供固定 80×24 的 cmd 标签,可新增、切换、独立关闭并在应用退出前核对会话回收;[限定验证](experiments/shell/results/2026-10-28-qt-tabs/README.md)不继承旧配置能力。当前默认入口和上面的操作说明仍属于既有 Win32 原型。分屏、其他 shell、工作区恢复、完整文字交互及动态 resize 尚待迁移,不能据此声明完整开发版。 | 要了解什么 | 唯一入口 | |---|---| diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index 48b9f217..0988c1db 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -8,6 +8,7 @@ | 配置 | 证据定位 | |---|---| +| Qt cmd 标签与退出前回收 | [固定结果](../experiments/shell/results/2026-10-28-qt-tabs/README.md)、[验证身份](../experiments/shell/results/2026-10-28-qt-tabs/validation.json):双 cmd 隔离、失败页、关闭与四个实际事件循环边界;Qt 1 项和其他 49 项诊断及内部跳过分列,旧中断轮不拼接 | | Qt 正式入口的独立 hosted 构建 | [固定结果](../experiments/shell/results/2026-10-28-qt-cpp-ci/README.md)、[验证身份](../experiments/shell/results/2026-10-28-qt-cpp-ci/validation.json):固定 SDK、VS 2026 hosted 与真实 cmd 的 offscreen / OwnedJob 检查;原依赖换行和 generator 失败保留。没有原生上屏、完整部署哈希或产品包验收 | | 自主 C++ + Qt Widgets + 固定微软 ConPTY 正式目标 | [ADR-0007](adr/0007-bundled-microsoft-conpty.md) / [ADR-0008](adr/0008-cpp-core-qt-widgets.md) 记录用户确认;新组合的实现与验证另行留证,不引用旧默认或 Alacritty 通过结果代替 | | 既有默认/Alpha:自主 C++ + 系统 ConPTY + Win32 | R1 限定总验收和有限 VT/Unicode 子集;与当前 Qt/固定后端目标组合分列 | diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 4cb58b5e..cfe67a5f 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -23,7 +23,7 @@ | 新路线迁移入口 | 当前状态与边界 | 目标差距 | 路标 | |---|---|---|---| -| 自主 C++ / Qt / 固定微软 ConPTY | **实现中,未验收**:[显式 Qt 入口](../src/qt/README.md)提供固定 80×24 的单个真实 cmd、有界输入/输出、基本 ASCII/16 色绘制及错误/关闭处理;与旧 Win32/Alacritty 配置分别验证 | 完整工作区、动态 resize、历史/选择/Find、Unicode/IME、真实 DPI、应用兼容与性能仍需实现或迁移;当前为本地构建,不是可发布开发包 | R2 | +| 自主 C++ / Qt / 固定微软 ConPTY | **实现中,未验收**:[显式 Qt 入口](../src/qt/README.md)提供固定 80×24 的 cmd 标签、基本 ASCII/16 色及各会话独立有界队列;双 cmd 输入、关闭和错误隔离及真实事件循环退出前回收有[限定验证](../experiments/shell/results/2026-10-28-qt-tabs/README.md)。Qt 1 项与其余 49 项既有/独立诊断分列 | 分屏、其他 shell、工作区恢复、动态 resize、历史/选择/Find、Unicode/IME、真实 DPI、应用兼容与性能仍需实现或迁移;应用/标签退出策略沿原型合同,完整 WindTerm 标签行为对照未完成;当前不是可发布开发包 | R2 | 官方参考的基础文字行为已按 [R1-01 合同](research/windterm-r1-text-contract.md)完成两组各两轮的自动取证;这为独立实现提供行为依据,产品能力仍按下表分别验收。 diff --git a/docs/TRACKING.md b/docs/TRACKING.md index 9f08e3d4..36cca13c 100644 --- a/docs/TRACKING.md +++ b/docs/TRACKING.md @@ -21,7 +21,7 @@ | 工作 | 责任任务 | 硬前置或联合条件 | |---|---|---| | 正式路线最小单会话与专用 CI | [单会话 #115](https://github.com/JavaZeroo/termforge/issues/115)、[Qt CI #117](https://github.com/JavaZeroo/termforge/issues/117) | 均已限定合入 main;固定依赖的 Windows Qt 构建与会话结果独立验证,不能由默认/参考 CI 代替 | -| Qt 多 cmd 标签迁移 | [会话隔离 #124](https://github.com/JavaZeroo/termforge/issues/124) | #115 已解除前置;先验证输入/输出、关闭和错误隔离,完整工作区/恢复与复杂输入仍另验 | +| Qt 多 cmd 标签迁移 | [会话隔离 #124](https://github.com/JavaZeroo/termforge/issues/124) | 固定 cmd 双会话的输入/输出、关闭和错误隔离及应用退出前回收已有[限定验证](../experiments/shell/results/2026-10-28-qt-tabs/README.md);完整工作区/恢复、原版标签交互与复杂输入仍另验 | | 原版备用屏可观察合同 | [1049 取证 #118](https://github.com/JavaZeroo/termforge/issues/118)、[样式/活动缓冲区 #123](https://github.com/JavaZeroo/termforge/issues/123) | #118 已限定合入 main,提供原版光标/文字投影;#123 补剩余观察。末端、local-shell transport 和未知项分别记录 | | 原版本地输出正确性自动化 | [小负载完整文本 #119](https://github.com/JavaZeroo/termforge/issues/119) | 已作为 #3 的独立切片合入 main;生产事件、全文与回收绑定,尚不验收 C0、消费/呈现端点、扰动或正式 #4 基线 | | 历史候选/官方后端联合基线 | [联合配置基线 #55](https://github.com/JavaZeroo/termforge/issues/55) | 保留原固定源码与红/绿结果;可复用输入和接入经验,不替代自主 C++/Qt/固定 ConPTY 的新配置验收 | diff --git a/docs/milestones/R2-local-terminal.md b/docs/milestones/R2-local-terminal.md index bbf3f481..5286b199 100644 --- a/docs/milestones/R2-local-terminal.md +++ b/docs/milestones/R2-local-terminal.md @@ -8,7 +8,7 @@ ## 当前站位 -R2 的平台调查、有限会话与恢复及 Alacritty 参考实验已有各自限定交付。当前正式路线为**自主 C++ 核心、Qt Widgets 与随包固定微软 ConPTY**;[最小可构建单会话入口 #115](https://github.com/JavaZeroo/termforge/issues/115)与[固定依赖专用 CI #117](https://github.com/JavaZeroo/termforge/issues/117)已合入 main,完成固定 80×24、本地 cmd 输入输出、错误和关闭回收的限定验证。接续补 C++ 核心能力、Qt 多会话和完整用户工作流。[原版模式 #118](https://github.com/JavaZeroo/termforge/issues/118)与[本地 cmd 正确性工具 #119](https://github.com/JavaZeroo/termforge/issues/119)已分别交付限定证据,剩余样式、C0 和性能端点继续调查。现有 C++ 核心仍是有限子集;候选的首批字素、TUI 与重排成功不转记为 C++ 能力。已有 Alpha、Win32 原型、Alacritty 和 Draft 后端保留固定身份,新组合需要自己的证据。 +R2 的平台调查、有限会话与恢复及 Alacritty 参考实验已有各自限定交付。当前正式路线为**自主 C++ 核心、Qt Widgets 与随包固定微软 ConPTY**;[最小可构建单会话入口 #115](https://github.com/JavaZeroo/termforge/issues/115)与[固定依赖专用 CI #117](https://github.com/JavaZeroo/termforge/issues/117)已合入 main,完成固定 80×24、本地 cmd 输入输出、错误和关闭回收的限定验证。Qt 固定 cmd 多标签已有[会话隔离与退出前回收的限定验证](../../experiments/shell/results/2026-10-28-qt-tabs/README.md),接续补 C++ 核心能力、工作区恢复和完整用户工作流。[原版模式 #118](https://github.com/JavaZeroo/termforge/issues/118)与[本地 cmd 正确性工具 #119](https://github.com/JavaZeroo/termforge/issues/119)已分别交付限定证据,剩余样式、C0 和性能端点继续调查。现有 C++ 核心仍是有限子集;候选的首批字素、TUI 与重排成功不转记为 C++ 能力。已有 Alpha、Win32 原型、Alacritty 和 Draft 后端保留固定身份,新组合需要自己的证据。 历史联合源码 `3e5b96d` 的三配置[真实基线](../research/joint-conpty-baseline.md)及其后续限定修复继续用于复用输入、接入经验和复验原失败;它们不证明新的 C++/Qt/固定后端组合已通过。Draft #50 的后端成果接入当前路线后仍须验证原合同,路线确认不解除失败或跳过项。 @@ -43,7 +43,7 @@ flowchart LR ## 下一批的任务边界 -1. **从正式路线最小整合接续 Qt 多会话和后端复验。** #115 已限定验证固定 80×24 单会话,#117 已完成固定依赖的独立 Windows CI;[#124](https://github.com/JavaZeroo/termforge/issues/124)迁移多 cmd 标签和关闭隔离。动态尺寸、C++ 核心模式和 Qt 交互缺口继续保留。#55 已交付含红的真实基线;#51 原生 viewport resize、#63 工作区原生 viewport,以及 #62 的[真实 writer 持块、取消与停止夹具](../research/sender-fence.md)和 #53 原 UI/3MB/八会话 data-only 合同均已在各自范围验证并整合到实验父分支。接续 #52 GUI Vim 与 #47 的完整官方矩阵、同次真实 CF/IME;限定切片关闭不等于联合验收通过。未完成项可以用原冻结输入独立定位,最终修复验收须在联合配置重建;期限、负载和原失败保留,一次未复现不自动构成因果修复。 +1. **从正式路线最小整合接续工作区和后端复验。** #115 已限定验证固定 80×24 单会话,#117 已完成固定依赖的独立 Windows CI;[#124](https://github.com/JavaZeroo/termforge/issues/124)已限定验证 cmd 标签输入/关闭隔离与事件循环退出前回收,完整工作区和恢复仍另行迁移。动态尺寸、C++ 核心模式和 Qt 交互缺口继续保留。#55 已交付含红的真实基线;#51 原生 viewport resize、#63 工作区原生 viewport,以及 #62 的[真实 writer 持块、取消与停止夹具](../research/sender-fence.md)和 #53 原 UI/3MB/八会话 data-only 合同均已在各自范围验证并整合到实验父分支。接续 #52 GUI Vim 与 #47 的完整官方矩阵、同次真实 CF/IME;限定切片关闭不等于联合验收通过。未完成项可以用原冻结输入独立定位,最终修复验收须在联合配置重建;期限、负载和原失败保留,一次未复现不自动构成因果修复。 2. **并行处理原版对照、平台语义与性能。** [原生语义处理路径 #56](https://github.com/JavaZeroo/termforge/issues/56)先明确 #41 原合同需要什么、哪些路径可支持;保留必要的上游或独立实现工作,不用“平台不同”豁免缺口。#3/#4 优先建立原版 Windows 本地终端基线;#54 现在冻结重复方法与完成端点并定位吞吐成本,数值预算作为提案交用户确认,不等到 #34 才补性能。 3. **按原版合同补自主 C++ 能力。** #29 核对极窄/极短、软硬行、历史/淘汰、选择/Find、连续 resize 和 child ack;#30 核对 IME 组合/提交/取消与输出/resize/切换、应用键盘/粘贴接收、实际复制及同次八会话。已验收的 #39 提供的是参考实现与固定场景;自主 C++ 是否具备同等字素能力必须独立实现和验证。 4. **补终端模式和应用矩阵。** #27 明确 DECOM/DECAWM、pending-wrap、保存恢复、异常序列与 margin×resize;#28 先固定应用版本/入口/动作及协议字节预期,再逐应用完成进入、操作、resize、退出、主屏恢复。Vim 的一个修复不替代完整 TUI 验收。 diff --git a/experiments/shell/results/2026-10-28-qt-tabs/README.md b/experiments/shell/results/2026-10-28-qt-tabs/README.md new file mode 100644 index 00000000..8878a672 --- /dev/null +++ b/experiments/shell/results/2026-10-28-qt-tabs/README.md @@ -0,0 +1,80 @@ +# Qt 独立 cmd 标签最小切片(#124) + +状态:**固定 80×24 双 cmd 标签切片及应用退出前回收检查通过;完整 R2 未验收**。本记录绑定源码 `cf8b40b2e44f9e475cfc42b5008d46c670ff1756`,后续整理证据的文档提交不改变该运行身份。 + +按 [ADR-0008](../../../../docs/adr/0008-cpp-core-qt-widgets.md)使用自主 C++ 核心和 Qt Widgets,后端为[随包固定微软 ConPTY](../../../../third_party/conpty/README.md)。标签外观、最后会话结束和应用退出策略沿当前有限原型入口的工程合同;**尚未完成 WindTerm 2.7.0 完整标签交互对照**,不据此宣布原版行为一致或新策略已获用户确认。 + +## 固定配置与完整运行 + +Windows 11 Pro 10.0.26200、x64 Release;Qt **6.10.3 / win64_msvc2022_64**,应用动态链接 Core/Gui/Widgets,测试另用 Test;MSVC **19.42.34433.0**、Windows SDK **10.0.26100.0**、Python **3.12.10**;固定微软 ConPTY **1.25.260930003**。候选适配器关闭。Qt 检查使用 **offscreen** 平台及独立取得的 OFL Roboto Mono,`presented_done=null`。 + +| 同一最终源码的检查 | 结果 | 解释 | +|---|---|---| +| 完整构建 | 退出 **0** | 根代理直接执行,并在冻结前以显式退出码守卫核对;没有独立 build-exit JSON,不补造附件 | +| 完整 CTest | **50/50,退出 0,325.94 秒** | 1 项 Qt 产品入口与其他 49 项既有/独立 core、system、离线诊断分列;不能合成 Qt 的全部能力验收 | +| 其中 `qt_cpp_shell` | **1/1,1.53 秒** | 外层未超时、native exit=0;自有 Job active=0、total=26、terminated=0 | +| 冻结后核对 | **7151 项全部匹配** | 源码 2694、Qt 产品运行时 25、smoke 运行时 27、旧诊断运行时 47、Qt SDK 4358;`changed=[]` | + +完整运行由根代理在自己的监督进程中执行至终态。`final-reviewed-build.log`、`final-reviewed-ctest.log`、`LastTest.log`、`final-reviewed-exit.json`、`frozen-inputs.json` 和 `frozen-after.json` 的实际字节数与 SHA-256 见 [validation.json](validation.json)。它同时保留 20 项精选源码/部署身份和完整本地 manifest 的哈希,**不是完整部署清单副本**。SDK 的 4358 文件树摘要为 `a35ab6fe76b62e4d9bd6d3c6b0d476c63583070cea5c9118ef5bb02042ffe266`,与原始官方归档、aqt 修补 SDK 的身份分别对待。 + +完整构建仍有未部署翻译的警告。当前只是本地构建输出;未包含编译器运行库部署,完整许可及对应源码审计、干净机器验证和可分发包均未验收。依赖来源及准备方法见 [Qt 依赖说明](../../../../third_party/qt/README.md)。 + +## Qt 实际执行的行为 + +保留原单会话检查:向真实 cmd 输入 `set /a 731+19`,独立字面期望 **750**,结果三格有新增正文墨迹;自然退出 **37**、启动失败可见错误和人工关闭回收均执行。 + +多标签检查实际点击 `+`、标签及关闭按钮,核对当前输入焦点。两个真实 cmd 分别执行 `731+19` 和 `401+26`,独立期望 **750 / 427**,命令与结果不串入另一个会话;父窗口图像中对应正文格有新增墨迹。关闭一个标签后,存活页继续得到 **778**,自然退出 **41** 后完成容器。另一个场景中,启动失败标签显示错误且不妨碍存活页得到 **147**;父窗口关闭回收全部会话,单独失败启动页在用户关闭前保留可见错误。 + +应用退出另使用 smoke 内真实 `QApplication::exec()`,沿产品相同的 `SessionTabs`、`quitOnLastWindowClosed(false)` 及 `on_all_exited → app.exit` 策略覆盖四条边界: + +1. 关闭含两个活动 cmd 的父窗口。 +2. 关闭最后一个活动标签。 +3. 关闭单独的启动失败页。 +4. 关闭已自然结束的标签。 + +在统一完成回调、`aboutToQuit` 和 `exec` 返回三个检查点,核对所有会话模型已结束、回收可验证且各自 Job active=0。已移除页若已被延迟删除,采用其仍存活时取得的回收记录;不使用 `exec` 返回后的模型析构或仅外层 Job=0 来替代退出前证明。这是与产品相同入口合同的应用窗口测试,**不是另行黑盒操纵产品 EXE 的完整交互验收**。 + +人工关闭成功返回 0;源码对“模型已结束但回收不可验证”返回 1,不等待一个不会再变化的标志。失败状态分支由源码及最终审查核对,未声称另做应用级回收失败注入。成功启动的最后会话自然结束沿原型入口的退出路径和状态;启动失败页由用户关闭。外层期限仍为 **25 秒**,CTest 期限仍为 **35 秒**。 + +## 分次红绿与身份缺口 + +| 片段 | 实际运行红 | 随后局部绿 | 身份边界 | +|---|---|---|---| +| 新增/选择双 cmd | 5.64 秒:新增动作未创建独立标签 | 0.78 秒 | 保留红源码快照;缺当时 EXE 身份及独立 at-run 绑定记录 | +| 关闭单个标签 | 0.73 秒:缺少标签关闭动作 | 0.83 秒 | 红的部分源码及 smoke EXE 哈希有保存 | +| 关闭父窗口 | 6.10 秒:继续泵事件仍未回收全部会话 | 1.00 秒 | 红的部分源码及 smoke EXE 哈希有保存;该绿不能证明真实应用退出前已回收 | +| 最后会话结束 | 5.78 秒:容器未完成 | 1.08 秒 | 红的部分源码及 smoke EXE 哈希有保存 | +| AppExec Spec P2 | 1.33 秒:事件循环先于回收检查退出 | 1.34 秒首次绿;1.45 秒四边界绿;提交后 1.51 秒局部绿 | 红及首次绿有部分源码/EXE 身份;提交后局部绿绑定 `76070a6`;最终完整运行另绑定 `cf8b40b` | + +前四条局部 green 没有独立保存的源码/EXE 身份;历史中间工作树的精确提交身份也不从后来的提交倒推。JSON 对这些缺口明确保留 `null`/unknown,已保存的源码快照、身份记录、运行及构建日志分别列出实际哈希。以上秒数均为测试耗时,不是终端性能指标。 + +另保留两类**编译失败**:`interface-red` 的 C1083 缺少头文件,及 `all-exited-green-build` 的 C2662 const getter 错误;后者补 const 重载后才完成后续构建和运行绿。它们不能计作已复现的运行红,也没有“失败编译生成的新 EXE”。 + +AppExec P2 的原缺口是 smoke 关闭窗口后继续泵事件,而 main 默认随最后窗口关闭退出;原通过不足以证明主入口退出前的 timer 排空。新红实际复现了退出时序合同失败,**未据此认定泄漏或死锁**。红的外层最终 active=0 不会把失败检查改判为通过。 + +## 内部跳过与中断旧轮 + +完整 50 项运行中另有 **12 条内部 skip**,逐条测试标识和原始原因见 JSON: + +- 4 个可选参考整类:`CandidateComparisonTests`、`CapabilityMatrixTests`、`FuzzMatrixTests`、`RealApplicationMatrixTests`。 +- 7 个实际系统剪贴板场景:actions 1、shell 3、selection_bytes 2、selection_context 1。 +- 1 个未启用的原版失败注入:`test_suspended_target_is_reclaimed_if_job_assignment_fails`。 + +跳过不计为对应能力通过;整类跳过也不等于执行了一个测试。Qt 单项的上述断言均实际执行。原版研究/旧 Win32、system 或参考配置的独立成功不转记给 Qt 产品入口。 + +较早源码 `aa068c99b2198a76f230e101ea25c95f5d3fc6a5` 的整轮预期 49 项,**仅 29 项完成,停在第 30 项 `workflow_fixture` 开始,退出码未知**。监督会话无法恢复,未观察到完成页脚;当轮 7143 个冻结文件虽全部匹配,也不能将中断轮写为通过。其封存原件及摘要哈希单列保留,不与新的 50 项运行拼接。 + +最终 Standards/Spec 两轴运行时代码审查的未解决项均为 0,依据根代理的最终汇总;没有独立审查报告附件,因此不伪造报告哈希。 + +## 复现及公开边界 + +配置模板与完整参数见 JSON 和 [入口说明](../../../../src/qt/README.md)。本次完整构建及测试主体命令为: + +```powershell +cmake --build build/qt-tabs --config Release --parallel 2 +ctest --test-dir build/qt-tabs -C Release --verbose +``` + +根代理仅在本次 PowerShell 进程中为既有诊断加入 Git 工具目录,并设置 `PYTHONUTF8=1`、`TERMFORGE_REQUIRE_SYSTEM_CLIPBOARD=0`。Qt offscreen 由该 CTest 项的环境设置生效;没有改全局环境、剪贴板或 DPI。本证据整理只读取既存原件,未重跑构建或测试。 + +双会话结果不继承八会话、分屏、持久化、动态 resize、完整键盘/IME、DPI、实际上屏、物理输入或性能验收,也不证明 Linux/macOS、日常使用版本或可分发包。公开内容仅含合成断言、脱敏汇总及精选哈希;原始日志、图片、私人路径、进程标识、完整部署/SDK 清单和 WindTerm 私有材料均不随记录发布。 diff --git a/experiments/shell/results/2026-10-28-qt-tabs/validation.json b/experiments/shell/results/2026-10-28-qt-tabs/validation.json new file mode 100644 index 00000000..7286140a --- /dev/null +++ b/experiments/shell/results/2026-10-28-qt-tabs/validation.json @@ -0,0 +1,1493 @@ +{ + "schema": "termforge-qt-tabs-validation-v1", + "status": "limited_fixed_size_cmd_tabs_verified_full_R2_incomplete", + "scope": "Issue #124: independent fixed 80x24 local cmd tabs in the C++/Qt/fixed-ConPTY migration entry; no complete WindTerm tab interaction parity or release acceptance.", + "configuration": { + "host": "Windows 11 Pro", + "os_version": "10.0.26200", + "architecture": "x64", + "build": "Release", + "core": "independently developed C++", + "gui": "Qt Widgets 6.10.3", + "qt_sdk_target": "win64_msvc2022_64", + "generator": "Visual Studio 17 2022", + "compiler": "MSVC 19.42.34433.0", + "windows_sdk": "10.0.26100.0", + "python": "3.12.10", + "backend": "bundled fixed Microsoft ConPTY 1.25.260930003", + "columns": 80, + "rows": 24, + "qt_platform": "offscreen", + "font": "independently acquired OFL Roboto Mono", + "candidate_adapter": false, + "presented_done": null + }, + "source_identity": { + "final_source_commit": "cf8b40b2e44f9e475cfc42b5008d46c670ff1756", + "worktree_at_freeze": "clean", + "frozen_manifest": { + "artifact_name": "frozen-inputs.json", + "bytes": 1561346, + "sha256": "f1ddbca6f95fceb5c2dde9655c2c009f9412be842ddcaef20714a185e57f2972", + "published": false + }, + "post_run_check": { + "artifact_name": "frozen-after.json", + "bytes": 185, + "sha256": "77f8736c6174d8bff6652a76e7fbe553c72ce85d57bcc4f405e3eeeb77c10105", + "published": false + }, + "counts": { + "source": 2694, + "product_runtime": 25, + "smoke_runtime": 27, + "legacy_diagnostic_runtime": 47, + "qt_sdk": 4358 + }, + "files_checked": 7151, + "post_run_changed": [], + "post_run_status": "passed", + "selected_file_identities": [ + { + "scope": "source", + "path": "CMakeLists.txt", + "bytes": 23609, + "sha256": "8e000dd91cab10480fcac6a399cfff0413129739dbb4ed0bcc11ac62e5ea991d" + }, + { + "scope": "source", + "path": "src/qt/main.cpp", + "bytes": 1137, + "sha256": "4af7c6722ba6847d90d3d9938849aa3b69a8de749c043a278c8725dc46011796" + }, + { + "scope": "source", + "path": "src/qt/session_tabs.hpp", + "bytes": 7004, + "sha256": "7cd21641d748c2ad864731d94a35b1140377bb242f07781d5e1bbbe797020f0e" + }, + { + "scope": "source", + "path": "src/qt/shell_smoke.cpp", + "bytes": 25287, + "sha256": "86022250a4c5d50824e112d6748c56bb549cb055c0737288f8a3c88f3733c8d0" + }, + { + "scope": "source", + "path": "src/qt/terminal_session.hpp", + "bytes": 9446, + "sha256": "3cdb5bfa28019a5de9fdd92c9f976f0bf14899f2de324a8f783574a16492b2af" + }, + { + "scope": "source", + "path": "src/qt/terminal_widget.hpp", + "bytes": 6844, + "sha256": "e6bd2759601de942560dc367f5c25961c0ef08278a0e327c3321ebe142bf55b7" + }, + { + "scope": "source", + "path": "src/qt/run_smoke.py", + "bytes": 4287, + "sha256": "25f4d6adec4e9bfad77698300c44d9162511ac1ebb722993d5a52cdb8a863983" + }, + { + "scope": "source", + "path": "third_party/qt/manifest.json", + "bytes": 5806, + "sha256": "2796294f254f7fe0370be99d2de8f7745734c7352ac2648b1f8d9a8fab5ad2cd" + }, + { + "scope": "source", + "path": "third_party/conpty/manifest.json", + "bytes": 1467, + "sha256": "e75e522a3355c0a66c6d60cafa02413a83d8757262747f650b77c959c09ed439" + }, + { + "scope": "product_runtime", + "path": "build/qt-tabs/qt-bin/Release/termforge_qt_shell.exe", + "bytes": 145920, + "sha256": "0bfe1f63a6e12bba8bc4a092b897ef0c341825d0be408ac818a09c7d6033f7bc" + }, + { + "scope": "smoke_runtime", + "path": "build/qt-tabs/qt-smoke-bin/Release/qt_cpp_shell_smoke.exe", + "bytes": 202240, + "sha256": "80201411b9bb8b0dc073c133d8039b7468f87098fc36df3051d7ea5044a781eb" + }, + { + "scope": "product_runtime", + "path": "build/qt-tabs/qt-bin/Release/conpty-backend/conpty.dll", + "bytes": 119608, + "sha256": "feeef341d891643c62d30b6b07800bc70f0bc148f44cb8c3bee84aa557ae805a" + }, + { + "scope": "product_runtime", + "path": "build/qt-tabs/qt-bin/Release/conpty-backend/OpenConsole.exe", + "bytes": 1089376, + "sha256": "3d66b23d0a71bb8eed2b77edc8b9df9bf54ce6c8fb74c863a30e760997f80586" + }, + { + "scope": "product_runtime", + "path": "build/qt-tabs/qt-bin/Release/Qt6Core.dll", + "bytes": 10250040, + "sha256": "b712b4754588e89f855dc0aa087d6304b2fd21b0e0639d1cdeb9cb969bd5fb72" + }, + { + "scope": "product_runtime", + "path": "build/qt-tabs/qt-bin/Release/Qt6Gui.dll", + "bytes": 9514296, + "sha256": "03ba47a2025036a04f5548bce242788ac21edf551e78d5b42e1b87beb799904f" + }, + { + "scope": "product_runtime", + "path": "build/qt-tabs/qt-bin/Release/Qt6Widgets.dll", + "bytes": 6577464, + "sha256": "61ddd0316a731b8dbabaf2924cd0d47b756461e37329b4f535902cf450416127" + }, + { + "scope": "product_runtime", + "path": "build/qt-tabs/qt-bin/Release/platforms/qwindows.dll", + "bytes": 997688, + "sha256": "cafd5bfdef3d89d176ee26725c8d4759fd6cb007645b645a4cbf6386364a3f36" + }, + { + "scope": "smoke_runtime", + "path": "build/qt-tabs/qt-smoke-bin/Release/platforms/qoffscreen.dll", + "bytes": 113976, + "sha256": "a35f87f597b91121cfc0ef39dcb0a5e7ad2876850fbfaf6377edf80eaa067bfd" + }, + { + "scope": "smoke_runtime", + "path": "build/qt-tabs/qt-smoke-bin/Release/Qt6Test.dll", + "bytes": 380216, + "sha256": "9d0473552b332aa378c60da05af97d741fcce06b0fc4dd68e36d885015eb5187" + }, + { + "scope": "product_runtime", + "path": "build/qt-tabs/qt-bin/Release/assets/fonts/RobotoMono-Regular.ttf", + "bytes": 125748, + "sha256": "af0bff7599c3df3831755c16e39b3c496df74b8c8d8a1161b14dc8461be17cb4" + } + ], + "selection_note": "20 selected public source/deployment identities only; not a copy of the full local manifest or all deployed files. The complete 7151-file manifest is retained locally and hashed. No test/build was rerun to prepare this summary." + }, + "dependencies": { + "qt_manifest": { + "artifact_name": "manifest.json", + "bytes": 5806, + "sha256": "2796294f254f7fe0370be99d2de8f7745734c7352ac2648b1f8d9a8fab5ad2cd", + "published": false + }, + "qt_version": "6.10.3", + "qt_original_archive_sha256": "4db84dee7fe3c558f242bef0a88852613af76580dc6d2b24596479f47004dad7", + "qt_sdk_files": 4358, + "qt_sdk_tree_sha256": "a35ab6fe76b62e4d9bd6d3c6b0d476c63583070cea5c9118ef5bb02042ffe266", + "qt_sdk_preparation": "official archive extraction plus relative bin/qt.conf; SDK tree identity is distinct from the original archive and aqt-patched SDK", + "application_modules": [ + "Core", + "Gui", + "Widgets" + ], + "test_only_modules": [ + "Test" + ], + "backend_manifest": { + "artifact_name": "manifest.json", + "bytes": 1467, + "sha256": "e75e522a3355c0a66c6d60cafa02413a83d8757262747f650b77c959c09ed439", + "published": false + }, + "backend_version": "1.25.260930003", + "backend_package_sha256": "6d6f8b008c655d814c498d8205d4012dcd4d0b6dfc1096461d0a185d1e3caf03", + "backend_pair_sha256": { + "conpty.dll": "feeef341d891643c62d30b6b07800bc70f0bc148f44cb8c3bee84aa557ae805a", + "OpenConsole.exe": "3d66b23d0a71bb8eed2b77edc8b9df9bf54ce6c8fb74c863a30e760997f80586" + }, + "deployment_limit": "Local windeployqt output only. No translations or compiler-runtime deployment; clean-machine validation, complete notices/corresponding-source review and distributable package acceptance remain pending." + }, + "commands": { + "configure_public_template": "cmake -S . -B build/qt-tabs -G \"Visual Studio 17 2022\" -A x64 -DTERMFORGE_BUILD_QT_SHELL=ON -DTERMFORGE_BUILD_CANDIDATE_ADAPTER=OFF -DBUILD_TESTING=ON -DQt6_DIR=\"/lib/cmake/Qt6\" -DTERMFORGE_CONPTY_BACKEND_DIR=\"\" -DPython3_EXECUTABLE=\"\"", + "final_build": "cmake --build build/qt-tabs --config Release --parallel 2", + "final_ctest": "ctest --test-dir build/qt-tabs -C Release --verbose", + "qt_targeted": "ctest --test-dir build/qt-tabs -C Release -R '^qt_cpp_shell$' --no-tests=error --verbose", + "configuration_paths": "Public templates use placeholders; no private absolute paths are published.", + "final_ctest_process_environment": { + "PATH": "prepend Git usr/bin to the inherited PATH in this PowerShell process only; full PATH not published", + "PYTHONUTF8": "1", + "TERMFORGE_REQUIRE_SYSTEM_CLIPBOARD": "0" + }, + "qt_test_environment": { + "QT_QPA_PLATFORM": "offscreen" + }, + "qt_test_environment_scope": "CTest qt_cpp_shell ENVIRONMENT property; not a process-wide or global Qt setting" + }, + "final_build": { + "exit_code": 0, + "observation": "Parent directly observed exit 0 with an explicit LASTEXITCODE failure guard before freezing clean cf8b40b source. No independent build-exit JSON exists.", + "log": { + "artifact_name": "final-reviewed-build.log", + "bytes": 10286, + "sha256": "8b67d01fbc1adb129e55c03325a8e9950ab232dbec432d3e13d75131962250f9", + "published": false + }, + "exit_artifact": null, + "warnings": [ + "windeployqt: translations catalog unavailable; deployment did not include translations" + ] + }, + "final_ctest": { + "source_commit": "cf8b40b2e44f9e475cfc42b5008d46c670ff1756", + "status": "completed", + "exit_code": 0, + "expected_entries": 50, + "passed_entries": 50, + "failed_entries": 0, + "real_seconds": 325.94, + "configuration_split": { + "C++/Qt/fixed-ConPTY": 1, + "independent_existing_core_system_offline_diagnostics": 49 + }, + "tests": [ + { + "number": 1, + "name": "shell_launch", + "status": "Passed", + "seconds": 0.01, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 2, + "name": "runtime_observation", + "status": "Passed", + "seconds": 2.84, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 3, + "name": "ime_input_expectation", + "status": "Passed", + "seconds": 0.04, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 4, + "name": "sent_message_wait", + "status": "Passed", + "seconds": 0.38, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 5, + "name": "find_bar", + "status": "Passed", + "seconds": 0.85, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 6, + "name": "renderer_metrics", + "status": "Passed", + "seconds": 0.95, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 7, + "name": "ime_context", + "status": "Passed", + "seconds": 1.02, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 8, + "name": "layout_state", + "status": "Passed", + "seconds": 0.16, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 9, + "name": "workspace_state", + "status": "Passed", + "seconds": 0.32, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 10, + "name": "transport_completion", + "status": "Passed", + "seconds": 0.01, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 11, + "name": "scenario_tick_gate", + "status": "Passed", + "seconds": 0.01, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 12, + "name": "resize_core", + "status": "Passed", + "seconds": 0.01, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 13, + "name": "history_core", + "status": "Passed", + "seconds": 0.02, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 14, + "name": "selection_core", + "status": "Passed", + "seconds": 0.08, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 15, + "name": "search_core", + "status": "Passed", + "seconds": 0.12, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 16, + "name": "paste", + "status": "Passed", + "seconds": 0.01, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 17, + "name": "clipboard_policy", + "status": "Passed", + "seconds": 0.01, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 18, + "name": "keyboard", + "status": "Passed", + "seconds": 0.01, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 19, + "name": "text_action_policy", + "status": "Passed", + "seconds": 0.01, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 20, + "name": "core", + "status": "Passed", + "seconds": 5.4, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 21, + "name": "recovery", + "status": "Passed", + "seconds": 9.16, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 22, + "name": "measure", + "status": "Passed", + "seconds": 4.07, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 23, + "name": "binary_paths", + "status": "Passed", + "seconds": 0.17, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 24, + "name": "conpty_margin_contract", + "status": "Passed", + "seconds": 0.35, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 25, + "name": "benchmark_tools", + "status": "Passed", + "seconds": 5.41, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 26, + "name": "windterm_alternate_export", + "status": "Passed", + "seconds": 2.91, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 27, + "name": "windterm_alternate_style_reference", + "status": "Passed", + "seconds": 1.25, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 28, + "name": "differential", + "status": "Passed", + "seconds": 2.63, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 29, + "name": "ime_inventory", + "status": "Passed", + "seconds": 51.23, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 30, + "name": "text_actions", + "status": "Passed", + "seconds": 6.15, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 31, + "name": "workflow_fixture", + "status": "Passed", + "seconds": 22.68, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 32, + "name": "workflow_sessions", + "status": "Passed", + "seconds": 41.83, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 33, + "name": "workflow_resources", + "status": "Passed", + "seconds": 0.49, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 34, + "name": "workflow_resource_phases", + "status": "Passed", + "seconds": 4.31, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 35, + "name": "workflow_system_content", + "status": "Passed", + "seconds": 0.19, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 36, + "name": "workflow_system_runner", + "status": "Passed", + "seconds": 0.63, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 37, + "name": "copy_error_frame", + "status": "Passed", + "seconds": 0.15, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 38, + "name": "research_guards", + "status": "Passed", + "seconds": 4.99, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 39, + "name": "windterm_cmd_contract", + "status": "Passed", + "seconds": 0.48, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 40, + "name": "receive", + "status": "Passed", + "seconds": 0.19, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 41, + "name": "conpty", + "status": "Passed", + "seconds": 16.6, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 42, + "name": "conpty_ctrl_c", + "status": "Passed", + "seconds": 4.38, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 43, + "name": "conpty_margin", + "status": "Passed", + "seconds": 0.35, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 44, + "name": "presentation", + "status": "Passed", + "seconds": 4.24, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 45, + "name": "shell", + "status": "Passed", + "seconds": 102.21, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 46, + "name": "selection_bytes", + "status": "Passed", + "seconds": 4.52, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 47, + "name": "selection_context", + "status": "Passed", + "seconds": 4.73, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 48, + "name": "raw_console_receiver", + "status": "Passed", + "seconds": 0.43, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 49, + "name": "paste_system", + "status": "Passed", + "seconds": 15.33, + "configuration": "independent existing/core/system/offline diagnostic" + }, + { + "number": 50, + "name": "qt_cpp_shell", + "status": "Passed", + "seconds": 1.53, + "configuration": "C++/Qt/fixed-ConPTY product entry" + } + ], + "internal_skip_count": 12, + "internal_skip_counts": { + "optional_reference_class": 4, + "actual_system_clipboard": 7, + "opt_in_original_failure_injection": 1 + }, + "internal_skips": [ + { + "ctest_number": 28, + "ctest_name": "differential", + "case": "setUpClass (test_candidate_compare.CandidateComparisonTests)", + "reason": "optional Alacritty binary is not built; core-candidate workflow runs these checks", + "category": "optional_reference_class" + }, + { + "ctest_number": 28, + "ctest_name": "differential", + "case": "setUpClass (test_capability_matrix.CapabilityMatrixTests)", + "reason": "optional Alacritty binary is not built; core-candidate workflow runs these checks", + "category": "optional_reference_class" + }, + { + "ctest_number": 28, + "ctest_name": "differential", + "case": "setUpClass (test_fuzz_matrix.FuzzMatrixTests)", + "reason": "optional Alacritty binary is not built; core-candidate workflow runs these checks", + "category": "optional_reference_class" + }, + { + "ctest_number": 28, + "ctest_name": "differential", + "case": "setUpClass (test_real_app_matrix.RealApplicationMatrixTests)", + "reason": "optional real application capture dependencies are not built", + "category": "optional_reference_class" + }, + { + "ctest_number": 30, + "ctest_name": "text_actions", + "case": "test_actual_system_copy_paste_and_shortcuts_twice (experiments.shell.test_actions.ActionWindowTest.test_actual_system_copy_paste_and_shortcuts_twice)", + "reason": "actual system Copy/Paste requires a disposable hosted VM", + "category": "actual_system_clipboard" + }, + { + "ctest_number": 38, + "ctest_name": "research_guards", + "case": "test_suspended_target_is_reclaimed_if_job_assignment_fails (test_ui_r1.EvidenceBoundaryTests.test_suspended_target_is_reclaimed_if_job_assignment_fails)", + "reason": "opt-in pinned reference failure injection", + "category": "opt_in_original_failure_injection" + }, + { + "ctest_number": 45, + "ctest_name": "shell", + "case": "test_mouse_selection_to_real_system_clipboard (experiments.shell.test_shell.ShellTests.test_mouse_selection_to_real_system_clipboard)", + "reason": "system copy UI gate requires a disposable hosted VM", + "category": "actual_system_clipboard" + }, + { + "ctest_number": 45, + "ctest_name": "shell", + "case": "test_system_clipboard_product_read_errors_cancel_and_close (experiments.shell.test_shell.ShellTests.test_system_clipboard_product_read_errors_cancel_and_close)", + "reason": "product clipboard read gate requires a disposable hosted VM", + "category": "actual_system_clipboard" + }, + { + "ctest_number": 45, + "ctest_name": "shell", + "case": "test_system_clipboard_shortcut_to_window_confirmation_and_receiver (experiments.shell.test_shell.ShellTests.test_system_clipboard_shortcut_to_window_confirmation_and_receiver)", + "reason": "real system clipboard UI gate requires a disposable hosted VM", + "category": "actual_system_clipboard" + }, + { + "ctest_number": 46, + "ctest_name": "selection_bytes", + "case": "test_owned_clipboard_contention_preserves_write_and_read_failures (experiments.shell.test_selection_bytes.SelectionBytesWindowTest.test_owned_clipboard_contention_preserves_write_and_read_failures)", + "reason": "system CF requires a disposable hosted VM", + "category": "actual_system_clipboard" + }, + { + "ctest_number": 46, + "ctest_name": "selection_bytes", + "case": "test_real_system_copy_shortcuts_for_styled_fixture_twice (experiments.shell.test_selection_bytes.SelectionBytesWindowTest.test_real_system_copy_shortcuts_for_styled_fixture_twice)", + "reason": "system CF requires a disposable hosted VM", + "category": "actual_system_clipboard" + }, + { + "ctest_number": 47, + "ctest_name": "selection_context", + "case": "test_real_system_copy_and_edit_context_twice (experiments.shell.test_selection_context.SelectionContextWindowTest.test_real_system_copy_and_edit_context_twice)", + "reason": "real CF/EDIT Copy requires a disposable hosted VM", + "category": "actual_system_clipboard" + } + ], + "skip_note": "CTest entry success includes these explicitly skipped internal unittest cases/classes. Skips are not passing coverage, and an entire skipped class is not one executed test.", + "proofs": { + "verbose_log": { + "artifact_name": "final-reviewed-ctest.log", + "bytes": 129339, + "sha256": "6a4cf2c5acccdd1beb15bac973c964ada386ed04c8c32b6bff2f96a3f4f655b8", + "published": false + }, + "last_test_log": { + "artifact_name": "LastTest.log", + "bytes": 133408, + "sha256": "57b933ccc8bb4c321352857b1a1355e3a26449a4f8324626e002760f26a40511", + "published": false + }, + "observed_exit_record": { + "artifact_name": "final-reviewed-exit.json", + "bytes": 140, + "sha256": "50317704e1b971872e47acc40f7c59a6f28162bf211e155a7592a7498cfc4a09", + "published": false + } + } + }, + "qt_independent_result": { + "ctest_name": "qt_cpp_shell", + "test_seconds": 1.53, + "outer_supervisor_deadline_seconds": 25, + "ctest_timeout_seconds": 35, + "supervisor_and_sample": { + "schema": "termforge-qt-cpp-supervisor-v1", + "status": "passed", + "timed_out": false, + "exit_code": 0, + "job_after": { + "active": 0, + "total": 26, + "terminated": 0 + }, + "sample": { + "schema": "termforge-qt-cpp-smoke-v1", + "qt": "6.10.3", + "core": "cpp", + "backend": "fixed-microsoft-conpty", + "arithmetic": 750, + "child_exit": 37, + "cleanup_verified": true, + "image_changed": true, + "startup_error_visible": true, + "manual_close_reclaimed": true, + "tabs": { + "arithmetic": [ + 750, + 427 + ], + "survivor_after_close": 778, + "survivor_after_startup_failure": 147, + "natural_exit": 41, + "input_isolation": true, + "answer_ink_visible": true, + "independent_close": true, + "startup_error_visible": true, + "window_close_reclaimed": true, + "all_exited_status": 41, + "failed_only_kept_visible": true + }, + "app_exec": { + "window_close": true, + "last_tab_close": true, + "failed_only_close": true, + "completed_tab_close": true, + "cleanup_before_quit": true, + "manual_status": 0 + }, + "presented_done": null + } + }, + "evidence_scope": "Programmatic public Qt actions and key events with two real cmd sessions under offscreen; parent-window cell-ink checks. Does not measure native on-screen presentation or physical keyboard behavior.", + "job_note": "Job totals are aggregate accounting, not per-process identities. Outer active=0 alone does not establish pre-exit cleanup." + }, + "application_lifecycle": { + "policy_origin": "Current limited prototype entry contract; full WindTerm 2.7.0 tab/application exit interaction comparison is incomplete.", + "product_exit_policy": "quitOnLastWindowClosed(false); SessionTabs completes all visible/retired models before on_all_exited calls app.exit.", + "tested_app_exec_cases": [ + "parent close with two live cmd sessions", + "close the last live tab", + "close a failed-only startup page", + "close a previously naturally finished tab" + ], + "checkpoints": [ + "unified completion callback", + "QApplication aboutToQuit", + "QApplication exec return" + ], + "required_before_exit": { + "models_finished": true, + "cleanup_verified": true, + "each_session_job_active": 0 + }, + "retired_page_evidence": "If DeferredDelete already released a retired page, use the verified live-model record captured before destruction. No destructor-after-exec proof substitutes for these checkpoints.", + "manual_close_status": 0, + "terminal_cleanup_failure_status": 1, + "terminal_cleanup_failure_verification": "Source policy and final review; no dedicated injected cleanup-failure app.exec run is claimed. A finished model with cleanup_verified=false yields failure instead of waiting forever.", + "failed_only_behavior": "Keep startup error visible until user closes the page/window.", + "natural_completion_behavior": "Preserve the prototype final-session completion path and exit status.", + "test_seam_limit": "Actual QApplication::exec inside the smoke executable with the same SessionTabs/exit policy as main; not a separately automated black-box interaction run of the product executable." + }, + "runtime_red_green_history": [ + { + "slice": "new-tab", + "purpose": "new/select two real cmd tabs and route keys independently", + "red": { + "log": { + "artifact_name": "new-tab-red.log", + "bytes": 1875, + "sha256": "1055f1e1eeb9eed2667fc2b7db5349e56e42d4e2511ddd0b4520a63bfedbaa85", + "published": false + }, + "ctest_entries": 1, + "test_seconds": 5.64, + "supervisor_status": "failed", + "native_exit_code": 1, + "timed_out": false, + "job_after": { + "active": 0, + "total": 9, + "terminated": 0 + }, + "classification": "runtime_red", + "observed_error": "new cmd action did not create an independent tab", + "identity": { + "source_commit": null, + "source_commit_status": "unknown: intermediate working tree; not assigned a later commit", + "source_files": [ + { + "path": "src/qt/session_tabs.hpp", + "sha256": "04c3116a3bd2cd18670179c63c11447f24298b298d3edea2796cb43253319213", + "retained_snapshot": { + "artifact_name": "new-tab-red-session_tabs.hpp", + "bytes": 1863, + "sha256": "04c3116a3bd2cd18670179c63c11447f24298b298d3edea2796cb43253319213", + "published": false + } + }, + { + "path": "src/qt/shell_smoke.cpp", + "sha256": "bf3e256699c8418e7321c9dd0aed00ef28daaeb503b7e1beaa889be27a359678", + "retained_snapshot": { + "artifact_name": "new-tab-red-shell_smoke.cpp", + "bytes": 11858, + "sha256": "bf3e256699c8418e7321c9dd0aed00ef28daaeb503b7e1beaa889be27a359678", + "published": false + } + } + ], + "smoke_executable_sha256": null, + "product_executable_sha256": null, + "identity_scope": "Retained filename-associated source snapshots hashed offline; no saved at-run identity record or executable hash, so snapshot-to-executable binding is unknown." + } + }, + "green": { + "log": { + "artifact_name": "new-tab-green.log", + "bytes": 1827, + "sha256": "20eb9f1a00118bda6f7cc5ca65c77cd5a3582d53911dba71fb93a4d75295250b", + "published": false + }, + "ctest_entries": 1, + "test_seconds": 0.78, + "supervisor_status": "passed", + "native_exit_code": 0, + "timed_out": false, + "job_after": { + "active": 0, + "total": 11, + "terminated": 0 + }, + "classification": "targeted_runtime_green", + "identity": { + "source_commit": null, + "source_files": null, + "smoke_executable_sha256": null, + "product_executable_sha256": null, + "status": "unknown: no independent saved identity record; not inferred from later files or commits" + }, + "build_log": { + "artifact_name": "new-tab-green-build.log", + "bytes": 2845, + "sha256": "7fc13c55b0235e3e2e4724767ca9555481f623117c7ccd2cda6f95a2822ff478", + "published": false + } + } + }, + { + "slice": "tab-close", + "purpose": "close one tab while the survivor still accepts input", + "red": { + "log": { + "artifact_name": "tab-close-red.log", + "bytes": 1859, + "sha256": "3e8d57f35fc14bb4da6a5398639ed8faf4c300895c1fd53ca51e2b2f545e8160", + "published": false + }, + "ctest_entries": 1, + "test_seconds": 0.73, + "supervisor_status": "failed", + "native_exit_code": 1, + "timed_out": false, + "job_after": { + "active": 0, + "total": 11, + "terminated": 0 + }, + "classification": "runtime_red", + "observed_error": "cmd tab close action is missing", + "identity": { + "source_commit": null, + "source_commit_status": "unknown: intermediate working tree; not assigned a later commit", + "source_files": [ + { + "path": "src/qt/session_tabs.hpp", + "sha256": "d72dbaf7d934ca7b2157ca675bc6e8e2bd3c0e8331d37825fba8ba494d52f468", + "retained_snapshot": { + "artifact_name": "tab-close-red-session_tabs.hpp", + "bytes": 2762, + "sha256": "d72dbaf7d934ca7b2157ca675bc6e8e2bd3c0e8331d37825fba8ba494d52f468", + "published": false + } + }, + { + "path": "src/qt/shell_smoke.cpp", + "sha256": "616c73b9e24f48161119e36ef6cb870f81aa7834b6a0a5bfd8ff7509dab9f39c", + "retained_snapshot": { + "artifact_name": "tab-close-red-shell_smoke.cpp", + "bytes": 13428, + "sha256": "616c73b9e24f48161119e36ef6cb870f81aa7834b6a0a5bfd8ff7509dab9f39c", + "published": false + } + } + ], + "smoke_executable_sha256": "3d4c4cfe4ab3207d0561343714b9d4d9736a50f0a0bf89a848cb4bbe7c64b3c0", + "product_executable_sha256": null, + "identity_record": { + "artifact_name": "tab-close-red-identities.json", + "bytes": 619, + "sha256": "9f04b3be5167453b1a8db231068562bfed435652ec9851f4ea8c92f27f68adb0", + "published": false + }, + "identity_scope": "Recorded file hashes; source list is partial. Historical executable bytes are not published. Missing files/committed source remain unknown." + }, + "build_log": { + "artifact_name": "tab-close-red-build.log", + "bytes": 2845, + "sha256": "030c8f62f5e61f50b4b608128bdd93955b2eec611cfa18230b436cf1558dddc2", + "published": false + } + }, + "green": { + "log": { + "artifact_name": "tab-close-green.log", + "bytes": 1827, + "sha256": "2bed325db49cdc0e2a0f116ae03c6f577d37a64d0da8c6889c05e6ea51e79024", + "published": false + }, + "ctest_entries": 1, + "test_seconds": 0.83, + "supervisor_status": "passed", + "native_exit_code": 0, + "timed_out": false, + "job_after": { + "active": 0, + "total": 11, + "terminated": 0 + }, + "classification": "targeted_runtime_green", + "identity": { + "source_commit": null, + "source_files": null, + "smoke_executable_sha256": null, + "product_executable_sha256": null, + "status": "unknown: no independent saved identity record; not inferred from later files or commits" + }, + "build_log": { + "artifact_name": "tab-close-green-build.log", + "bytes": 2845, + "sha256": "030c8f62f5e61f50b4b608128bdd93955b2eec611cfa18230b436cf1558dddc2", + "published": false + } + } + }, + { + "slice": "window-close", + "purpose": "close the parent and reclaim every session while pumping events", + "red": { + "log": { + "artifact_name": "window-close-red.log", + "bytes": 1888, + "sha256": "da6b4d4e603c28f4deeb9a3c178372406f025fd752defa15048d46f3844e5105", + "published": false + }, + "ctest_entries": 1, + "test_seconds": 6.1, + "supervisor_status": "failed", + "native_exit_code": 1, + "timed_out": false, + "job_after": { + "active": 0, + "total": 16, + "terminated": 0 + }, + "classification": "runtime_red", + "observed_error": "closing the tab window did not reclaim every running session", + "identity": { + "source_commit": null, + "source_commit_status": "unknown: intermediate working tree; not assigned a later commit", + "source_files": [ + { + "path": "src/qt/session_tabs.hpp", + "sha256": "4febbb80e8ae23254612cb69d4c4a6e9a2110fa65dc16b20b0a46bd6b5c5d506", + "retained_snapshot": { + "artifact_name": "window-close-red-session_tabs.hpp", + "bytes": 3560, + "sha256": "4febbb80e8ae23254612cb69d4c4a6e9a2110fa65dc16b20b0a46bd6b5c5d506", + "published": false + } + }, + { + "path": "src/qt/shell_smoke.cpp", + "sha256": "e362bb52489090564af5a9e4526f77b720e9633d03bbc227366eea6842e986c7", + "retained_snapshot": { + "artifact_name": "window-close-red-shell_smoke.cpp", + "bytes": 16082, + "sha256": "e362bb52489090564af5a9e4526f77b720e9633d03bbc227366eea6842e986c7", + "published": false + } + } + ], + "smoke_executable_sha256": "051fd46cf62b21480c7e2fe26ca73c8a57cb2a69c874c7943ff52565b3ba4f87", + "product_executable_sha256": null, + "identity_record": { + "artifact_name": "window-close-red-identities.json", + "bytes": 619, + "sha256": "2dadefe73a8d9c7694637799fd7f24e6bee606a590c5fee8005c8e2d32eb010d", + "published": false + }, + "identity_scope": "Recorded file hashes; source list is partial. Historical executable bytes are not published. Missing files/committed source remain unknown." + }, + "build_log": { + "artifact_name": "window-close-red-build.log", + "bytes": 2845, + "sha256": "be31e9fc91495714764192157f710b2225f8b349c530ab6741dad66227e55c45", + "published": false + } + }, + "green": { + "log": { + "artifact_name": "window-close-green.log", + "bytes": 1827, + "sha256": "1135fa3c87abdc95354ef08fc67966e7739442888e5d8290d11d9293711afcf5", + "published": false + }, + "ctest_entries": 1, + "test_seconds": 1.0, + "supervisor_status": "passed", + "native_exit_code": 0, + "timed_out": false, + "job_after": { + "active": 0, + "total": 16, + "terminated": 0 + }, + "classification": "targeted_runtime_green", + "identity": { + "source_commit": null, + "source_files": null, + "smoke_executable_sha256": null, + "product_executable_sha256": null, + "status": "unknown: no independent saved identity record; not inferred from later files or commits" + }, + "build_log": { + "artifact_name": "window-close-green-build.log", + "bytes": 2845, + "sha256": "be31e9fc91495714764192157f710b2225f8b349c530ab6741dad66227e55c45", + "published": false + } + } + }, + { + "slice": "all-exited", + "purpose": "complete the container after the final launched cmd exits", + "red": { + "log": { + "artifact_name": "all-exited-red.log", + "bytes": 1873, + "sha256": "eecc311bc5e421b0b14d10777e81f28c33f5c66573090c1dd39fd8eddb0d6477", + "published": false + }, + "ctest_entries": 1, + "test_seconds": 5.78, + "supervisor_status": "failed", + "native_exit_code": 1, + "timed_out": false, + "job_after": { + "active": 0, + "total": 11, + "terminated": 0 + }, + "classification": "runtime_red", + "observed_error": "last cmd exit did not complete the tab window", + "identity": { + "source_commit": null, + "source_commit_status": "unknown: intermediate working tree; not assigned a later commit", + "source_files": [ + { + "path": "src/qt/session_tabs.hpp", + "sha256": "86625aee1f2abe9b9a92a75e66f9f24faafbfffa69ed319defe3b8733ea8d924", + "retained_snapshot": { + "artifact_name": "all-exited-red-session_tabs.hpp", + "bytes": 4127, + "sha256": "86625aee1f2abe9b9a92a75e66f9f24faafbfffa69ed319defe3b8733ea8d924", + "published": false + } + }, + { + "path": "src/qt/shell_smoke.cpp", + "sha256": "50b6e6cac2ef05da12edc77e07279f077cbb1f6eeeb0ce30ebc69fab8074ac19", + "retained_snapshot": { + "artifact_name": "all-exited-red-shell_smoke.cpp", + "bytes": 16887, + "sha256": "50b6e6cac2ef05da12edc77e07279f077cbb1f6eeeb0ce30ebc69fab8074ac19", + "published": false + } + } + ], + "smoke_executable_sha256": "fae6ac143317397b5fd5ed53225b30b124a0a242dfa923c6a39016c151f16943", + "product_executable_sha256": null, + "identity_record": { + "artifact_name": "all-exited-red-identities.json", + "bytes": 619, + "sha256": "060d71d172413090d8c736fceabf50c0296fb85d2d9fefb12680e3910f054a76", + "published": false + }, + "identity_scope": "Recorded file hashes; source list is partial. Historical executable bytes are not published. Missing files/committed source remain unknown." + }, + "build_log": { + "artifact_name": "all-exited-red-build.log", + "bytes": 2845, + "sha256": "f02a473d8b2920fac4b5b9a0c549f92866bb9e93d3d636095cefb58909c524c0", + "published": false + } + }, + "green": { + "log": { + "artifact_name": "all-exited-green.log", + "bytes": 1827, + "sha256": "01de58fd9ccdaa1291e47eedfcb999e348c7154766b8dd2f23691c3f3e24c07d", + "published": false + }, + "ctest_entries": 1, + "test_seconds": 1.08, + "supervisor_status": "passed", + "native_exit_code": 0, + "timed_out": false, + "job_after": { + "active": 0, + "total": 16, + "terminated": 0 + }, + "classification": "targeted_runtime_green", + "identity": { + "source_commit": null, + "source_files": null, + "smoke_executable_sha256": null, + "product_executable_sha256": null, + "status": "unknown: no independent saved identity record; not inferred from later files or commits" + }, + "build_log": { + "artifact_name": "all-exited-green-build2.log", + "bytes": 5655, + "sha256": "906b6eed01245f9ca071c194d2be5a940b5b7d1c05851f28f18e83e0019a2a67", + "published": false + } + } + } + ], + "compilation_failures": [ + { + "stage": "interface-red", + "classification": "compile_failure_only", + "log": { + "artifact_name": "interface-red.log", + "bytes": 528, + "sha256": "bb153a16252436bfe65f50ff2775796ce579f67cd3026e3fc90e1c94b4689071", + "published": false + }, + "observed_error": "C1083: session_tabs.hpp not found", + "source_commit": null, + "source_identity": "unknown", + "new_executable_identity": "not produced by failed compile", + "runtime_red": false + }, + { + "stage": "all-exited-green-build", + "classification": "compile_failure_only", + "log": { + "artifact_name": "all-exited-green-build.log", + "bytes": 1739, + "sha256": "e4b4252ea9ae3f4c64d34e1a833568d7bd5f1743a6943959049f1af1fed7c1e2", + "published": false + }, + "observed_error": "C2662: const SessionPage cannot call non-const model getter", + "repair": "add const model() overload; subsequent build and targeted runtime pass retained separately", + "source_commit": null, + "source_identity": "unknown", + "new_executable_identity": "not produced by failed compile", + "runtime_red": false + } + ], + "app_exec_spec_P2": { + "gap": "Earlier smoke pumped events after window.close while main used automatic last-window quit; that did not prove event-loop drainage before app.exec returned.", + "red": { + "log": { + "artifact_name": "app-exec-red.log", + "bytes": 1900, + "sha256": "cc44e3a12a8c094d6179a4be428c4d9c2e156a2c385339da73267500a0a989fa", + "published": false + }, + "ctest_entries": 1, + "test_seconds": 1.33, + "supervisor_status": "failed", + "native_exit_code": 1, + "timed_out": false, + "job_after": { + "active": 0, + "total": 21, + "terminated": 0 + }, + "identity": { + "source_commit": null, + "source_commit_status": "unknown: intermediate working tree; not assigned a later commit", + "source_files": [ + { + "path": "src/qt/session_tabs.hpp", + "sha256": "6b8c89fd5e1afb47ef6b4013f3c67814994fa6bdd66706292531372572852a3f", + "retained_snapshot": { + "artifact_name": "app-exec-red-session_tabs.hpp", + "bytes": 6654, + "sha256": "6b8c89fd5e1afb47ef6b4013f3c67814994fa6bdd66706292531372572852a3f", + "published": false + } + }, + { + "path": "src/qt/main.cpp", + "sha256": "a6ca7b914ddb26b14d4b30bf0cfa1d8aebe19c2ceb04ae83dbfc911e77be8187", + "retained_snapshot": null + }, + { + "path": "src/qt/shell_smoke.cpp", + "sha256": "099e1d3e7efee53c133f4c82e235e51c70c7bd192f6cd31163bd988ceca5cc26", + "retained_snapshot": { + "artifact_name": "app-exec-red-shell_smoke.cpp", + "bytes": 21973, + "sha256": "099e1d3e7efee53c133f4c82e235e51c70c7bd192f6cd31163bd988ceca5cc26", + "published": false + } + } + ], + "smoke_executable_sha256": "b9177f4c8372e8e94e43707c763f523567cd375820f0abdc688050ec2267bbc4", + "product_executable_sha256": null, + "identity_record": { + "artifact_name": "app-exec-red-identities.json", + "bytes": 804, + "sha256": "cbdba4b91318c34192e4f9762b3612ce3d63cef2a2b725f990bea3a5cbdc474f", + "published": false + }, + "identity_scope": "Recorded file hashes; source list is partial. Historical executable bytes are not published. Missing files/committed source remain unknown." + }, + "build_log": { + "artifact_name": "app-exec-red-build.log", + "bytes": 2845, + "sha256": "19e76f72859daea1f01559fcb67c6ec4091affe0b064983f3f937875e30f021b", + "published": false + }, + "observed_error": "application event loop quit before every owned session completed cleanup" + }, + "first_green": { + "log": { + "artifact_name": "app-exec-green.log", + "bytes": 2153, + "sha256": "04be5c1c362bcaab511f8ff57e90d0a4c2a7b1a610af53e4062ce4e635528428", + "published": false + }, + "ctest_entries": 1, + "test_seconds": 1.34, + "supervisor_status": "passed", + "native_exit_code": 0, + "timed_out": false, + "job_after": { + "active": 0, + "total": 21, + "terminated": 0 + }, + "identity": { + "source_commit": null, + "source_commit_status": "unknown: intermediate working tree; not assigned a later commit", + "source_files": [ + { + "path": "src/qt/main.cpp", + "sha256": "4af7c6722ba6847d90d3d9938849aa3b69a8de749c043a278c8725dc46011796", + "retained_snapshot": null + }, + { + "path": "src/qt/session_tabs.hpp", + "sha256": "ddaf5d1919d6e2a0a8f80c3771ae3e2ae64529ee27918e9c7047a18edf384e37", + "retained_snapshot": { + "artifact_name": "app-exec-green-session_tabs.hpp", + "bytes": 6871, + "sha256": "ddaf5d1919d6e2a0a8f80c3771ae3e2ae64529ee27918e9c7047a18edf384e37", + "published": false + } + }, + { + "path": "src/qt/shell_smoke.cpp", + "sha256": "7c481c114d4003d6eff111e719763452d807f09c9085129ff1df356a3291f5ea", + "retained_snapshot": { + "artifact_name": "app-exec-green-shell_smoke.cpp", + "bytes": 21974, + "sha256": "7c481c114d4003d6eff111e719763452d807f09c9085129ff1df356a3291f5ea", + "published": false + } + } + ], + "smoke_executable_sha256": "066a1854ee8aab2206ee6af81b9df0bc009a223cd524b63b322a490f2808edd4", + "product_executable_sha256": null, + "identity_record": { + "artifact_name": "app-exec-green-identities.json", + "bytes": 804, + "sha256": "78cf29d82d85a21953cb743df5dc88c8992dee789692285774ef4b1368b70b7a", + "published": false + }, + "identity_scope": "Recorded file hashes; source list is partial. Historical executable bytes are not published. Missing files/committed source remain unknown." + }, + "build_log": { + "artifact_name": "app-exec-green-build.log", + "bytes": 5287, + "sha256": "570369b3d77c4fb8f1a9cec99add73dc10322ac50f79a600a4a4ccb3cac514d6", + "published": false + } + }, + "four_case_green": { + "log": { + "artifact_name": "app-exec-boundaries.log", + "bytes": 2318, + "sha256": "70d77ef70229019690a9eadd1647ea56da2a3a4857629496d732ffdb853d62f0", + "published": false + }, + "ctest_entries": 1, + "test_seconds": 1.45, + "supervisor_status": "passed", + "native_exit_code": 0, + "timed_out": false, + "job_after": { + "active": 0, + "total": 26, + "terminated": 0 + }, + "build_log": { + "artifact_name": "app-exec-boundaries-build.log", + "bytes": 5275, + "sha256": "ccdce4ca414734509df6223e325929f1534094a75ee19a36b62608c428b7546f", + "published": false + }, + "identity": { + "source_commit": null, + "source_files": null, + "smoke_executable_sha256": null, + "product_executable_sha256": null, + "status": "unknown: no independent saved identity record; not inferred from later files or commits" + } + }, + "committed_targeted_green": { + "log": { + "artifact_name": "app-exec-final.log", + "bytes": 2318, + "sha256": "397c5602d273ced7358198c2d3da1dc3e6de37ca8cd837286a57f8153a11902c", + "published": false + }, + "ctest_entries": 1, + "test_seconds": 1.51, + "supervisor_status": "passed", + "native_exit_code": 0, + "timed_out": false, + "job_after": { + "active": 0, + "total": 26, + "terminated": 0 + }, + "identity": { + "source_commit": "76070a6227f4dd79fe4cdc16e21f03919f1549de", + "source_commit_status": "Recorded committed targeted run; all three recorded source-file hashes also match git object bytes at this commit", + "source_files": [ + { + "path": "src/qt/main.cpp", + "sha256": "4af7c6722ba6847d90d3d9938849aa3b69a8de749c043a278c8725dc46011796", + "retained_snapshot": null + }, + { + "path": "src/qt/session_tabs.hpp", + "sha256": "7cd21641d748c2ad864731d94a35b1140377bb242f07781d5e1bbbe797020f0e", + "retained_snapshot": null + }, + { + "path": "src/qt/shell_smoke.cpp", + "sha256": "86022250a4c5d50824e112d6748c56bb549cb055c0737288f8a3c88f3733c8d0", + "retained_snapshot": null + } + ], + "smoke_executable_sha256": "80201411b9bb8b0dc073c133d8039b7468f87098fc36df3051d7ea5044a781eb", + "product_executable_sha256": "0bfe1f63a6e12bba8bc4a092b897ef0c341825d0be408ac818a09c7d6033f7bc", + "identity_record": { + "artifact_name": "app-exec-final-identities.json", + "bytes": 1027, + "sha256": "550f0a0dbdbb184a5302e1837c333914aaf76e80d5fb5fe0f88149d5b3c28eb4", + "published": false + }, + "identity_scope": "Recorded file hashes; source list is partial. Historical executable bytes are not published. Missing files/committed source remain unknown." + }, + "build_log": { + "artifact_name": "app-exec-final-build.log", + "bytes": 5287, + "sha256": "042c457fd27e779f684f688901f3cc4e79539976dbfdc49ea6eb220859802ed4", + "published": false + }, + "recorded_source_commit": "76070a6227f4dd79fe4cdc16e21f03919f1549de" + }, + "interpretation": "Runtime red proves early event-loop exit relative to the pre-exit cleanup contract, not an established leak/deadlock. Red outer Job active=0 cannot turn the failed application cleanup checkpoint into a pass. Final cf8b40b full run separately includes all four app.exec cases." + }, + "interrupted_earlier_full_run": { + "source_commit": "aa068c99b2198a76f230e101ea25c95f5d3fc6a5", + "status": "incomplete", + "expected_entries": 49, + "completed_passed_entries": 29, + "stopped_at_start": { + "number": 30, + "name": "workflow_fixture" + }, + "exit_code": null, + "frozen_files_checked": 7143, + "post_run_changed": [], + "post_run_freeze_status": "passed", + "proofs": { + "final-ctest.log": { + "artifact_name": "final-ctest.log", + "bytes": 74252, + "sha256": "f0e8e36d6f1b649f4f5f3c57e0272842aa3417ee50a6081716552ef223913d7b", + "published": false + }, + "LastTest.log.tmp": { + "artifact_name": "LastTest.log.tmp", + "bytes": 77797, + "sha256": "b1089a76b177d13b4b81b0abf054f61ea3bf705d91fbc5b0b75a0a1b5b6c10bc", + "published": false + }, + "frozen-inputs.json": { + "artifact_name": "frozen-inputs.json", + "bytes": 1559640, + "sha256": "289f60644c280ae67cc3c5e785e30c67ab8b18ce0c81c39f85b908198b7a2105", + "published": false + }, + "frozen-after.json": { + "artifact_name": "frozen-after.json", + "bytes": 185, + "sha256": "cdb84daa598b3d981ea9da921f833d898ba236247d8d8b38bced1cbed4cd1f6c", + "published": false + }, + "supervision.json": { + "artifact_name": "supervision.json", + "bytes": 315, + "sha256": "d6d502dc8dcdee44deef2334e27db80aad44b8bac41307b6ed80196104f2150b", + "published": false + } + }, + "interpretation": "Unrecoverable supervisor session; no completion footer/final exit observed. The 29 passes and unchanged files do not constitute a complete run, and are not combined with the later 50-entry run." + }, + "review_summary": { + "source_commit": "cf8b40b2e44f9e475cfc42b5008d46c670ff1756", + "standards_unresolved": 0, + "spec_unresolved": 0, + "provenance": "Parent reports final independent two-axis runtime review complete with no unresolved findings.", + "independent_report_artifact": null, + "artifact_sha256": null, + "note": "No separate review-report artifact exists; no hash is fabricated." + }, + "limitations": [ + "Full R2 and complete WindTerm 2.7.0 parity are incomplete.", + "Two-session fixed-size cmd slice does not establish eight-session, split, persistence, workspace restoration or dynamic resize acceptance.", + "No inherited IME, complete keyboard, Find, selection/copy, DPI, native on-screen rendering, physical-input or Alacritty-reference capability acceptance.", + "No throughput, interactive-latency, memory or WindTerm performance-equivalence claim; reported seconds are test durations.", + "No Linux/macOS client, daily-use release, clean-machine runtime or distributable-package acceptance.", + "Only synthetic assertions, selected hashes and sanitized summaries are public; raw logs/images, private paths/process IDs and complete deployment/SDK manifests remain local." + ] +} diff --git a/src/qt/README.md b/src/qt/README.md index bb1150cf..34fc56fe 100644 --- a/src/qt/README.md +++ b/src/qt/README.md @@ -1,8 +1,12 @@ # C++ / Qt 本地会话迁移入口 -按 [ADR-0008](../../docs/adr/0008-cpp-core-qt-widgets.md)采用自主 C++ 核心和 Qt Widgets,Windows 进程由[固定微软 ConPTY](../../third_party/conpty/README.md)承载。当前入口仅打通固定 80×24 的单个本地 cmd、基本 ASCII 按键、16 色绘制、启动错误与关闭回收;不是完整 R2 或 WindTerm 界面。 +按 [ADR-0008](../../docs/adr/0008-cpp-core-qt-widgets.md)采用自主 C++ 核心和 Qt Widgets,Windows 进程由[固定微软 ConPTY](../../third_party/conpty/README.md)承载。当前入口提供固定 80×24 的本地 cmd 标签、基本 ASCII 按键、16 色绘制、启动错误与关闭回收;不是完整 R2 或 WindTerm 界面。 -现有 C++ 核心的[能力限制](../../experiments/core/README.md)继续有效。Qt 尚未迁移原 Win32 的工作区、历史浏览、选择/复制、Find、完整键盘/IME、动态 resize 或会话恢复,也不能继承 Alacritty 分支的通过结果。 +现有 C++ 核心的[能力限制](../../experiments/core/README.md)继续有效。Qt 尚未迁移原 Win32 的完整工作区、分屏、历史浏览、选择/复制、Find、完整键盘/IME、动态 resize 或会话恢复,也不能继承 Alacritty 分支的通过结果。 + +标签栏的 `+` 创建同类型 cmd,点击标签选择输入目标,关闭按钮只取消对应会话;关闭父窗口取消所有会话。后台标签继续独立排空输出,已关闭标签完成自有进程回收后释放。成功启动的最后会话结束时沿用原最小入口的应用退出路径;有失败启动页时保留该页错误供关闭。当前标签外观、默认动作和多会话退出汇总仅为这批迁移入口的工程合同,未作 WindTerm 完整标签行为对比。 + +产品入口禁用 Qt 的最后窗口自动退出。关闭父窗口或最后标签后,事件循环继续排空可见及已移除会话,等模型结束并核对回收后统一退出;用户关闭的成功状态为 0,已结束但回收无法验证时为 1,不等待一个不会再变化的验证标志。自然结束继续返回最后会话状态;启动失败页在用户关闭前保持可见。 ## 构建与运行 @@ -25,6 +29,10 @@ ctest --test-dir build/qt-cpp -C Release -R '^qt_cpp_shell$' --output-on-failure Parser/Screen 仅在 Qt 线程修改;ConPTY reader 向有界输出队列写入,输入由独立有界 worker 发送。自然退出等待子进程、reader EOF、输出排空和 Parser.finish;人工关闭按取消处理。外层监督器在 GO 前把测试根进程纳入自有 Job,超时或残留不能通过。 -`qt_cpp_shell` 通过 Qt 按键事件向真实 cmd 输入 `set /a 731+19`,检查独立字面量 `750` 与正文三格新增墨迹,再检查自然退出 37、启动失败的可见错误及人工关闭。该测试使用 Qt offscreen 平台和本项目独立取得的 OFL Roboto Mono 字体;没有物理键盘、实际不同 DPI、Windows 原生窗口上屏或用户呈现时间验收,`presented_done=null`。测试耗时不是终端性能指标。 +`qt_cpp_shell` 保留单会话真实 cmd 的 `750` 正文三格新增墨迹、自然退出 37、启动失败可见错误与人工关闭检查。多标签部分实际点击新增、切换及关闭,再核对独立字面量 `750` / `427` 与父窗口中的正文墨迹、输入隔离、关闭后存活页的 `778` 与自然退出 41;失败页关闭后存活页继续得到 `147`,父窗口关闭回收所有会话,单个失败启动页保持可见。外层自有 Job 和原有期限不变。 + +退出路径另在真实 `app.exec()` 中覆盖双会话父窗口关闭、最后活动标签关闭、单个失败启动页关闭,以及已自然结束标签关闭。在统一完成回调、`aboutToQuit` 和 `exec` 返回时分别核对模型已结束、回收可验证和各自 Job active=0;已移除页的记录在其销毁前取得,不用析构后的外层 active=0 代替退出前证明。 + +该测试使用 Qt offscreen 平台和本项目独立取得的 OFL Roboto Mono 字体;没有物理键盘、实际不同 DPI、Windows 原生窗口上屏或用户呈现时间验收,`presented_done=null`。两会话隔离不代表八会话、IME 或完整工作区验收,测试耗时不是终端性能指标。 构建开关默认关闭只是从旧原型向新入口迁移的状态;正式路线已确认,旧构建保留为回归对照。Qt 文字与图形资源均独立取得,不使用 WindTerm 的专有字体、图标或主题。 diff --git a/src/qt/main.cpp b/src/qt/main.cpp index 4e790177..954afa14 100644 --- a/src/qt/main.cpp +++ b/src/qt/main.cpp @@ -1,4 +1,4 @@ -#include "terminal_widget.hpp" +#include "session_tabs.hpp" #include #include #include @@ -6,25 +6,17 @@ int main(int argc, char** argv) { QApplication app(argc, argv); + // SessionTabs keeps draining its owned sessions before requesting exit. + QApplication::setQuitOnLastWindowClosed(false); QApplication::setApplicationName(QStringLiteral("TermForge")); try { - termforge::qt::SessionModel model; - termforge::qt::TerminalWidget window(model); - bool launched = false; - window.setWindowTitle(QStringLiteral("TermForge — local shell")); - model.on_changed = [&] { - window.update(); - if (launched && model.finished()) app.exit(model.natural_exit() ? static_cast(model.exit_code()) : 1); - }; wchar_t directory[MAX_PATH]{}; const UINT length = GetSystemDirectoryW(directory, MAX_PATH); if (!length || length >= MAX_PATH) throw std::runtime_error("GetSystemDirectory failed"); + termforge::qt::SessionTabs window(std::wstring(directory) + L"\\cmd.exe", L"/D", QDir::homePath().toStdWString()); + window.setWindowTitle(QStringLiteral("TermForge — local shells")); + window.on_all_exited = [&](int status) { app.exit(status); }; window.show(); - launched = model.start(std::wstring(directory) + L"\\cmd.exe", L"/D", QDir::homePath().toStdWString()); - if (!launched) { - window.setWindowTitle(QStringLiteral("TermForge — shell startup failed")); - window.update(); // Keep the error visible until the user closes it. - } return app.exec(); } catch (const std::exception& error) { qCritical("%s", error.what()); diff --git a/src/qt/session_tabs.hpp b/src/qt/session_tabs.hpp new file mode 100644 index 00000000..4a87d1b4 --- /dev/null +++ b/src/qt/session_tabs.hpp @@ -0,0 +1,159 @@ +// Each tab owns its terminal state and transport independently. +#pragma once + +#include "terminal_widget.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace termforge::qt { + +class SessionPage final : public QWidget { + SessionModel model_; + TerminalWidget terminal_; + bool retired_ = false; + bool launched_ = false; +public: + std::function on_changed; + explicit SessionPage(QWidget* parent = nullptr) : QWidget(parent), terminal_(model_, this) { + auto* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(&terminal_); + model_.on_changed = [this] { + terminal_.update(); + if (on_changed) on_changed(); + if (retired_ && model_.finished()) deleteLater(); + }; + } + ~SessionPage() override { model_.on_changed = {}; } + SessionModel& model() { return model_; } + const SessionModel& model() const { return model_; } + TerminalWidget& terminal() { return terminal_; } + bool start(const std::wstring& executable, const std::wstring& arguments, + const std::wstring& working_directory) { + launched_ = model_.start(executable, arguments, working_directory); + return launched_; + } + bool launched() const { return launched_; } + void retire() { + retired_ = true; + model_.close(); + if (model_.finished()) deleteLater(); + } +}; + +class SessionTabs final : public QWidget { + QTabWidget tabs_{this}; + QToolButton create_; + std::wstring executable_, arguments_, working_directory_; + bool destroying_ = false; + bool closing_ = false; + bool completion_notified_ = false; + bool retirement_failed_ = false; + int last_exit_status_ = 0; + std::vector> retired_; + void complete_if_finished() { + if (destroying_) return; + // A hidden, closed tab still owns transport until its completion poll. + // Do not end the application while that cleanup needs the Qt event loop. + retired_.erase(std::remove_if(retired_.begin(), retired_.end(), [this](const auto& page) { + if (!page) { retirement_failed_ = true; return true; } + if (!page->model().finished()) return false; + retirement_failed_ = retirement_failed_ || !page->model().cleanup_verified(); + return true; + }), retired_.end()); + if (completion_notified_ || (!closing_ && tabs_.count() == 0)) return; + if (!retired_.empty()) return; + bool abnormal = retirement_failed_; + for (int index = 0; index < tabs_.count(); ++index) { + const auto* page = static_cast(tabs_.widget(index)); + // A failed launch remains visible until the user closes the window. + if ((!closing_ && !page->launched()) || !page->model().finished()) return; + // A finished cleanup failure is terminal; report failure, do not wait. + abnormal = abnormal || !page->model().cleanup_verified() || (!closing_ && !page->model().natural_exit()); + } + completion_notified_ = true; + const int normal_status = closing_ ? 0 : last_exit_status_; + if (on_all_exited) on_all_exited(abnormal ? 1 : normal_status); + } + void session_changed(SessionPage* page) { + if (destroying_) return; + const int index = tabs_.indexOf(page); + if (index >= 0 && page->model().finished() && page->launched()) { + tabs_.setTabText(index, QStringLiteral("cmd — exited %1").arg(page->model().exit_code())); + last_exit_status_ = page->model().natural_exit() ? static_cast(page->model().exit_code()) : 1; + } + complete_if_finished(); + } + void focus_current() { + if (destroying_ || closing_) return; + if (auto* page = current_session()) page->terminal().setFocus(Qt::OtherFocusReason); + } + void closeEvent(QCloseEvent* event) override { + if (!closing_) completion_notified_ = false; + closing_ = true; + create_.setEnabled(false); + // Removed pages already requested cancellation before retirement. + // Keep visible pages alive while their timers drain and verify cleanup. + for (int index = 0; index < tabs_.count(); ++index) + static_cast(tabs_.widget(index))->model().close(); + QWidget::closeEvent(event); + complete_if_finished(); + } +public: + std::function on_all_exited; + SessionTabs(const std::wstring& executable, const std::wstring& arguments, + const std::wstring& working_directory, QWidget* parent = nullptr) + : QWidget(parent), executable_(executable), arguments_(arguments), working_directory_(working_directory) { + auto* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(&tabs_); + create_.setText(QStringLiteral("+")); + create_.setToolTip(QStringLiteral("New cmd session")); + create_.setObjectName(QStringLiteral("newCmdSession")); + create_.setFocusPolicy(Qt::NoFocus); + tabs_.setTabsClosable(true); + tabs_.setCornerWidget(&create_); + QObject::connect(&create_, &QToolButton::clicked, this, [this] { + new_session(executable_, arguments_, working_directory_); + }); + QObject::connect(&tabs_, &QTabWidget::currentChanged, this, [this] { focus_current(); }); + QObject::connect(&tabs_, &QTabWidget::tabCloseRequested, this, [this](int index) { + if (index < 0 || index >= tabs_.count()) return; + auto* page = static_cast(tabs_.widget(index)); + tabs_.removeTab(index); + page->setParent(this); + page->hide(); + retired_.push_back(page); + page->retire(); + focus_current(); + if (tabs_.count() == 0) close(); + else complete_if_finished(); + }); + new_session(executable_, arguments_, working_directory_); + setFixedSize(sizeHint()); + } + ~SessionTabs() override { destroying_ = true; } + SessionPage* new_session(const std::wstring& executable, const std::wstring& arguments, + const std::wstring& working_directory) { + if (closing_) return nullptr; + completion_notified_ = false; + auto* page = new SessionPage; + page->on_changed = [this, page] { session_changed(page); }; + tabs_.addTab(page, QStringLiteral("cmd")); + tabs_.setCurrentWidget(page); + if (!page->start(executable, arguments, working_directory)) + tabs_.setTabText(tabs_.indexOf(page), QStringLiteral("cmd — startup failed")); + focus_current(); + return page; + } + SessionPage* current_session() const { + return static_cast(tabs_.currentWidget()); + } +}; + +} // namespace termforge::qt diff --git a/src/qt/shell_smoke.cpp b/src/qt/shell_smoke.cpp index 8f1694d7..d763b2a2 100644 --- a/src/qt/shell_smoke.cpp +++ b/src/qt/shell_smoke.cpp @@ -1,14 +1,22 @@ // Public Qt input, Screen output, rendered image and real child lifecycle. #include "terminal_widget.hpp" +#include "session_tabs.hpp" #include #include #include #include #include #include +#include +#include +#include +#include #include #include +#include #include +#include +#include static void require(bool condition, const char* message) { if (!condition) throw std::runtime_error(message); @@ -41,6 +49,244 @@ static bool has_text(const termforge::Screen& screen, std::string_view expected) } return false; } +static bool has_error_ink(termforge::qt::TerminalWidget& window) { + const QImage image = window.grab().toImage(); + const int cell_height = qCeil(QFontMetricsF(window.font()).height()) + 2; + for (int y = image.height() - cell_height - 16; y < image.height(); ++y) + for (int x = 0; x < image.width(); ++x) { + const QColor pixel = image.pixelColor(x, y); + if (pixel.red() > pixel.green() + 40 && pixel.red() > pixel.blue() + 40) return true; + } + return false; +} +static void require_answer_ink(QWidget& window, termforge::qt::TerminalWidget& terminal, + const termforge::Screen& screen, std::string_view expected, const QImage& before) { + int answer_row = -1; + for (int row = 0; row < screen.height(); ++row) { + std::string text; + for (const auto& cell : screen.lines[static_cast(row)]) if (cell.width) text += cell.text; + while (!text.empty() && text.back() == ' ') text.pop_back(); + if (text == expected) answer_row = row; + } + require(answer_row >= 0 && terminal.isVisibleTo(&window), "arithmetic answer is not in the selected view"); + const QFontMetricsF font(terminal.font()); + const int width = qCeil(font.horizontalAdvance(QLatin1Char('M'))); + const int height = qCeil(font.height()) + 2; + const QPoint origin = terminal.mapTo(&window, QPoint(8, 8)); + const QImage after = window.grab().toImage(); + for (int column = 0; column < static_cast(expected.size()); ++column) { + const QRect cell(origin.x() + column * width, origin.y() + answer_row * height, width, height); + require(window.rect().contains(cell) && after.copy(cell) != before.copy(cell), + "selected tab arithmetic cell has no new visible ink"); + } +} +static void tab_input_isolation(const std::wstring& cmd, const std::wstring& directory) { + termforge::qt::SessionTabs window(cmd, L"/D /Q /K \"prompt TF_QT_TAB$G\"", directory); + std::optional completed; + window.on_all_exited = [&](int status) { completed = status; }; + window.show(); + QApplication::processEvents(); + auto* first = window.current_session(); + require(first != nullptr, "initial cmd tab is missing"); + await([&] { return has_text(first->model().screen(), "TF_QT_TAB>"); }, first->model(), + "initial tab did not reach its prompt"); + require(QApplication::focusWidget() == &first->terminal(), "initial tab did not focus its terminal"); + auto* create = window.findChild(QStringLiteral("newCmdSession")); + require(create != nullptr, "new cmd action is missing"); + QTest::mouseClick(create, Qt::LeftButton); + QElapsedTimer timer; timer.start(); + while (window.current_session() == first && timer.elapsed() < 5000) QTest::qWait(5); + auto* second = window.current_session(); + require(second && second != first, "new cmd action did not create an independent tab"); + await([&] { return has_text(second->model().screen(), "TF_QT_TAB>"); }, second->model(), + "second tab did not reach its prompt"); + auto* bar = window.findChild(); + require(bar && bar->count() == 2, "cmd tabs are missing from the visible tab bar"); + QTest::mouseClick(bar, Qt::LeftButton, Qt::NoModifier, bar->tabRect(0).center()); + QApplication::processEvents(); + require(QApplication::focusWidget() == &first->terminal(), "selecting a tab did not focus its terminal"); + const QImage first_before = window.grab().toImage(); + QTest::keyClicks(QApplication::focusWidget(), QStringLiteral("set /a 731+19")); + QTest::keyClick(QApplication::focusWidget(), Qt::Key_Return); + await([&] { return has_line(first->model().screen(), "750"); }, first->model(), + "first tab did not receive its selected keyboard input"); + require(!has_text(second->model().screen(), "731+19") && !has_line(second->model().screen(), "750"), + "first tab input leaked to the second session"); + require_answer_ink(window, first->terminal(), first->model().screen(), "750", first_before); + QTest::mouseClick(bar, Qt::LeftButton, Qt::NoModifier, bar->tabRect(1).center()); + QApplication::processEvents(); + require(QApplication::focusWidget() == &second->terminal(), "second tab did not receive focus"); + const QImage second_before = window.grab().toImage(); + QTest::keyClicks(QApplication::focusWidget(), QStringLiteral("set /a 401+26")); + QTest::keyClick(QApplication::focusWidget(), Qt::Key_Return); + await([&] { return has_line(second->model().screen(), "427"); }, second->model(), + "second tab did not execute its independent arithmetic"); + require(!has_text(first->model().screen(), "401+26") && !has_line(first->model().screen(), "427"), + "second tab input leaked to the first session"); + require_answer_ink(window, second->terminal(), second->model().screen(), "427", second_before); + QPointer closed(first); + auto* close = bar->tabButton(0, QTabBar::RightSide); + if (!close) close = bar->tabButton(0, QTabBar::LeftSide); + require(close != nullptr, "cmd tab close action is missing"); + QTest::mouseClick(close, Qt::LeftButton); + QElapsedTimer close_timer; close_timer.start(); + while ((bar->count() != 1 || closed) && close_timer.elapsed() < 5000) QTest::qWait(5); + require(bar->count() == 1 && !closed, "closing a tab did not finish and release its owned session"); + require(window.current_session() == second && second->model().running(), + "closing one tab affected the surviving session"); + require(!completed, "closing one tab ended the still-live tab window"); + require(QApplication::focusWidget() == &second->terminal(), "tab close did not restore terminal focus"); + QTest::keyClicks(QApplication::focusWidget(), QStringLiteral("set /a 999-221")); + QTest::keyClick(QApplication::focusWidget(), Qt::Key_Return); + await([&] { return has_line(second->model().screen(), "778"); }, second->model(), + "surviving tab stopped accepting input after another tab closed"); + QTest::keyClicks(QApplication::focusWidget(), QStringLiteral("exit 41")); + QTest::keyClick(QApplication::focusWidget(), Qt::Key_Return); + await([&] { return second->model().finished(); }, second->model(), "surviving cmd did not finish"); + require(second->model().natural_exit() && second->model().exit_code() == 41 && second->model().cleanup_verified(), + "surviving tab did not exit naturally and reclaim its owned processes"); + await([&] { return completed.has_value(); }, second->model(), "last cmd exit did not complete the tab window"); + require(*completed == 41, "tab window lost its last cmd exit status"); +} +static void tab_failure_and_window_close(const std::wstring& cmd, const std::wstring& directory) { + termforge::qt::SessionTabs window(cmd, L"/D /Q /K \"prompt TF_QT_WINDOW$G\"", directory); + window.show(); + auto* first = window.current_session(); + await([&] { return has_text(first->model().screen(), "TF_QT_WINDOW>"); }, first->model(), + "window-close first session did not reach its prompt"); + QPointer failed = window.new_session( + directory + L"\\missing-tab-shell.exe", L"", directory); + QElapsedTimer timer; timer.start(); + while (!failed->model().finished() && timer.elapsed() < 5000) QTest::qWait(5); + require(failed->model().finished() && !failed->model().error().isEmpty() && failed->model().cleanup_verified(), + "failed tab lost its startup error or cleanup"); + require(window.current_session() == failed && has_error_ink(failed->terminal()), + "failed tab did not display its own startup error"); + require(first->model().running() && first->model().error().isEmpty(), + "startup failure affected an independent running tab"); + auto* bar = window.findChild(); + auto* close = bar->tabButton(1, QTabBar::RightSide); + if (!close) close = bar->tabButton(1, QTabBar::LeftSide); + require(close != nullptr, "failed tab has no close action"); + QTest::mouseClick(close, Qt::LeftButton); + timer.restart(); + while (failed && timer.elapsed() < 5000) QTest::qWait(5); + require(!failed && first->model().running(), "failed tab close affected its live peer"); + require(QApplication::focusWidget() == &first->terminal(), "failed tab close did not focus its peer"); + QTest::keyClicks(QApplication::focusWidget(), QStringLiteral("set /a 83+64")); + QTest::keyClick(QApplication::focusWidget(), Qt::Key_Return); + await([&] { return has_line(first->model().screen(), "147"); }, first->model(), + "live peer stopped accepting input after a tab startup failure"); + auto* create = window.findChild(QStringLiteral("newCmdSession")); + QTest::mouseClick(create, Qt::LeftButton); + auto* second = window.current_session(); + require(second && second != first, "window-close second session is missing"); + await([&] { return has_text(second->model().screen(), "TF_QT_WINDOW>"); }, second->model(), + "window-close second session did not reach its prompt"); + window.close(); + await([&] { return first->model().finished() && second->model().finished(); }, first->model(), + "closing the tab window did not reclaim every running session"); + require(first->model().cleanup_verified() && second->model().cleanup_verified() && + !first->model().natural_exit() && !second->model().natural_exit(), + "tab window close lost cleanup or was reported as natural shell exit"); + + termforge::qt::SessionTabs failed_only(directory + L"\\missing-only-shell.exe", L"", directory); + std::optional completed; + failed_only.on_all_exited = [&](int status) { completed = status; }; + failed_only.show(); + auto* only = failed_only.current_session(); + timer.restart(); + while (!only->model().finished() && timer.elapsed() < 5000) QTest::qWait(5); + require(only->model().finished() && only->model().cleanup_verified() && has_error_ink(only->terminal()), + "single failed tab lost its visible startup error or cleanup"); + require(failed_only.isVisible() && !completed, "failed-only startup hid its error by ending the application"); + failed_only.close(); +} +static void exec_close_and_verify(QApplication& app, termforge::qt::SessionTabs& window, + const std::vector>& pages, + const std::function& close_action) { + require(!QApplication::quitOnLastWindowClosed(), "app.exec case does not use the product exit policy"); + std::vector verified(pages.size(), false); + const auto cleanup_ready = [&] { + bool ready = true; + for (size_t index = 0; index < pages.size(); ++index) { + if (const auto& page = pages[index]) + verified[index] = page->model().finished() && page->model().cleanup_verified() && + page->model().active_processes_after() == 0; + // A retired page may receive DeferredDelete after its completion + // callback. Require the live model's verified state before that. + ready = ready && verified[index]; + } + return ready; + }; + bool quit_seen = false, cleanup_at_quit = false, cleanup_at_completion = false; + int completions = 0; + std::optional completion_status; + const auto quitting = QObject::connect(&app, &QCoreApplication::aboutToQuit, &window, [&] { + quit_seen = true; + cleanup_at_quit = cleanup_ready(); + }); + window.on_all_exited = [&](int status) { + ++completions; + completion_status = status; + cleanup_at_completion = cleanup_ready(); + app.exit(status); + }; + QTimer deadline; + deadline.setSingleShot(true); + QObject::connect(&deadline, &QTimer::timeout, &window, [&] { app.exit(99); }); + QTimer::singleShot(0, &window, close_action); + deadline.start(5000); + const int status = app.exec(); + deadline.stop(); + QObject::disconnect(quitting); + window.on_all_exited = {}; + require(quit_seen && cleanup_at_completion && cleanup_at_quit && cleanup_ready(), + "application event loop quit before every owned session completed cleanup"); + require(status == 0 && completions == 1 && completion_status == 0, + "manual window close did not end app.exec through verified session completion"); +} +static void app_exec_window_close(QApplication& app, const std::wstring& cmd, const std::wstring& directory) { + termforge::qt::SessionTabs window(cmd, L"/D /Q /K \"prompt TF_QT_EXEC$G\"", directory); + window.show(); + auto* first = window.current_session(); + await([&] { return has_text(first->model().screen(), "TF_QT_EXEC>"); }, first->model(), + "app.exec first cmd did not reach its prompt"); + QTest::mouseClick(window.findChild(QStringLiteral("newCmdSession")), Qt::LeftButton); + auto* second = window.current_session(); + await([&] { return has_text(second->model().screen(), "TF_QT_EXEC>"); }, second->model(), + "app.exec second cmd did not reach its prompt"); + exec_close_and_verify(app, window, {first, second}, [&] { window.close(); }); +} +static void app_exec_last_tab_close(QApplication& app, const std::wstring& cmd, const std::wstring& directory, + bool already_exited) { + termforge::qt::SessionTabs window(cmd, already_exited ? L"/D /Q /C exit 19" : L"/D /Q /K \"prompt TF_QT_LAST$G\"", directory); + window.show(); + auto* page = window.current_session(); + if (already_exited) { + await([&] { return page->model().finished(); }, page->model(), "completed-tab cmd did not finish"); + require(page->model().natural_exit() && page->model().exit_code() == 19, + "completed-tab cmd lost its natural exit status"); + } else { + await([&] { return has_text(page->model().screen(), "TF_QT_LAST>"); }, page->model(), + "last-tab cmd did not reach its prompt"); + } + auto* bar = window.findChild(); + auto* close = bar->tabButton(0, QTabBar::RightSide); + if (!close) close = bar->tabButton(0, QTabBar::LeftSide); + require(close != nullptr, "last-tab close action is missing"); + exec_close_and_verify(app, window, {page}, [&] { QTest::mouseClick(close, Qt::LeftButton); }); +} +static void app_exec_failed_only_close(QApplication& app, const std::wstring& directory) { + termforge::qt::SessionTabs window(directory + L"\\missing-exec-shell.exe", L"", directory); + window.show(); + auto* page = window.current_session(); + QElapsedTimer timer; timer.start(); + while (!page->model().finished() && timer.elapsed() < 5000) QTest::qWait(5); + require(page->model().finished() && page->model().cleanup_verified() && window.isVisible(), + "failed-only app.exec case did not retain its error window"); + exec_close_and_verify(app, window, {page}, [&] { window.close(); }); +} int main(int argc, char** argv) { SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); @@ -130,14 +376,7 @@ int main(int argc, char** argv) { while (!failed_model.finished() && failure_timer.elapsed() < 5000) QTest::qWait(5); require(failed_model.finished() && !failed_model.error().isEmpty() && failed_model.cleanup_verified(), "failed shell startup lost its error or owned cleanup"); - const QImage failed_image = failed_window.grab().toImage(); - bool error_ink = false; - for (int y = failed_image.height() - cell_height - 16; y < failed_image.height(); ++y) - for (int x = 0; x < failed_image.width(); ++x) { - const QColor pixel = failed_image.pixelColor(x, y); - if (pixel.red() > pixel.green() + 40 && pixel.red() > pixel.blue() + 40) error_ink = true; - } - require(error_ink, "shell startup error has no visible error text"); + require(has_error_ink(failed_window), "shell startup error has no visible error text"); failed_window.close(); termforge::qt::SessionModel closing_model; @@ -153,11 +392,26 @@ int main(int argc, char** argv) { "closing the Qt window did not reclaim its running shell"); require(!closing_model.natural_exit() && closing_model.cleanup_verified(), "manual close was reported as natural exit or failed cleanup"); + tab_input_isolation(std::wstring(directory) + L"\\cmd.exe", workspace.path().toStdWString()); + tab_failure_and_window_close(std::wstring(directory) + L"\\cmd.exe", workspace.path().toStdWString()); + window.close(); + QApplication::setQuitOnLastWindowClosed(false); + app_exec_window_close(app, std::wstring(directory) + L"\\cmd.exe", workspace.path().toStdWString()); + app_exec_last_tab_close(app, std::wstring(directory) + L"\\cmd.exe", workspace.path().toStdWString(), false); + app_exec_failed_only_close(app, workspace.path().toStdWString()); + app_exec_last_tab_close(app, std::wstring(directory) + L"\\cmd.exe", workspace.path().toStdWString(), true); std::cout << "{\"schema\":\"termforge-qt-cpp-smoke-v1\",\"qt\":\"" << qVersion() << "\",\"core\":\"cpp\",\"backend\":\"fixed-microsoft-conpty\"," "\"arithmetic\":750,\"child_exit\":37,\"cleanup_verified\":true," "\"image_changed\":true,\"startup_error_visible\":true," - "\"manual_close_reclaimed\":true,\"presented_done\":null}\n"; + "\"manual_close_reclaimed\":true," + "\"tabs\":{\"arithmetic\":[750,427],\"survivor_after_close\":778," + "\"survivor_after_startup_failure\":147,\"natural_exit\":41," + "\"input_isolation\":true,\"answer_ink_visible\":true,\"independent_close\":true," + "\"startup_error_visible\":true,\"window_close_reclaimed\":true," + "\"all_exited_status\":41,\"failed_only_kept_visible\":true}," + "\"app_exec\":{\"window_close\":true,\"last_tab_close\":true,\"failed_only_close\":true," + "\"completed_tab_close\":true,\"cleanup_before_quit\":true,\"manual_status\":0},\"presented_done\":null}\n"; return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n';