用户要求这份记忆跟着仓库走,而不是只留在本机。 .gitignore 里挡路的是 `.workbuddy/`。改成反忽略六行,并且**只放行 .md**: 别用 `!.workbuddy/memory/**`——它会把 memory 下任何文件都放行,本项目真发生过 `web.test.exe` 混进暂存。复核用 `git check-ignore -q --no-index` 逐条打靶: memory/*.png、memory/sub/x.md、memory/MEMORY.md.bak、plans/a.md 全 IGNORED, 17 个 .md 全 included;`git add -An .workbuddy/` 恰好 16 个文件。 其余 .workbuddy 内容(计划、本地设置、临时产物)继续忽略。记忆里没有真凭据 (唯一命中是 2026-09-14.md 里的 `password=rond-admin`,那是项目默认口令)。 顺带做两件整理: 1. 取证了注入上限。以前按「字节数」判断 MEMORY.md 是否超限是错的: WorkBuddy 的 `getWorkingMemoryContent()` 用 `MAX_MEMORY_CHARS = 1e4` 比 JS `content.length`,即**字符数**(中文 1 字算 1);超了 substring 截断、尾部静默丢失。 本 details 与每日日志都不参与注入,只有 MEMORY.md 受约束。 现已从 6604 字符收到上限的 66%,并把这条上限写进索引抬头。 2. 把「备份合并」「导出/恢复三条铁律」两段长文迁进 MEMORY-details.md 的卅一/卅二节, 索引只留关键词指针。行尾统一为 LF(仓库 .gitattributes 是 `* text=auto eol=lf`)。
29 KiB
2026-09-21
记忆维护
MEMORY.md超限(25KB)被注入截断 ⇒ 已收敛:把 details 未覆盖的新知识(跨备份 UUID、坐标键主键、合并执行、迷雾三段式、tb-stack、活动/类别、补全页交互、导出排除顺序)并入MEMORY-details.md(新增第 19–27 节,36.7KB),MEMORY.md重写为 ~6.5KB 索引(只留「违反了会静默出 bug」的判据)。
全量排查(产出 audit-2026-09-21.md)
范围:77 条路由 / 23 个页面集 / 96 个 Go 文件 / 24 模板 / 4 前端脚本 + app.css。方法:4 路只读深挖 + 关键结论回读复验 + 模板行为实测 + go build/vet/test 实跑。
新确认的 P0(本次实测坐实)
internal/web/parallel.go:43-57runJobs子 goroutine 无 recover ⇒ 任一 job panic 终止整个进程(server.go:317的 recover 只管请求 goroutine)。可用性最严重。internal/auth/auth.go:199hash[:min(len(hash),16)]恒为常量(pbkdf2$sha256$21)⇒ MAC 只绑uid|exp,改密码不失效旧会话(注释声称相反)。SignPending2FA同。
新确认的 P1
layout.html:215{{d .Dataset.Last}}:d收time.Time而.Last是*time.Time。实测:非 nil 会被模板解一层指针、正常工作;nil 时报错并截断输出。refreshDatasetAgg(LEFT JOIN)在数据集零到访时写 NULL,编辑路径调用 9 次 ⇒ 可达。后果是 HTTP 200 + 页脚之后整段不渲染(含 Leaflet/脚本引入),服务端只有一行 log。server.go:447serverError回显err.Error(),调用点含home/mapPage/placesPage/placePage/reportPage等公开页与公开 JSON 接口。stats.go:195movementsWhere比visitsWhere少了 City/Province/ActivityIDs/MinDwell/TagID/Keyword ⇒ 按这些维度筛选时「总里程/出行方式」仍是全量。rond-map.jstooltip 未转义(382/772/818/836)而 popup 已esc()(340-348)——同文件内不一致。- 多语句写操作未包事务:
handlers_edit.go:699/1235/1411/1332+1342/1832+1848(建地点+建到访、合并两处反而是对的)。 - 解压无上限:
rond/parse.go:201、fog/fog.go:134/199(上传只限压缩后体积)。 ^-?[0-9.]+$不是浮点校验却紧跟::float8:handlers_edit.go:2315(suspectWGS)、coord.go:40;:187那处连正则都没有。main.go:50-52默认口令rond-admin,登录只有一次性验证码、无失败限速。
其他值得记的
go test ./...全绿是假信号:本机 62 SKIP / 59 PASS(缺 5 个环境变量),internal/web/rond/stats/fog的重用例全在 skip 里。html/index.html(856 行)已不被服务(go:embed与 routes 都无引用)。- ✅ 已核对不是问题:SQL 全参量化、
ORDER BY白名单、serveMedia路径穿越、无 SSRF、验证码不可重放、无模板 XSS 与 405、26 张 tb-stack 表类名成对且 data-label 全齐。
修复落地:第 1–4 批 + 途中新发现 1 个 panic
纪律:每条都先做变异验证(先确认测试能变红,再还原确认变绿)。全量套件(含 -p 1 验收)0 失败。
新增回归
web/tx_atomicity_test.go、web/server_error_test.go、web/map_escape_test.go、web/tb_static_test.go、rond/limit_test.go、fog/limit_test.go、auth/guard_test.go、web/login_guard_test.go、stats/placecols_test.go、web/topic_stats_test.go、fog/tilename_test.go。
性能 P1:placeCols 的相关子查询缺索引前导列(已实测)
idx_visits_place 是 (dataset_id, place_id),6 个子查询只写 place_id=p.id ⇒ 每轮为库里每个 dataset 各下一次索引。
Index Searches 2371→366、Buffers 23737→5669、actual time 53.4ms→31.5ms。跨 dataset 的到访 0 条 ⇒ 该谓词语义冗余。
性能 P2:专题列表 N+1 → 一条 UNION ALL
新增 fillTopicStatsBatch(顺手发现「天数」查询与点位查询的到访集合完全相同,纯重复);删掉 fillTopicStats,列表页与详情页共用 topicAreaCond。回归以详情页 topicContent 为预言机(3 轮 × 5 专题)。
新发现 bug:fog.tileName/tileIDFromName 单数字编号越界 panic
- 导出:
Encode里id := t[0]+t[1]*MapTiles,第一列就是 0~9 ⇒ M2 尾缀循环起点len(s)-2 = -1⇒s[-1]越界。 - 导入:长度守卫
len(name)<7恰好放行「4 位 md5 + 1 位掩码 + 2 位尾缀」的 7 字节名,md5 公开可算 ⇒ 构造 .fwss 上传即 panic(DoS)。 - 修复:
tileDigits(id)不足两位补前导 0、编解码共用;守卫收紧到len(name)<8。真机编号 ≥2 位 ⇒ 导出字节不变。
两个踩过的坑(会静默浪费时间)
time.Time的==会连内部*Location指针一起比:pgx 每次 Scan 都新建 Location ⇒ 「值相同」也判不等(366 行里假报 325 行差异)。比对数据库行一律json.Marshal后比字符串。- 静态 SQL 守卫必须按子查询分段:
placeCols里 v2 出现 3 次,全文Contains会被另外两处顶包,变异跑不出红。要先按FROM visits (vN)的匹配位置切段再判。
尚未处理
P2 的 S1–S9 / D1–D8 / F4–F10、F14、F16–F18,性能 P3–P8。
建议优先:S5(CSRF token / JSON 端点 Origin 校验)、S4(cookie 无 Secure)、S3(两处 //evil.com 协议相对跳转)、D2(writeRawRow 的 marshal 失败只 log ⇒ 留档静默丢失)、P4(导出整包进 buffer,受 WriteTimeout 120s 约束)。
修复落地(第二批):S3 / S4 / S5 / D2
S3 协议相对跳转 ✅
同一份判据在仓库里有三份实现:view.go 的 backFromRequest 是加固过的正确版,
handlers_admin.go 的 next 与 handlers_vote.go 的投票后跳转是手写的弱化版(只判 HasPrefix(next, "/"),放行 //evil.com)。
⇒ 抽成纯函数 safeInternalPath,三处 + 登录跳转共用。回归:redirect_target_test.go
(21 个拒绝样例 + 静态守卫 TestNoWeakRedirectPrefixCheck,守卫还自查扫到的文件数防假绿)。
S4 会话 cookie 补 Secure ✅
不能无条件打:本机 http 调试与 CDP 验证流程都跑在 http,打了 Secure 谁都登不进去。
按「本次请求是否 https」判断:r.TLS != nil,或 X-Forwarded-Proto(多段取首段)。login 签名多了 *http.Request。
回归:session_cookie_test.go 五种来源 + 断言 HttpOnly/Path/SameSite 未被顺手改掉。
S5 CSRF 纵深防御 ✅
先取证:全部改状态的路由都是 POST(没有 GET 改状态)✓,cookie 已是 SameSite=Lax(跨站 POST 本就带不上 cookie)。
故定位为纵深防御。新增 csrf.go 的 csrfOriginGuard(只对 POST/PUT/PATCH/DELETE 生效):
有 Origin 必须本站(null 一律拒)→ 无则看 Referer → 都没有则放行(跨站 POST 浏览器必带 Origin)。
一行模板都不用改。拒绝时日志带 Origin/Referer/Host,反代配错一眼可见。
部署前提:反代要传原始 Host(或用 X-Forwarded-Host 兜底)。
回归:csrf_test.go(14 例单元 + 走完整中间件链的接线测试)。变异两轮:①摘掉中间件(只有接线测试红)②判据恒真(都红)。
D2 留档静默丢失 ✅
writeRawRow 的 json.Marshal 失败原来只 log 后 return nil ⇒ 调用方以为写成功、合并照常提交、原始列永久缺失。
改为上抛由 withTx 整笔回滚。回归:entity_raw_test.go 用 math.NaN() 触发真实序列化失败,
断言「必须报错 + 失败不留半截行 + 正常值仍写得进去」(第三条防断言假绿)。
下一批建议
🔴 S1(fogResetIndex() 无锁写,全报告唯一真 data race)、🔴 S7(TOTP 不消耗验证码 + login2FA 无限速,auth.Guard 现成可复用)、
🟠 S2(图片上传缺 MaxBytesReader,一行改动)、🟠 D3(parse.go 不查 rows.Err())、🟠 P4(导出整包 buffer + WriteTimeout)。
第三批(S1 / S7 / S2 / D3 / P4)✅
先说两个会让验证白跑的坑
-race需要 cgo + C 编译器,本机原本没有 gcc(go env CC写着 gcc 但 PATH 里没有)。 解法:scoop install mingw(装到~/scoop/apps/mingw/current/bin), 跑法:export PATH="$HOME/scoop/apps/mingw/current/bin:$PATH"; CGO_ENABLED=1 go test -race ...。- 用
newMovementFixture的新测试会静默 SKIP(它同时要ROND_TEST_CONF与ROND_SRC)。 只带ROND_TEST_CONF时结果是ok,看起来全绿其实一条没跑——这轮就这样骗过了一次变异验证 (变异后仍「通过」,一打印 body 才发现根本没进函数)。验收一律用-v数 SKIP,别只看 ok。
S1 迷雾索引 data race ✅
fogIndexV 普通指针 → atomic.Pointer[fog.Index](Load/Store)。
这条是全报告唯一真正的 data race,也是第一次真跑出 -race 报告:
变异回普通指针后 -race 报 WARNING: DATA RACE(write @fog.go:453 fogResetIndex vs read @fog.go:45 fogIndex),
恢复 atomic 后干净通过。回归 fog_race_test.go(8 读者 + 1 重置者)。
S7 两步验证限速 + 验证码一次性 ✅
login2FA 接 loginGuard,键 "2fa:"+用户名(与口令环分开计数);新增 auth.CodeReplay
(Consume(uid, code),replayTTL = 3*totpStep = 90s 覆盖 ±1 步窗口)。
重放不计入爆破计数(用户手抖重复提交也会撞上,不该越锁越久)。
阈值是第 5 次失败才上锁(guardFreeTries=5)⇒ 测试里「几次才锁」要写成循环探测,别硬编码次数。
变异两轮:摘掉入口 RetryAfter(锁定期内递正确码能登录成功)、Consume 短路成 if false(同码拿到 303)——都红。
S2 图片上传缺请求体上限 ✅
补 r.Body = http.MaxBytesReader(w, r.Body, imageMaxBytes+1<<20)。
判据要认准:ParseMultipartForm 的 maxMemory 只限内存,超出无上限落临时文件;
mime/multipart 内部另有 maxMemory+10MB 的松量,所以拿 9MB 去打 8MB 是能过的——必须大到越过上限。
变异后文案变 图片超过 6MB(走的是单张 6MB 上限那条),据此判红。
D3 rows.Err() ✅
parse.go 加 drainRows(rows, what),13 处循环收尾全走它。
守卫 internal/rond/parse_rows_test.go:抓出所有 for x.Next() 的变量名,逐个要求 x.Err() 或 drainRows(x,;
再单独校验 drainRows 本体确实既查 Err() 又 Close()(防空壳)。
往返验收全等(823/323/8/30/791/557/2246,坐标 0.000000000、时间 0s)。
P4 导出下载 ✅
导出补 Content-Length + 用 http.NewResponseController(w).SetWriteDeadline(+10min) 只放宽这一条路由;
gzipWriter 补 Unwrap() http.ResponseWriter(少了它 ResponseController 一律 ErrNotSupported)。
注意:测试里用 httptest.ResponseRecorder 会打日志「无法延长写超时」,那是预期的降级路径,不是 bug。
验收
- 全量
go test -count=1 -v ./...:209 PASS / 0 FAIL;只带单次 env 时 5 SKIP(全是两段式导出的), 预置ROND_CLEAN_OUT/ROND_EXPORT产物后 0 SKIP。两段式单独跑也全绿。 gofmt -l干净、go vet干净、无残留 MUTATION 标记。
⚠️ 待查:TestPlaceUpdateBatchActivity 偶发失败(4 次全量里 1 次)
症状:导出包里只有 245/246 条到访(place_activity_test.go)。
已排除本轮改动:
- 导出链路完全不经过
rond.Parse(只有backup_diff/backup_merge/handlers_admin上传用),D3 无关; f.export读的是 recorder 的 body,zip 若被截断rond.Open会直接失败(中央目录没了), 所以 P4 的WriteHeader/Content-Length/buf.WriteTo也无关;- 那一轮和上一轮的唯一差别是 3 个模板/JS 文件的 CRLF→LF,影响不到到访条数。
已加诊断:失败时打印「包里缺哪些 Z_PK / pks 自身有无重复 / 包内 ZVISIT 总行数」,
三种成因(少写一行 / 两行撞号 / 选错了数据集)一眼可分。
怀疑方向(未证实):
datasetFor是全局取活跃数据集 (WHERE is_active AND status='done' ORDER BY uploaded_at DESC LIMIT 1,没有 tiebreaker), 而用例的pks来自 fixture 自己的dsID;若有两份导入同时is_active且uploaded_at同秒, 导出可能取到另一份「几乎一样」的数据集 —— 差异正好是这种个位数级别。 下次复发时看新诊断输出即可定性。
收尾
- 全量
./internal/web/(带两段式产物,CRLF 归一后重跑):113 PASS / 0 FAIL / 0 SKIP。 - 全量
./...:214 PASS / 0 FAIL / 0 SKIP。
第四批:S6 / D5 / D1 / P5 + N1 定位(2026-09-21 下午)
顺序:先把上一批留下的 D1 编译中断修完(activityFlags 签名改 3 返回值、fillup.go:384 补错误处理、
handlers_photos.go:236 接 logAggFailure),再依次做 S6/D5/P5。
S6 X-Forwarded-For 只认最后一段 ✅
clientIP 改为「末段 + netip.ParseAddr 校验,不合法退回 RemoteAddr」。
回归 client_ip_test.go(12 组表驱动 + 伪造前缀不改身份);变异回「取首段」⇒ 红。
副作用写进注释:投票身份 = hash(cookie, IP),取法变即身份变;单反代没伪造时首末段相同、无影响;
反代前再叠 CDN 时末段变 CDN 的 IP ⇒ 访客被叠成同一来源(cookie 仍各异,不会认错人)。
D5 收藏切换一条语句 ✅
UPDATE ... RETURNING bookmarked。静态守卫(不许再有 SELECT bookmarked / err == nil && on)+
行为用例(连点两次,提示与库内实况必须一致;不存在的记录不许回成功提示)。
D1 聚合刷新不再吞错 ✅
四个助手统一返回 error:事务内上抛、提交后走 logAggFailure(只记日志不改判请求)。
回归 agg_error_test.go:取消 context ⇒ 必错;裸调用静态扫描;签名必须以 ) error 结尾。
变异两处(return nil 吞错 / 改回裸调用)分别打红。顺带全仓确认再无「语句形式丢弃返回值」的 DB 调用。
P5 findSourceArchive 先按大小筛再算摘要 ✅
新增 pickArchiveBySha(files, sha, size, hash);sha='' 的退化路径不筛。
hash 做成参数是为了让用例数「算了几次摘要」——只看返回哪个文件新旧写法没有区别。
变异删掉大小筛 ⇒ 红并报「实际算了 3 次(白读了 2 份整包)」。
⚠️ 自查:静态守卫的 cwd 依赖(本轮自己踩出来的坑)
TestVisitBookmarkTogglesInOneStatement 用 os.ReadFile("handlers_edit.go") 单跑绿、跟夹具混跑红
——因为 newMovementFixture 会 os.Chdir 到仓库根,而 -run 选中的用例集不同,cwd 就不同。
TestNoWeakRedirectPrefixCheck 的 filepath.Walk(".") 同理,更阴:不报错,但扫的子树从
internal/web 悄悄变成整仓。
修法:新增 internal/web/testpaths_test.go(moduleRoot(t) / pkgFile(t,name),从
runtime.Caller(0) 往上找 go.mod),三处守卫全部改用它;Walk 起点改成仓库根并把「自查文件数」
从「只在 found==0 时查」改成无条件查。验收跑了「单跑」+「跟夹具混跑」两遍,并在包外
(internal/config/config.go)注入一次探针确认整仓覆盖真的生效。
→ 已同步到技能 rond-fix-verify。
★ N1 定位:不是导出丢行,是测试把 SQL 参数绑错了位
症状「导出包里只有 245/246 条到访(缺 [],pks 内重复 [])」在第四轮全量里复现。
根因:断言 SQL 是 SELECT count(*), count(*) FILTER (WHERE ZACTIVITY_=?) FROM ZVISIT WHERE Z_PK IN (?,…),
FILTER 的 ? 是第一个占位符,而 args 是 [pks…, actSrc] ⇒ FILTER 绑到 pks[0],
IN 列表变成 pks[1:] ∪ {actSrc},于是条数偶尔少 1、converted 只有碰巧 pks[0]==actSrc 时才成立。
(「缺失列表为空」正是错位的指纹:缺的 pks[0] 被 actSrc 顶替了。)
修法:actSrc 放最前、ph/args 同循环成对生成 + Count(q,"?") != len(args) 自检。
验证:连跑 6 次全绿;变异 export.go 的 actPK = sp → actPK = v.ActivityID.Int64 ⇒ 稳定红
(导出包里只有 0/246 条到访的活动换成了 #1(站点内部 id 75881))。
意味着这条断言以前是靠巧合绿的,等于没守住「导出写的是 rond 的 src_pk」。
先前怀疑的 datasetFor 全局取数据集已排除(包内 ZVISIT 始终 824 行)。
验收(本轮收尾)
- 全量
go test -count=1 -v ./...:211 PASS / 0 FAIL / 0 SKIP,exit=0(12 包全 ok)。 - 两段式:
TestMovementCleanExport→TestExportRoundtrip,823/323/8/30/791/557/2246 全等、 坐标 0.000000000、时间 0s;迷雾 8701 块一致。 gofmt -l干净、go build/go vet干净、export.go与 HEAD 无差异(变异已完全还原)。- ⚠️ 本轮踩了一次环境变量:
ROND_DSN指了postgres维护库 ⇒TestChangeUsername失败 (关系 users 不存在)+ 6 个 stats skip +TestVisitorHomeStoreskip。必须指rond_fog_test。 - 清理:删掉临时
tmp-verify/(含日志与中间包)与探针文件。
遗留
- D7 待用户定语义:
Filter.Verdict只在Places生效,Cities/ActivityCounts/ActivityStats连Filter都没接。直接加会让「点被藏了、轨迹还在」——需要先决定「verdict 过滤是否也管轨迹」。 - 其余:S8、S9、D4、D6、D8、F4–F10、F14、F16–F18、P3、P6–P8。
第五批(S8 / S9 / D7)——按用户「全部,但先从安全入手」
S8 GPX 轨迹两个入口都按属主收口
/api/gpx(公开无鉴权)原来全表吐;adminGpxDelete只按id删。改成:取生效数据集的user_id(is_active AND status='done'最近一份)→ 取不到返空,WHERE user_id=$1; delete 用WHERE id=$1 AND user_id=$2+ 查RowsAffected()==0(只改 WHERE 不看行数 ⇒ 删他人轨道静默「成功」)。- 守卫
gpx_owner_test.go(行为 + 静态)。静态守卫差点假绿:写成strings.Contains(body,"user_id")时,一句中文注释就能糊过去 ⇒ 改成只扫反引号里的 SQL 文本 + 「一段 SQL 都没扫到就 Fatal」。 变异:删WHERE user_id=$2⇒ 红。
S9 下载文件名可逃出引号
- 四个
Content-Disposition(geojson/gpx/rondbackup/fwss)裸拼datasets.original_name⇒ 带"或换行即可注入头部。新增downloadDisposition(filename=ASCII 兜底 +filename*=UTF-8''…)、rfc5987Escape、asciiSafe、hasAlnum;四个出口全部改走它。 - 守卫
download_name_test.go三条。反过来抓出实现自身两处 bug:正则漏-(合法名判不符)、asciiSafe漏=(a;b=c能溜过)——都修了。变异:某调用点改回裸拼 ⇒ 红;asciiSafe删=⇒ 红。
D7 verdict 筛选全域一致(用户定「全域一致」口径)
- 原来
Filter.Verdict只在Places生效,Cities/ActivityCounts/ActivityStats不认。 把 verdict 条件下沉到visitsWhere(place_key坐标键EXISTS(place_votes…)),所有走visitsWhere的统计自动一致;hasVisitConds()同步加|| f.Verdict != 0;Places那段重复 特殊-case 删掉。place_key表达式抽成placeKeyExpr(alias)(原来散在三处)。 - 守卫
stats/verdict_scope_test.go:fixture 自建封闭数据集(坐标 89.9/100.x、独立 user、 故意不设 is_active,避免并行测试按坐标键串味);Verdict=99⇒ 14 口径全空;只投 p1/p2 ⇒ visits=3/dwell=110/places=2/cities=2/provinces=1/movements=2/distance=12.00/weather=1。 变异:去掉 visitsWhere 下推 ⇒ 红;从 hasVisitConds 删 verdict ⇒ 只 movements/weather 清空 ⇒ 红。 - 全量回归结果见下方「第五批验收」。
第五批验收
- 全量
go test -count=1 -v ./...(四件套齐全、DSN 指rond_fog_test):218 PASS / 0 FAIL / 0 SKIP,exit=0 (第四批 211 + 本批 S8/S9/D7 共 7 条)。两段式 823/323/8/30/791/557/2246 全等、坐标0.000000000、时间0s、迷雾 8701 块一致。 - 已更新
audit-2026-09-21.md(新增「修复记录(第五批)」+ 重写「尚未处理」表,删掉 S8/S9/D7/P5;验收数字改 218)。 - 已更新
.workbuddy/memory/MEMORY.md(GPX 属主 / Content-Disposition / verdict 全域一致三条硬规矩)与技能rond-fix-verify(静态守卫三反模式)。
第六批(D4)——脏备份的伪时间与越界坐标
实测复现(临时探针调 appleTime)
Go 对超范围浮点转 int64 是回绕不是饱和:NaN/±Inf/1e30/±1e30 → 2001-01-01(恰好回绕到基准点,
看着完全正常)、1e12 → 2123-10-28、-1e12 → 1878-03-08。
要命的是 ingest 拿所有到达时间做 min/max 写 datasets.first_visit_at/last_visit_at ⇒ 一条脏行让
整条时间轴从 2001 年起算。只卡结果没用(回绕后可能又落回合法区间),必须先卡输入秒数再乘。
改法
parse.appleTime:NaN/±Inf或秒数超出[1970,2100)⇒ nil(接上既有「装不下就 skip+留档」契约)。- 新增
parse.validCoord:NaN/±Inf/越界 ⇒ 读ZLOCATION时归一成(0,0)(与COALESCE(NULL,0)一致, 顺带让place_key/contentHash对脏行稳定)。 - 位移的脏时间保留行、时间置空(本就可空、导出走
sql.NullTime),不制造「无声消失的行程」。
回归 + 变异
internal/rond/dirty_test.go:纯函数 + 静态守卫 + 真的建一个 Core Data 版 sqlite 走一遍Parse。internal/ingest/dirty_import_test.go(DB,独立用户跑完即删):脏到访不进 visit_count、留档为 skipped、first_visit_at不被污染。- 变异 5 次全红:去窗口检查 / 去 NaN 检查(两者互补) / 去地点归一化(静态+端到端双红) / ingest 改「补零时间」/ ingest 只跳过不留档。
- 已更新
audit-2026-09-21.md(新增「修复记录(第六批)」+ 尚未处理表移除 D4)。
⚠️ 自查:新写的 ingest 用例把统计用例「串」成了假 skip(本轮自己踩出来的)
第六批全量首跑 220 PASS / 0 FAIL / 4 SKIP——4 条 stats 用例报「没有天气数据可测 / 找不到可用省份」。
根因两层:
stats夹具是SELECT id FROM datasets WHERE is_active LIMIT 1,不带 user 过滤;我的 ingest 用例Import时会把临时数据集的is_active切成 true ⇒ 运行期间别的包可能选中这份几乎空的数据集 ⇒ skip。- 更糟的是清理没生效:
defer d.Close()在t.Cleanup之前执行(defer 先跑),Cleanup 里d.Exec(DELETE FROM users …)拿到的连接已经关了,静默失败 ⇒ 库里留下 5 个__dirty_*用户、 5 个生效数据集,把所有 stats 用例永久污染。 修法:派生专用库(ROND_DSN库名加_ingest后缀、db.Open自动建)+ 把d.Close()也放进t.Cleanup并先注册(后进先出 ⇒ 删用户时才还连着)。清掉残留用户后重跑,共享库users=1/active=1。 → 已同步到技能rond-fix-verify与MEMORY.md。
第六批验收
- 全量
go test -count=1 -v ./...:224 PASS / 0 FAIL / 0 SKIP,exit=0(12 包全 ok;第五批 218 + D4 新增 6)。 - 两段式导出数字与第五批完全一致(823/323/8/30/791/557/2246、坐标 0.000000000、时间 0s、迷雾 8701 块) —— D4 只拦脏值,干净备份路径零变化。
- 验收数字已写入
audit-2026-09-21.md「验收状态」。
第七批(F4/F7)——异步回包把已关掉的图层又画回来
症状与根因
勾选图层 → 请求发出 → 立刻取消 → 回包 → 图层照画(开关显示「关」,地图上却有东西)。
四个出口全中:setGpx/setHeat/setCityAgg(rond-map.js)+ 模板里的行程轨迹 refreshTrack(map.html)。
共同写法是取消时 xxxLayer = null 就 return,在途那次请求没人作废。
改法
每次调用发递增序号;关键是序号必须在 if (!on) return; 之前递增(否则取消这条路径不改序号,
在途请求照样合法),回包首行 if (seq !== xxxSeq) return;。序号顺带解决「连点筛选/切底图」的旧回包盖新回包。
refreshTrack 的 .then 与 .catch 两个分支都要挡。
验证
- 守卫
map_async_cancel_test.go:按大括号配平取函数体,断言两个异步分支的相对顺序 (递增早于早退、作废检查早于画)。变异 3 次全红。 - CDP A/B(比静态守卫硬):
--inject给/api/track|gpx|cities回包各加 900ms 延迟撑开竞态窗口, 探针「勾选 → 120ms → 取消 → 2.2s」后数path.leaflet-interactive: 有守卫2→2 delta=0;去掉守卫2→758 delta=756(开关关着画了 756 条线,且 errs=0)。 - 临时的 inject/探针脚本写在
storage/_*,跑完已删。 - → CDP 手法(延迟注入 + 数 DOM 做 A/B)已同步到技能
rond-web-verify。
第八批(D8 / P8)
D8 上传失败后把半成品留在 uploads 里
adminUpload 先落盘再解析(rond.Open/Parse/ingest.Import 任一失败都直接 return),
原来只有「sha 完全相同」那条早退分支才 os.Remove ⇒ 坏包反复上传堆垃圾,而 findSourceArchive
正要遍历这个目录。改法:落盘后挂 keepStored + defer,只有「新导入成功」那一支置 true
(那份上传件是导出的基线源);顺带修掉 st.Identical 分支的同类泄漏。
回归 upload_cleanup_test.go(DB):投垃圾 .rondbackup,断言 303 带 err + 目录无新增;
带前提守卫(Location 必须含「解析备份失败」,否则落在更早分支时断言会空过)。
变异:清理改空操作 ⇒ 红并报出泄漏文件名。
P8 recover 在响应已写出时追加错误体
http.Error 无条件调用 ⇒ handler 写了半页才 panic 时会二次 WriteHeader
(superfluous response.WriteHeader)并把错误体接在半个页面后面。
改法:recover 用 headerSent 包装 ResponseWriter(WriteHeader/Write 任一即置 sent),
已写出就只记日志。包装层必须实现 Unwrap()(又踩到 P4 给 gzipWriter 补过的同一个坑:
少了它 NewResponseController 拿不到 SetWriteDeadline,导出的 10 分钟写死线静默失效)。
回归 server_error_test.go 三条(半页+panic / 未写+panic / 用现成 deadlineRecorder 验 Unwrap)。
变异:还原无条件 http.Error ⇒ 红;删 Unwrap ⇒ 红(feature not supported)。
第八批验收
- 全量
go test -count=1 -v ./...:229 PASS / 0 FAIL / 0 SKIP,exit=0(12 包全 ok;第八批新增 4 条)。 - 两段式导出与第六批逐项相同(823/323/8/30/791/557/2246、坐标 0.000000000、时间 0s、迷雾 8701 块) —— 七、八两批都没碰数据路径。
- ⚠️ 变异验证会在
storage/uploads-test/留一个泄漏文件(清理被改成空操作那轮), 已清掉;正常测试轮不留(用例自带断言)。
第九批(D6)——轨迹图层与里程统计的区域判定不一致
原报告写「可见性口径不一致」,查清后具体是区域判定的连接方式不同(与迷雾遮罩无关):
apiTrack 是 regionSQL("pf") + regionSQL("pt") ⇒ 白名单下两端都要命中;
movementsWhere 是 EXISTS(... pmv.id IN (from,to) ...) ⇒ 任一端命中。
黑名单下两种写法恰好等价,白名单下才分叉:一端在北京、一端在上海的位移,白名单只选北京时
图层画不出来(缺上海那端)、总里程却算进去。图层那侧「两端都要在」才是安全侧
(白名单的用意就是不暴露区域外坐标)。
改法:movementsWhere 的区域条件改成对两端各判一次、两端 AND(与 regionSQL 同款),
并把 IN 换成 COALESCE(col,'') = $n —— district 常 NULL,NULL IN (...) 得 NULL,
黑名单的 NOT (...) 跟着变 NULL ⇒ 整行被误删(regionSQL 早用 COALESCE 绕过,这是第三点分叉)。
回归:internal/web/region_consistency_test.go 是对账测试(不是各写一遍 SQL 互比):
自建封闭数据集(3 地点 / 2 段位移:一端在外 10km、两端在内 20km),一边跑真 regionSQL 拼的
图层谓词、一边跑真 stats.Overview,断言相等。变异:还原成「任一端命中」⇒ 红,
报「图层 20.00 km,统计 30.00 km」。
→ 已更新 audit-2026-09-21.md 与 MEMORY.md(区域判定必须两端 AND + COALESCE)。
第九批验收
- 全量
go test -count=1 -v ./...:230 PASS / 0 FAIL / 0 SKIP,exit=0(12 包全 ok;第九批新增 1 条对账用例)。 - 两段式导出 823/323/8/30/791/557/2246 全等、坐标 0.000000000、时间 0s;迷雾 8701 块逐字节一致(未碰数据路径)。