diff --git a/docs/superpowers/plans/2026-10-14-uestc-undergraduate.md b/docs/superpowers/plans/2026-10-14-uestc-undergraduate.md index 10b504a..3c09fc1 100644 --- a/docs/superpowers/plans/2026-10-14-uestc-undergraduate.md +++ b/docs/superpowers/plans/2026-10-14-uestc-undergraduate.md @@ -80,7 +80,7 @@ Flutter命令使用已有工具链;此环境如出现SDK执行权限限制, - `ExamRecord`新增 `String date = '', status = '', notes = ''`、`int? examTypeId`;原有字段语义保持,保存完整安排于 `time`。`ExamSchedule`新增 `SupportedSchool school = SupportedSchool.uestcGraduate`、`Set requestedExamTypes = const {}`;本科必须有效学期及非空1–3类型集合,记录类型须属于该集合,按复合字段去重;研究生保留原code唯一规则。 - `AppSnapshot`新增 `SupportedSchool? selectedSchool`及copyWith,JSON字段名为 `selectedSchoolId`(稳定字符串或null);`ExamSchedule`的学校JSON字段名为 `schoolId`,类型字段为 `requestedExamTypes`(排序的整数数组)。旧数据按可靠学校来源恢复,不能将本科考试编号推断为研究生课表学期。新字段缺失保持旧默认,有明确未知标识则拒绝。 -- [ ] **Step 1 — 写失败测试。** 验证本科483/503/523/543/563标签、523期末URL精确等于用户地址、空类型/编号504拒绝、学校不同编号相同不相等、`{'id':73}`仍为研究生;本科同课程期中/期末均保留,研究生重复code仍拒绝;旧快照round-trip兼容,未知学校/损坏类型拒绝;本科12合法、未标本科12非法,旧课程新增字段为空。 +- [x] **Step 1 — 写失败测试。** 验证本科483/503/523/543/563标签、523期末URL精确等于用户地址、空类型/编号504拒绝、学校不同编号相同不相等、`{'id':73}`仍为研究生;本科同课程期中/期末均保留,研究生重复code仍拒绝;旧快照round-trip兼容,未知学校/损坏类型拒绝;本科12合法、未标本科12非法,旧课程新增字段为空。 测试名 `undergraduate semester 523 uses the final exam URL` 的核心断言: @@ -93,10 +93,10 @@ expect(query.uriFor(UndergraduateExamType.finalExam).toString(), expect(SchoolTerm.fromJson({'id': 73}).school, SupportedSchool.uestcGraduate); ``` -- [ ] **Step 2 — 验证失败。** `flutter.bat test --no-pub test/app/school_term_test.dart test/domain/undergraduate_models_test.dart test/data/snapshot_store_test.dart`,预期缺少新接口/行为失败,不能只因找不到工具链失败。 -- [ ] **Step 3 — 实现上述接口及序列化边界。** 不更改旧枚举CourseSource及schemaVersion;本科年份用 `(id-3)~/20` 推导,位于模型中的验证不依赖界面。 -- [ ] **Step 4 — 重跑上述测试。** 预期全部PASS,包括既有研究生测试。 -- [ ] **Step 5 — 提交。** `feat: model undergraduate schools terms and exam queries`,只暂存本任务文件。 +- [x] **Step 2 — 验证失败。** `flutter.bat test --no-pub test/app/school_term_test.dart test/domain/undergraduate_models_test.dart test/data/snapshot_store_test.dart`,预期缺少新接口/行为失败,不能只因找不到工具链失败。 +- [x] **Step 3 — 实现上述接口及序列化边界。** 不更改旧枚举CourseSource及schemaVersion;本科年份用 `(id-3)~/20` 推导,位于模型中的验证不依赖界面。 +- [x] **Step 4 — 重跑上述测试。** 预期全部PASS,包括既有研究生测试。 +- [x] **Step 5 — 提交。** `feat: model undergraduate schools terms and exam queries`,只暂存本任务文件。 ### Task 2: 原子保存与课表/考试学期分离 @@ -104,7 +104,7 @@ expect(SchoolTerm.fromJson({'id': 73}).school, SupportedSchool.uestcGraduate); **Interfaces:** Consumes任务1类型。新增 `Future AppController.selectSchool(SupportedSchool school)`:通过既有串行队列保存,成功true;失败false且lastError可见,不提前更新内存。保留 `applyImport(ImportDraft,{required bool confirmedEmpty})`、`applyExams(ExamSchedule)`签名。 -- [ ] **Step 1 — 写失败测试。** 本科503考试替换考试列表后原研究生73课表、日期、颜色、提醒偏好及calendarEventIds相同;通知/日历fake调用数不增加。首次本科导课清开始日但保留偏好和独立考试;同本科同学期保留开始日;旧研究生跨学期规则仍成立。存储失败保存前后JSON相同、selectSchool返回false;本科JSON缺少/矛盾学校、混学期、非法12拒绝;旧版本1JSON正常。 +- [x] **Step 1 — 写失败测试。** 本科503考试替换考试列表后原研究生73课表、日期、颜色、提醒偏好及calendarEventIds相同;通知/日历fake调用数不增加。首次本科导课清开始日但保留偏好和独立考试;同本科同学期保留开始日;旧研究生跨学期规则仍成立。存储失败保存前后JSON相同、selectSchool返回false;本科JSON缺少/矛盾学校、混学期、非法12拒绝;旧版本1JSON正常。 测试名 `historical undergraduate exams preserve the course semester`,使用既有controller/fake初始化后: @@ -119,10 +119,10 @@ expect(controller.snapshot.settings.semester, before.settings.semester); expect(controller.snapshot.examSchedule!.termId, '503'); ``` -- [ ] **Step 2 — 运行。** `flutter.bat test --no-pub test/app/undergraduate_controller_test.dart test/app/app_controller_test.dart test/data/json_schedule_import_test.dart`;预期当前applyExams清课表等断言失败。 -- [ ] **Step 3 — 实现分支。** 本科applyExams仅更新examSchedule,本科applyImport不清独立考试;新课表来源/学期才清开始日。研究生分支只接收研究生SchoolTerm。JSON显式schoolTerm用于本科来源,不由本科考试推断课表来源。保存成功后才更新内存及协调课程服务;偏好保存不协调服务。 -- [ ] **Step 4 — 重跑。** 上述测试PASS;恢复备份/损坏存储既有测试不得退化。 -- [ ] **Step 5 — 提交。** `feat: keep undergraduate exams independent of course terms`。 +- [x] **Step 2 — 运行。** `flutter.bat test --no-pub test/app/undergraduate_controller_test.dart test/app/app_controller_test.dart test/data/json_schedule_import_test.dart`;预期当前applyExams清课表等断言失败。 +- [x] **Step 3 — 实现分支。** 本科applyExams仅更新examSchedule,本科applyImport不清独立考试;新课表来源/学期才清开始日。研究生分支只接收研究生SchoolTerm。JSON显式schoolTerm用于本科来源,不由本科考试推断课表来源。保存成功后才更新内存及协调课程服务;偏好保存不协调服务。 +- [x] **Step 4 — 重跑。** 上述测试PASS;恢复备份/损坏存储既有测试不得退化。 +- [x] **Step 5 — 提交。** `feat: keep undergraduate exams independent of course terms`。 ### Task 3: 本科课表解析和离线课表导入 @@ -130,7 +130,7 @@ expect(controller.snapshot.examSchedule!.termId, '503'); **Interfaces:** `UndergraduateTimetableAdapter.extract(String html, Uri uri, {CourseSource source = CourseSource.school}) -> ImportDraft`。内部 `parseActivityData(String script,{required int unitCount}) -> List`;`UndergraduateActivity`保存teacher/name/courseCode/room、`Set indices, weeks`及是否停课,只供适配器使用。 -- [ ] **Step 1 — 写脱敏等价fixture及失败测试。** 保留附件10门课程、19条活动/18上课+1停课、3条涉及12节、周位串及索引结构;assert每门课程名称/教师/房间/周次/星期/节次映射,停课周6不产生对应occurrence。另加转义引号/Unicode字符串、同课程不同教室和周、多教师、连续节次分组、重复去重、含中文括号名称、名单备注保留;非法索引、缺教师/房间、超范围周、登录页/加载空表/学期矛盾报错;明确空课提示才explicitlyEmpty。 +- [x] **Step 1 — 写脱敏等价fixture及失败测试。** 保留附件10门课程、19条活动/18上课+1停课、3条涉及12节、周位串及索引结构;assert每门课程名称/教师/房间/周次/星期/节次映射,停课周6不产生对应occurrence。另加转义引号/Unicode字符串、同课程不同教室和周、多教师、连续节次分组、重复去重、含中文括号名称、名单备注保留;非法索引、缺教师/房间、超范围周、登录页/加载空表/学期矛盾报错;明确空课提示才explicitlyEmpty。 测试名 `undergraduate activity data preserves period twelve` 的核心断言: @@ -144,10 +144,10 @@ expect(draft.courses.where((c) => c.endPeriod == 12), hasLength(3)); expect(draft.courses.any((c) => c.room == '停课'), isFalse); ``` -- [ ] **Step 2 — 运行。** `flutter.bat test --no-pub test/school/undergraduate_timetable_adapter_test.dart test/school/import_boundaries_test.dart`,新解析器尚不存在,预期FAIL。 -- [ ] **Step 3 — 实现。** 用HTML DOM寻找manualArrangeCourseTable及课程名单表,解析实际semester.id;受限字符串tokenizer处理TaskActivity参数及 `index = 常量 * unitCount + 常量`、对应activities赋值。按相同教师/房间/周次的连续索引分组,停课按课程序号/星期/节次扣周;稳定ID用完整规范化安排的确定性摘要,不包含学生姓名/学号。名单补课程序号/备注,不能执行任何脚本。页面和文件均检查UTF8字节总量≤2MB。 -- [ ] **Step 4 — 分派并验证。** ScheduleFileImport按本科课表明确特征分派,其余保持旧graduate/JSON;重跑任务测试PASS。直接读取用户原附件的本地验证只记录数量/映射结果,不将原文件或个人信息提交。 -- [ ] **Step 5 — 提交。** `feat: parse undergraduate timetable activity data`,含脱敏fixture及测试,不含原附件。 +- [x] **Step 2 — 运行。** `flutter.bat test --no-pub test/school/undergraduate_timetable_adapter_test.dart test/school/import_boundaries_test.dart`,新解析器尚不存在,预期FAIL。 +- [x] **Step 3 — 实现。** 用HTML DOM寻找manualArrangeCourseTable及课程名单表,解析实际semester.id;受限字符串tokenizer处理TaskActivity参数及 `index = 常量 * unitCount + 常量`、对应activities赋值。按相同教师/房间/周次的连续索引分组,停课按课程序号/星期/节次扣周;稳定ID用完整规范化安排的确定性摘要,不包含学生姓名/学号。名单补课程序号/备注,不能执行任何脚本。页面和文件均检查UTF8字节总量≤2MB。 +- [x] **Step 4 — 分派并验证。** ScheduleFileImport按本科课表明确特征分派,其余保持旧graduate/JSON;重跑任务测试PASS。直接读取用户原附件的本地验证只记录数量/映射结果,不将原文件或个人信息提交。 +- [x] **Step 5 — 提交。** `feat: parse undergraduate timetable activity data`,含脱敏fixture及测试,不含原附件。 ### Task 4: 本科考试解析及文件分派 @@ -155,7 +155,7 @@ expect(draft.courses.any((c) => c.room == '停课'), isFalse); **Interfaces:** `UndergraduateExamAdapter.extract(String html,{SchoolTerm? expectedTerm, UndergraduateExamType? expectedType}) -> ExamSchedule`;`merge(UndergraduateExamQuery query,List parts) -> ExamSchedule`;`ExamFileImport.parse(String html) -> ExamSchedule`。extract离线从实际结果字段取学期及类型;merge要求所选类型恰好全部成功、同school/term并去完全重复。 -- [ ] **Step 1 — 写失败测试。** fixture4条考试归503,外层默认523不能污染;日期、完整时间、地点、座位、情况、类型、说明round-trip。期中及期末同课程均保留;全重复仅一次;缺一类型/mismatched term/type/超500报错;九列表头+有效查询证据+明确无考试提示为空成功,只有表头/登录页不能当空;异常字段不猜时间。ExamFileImport明确分派本科/研究生,未知HTML拒绝。 +- [x] **Step 1 — 写失败测试。** fixture4条考试归503,外层默认523不能污染;日期、完整时间、地点、座位、情况、类型、说明round-trip。期中及期末同课程均保留;全重复仅一次;缺一类型/mismatched term/type/超500报错;九列表头+有效查询证据+明确无考试提示为空成功,只有表头/登录页不能当空;异常字段不猜时间。ExamFileImport明确分派本科/研究生,未知HTML拒绝。 测试名 `actual result term wins over the outer default` 的核心断言: @@ -168,10 +168,10 @@ expect(exams.requestedExamTypes, {1}); expect(exams.records.first.date, '2026-06-22'); ``` -- [ ] **Step 2 — 运行。** `flutter.bat test --no-pub test/school/undergraduate_exam_adapter_test.dart test/school/exam_adapter_test.dart`,预期FAIL。 -- [ ] **Step 3 — 实现。** 按九列表头定位实际排考表,读取表单隐藏字段和结果params;两者矛盾拒绝,忽略控件初始化脚本。通过任务1复合身份合并、类型集合保存于ExamSchedule;不给空日期生成伪造日期。 -- [ ] **Step 4 — 重跑。** 上述测试PASS;用户原考试附件本地验证4条且503,无原HTML进仓库。 -- [ ] **Step 5 — 提交。** `feat: parse and merge undergraduate exam schedules`。 +- [x] **Step 2 — 运行。** `flutter.bat test --no-pub test/school/undergraduate_exam_adapter_test.dart test/school/exam_adapter_test.dart`,预期FAIL。 +- [x] **Step 3 — 实现。** 按九列表头定位实际排考表,读取表单隐藏字段和结果params;两者矛盾拒绝,忽略控件初始化脚本。通过任务1复合身份合并、类型集合保存于ExamSchedule;不给空日期生成伪造日期。 +- [x] **Step 4 — 重跑。** 上述测试PASS;用户原考试附件本地验证4条且503,无原HTML进仓库。 +- [x] **Step 5 — 提交。** `feat: parse and merge undergraduate exam schedules`。 ### Task 5: 本科导航、认证证据与按需捕获 @@ -179,7 +179,7 @@ expect(exams.records.first.date, '2026-06-22'); **Interfaces:** `UndergraduateNavigationPolicy.canNavigate(Uri uri,{required bool isMainFrame}) -> bool`、`isDataPage(Uri uri) -> bool`、`isLogout(Uri uri) -> bool`、`secureIdentityRedirect(Uri uri) -> Uri?`。`undergraduateStatusProbeScript`只返回认证状态证据;`undergraduateCaptureScript`返回限定数据片段。`UndergraduatePageEvidence.decode(Object raw,Uri current) -> UndergraduatePageEvidence`字段 `bool authenticated; List pages`;fragment字段 `String kind, html; Uri url`,kind仅timetable/exams;异常/超限抛FormatException。 -- [ ] **Step 1 — 写失败测试。** exact HTTPS eams/idas/eportal导航、HTTP idas指定登录地址升级;用户信息/自定义端口/相似恶意域名拒绝。状态脚本不包含课表HTML、密码或验证码。capture同源iframe/AJAX DOM实际value523/503覆盖旧attribute;跨源/登录文档不捕获;总量超2MB、深度>4、文档>16拒绝而非部分导入;decode顶层URL不一致/跨源片段拒绝;未知认证回调给明确错误。 +- [x] **Step 1 — 写失败测试。** exact HTTPS eams/idas/eportal导航、HTTP idas指定登录地址升级;用户信息/自定义端口/相似恶意域名拒绝。状态脚本不包含课表HTML、密码或验证码。capture同源iframe/AJAX DOM实际value523/503覆盖旧attribute;跨源/登录文档不捕获;总量超2MB、深度>4、文档>16拒绝而非部分导入;decode顶层URL不一致/跨源片段拒绝;未知认证回调给明确错误。 测试名 `capture accepts only undergraduate data pages` 的核心断言: @@ -192,10 +192,10 @@ expect(UndergraduateNavigationPolicy.canNavigate( Uri.parse('https://eams.uestc.edu.cn.attacker.test/eams/'), isMainFrame: true), isFalse); ``` -- [ ] **Step 2 — 运行。** `flutter.bat test --no-pub test/school/undergraduate_page_probe_test.dart` 和 `node --test test/school/undergraduate_page_probe_js_test.cjs`,预期缺接口FAIL。 -- [ ] **Step 3 — 实现。** 同步JS返回JSON字符串,不用WebView不会等待的Promise;认证证据需学校系统页面的已登录菜单/退出入口,不能仅依据host。捕获仅识别表、actual输入字段、名单和必要活动数据,含frame位置;结果参数与实际隐藏值保留。只序列化数据相关节点,禁止整个登录HTML;页内JS只读取文本/DOM,不执行原脚本。 -- [ ] **Step 4 — 重跑。** 两种测试PASS;既有研究生probe/policy测试保持。 -- [ ] **Step 5 — 提交。** `feat: capture undergraduate pages with scoped evidence`。 +- [x] **Step 2 — 运行。** `flutter.bat test --no-pub test/school/undergraduate_page_probe_test.dart` 和 `node --test test/school/undergraduate_page_probe_js_test.cjs`,预期缺接口FAIL。 +- [x] **Step 3 — 实现。** 同步JS返回JSON字符串,不用WebView不会等待的Promise;认证证据需学校系统页面的已登录菜单/退出入口,不能仅依据host。捕获仅识别表、actual输入字段、名单和必要活动数据,含frame位置;结果参数与实际隐藏值保留。只序列化数据相关节点,禁止整个登录HTML;页内JS只读取文本/DOM,不执行原脚本。 +- [x] **Step 4 — 重跑。** 两种测试PASS;既有研究生probe/policy测试保持。 +- [x] **Step 5 — 提交。** `feat: capture undergraduate pages with scoped evidence`。 ### Task 6: 手动课表连接及自动考试批次 @@ -203,7 +203,7 @@ expect(UndergraduateNavigationPolicy.canNavigate( **Interfaces:** `UndergraduateSession extends ChangeNotifier`,延迟 `WebViewController get web`;`Future openTimetable()`、`openExams(UndergraduateExamQuery query)`、`readCurrentTimetable()`、`reauthenticate()`、`logout()`;`void setReadingActive(bool active)`、`invalidateAuthentication()`、`bool saved({required int generation})`。只读状态 `hasOpened, canLogout, busy, awaitingAuthentication, statusLabel, message, pageGeneration`,`ImportDraft? pendingDraft`、`ExamSchedule? pendingExams`及当前query。session不依赖AppController、不自行存储。 -- [ ] **Step 1 — 写失败测试。** 构造session不创建原生WebView;openTimetable只载eams根,登录状态probe不捕获课表,点击才capture并产生pendingDraft;切换页面时旧capture被丢弃。考试选择1+2+3时逐项精确GET,前两项不产生pendingExams,完成才一个合并结果;第三项失败/显式取消/加载错误保留旧状态无pending;单项45秒/整批120秒边界。等待登录模拟超过120秒不失败;过期认证丢弃已读片段,认证后从类型1重新读取。重复点击、旧finished回调、route关闭、网页logout、dispose不能推进旧批次;fake Cookie清理失败保持可重试退出状态。 +- [x] **Step 1 — 写失败测试。** 构造session不创建原生WebView;openTimetable只载eams根,登录状态probe不捕获课表,点击才capture并产生pendingDraft;切换页面时旧capture被丢弃。考试选择1+2+3时逐项精确GET,前两项不产生pendingExams,完成才一个合并结果;第三项失败/显式取消/加载错误保留旧状态无pending;单项45秒/整批120秒边界。等待登录模拟超过120秒不失败;过期认证丢弃已读片段,认证后从类型1重新读取。重复点击、旧finished回调、route关闭、网页logout、dispose不能推进旧批次;fake Cookie清理失败保持可重试退出状态。 测试名 `closing the route invalidates pending batch results`,fake驱动已打开批次后: @@ -215,10 +215,10 @@ expect(session.pendingExams, isNull); expect(session.saved(generation: openedGeneration), isFalse); ``` -- [ ] **Step 2 — 运行。** `flutter.bat test --no-pub test/school/undergraduate_session_test.dart test/school/undergraduate_exam_batch_test.dart`,预期FAIL。计时使用测试虚拟时间,不真实等120秒。 -- [ ] **Step 3 — 实现。** 为每次打开/查询分配generation,任何await后校验generation和routeActive;start/finish匹配当前URL及同源证据。自动GET采用loadRequest,finished后有限轮询DOM直到可识别结果;计时及轮询在离页/登录等待/取消时清除。认证过期停读,重新认证启动新generation重读全部;全部完成调用任务4merge一次。manual capture只有readCurrentTimetable调用。saved只消费匹配generation结果;旧结果不能消费新pending。 -- [ ] **Step 4 — 重跑。** 新session测试及 `test/school/school_session_flow_test.dart`、`test/school/lazy_school_session_test.dart`均PASS。 -- [ ] **Step 5 — 提交。** `feat: connect undergraduate manual timetable and exam batches`。 +- [x] **Step 2 — 运行。** `flutter.bat test --no-pub test/school/undergraduate_session_test.dart test/school/undergraduate_exam_batch_test.dart`,预期FAIL。计时使用测试虚拟时间,不真实等120秒。 +- [x] **Step 3 — 实现。** 为每次打开/查询分配generation,任何await后校验generation和routeActive;start/finish匹配当前URL及同源证据。自动GET采用loadRequest,finished后有限轮询DOM直到可识别结果;计时及轮询在离页/登录等待/取消时清除。认证过期停读,重新认证启动新generation重读全部;全部完成调用任务4merge一次。manual capture只有readCurrentTimetable调用。saved只消费匹配generation结果;旧结果不能消费新pending。 +- [x] **Step 4 — 重跑。** 新session测试及 `test/school/school_session_flow_test.dart`、`test/school/lazy_school_session_test.dart`均PASS。 +- [x] **Step 5 — 提交。** `feat: connect undergraduate manual timetable and exam batches`。 ### Task 7: 本科浏览器页面、查询选项和导入确认 @@ -226,7 +226,7 @@ expect(session.saved(generation: openedGeneration), isFalse); **Interfaces:** `UndergraduateAuthPage({required UndergraduateSession session,required AppController controller})`;`Future showUndergraduateExamQueryPicker(BuildContext context,{UndergraduateExamQuery? initial,required DateTime now})`。返回null取消,返回validquery才登录;跨school/term课表替换和空课表使用ImportPreview确认,已导入同本科同学期非空课表可直接保存。 -- [ ] **Step 1 — 写失败测试。** picker一个学期、三种类型多选、禁止空选择、cancel不请求、沿用503及已选类型。手动页显示导入按钮,登录后不自动保存;跨学校/学期取消保留快照;空课表未勾确认不能保存;save失败仍在页面且pending可重试。成功返回课表;考试整批完成保存一次并返回,partial不保存;关页后旧回调不pop其他页面;双击导入只处理一次。 +- [x] **Step 1 — 写失败测试。** picker一个学期、三种类型多选、禁止空选择、cancel不请求、沿用503及已选类型。手动页显示导入按钮,登录后不自动保存;跨学校/学期取消保留快照;空课表未勾确认不能保存;save失败仍在页面且pending可重试。成功返回课表;考试整批完成保存一次并返回,partial不保存;关页后旧回调不pop其他页面;双击导入只处理一次。 Widget测试名 `the manual importer waits for the import button`,登录后fake页面加载并pump后: @@ -236,10 +236,10 @@ expect(session.pendingDraft, isNull); expect(controller.snapshot.courses, isEmpty); ``` -- [ ] **Step 2 — 运行。** `flutter.bat test --no-pub test/widgets/undergraduate_auth_test.dart test/widgets/undergraduate_exam_picker_test.dart`,预期FAIL。 -- [ ] **Step 3 — 实现。** init/dispose启停读取并监听session;UI操作和保存用busy保护+mounted/generation检查。课表复制当前generation的draft,确认后调用applyImport并检查lastError;考试调用applyExams后检查错误,成功才saved/pop。替换确认说明学校/学期及开始日需要重设;本科网页选学期文案不引导研究生编号选择器。读失败显示类型、重试、修改查询选项;认证等待提示原网页操作。 -- [ ] **Step 4 — 重跑。** Widget测试PASS,无未处理异步错误/overflow。 -- [ ] **Step 5 — 提交。** `feat: add undergraduate import and exam selection screens`。 +- [x] **Step 2 — 运行。** `flutter.bat test --no-pub test/widgets/undergraduate_auth_test.dart test/widgets/undergraduate_exam_picker_test.dart`,预期FAIL。 +- [x] **Step 3 — 实现。** init/dispose启停读取并监听session;UI操作和保存用busy保护+mounted/generation检查。课表复制当前generation的draft,确认后调用applyImport并检查lastError;考试调用applyExams后检查错误,成功才saved/pop。替换确认说明学校/学期及开始日需要重设;本科网页选学期文案不引导研究生编号选择器。读失败显示类型、重试、修改查询选项;认证等待提示原网页操作。 +- [x] **Step 4 — 重跑。** Widget测试PASS,无未处理异步错误/overflow。 +- [x] **Step 5 — 提交。** `feat: add undergraduate import and exam selection screens`。 ### Task 8: 接入共用页面、12节及服务计算 @@ -247,8 +247,8 @@ expect(controller.snapshot.courses, isEmpty); **Interfaces:** `HepanApp`新增可选本科session/builder参数,保留原 `school`、`schoolBuilder`测试接口。`WeekGrid`新增 `int periodCount = 11`,只接受11/12;`lessonSlots`末尾加 `LessonSlot(12,1320,1365)`。研究生 `void invalidateAuthentication()`仅取消读取并清连接状态/待导入结果,不创建WebView。考试页新增可选查询选项回调,文件入口改任务4的ExamFileImport。 -- [ ] **Step 1 — 写失败测试。** 两学校都出现,cancel无副作用;本科选择保存失败不进入浏览器,重启恢复选择。课表刷新/更新学期仍manual,考试refresh复用上次query、查询选项可改;不同学校的term不能进入gradURL。共享退出使两连接失效但未使用WebView不创建,旧数据按用户原退出选项处理。source不同banner准确。 -- [ ] **Step 2 — 写显示及时间失败测试。** 本科无课当前周也12行,研究生/示例11;周切换后第12节可见,七天固定宽,教师/教室全量且同节数卡片等高;详情显示全称、courseCode、notes。第12节单节occurrence22:00–22:45,9–12连排19:30–22:45,多提醒offset及calendar结束时间均正确、停课周无occurrence。使用360dp及大字体检查溢出。 +- [x] **Step 1 — 写失败测试。** 两学校都出现,cancel无副作用;本科选择保存失败不进入浏览器,重启恢复选择。课表刷新/更新学期仍manual,考试refresh复用上次query、查询选项可改;不同学校的term不能进入gradURL。共享退出使两连接失效但未使用WebView不创建,旧数据按用户原退出选项处理。source不同banner准确。 +- [x] **Step 2 — 写显示及时间失败测试。** 本科无课当前周也12行,研究生/示例11;周切换后第12节可见,七天固定宽,教师/教室全量且同节数卡片等高;详情显示全称、courseCode、notes。第12节单节occurrence22:00–22:45,9–12连排19:30–22:45,多提醒offset及calendar结束时间均正确、停课周无occurrence。使用360dp及大字体检查溢出。 Widget测试名 `graduate rows stay at eleven after adding period twelve`,分别pump已导入学校的页面后断言: @@ -260,10 +260,10 @@ expect(lessonSlots.last.startMinute, 1320); expect(lessonSlots.last.endMinute, 1365); ``` -- [ ] **Step 3 — 运行。** `flutter.bat test --no-pub test/widgets/school_selection_test.dart test/widgets/undergraduate_navigation_test.dart test/widgets/undergraduate_grid_test.dart test/domain/schedule_engine_test.dart test/services/notification_service_test.dart test/services/calendar_service_test.dart`,预期新导航/12节相关FAIL。 -- [ ] **Step 4 — 实现。** main只构造Dart session,浏览器延迟;Shell先await selectSchool成功,再分派,取消/失败保持状态。本科考试首次显示picker,refresh用ExamSchedule成功选项;历史课程schoolTerm不被考试改写。退出仅操作已有连接并协调失效,失败保留重试入口。课表periodCount来源于实际schoolTerm,demo强制11;所有布局循环使用截取的slot数量而非新增全局长度。考试页显示自身school/term/types/date/status/notes,空结果文案按设计;任务3/4文件导入沿用preview。保留前台服务检查、Dock/手势及动画。 -- [ ] **Step 5 — 重跑。** 上述测试PASS;追加运行已有 `test/widgets/complete_metadata_test.dart test/widgets/conflict_metadata_test.dart test/widgets/course_length_test.dart test/widgets/week_swipe_test.dart test/widgets/week_transition_test.dart test/widgets/startup_slow_test.dart test/widgets/logout_recovery_test.dart`,确保教师全量、动画、启动和退出恢复不退化。 -- [ ] **Step 6 — 提交。** `feat: integrate undergraduate school workflows and period twelve`。 +- [x] **Step 3 — 运行。** `flutter.bat test --no-pub test/widgets/school_selection_test.dart test/widgets/undergraduate_navigation_test.dart test/widgets/undergraduate_grid_test.dart test/domain/schedule_engine_test.dart test/services/notification_service_test.dart test/services/calendar_service_test.dart`,预期新导航/12节相关FAIL。 +- [x] **Step 4 — 实现。** main只构造Dart session,浏览器延迟;Shell先await selectSchool成功,再分派,取消/失败保持状态。本科考试首次显示picker,refresh用ExamSchedule成功选项;历史课程schoolTerm不被考试改写。退出仅操作已有连接并协调失效,失败保留重试入口。课表periodCount来源于实际schoolTerm,demo强制11;所有布局循环使用截取的slot数量而非新增全局长度。考试页显示自身school/term/types/date/status/notes,空结果文案按设计;任务3/4文件导入沿用preview。保留前台服务检查、Dock/手势及动画。 +- [x] **Step 5 — 重跑。** 上述测试PASS;追加运行已有 `test/widgets/complete_metadata_test.dart test/widgets/conflict_metadata_test.dart test/widgets/course_length_test.dart test/widgets/week_swipe_test.dart test/widgets/week_transition_test.dart test/widgets/startup_slow_test.dart test/widgets/logout_recovery_test.dart`,确保教师全量、动画、启动和退出恢复不退化。 +- [x] **Step 6 — 提交。** `feat: integrate undergraduate school workflows and period twelve`。 ### Task 9: 全量验证、发布与签名/登记备份核对 @@ -271,11 +271,11 @@ expect(lessonSlots.last.endMinute, 1365); **Interfaces:** 上述全部任务完成且回归通过;复用 `scripts/build-android.ps1`、`scripts/signing-info.py`及已有Android16模拟器/发布流程,不重建密钥、不新增依赖。 -- [ ] **Step 1 — 升版本并完整验证。** 两个版本位置统一0.7.0+18;在mobile运行 `flutter.bat test --no-pub --enable-impeller --reporter expanded`、`flutter.bat analyze --no-pub`,预期全量PASS、No issues found。运行本科及既有研究生Node probe测试。失败追根定位、只在新增变更或失败修复后重跑必要检查,不能跳过。 -- [ ] **Step 2 — 构建与身份验证。** 先保留当前0.6.2 APK及摘要再构建;根目录运行 `./scripts/build-android.ps1 -ToolchainRoot 'F:/program/app/.tools' -Mode release` 和同命令 `-Mode debug`;构建返回0且APK存在。使用signing-info脚本比较旧/新签名,证书必须等于Global Constraints值;读取包名及versionCode18。校验 `dist/backups/TPSchedule-0.5.4-registration-20261014/SHA256清单.json`全部18项内容摘要不变。 -- [ ] **Step 3 — Android16/API36检查。** 在模拟器覆盖安装,核对旧数据/选择学校/取消/本科登录按钮/考试多选入口。脱敏离线fixture导入检查12行、课程详情、四条503考试、历史考试后课表不变、重启持久化、换周动画/Dock及无新增启动黑屏;抓关键页面截图。真实账号/验证码及学校回调未实测的部分在记录中明确,不能用模拟测试宣称真实导入通过。 -- [ ] **Step 4 — 记录和源码包。** 写真实测试总数、静态检查、APK SHA256及限制;更新学校接入说明、本科手动操作和12节作息。源码ZIP使用明确允许的源文件集合,排除原附件、用户个人HTML、`.tools`、密钥、构建缓存、`output`和登记材料;验证解包目录/版本及文件数。签名公开信息可保留,私钥不得进入包。 -- [ ] **Step 5 — 最终审查并交付。** 按 requesting-code-review 技能审查全量差异(只读),修复会导致错误导入/丢数据/提醒时间错误的发现后验证;核对git diff --check及仅预期改动,再提交 `feat: release undergraduate integration 0.7.0`。沿用已授权本地master集成方式;如果使用隔离工作树,合并前检查没有覆盖用户工作。最终提供APK/源码链接、操作说明和真实登录仍需账户本人验证的边界,不等待上线部署。 +- [x] **Step 1 — 升版本并完整验证。** 两个版本位置统一0.7.0+18;在mobile运行 `flutter.bat test --no-pub --enable-impeller --reporter expanded`、`flutter.bat analyze --no-pub`,预期全量PASS、No issues found。运行本科及既有研究生Node probe测试。失败追根定位、只在新增变更或失败修复后重跑必要检查,不能跳过。 +- [x] **Step 2 — 构建与身份验证。** 先保留当前0.6.2 APK及摘要再构建;根目录运行 `./scripts/build-android.ps1 -ToolchainRoot 'F:/program/app/.tools' -Mode release` 和同命令 `-Mode debug`;构建返回0且APK存在。使用signing-info脚本比较旧/新签名,证书必须等于Global Constraints值;读取包名及versionCode18。校验 `dist/backups/TPSchedule-0.5.4-registration-20261014/SHA256清单.json`全部18项内容摘要不变。 +- [x] **Step 3 — Android16/API36检查。** 在模拟器覆盖安装,核对旧数据/选择学校/取消/本科登录按钮/考试多选入口。脱敏离线fixture导入检查12行、课程详情、四条503考试、历史考试后课表不变、重启持久化、换周动画/Dock及无新增启动黑屏;抓关键页面截图。真实账号/验证码及学校回调未实测的部分在记录中明确,不能用模拟测试宣称真实导入通过。 +- [x] **Step 4 — 记录和源码包。** 写真实测试总数、静态检查、APK SHA256及限制;更新学校接入说明、本科手动操作和12节作息。源码ZIP使用明确允许的源文件集合,排除原附件、用户个人HTML、`.tools`、密钥、构建缓存、`output`和登记材料;验证解包目录/版本及文件数。签名公开信息可保留,私钥不得进入包。 +- [x] **Step 5 — 最终审查并交付。** 按 requesting-code-review 技能审查全量差异(只读),修复会导致错误导入/丢数据/提醒时间错误的发现后验证;核对git diff --check及仅预期改动,再提交 `feat: release undergraduate integration 0.7.0`。沿用已授权本地master集成方式;如果使用隔离工作树,合并前检查没有覆盖用户工作。最终提供APK/源码链接、操作说明和真实登录仍需账户本人验证的边界,不等待上线部署。 ## 自审与交接