diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index bd4c3a9..6985c3e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -15,5 +15,7 @@ - [ ] `npm run verify` 通过 - [ ] 未提交 `.env`、`.vercel`、secret key 或个人数据 - [ ] 数据库变更附带迁移文件 +- [ ] 已检查本次变更涉及的 README、docs、TODO、ROADMAP、CHANGELOG、发布说明及隐私/安全文档,并已同步实现差异 +- [ ] 代码、测试、迁移、配置和相关文档已作为同一项工作提交;若无文档改动,已在 PR 说明原因 - [ ] 用户可见文案不包含「Supabase」(用「云端」替代) - [ ] 无内联 `style=` 属性或内联 ` + + `; + expect(extractScriptSources(html).sort()).toEqual(["/src/app.js", "/src/cloud.js"]); + }); + + it("extracts hashed asset paths from production HTML", () => { + const html = ``; + expect(extractScriptSources(html)).toEqual(["/assets/index-a1b2c3.js"]); + }); + + it("ignores inline scripts and non-module scripts", () => { + const html = ` + + + + `; + expect(extractScriptSources(html)).toEqual(["/src/app.js"]); + }); + + it("returns empty array for HTML without module scripts", () => { + expect(extractScriptSources("")).toEqual([]); + }); +}); + +describe("scriptSourcesDiffer", () => { + it("returns false when sources are identical", () => { + expect(scriptSourcesDiffer(["/a.js", "/b.js"], ["/a.js", "/b.js"])).toBe(false); + }); + + it("returns true when a src hash changed", () => { + expect(scriptSourcesDiffer(["/assets/index-aaa.js"], ["/assets/index-bbb.js"])).toBe(true); + }); + + it("returns true when script count changed", () => { + expect(scriptSourcesDiffer(["/a.js"], ["/a.js", "/b.js"])).toBe(true); + }); + + it("returns false for empty arrays", () => { + expect(scriptSourcesDiffer([], [])).toBe(false); + }); +}); + +describe("createErrorTracker", () => { + it("does not trigger reload below threshold", () => { + const tracker = createErrorTracker({ maxErrors: 3, windowMs: 10_000 }); + tracker.record(); + tracker.record(); + expect(tracker.shouldReload()).toBe(false); + }); + + it("triggers reload when errors exceed threshold within window", () => { + const tracker = createErrorTracker({ maxErrors: 3, windowMs: 10_000 }); + tracker.record(); + tracker.record(); + tracker.record(); + expect(tracker.shouldReload()).toBe(true); + }); + + it("resets after the time window passes", async () => { + const tracker = createErrorTracker({ maxErrors: 2, windowMs: 50 }); + tracker.record(); + tracker.record(); + expect(tracker.shouldReload()).toBe(true); + await new Promise((r) => setTimeout(r, 60)); + expect(tracker.shouldReload()).toBe(false); + }); +}); diff --git a/vitest.config.js b/vitest.config.js index badb8bd..aaa6668 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -12,7 +12,7 @@ export default defineConfig({ // This gate is intentionally the deterministic unit/state layer. Browser // behavior in app.js and cloud.js is verified by Playwright; database RLS // and optimistic concurrency are verified by Supabase pgTAP. - include: ["src/lib.js", "src/learning-state.js"], + include: ["src/lib.js", "src/learning-state.js", "src/version-guard.js"], all: true, thresholds: { statements: 80,