Files
web/server/tests/unit.test.ts
T
xuyueandClaude Opus 5.5 26f05d1d76
Deploy / deploy (build, debian, 22) (push) Canceled after 0s
Deploy / deploy (build:staging, school, 8822) (push) Canceled after 0s
refactor: 后端从 Django 换成 Bun + SQLite
Django 多个 worker 内存占用 1-2 GB,换成 Bun 单进程后约 25 MB,
同时去掉 Postgres 和 Redis,只剩 Caddy + Bun 两个容器。

- server/:Hono + bun:sqlite + Bun 原生 WebSocket,接口与原 django-ninja
  保持兼容;表结构、时间/UUID 存储格式、密码哈希与 Django 一致,旧数据可直接使用
- 评分加权、贝叶斯平滑、成绩册的计算结果与原实现逐位一致(复刻 Python round/sum)
- scripts/:Postgres → SQLite 迁移、新建超管、提示词层级标注、重算分数
- 前端:Django Admin 去掉后,用户新建/修改改为页面内弹窗;
  非超管的挑战公开按钮置灰
- 权限不足返回 JSON 403(原来是重定向到不存在的登录页)
- docker-compose / Caddyfile / dev.sh / CLAUDE.md 更新,删除 AGENTS.md
- 服务器切换步骤见 docs/migrate-to-bun.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 17:45:09 +08:00

70 lines
2.9 KiB
TypeScript

import { describe, expect, test } from "bun:test"
import { pyRound, pySum } from "../src/services/pyround"
import { extractCode, parseGuidanceResponse } from "../src/services/llm"
import { checkPassword, makePassword } from "../src/auth"
import { formatMicros, iso, uuidHex, uuidOut } from "../src/db"
import { toCsv } from "../src/services/gradebook"
describe("pyRound", () => {
test("银行家舍入,与 Python round() 一致", () => {
expect(pyRound(2.5)).toBe(2)
expect(pyRound(3.5)).toBe(4)
expect(pyRound(0.125, 2)).toBe(0.12)
expect(pyRound(0.375, 2)).toBe(0.38)
// 2.675 的二进制值略小于 2.675
expect(pyRound(2.675, 2)).toBe(2.67)
expect(pyRound(1 / 3, 2)).toBe(0.33)
expect(pyRound(95.5)).toBe(96)
})
})
describe("pySum", () => {
test("补偿求和,与 Python 3.12+ 的 sum() 一致", () => {
expect(pySum([0.1, 0.2, 0.3])).toBe(0.6)
expect(pySum([1e100, 1, -1e100])).toBe(1)
expect(pySum([])).toBe(0)
})
})
describe("extractCode", () => {
test("单个 html 代码块拆出 style 和 script", () => {
const text = "说明\n```html\n<style>\na{}\n</style>\n<a>x</a>\n<script>\nf()\n</script>\n```"
expect(extractCode(text)).toEqual({ html: "<a>x</a>", css: "a{}", js: "f()" })
})
test("分开的代码块(旧格式)", () => {
const text = "```html\n<p/>\n```\n```css\np{}\n```\n```javascript\nx()\n```"
expect(extractCode(text)).toEqual({ html: "<p/>", css: "p{}", js: "x()" })
})
test("没有代码块", () => {
expect(extractCode("hello")).toEqual({ html: null, css: null, js: null })
})
})
test("parseGuidanceResponse", () => {
expect(parseGuidanceResponse("[READY]\n\n好")).toEqual(["好", true])
expect(parseGuidanceResponse("再想想")).toEqual(["再想想", false])
})
test("兼容 Django 的 pbkdf2 密码哈希", async () => {
// 由 Django 的 PBKDF2PasswordHasher 生成的 "secret" 的哈希
const django = "pbkdf2_sha256$1000$abcdefghijklmnopqrstuv$1nE4fvOmiD+ZePp39y6TYTXfkn5HEwJDyz8e6UkQE1w="
expect(await checkPassword("secret", django)).toBe(true)
expect(await checkPassword("Secret", django)).toBe(false)
expect(await checkPassword("secret", await makePassword("secret"))).toBe(true)
expect(await checkPassword("wrong", await makePassword("secret"))).toBe(false)
expect(await checkPassword("secret", "bcrypt$xxx")).toBe(false)
})
test("时间与 UUID 格式", () => {
expect(formatMicros(Date.UTC(2026, 0, 2, 3, 4, 5) * 1000 + 678901)).toBe("2026-01-02 03:04:05.678901")
expect(iso("2026-01-02 03:04:05.678901")).toBe("2026-01-02T03:04:05.678Z")
const hex = "0123456789abcdef0123456789abcdef"
expect(uuidOut(hex)).toBe("01234567-89ab-cdef-0123-456789abcdef")
expect(uuidHex("01234567-89AB-cdef-0123-456789abcdef")).toBe(hex)
expect(uuidHex("nope")).toBeNull()
})
test("CSV 转义", () => {
expect(toCsv([["a", 'b"c', "d,e"], [1, 2.5, ""]])).toBe('a,"b""c","d,e"\r\n1,2.5,\r\n')
})