|
|
@@ -0,0 +1,1026 @@
|
|
|
+/** @jsxImportSource @opentui/solid */
|
|
|
+import { TextareaRenderable } from "@opentui/core"
|
|
|
+import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
|
|
|
+import { testRender, useRenderer } from "@opentui/solid"
|
|
|
+import { expect, test } from "bun:test"
|
|
|
+import { mkdir } from "node:fs/promises"
|
|
|
+import path from "node:path"
|
|
|
+import { onCleanup, onMount } from "solid-js"
|
|
|
+import { tmpdir } from "../../fixture/fixture"
|
|
|
+import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
|
|
|
+import type { TuiKeybind } from "../../../src/config/keybind"
|
|
|
+import { TestTuiContexts } from "../../fixture/tui-environment"
|
|
|
+
|
|
|
+async function wait(fn: () => boolean, timeout = 2000) {
|
|
|
+ const start = Date.now()
|
|
|
+ while (!fn()) {
|
|
|
+ if (Date.now() - start > timeout) throw new Error("timed out waiting for condition")
|
|
|
+ await Bun.sleep(10)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function mountReview(input: {
|
|
|
+ root: string
|
|
|
+ reviewID?: string
|
|
|
+ onDraft?: (text: string) => void
|
|
|
+ width?: number
|
|
|
+ height?: number
|
|
|
+ keybinds: Partial<TuiKeybind.Keybinds>
|
|
|
+ connection: { url: string; apiKey: string }
|
|
|
+}) {
|
|
|
+ const state = path.join(input.root, "state")
|
|
|
+ await mkdir(state, { recursive: true })
|
|
|
+ await Bun.write(path.join(state, "kv.json"), "{}")
|
|
|
+
|
|
|
+ const [
|
|
|
+ { DialogProvider, useDialog },
|
|
|
+ { DialogSoftwareReview },
|
|
|
+ { KVProvider },
|
|
|
+ { ThemeProvider },
|
|
|
+ { TuiConfigProvider },
|
|
|
+ { ToastProvider },
|
|
|
+ { OpencodeKeymapProvider, registerOpencodeKeymap },
|
|
|
+ ] = await Promise.all([
|
|
|
+ import("../../../src/ui/dialog"),
|
|
|
+ import("../../../src/component/dialog-software-review"),
|
|
|
+ import("../../../src/context/kv"),
|
|
|
+ import("../../../src/context/theme"),
|
|
|
+ import("../../../src/config"),
|
|
|
+ import("../../../src/ui/toast"),
|
|
|
+ import("../../../src/keymap"),
|
|
|
+ ])
|
|
|
+
|
|
|
+ function OpenReview() {
|
|
|
+ const dialog = useDialog()
|
|
|
+ onMount(() =>
|
|
|
+ dialog.replace(() => (
|
|
|
+ <DialogSoftwareReview
|
|
|
+ reviewID={input.reviewID || "a".repeat(32)}
|
|
|
+ connection={input.connection}
|
|
|
+ onDraft={input.onDraft}
|
|
|
+ />
|
|
|
+ )),
|
|
|
+ )
|
|
|
+ return null
|
|
|
+ }
|
|
|
+
|
|
|
+ function Harness() {
|
|
|
+ const renderer = useRenderer()
|
|
|
+ const keymap = createDefaultOpenTuiKeymap(renderer)
|
|
|
+ const resolvedConfig = createTuiResolvedConfig({
|
|
|
+ keybinds: input.keybinds,
|
|
|
+ leader_timeout: 1000,
|
|
|
+ })
|
|
|
+ const off = registerOpencodeKeymap(keymap, renderer, resolvedConfig)
|
|
|
+ onCleanup(off)
|
|
|
+
|
|
|
+ return (
|
|
|
+ <TestTuiContexts
|
|
|
+ directory={input.root}
|
|
|
+ paths={{
|
|
|
+ home: input.root,
|
|
|
+ state,
|
|
|
+ worktree: input.root,
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <OpencodeKeymapProvider keymap={keymap}>
|
|
|
+ <TuiConfigProvider config={resolvedConfig}>
|
|
|
+ <KVProvider>
|
|
|
+ <ThemeProvider mode="dark">
|
|
|
+ <ToastProvider>
|
|
|
+ <DialogProvider>
|
|
|
+ <OpenReview />
|
|
|
+ </DialogProvider>
|
|
|
+ </ToastProvider>
|
|
|
+ </ThemeProvider>
|
|
|
+ </KVProvider>
|
|
|
+ </TuiConfigProvider>
|
|
|
+ </OpencodeKeymapProvider>
|
|
|
+ </TestTuiContexts>
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ const app = await testRender(() => <Harness />, {
|
|
|
+ kittyKeyboard: true,
|
|
|
+ width: input.width || 120,
|
|
|
+ height: input.height || 48,
|
|
|
+ })
|
|
|
+ return {
|
|
|
+ app,
|
|
|
+ async cleanup() {
|
|
|
+ app.renderer.destroy()
|
|
|
+ },
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const fixture = () => ({
|
|
|
+ id: "a".repeat(32),
|
|
|
+ revision: 2,
|
|
|
+ project: "CLI synthetic review",
|
|
|
+ phase: "awaiting_confirmation",
|
|
|
+ candidates: [
|
|
|
+ {
|
|
|
+ id: "b".repeat(24),
|
|
|
+ path: "Price.java",
|
|
|
+ symbol: "Price#total",
|
|
|
+ suggestion: "Separate assignment",
|
|
|
+ method_range: { start: 1, end: 4 },
|
|
|
+ source_excerpt: [{ line: 1, text: "if ((x = n) > 0)" }],
|
|
|
+ test_hints: [],
|
|
|
+ evidence: [],
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ decisions: { ["b".repeat(24)]: "selected" },
|
|
|
+ coverage: { analyzed_files: 1, java_files: 1 },
|
|
|
+ execution: null,
|
|
|
+ proposal: {
|
|
|
+ version: "c".repeat(64),
|
|
|
+ plan: {
|
|
|
+ goal: "Preserve arithmetic",
|
|
|
+ focus: ["Price.java"],
|
|
|
+ allowed_files: ["Price.java"],
|
|
|
+ invariants: ["Keep outputs"],
|
|
|
+ verification: { kind: "java-main" },
|
|
|
+ },
|
|
|
+ edits: [{ path: "Price.java", old: "if ((x = n) > 0)", new: "x=n; if (x>0)" }],
|
|
|
+ readiness: { status: "execution_required" },
|
|
|
+ },
|
|
|
+})
|
|
|
+
|
|
|
+test("native review shows complete patch and requires an explicit typed confirmation", async () => {
|
|
|
+ await using tmp = await tmpdir()
|
|
|
+ const state = fixture()
|
|
|
+ const calls: { method: string; path: string; body: unknown }[] = []
|
|
|
+ const server = Bun.serve({
|
|
|
+ hostname: "127.0.0.1",
|
|
|
+ port: 0,
|
|
|
+ async fetch(req) {
|
|
|
+ calls.push({
|
|
|
+ method: req.method,
|
|
|
+ path: new URL(req.url).pathname,
|
|
|
+ body: req.method === "POST" ? await req.json() : null,
|
|
|
+ })
|
|
|
+ return Response.json({ ...state, phase: req.method === "POST" ? "running" : state.phase })
|
|
|
+ },
|
|
|
+ })
|
|
|
+ const ui = await mountReview({
|
|
|
+ root: tmp.path,
|
|
|
+ keybinds: { input_submit: "return" },
|
|
|
+ connection: { url: `http://127.0.0.1:${server.port}`, apiKey: "test-key" },
|
|
|
+ })
|
|
|
+ try {
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("等待确认"))
|
|
|
+ // Filter the built-in menu, then enter the actual patch viewer.
|
|
|
+ const filter = ui.app.renderer.currentFocusedEditor
|
|
|
+ if (!filter) throw new Error("expected menu filter")
|
|
|
+ filter.insertText("查看完整")
|
|
|
+ ui.app.mockInput.pressEnter()
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("PgUp PgDn"))
|
|
|
+ ui.app.mockInput.pressKey("END")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("修改前"))
|
|
|
+ expect(ui.app.captureCharFrame()).toContain("if ((x = n) > 0)")
|
|
|
+ expect(calls.filter((c) => c.method === "POST")).toHaveLength(0)
|
|
|
+ ui.app.mockInput.pressKey("c")
|
|
|
+ await wait(() => ui.app.renderer.currentFocusedEditor instanceof TextareaRenderable)
|
|
|
+ ui.app.mockInput.pressEnter()
|
|
|
+ expect(calls.filter((c) => c.method === "POST")).toHaveLength(0)
|
|
|
+ const entry = ui.app.renderer.currentFocusedEditor
|
|
|
+ if (!(entry instanceof TextareaRenderable)) throw new Error("expected confirmation textarea")
|
|
|
+ entry.insertText("执行")
|
|
|
+ ui.app.mockInput.pressEnter()
|
|
|
+ await wait(() => calls.some((c) => c.method === "POST"))
|
|
|
+ expect(calls.filter((c) => c.method === "POST")).toEqual([
|
|
|
+ {
|
|
|
+ method: "POST",
|
|
|
+ path: "/api/v1/software-reviews/" + state.id + "/execute",
|
|
|
+ body: { revision: 2, version: state.proposal.version },
|
|
|
+ },
|
|
|
+ ])
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("执行中"))
|
|
|
+ } finally {
|
|
|
+ await ui.cleanup()
|
|
|
+ server.stop(true)
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+async function selectMenu(ui: Awaited<ReturnType<typeof mountReview>>, text: string) {
|
|
|
+ await wait(() => Boolean(ui.app.renderer.currentFocusedEditor))
|
|
|
+ ui.app.renderer.currentFocusedEditor!.insertText(text)
|
|
|
+ await ui.app.renderOnce()
|
|
|
+ await Bun.sleep(10)
|
|
|
+ ui.app.mockInput.pressEnter()
|
|
|
+}
|
|
|
+
|
|
|
+test("candidate decisions remain local until saved and saving never executes", async () => {
|
|
|
+ await using tmp = await tmpdir()
|
|
|
+ const state = { ...fixture(), phase: "awaiting_selection", proposal: null, decisions: {} }
|
|
|
+ const calls: { path: string; body: unknown }[] = []
|
|
|
+ const server = Bun.serve({
|
|
|
+ hostname: "127.0.0.1",
|
|
|
+ port: 0,
|
|
|
+ async fetch(req) {
|
|
|
+ if (req.method === "POST") {
|
|
|
+ const body = await req.json()
|
|
|
+ calls.push({ path: new URL(req.url).pathname, body })
|
|
|
+ return Response.json({ ...state, revision: 3, phase: "selected", decisions: body.decisions })
|
|
|
+ }
|
|
|
+ return Response.json(state)
|
|
|
+ },
|
|
|
+ })
|
|
|
+ const ui = await mountReview({
|
|
|
+ root: tmp.path,
|
|
|
+ keybinds: { input_submit: "return" },
|
|
|
+ connection: { url: `http://127.0.0.1:${server.port}`, apiKey: "test-key" },
|
|
|
+ })
|
|
|
+ try {
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("等待选择"))
|
|
|
+ await selectMenu(ui, "候选清单")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("候选选择 ·"))
|
|
|
+ await selectMenu(ui, "Price#total")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("s 选中"))
|
|
|
+ expect(ui.app.captureCharFrame()).toContain("if ((x = n) > 0)")
|
|
|
+ ui.app.mockInput.pressKey("s")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("[✓]"))
|
|
|
+ expect(calls).toHaveLength(0)
|
|
|
+ await selectMenu(ui, "交给智能体规划")
|
|
|
+ await wait(() => calls.length === 1)
|
|
|
+ expect(calls).toEqual([
|
|
|
+ {
|
|
|
+ path: `/api/v1/software-reviews/${state.id}/selection`,
|
|
|
+ body: { revision: 2, decisions: { ["b".repeat(24)]: "selected" }, scope: "methods" },
|
|
|
+ },
|
|
|
+ ])
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("下一步回到对话"))
|
|
|
+ } finally {
|
|
|
+ await ui.cleanup()
|
|
|
+ server.stop(true)
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+test("failed review returns directly to planning and never executes on reopen", async () => {
|
|
|
+ await using tmp = await tmpdir()
|
|
|
+ const state = { ...fixture(), phase: "blocked" }
|
|
|
+ const calls: { path: string; body: unknown }[] = []
|
|
|
+ const server = Bun.serve({
|
|
|
+ hostname: "127.0.0.1",
|
|
|
+ port: 0,
|
|
|
+ async fetch(req) {
|
|
|
+ if (req.method === "POST") {
|
|
|
+ calls.push({ path: new URL(req.url).pathname, body: await req.json() })
|
|
|
+ return Response.json({ ...state, revision: 3, phase: "selected", proposal: null })
|
|
|
+ }
|
|
|
+ return Response.json(state)
|
|
|
+ },
|
|
|
+ })
|
|
|
+ const ui = await mountReview({
|
|
|
+ root: tmp.path,
|
|
|
+ keybinds: { input_submit: "return" },
|
|
|
+ connection: { url: `http://127.0.0.1:${server.port}`, apiKey: "test-key" },
|
|
|
+ })
|
|
|
+ try {
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("执行阻塞"))
|
|
|
+ await selectMenu(ui, "原因已处理")
|
|
|
+ await wait(() => calls.length === 1)
|
|
|
+ expect(calls).toEqual([{ path: `/api/v1/software-reviews/${state.id}/reopen`, body: { revision: 2 } }])
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("下一步回到对话"))
|
|
|
+ } finally {
|
|
|
+ await ui.cleanup()
|
|
|
+ server.stop(true)
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+// Opt-in read-only smoke against a deployed review. A proxy rejects every write.
|
|
|
+test.skipIf(!process.env.SOFTWARE_REVIEW_SMOKE_CONNECTION)(
|
|
|
+ "deployed review renders candidates and evidence in native terminal without writes",
|
|
|
+ async () => {
|
|
|
+ await using tmp = await tmpdir()
|
|
|
+ const privateConfig = await Bun.file(process.env.SOFTWARE_REVIEW_SMOKE_CONNECTION!).json()
|
|
|
+ const url = process.env.SOFTWARE_REVIEW_SMOKE_URL!
|
|
|
+ const id = process.env.SOFTWARE_REVIEW_SMOKE_ID!
|
|
|
+ if (!/^[a-f0-9]{32}$/.test(id) || new URL(url).hostname !== new URL(privateConfig.url).hostname)
|
|
|
+ throw new Error("Invalid smoke target")
|
|
|
+ const route = `/api/v1/software-reviews/${id}`
|
|
|
+ const read = async (suffix = "") => {
|
|
|
+ const response = await fetch(url + route + suffix, {
|
|
|
+ headers: { Authorization: `Bearer ${privateConfig.api_key}` },
|
|
|
+ redirect: "error",
|
|
|
+ })
|
|
|
+ if (!response.ok) throw new Error(`Read failed: ${response.status}`)
|
|
|
+ return response.json()
|
|
|
+ }
|
|
|
+ const before = await read()
|
|
|
+ const writes: string[] = []
|
|
|
+ const proxy = Bun.serve({
|
|
|
+ hostname: "127.0.0.1",
|
|
|
+ port: 0,
|
|
|
+ async fetch(req) {
|
|
|
+ if (
|
|
|
+ req.method !== "GET" ||
|
|
|
+ ![route, route + "/evidence", route + "/artifact"].includes(new URL(req.url).pathname)
|
|
|
+ ) {
|
|
|
+ writes.push(req.method)
|
|
|
+ return new Response("Read-only smoke", { status: 403 })
|
|
|
+ }
|
|
|
+ return fetch(url + new URL(req.url).pathname + new URL(req.url).search, {
|
|
|
+ headers: { Authorization: `Bearer ${privateConfig.api_key}` },
|
|
|
+ redirect: "error",
|
|
|
+ })
|
|
|
+ },
|
|
|
+ })
|
|
|
+ const ui = await mountReview({
|
|
|
+ root: tmp.path,
|
|
|
+ reviewID: id,
|
|
|
+ keybinds: { input_submit: "return" },
|
|
|
+ connection: { url: `http://127.0.0.1:${proxy.port}`, apiKey: "read-only-proxy" },
|
|
|
+ })
|
|
|
+ try {
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes(before.project))
|
|
|
+ expect(ui.app.captureCharFrame()).toContain(String(before.coverage.analyzed_files))
|
|
|
+ await selectMenu(ui, "材料与记录")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("按需查看"))
|
|
|
+ await selectMenu(ui, "候选清单")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("候选选择 ·"))
|
|
|
+ await selectMenu(ui, before.candidates[0].symbol)
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("候选记录(只读)"))
|
|
|
+ expect(ui.app.captureCharFrame()).toContain(before.candidates[0].symbol)
|
|
|
+ ui.app.mockInput.pressKey("b")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("候选选择 ·"))
|
|
|
+ await selectMenu(ui, "返回菜单")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("仓库重构 / CLI"))
|
|
|
+ await selectMenu(ui, "查看四类结果")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("查看四类结果 · 只读"))
|
|
|
+ await selectMenu(ui, "3 · 测试结果")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("重构前基线"), 10000)
|
|
|
+ expect(ui.app.captureCharFrame()).toContain("测试结果")
|
|
|
+ ui.app.mockInput.pressKey("2")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("实际隔离副本变化"))
|
|
|
+ ui.app.mockInput.pressKey("4")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("本次修改后仍需关注"))
|
|
|
+ ui.app.mockInput.pressKey("1")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("分析覆盖"))
|
|
|
+ ui.app.mockInput.pressKey("j")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes('"ontology"'))
|
|
|
+ ui.app.mockInput.pressKey("j")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("分析覆盖"))
|
|
|
+ if (before.phase === "awaiting_review") {
|
|
|
+ ui.app.mockInput.pressKey("b")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("仓库重构 / CLI"))
|
|
|
+ await selectMenu(ui, "质量审查")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("a 填写通过意见"), 10000)
|
|
|
+ }
|
|
|
+ const after = await read()
|
|
|
+ expect(after.revision).toBe(before.revision)
|
|
|
+ expect(after.decisions).toEqual(before.decisions)
|
|
|
+ expect(writes).toEqual([])
|
|
|
+ if (process.env.SOFTWARE_REVIEW_SMOKE_REPORT)
|
|
|
+ await Bun.write(
|
|
|
+ process.env.SOFTWARE_REVIEW_SMOKE_REPORT,
|
|
|
+ JSON.stringify(
|
|
|
+ {
|
|
|
+ id,
|
|
|
+ revision: after.revision,
|
|
|
+ phase: after.phase,
|
|
|
+ candidates: after.candidates.length,
|
|
|
+ analyzed_files: after.coverage.analyzed_files,
|
|
|
+ native_candidate_detail: true,
|
|
|
+ native_execution_record: true,
|
|
|
+ four_evidence_views: true,
|
|
|
+ original_record_toggle: true,
|
|
|
+ writes: 0,
|
|
|
+ source_task_executed: false,
|
|
|
+ },
|
|
|
+ null,
|
|
|
+ 2,
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ } catch (error) {
|
|
|
+ console.error(ui.app.captureCharFrame())
|
|
|
+ throw error
|
|
|
+ } finally {
|
|
|
+ await ui.cleanup()
|
|
|
+ proxy.stop(true)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ 15000,
|
|
|
+)
|
|
|
+
|
|
|
+test("ontology view exposes architectural evidence without executing advisory findings", async () => {
|
|
|
+ await using tmp = await tmpdir()
|
|
|
+ const state = {
|
|
|
+ ...fixture(),
|
|
|
+ ontology: {
|
|
|
+ rules_status: "not_supplied",
|
|
|
+ statistics: { cross_shard_references: 4 },
|
|
|
+ architecture_findings: [{ kind: "dependency_cycle", paths: ["Pricing.java", "Catalog.java"], executable: false }],
|
|
|
+ },
|
|
|
+ }
|
|
|
+ const calls: string[] = []
|
|
|
+ const server = Bun.serve({
|
|
|
+ hostname: "127.0.0.1",
|
|
|
+ port: 0,
|
|
|
+ fetch(req) {
|
|
|
+ calls.push(req.method)
|
|
|
+ return Response.json(state)
|
|
|
+ },
|
|
|
+ })
|
|
|
+ const ui = await mountReview({
|
|
|
+ root: tmp.path,
|
|
|
+ keybinds: { input_submit: "return" },
|
|
|
+ connection: { url: `http://127.0.0.1:${server.port}`, apiKey: "test" },
|
|
|
+ })
|
|
|
+ try {
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("等待确认"))
|
|
|
+ await selectMenu(ui, "查看四类结果")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("查看四类结果 · 只读"))
|
|
|
+ await selectMenu(ui, "1 · 影响路径")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("分析覆盖"))
|
|
|
+ ui.app.mockInput.pressKey("END")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("Pricing.java"))
|
|
|
+ ui.app.mockInput.pressKey("c")
|
|
|
+ expect(ui.app.renderer.currentFocusedEditor instanceof TextareaRenderable).toBe(false)
|
|
|
+ expect(calls).toEqual(["GET"])
|
|
|
+ } finally {
|
|
|
+ await ui.cleanup()
|
|
|
+ server.stop(true)
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+test("quality review is an explicit operator decision bound to inspected evidence", async () => {
|
|
|
+ await using tmp = await tmpdir()
|
|
|
+ const state = { ...fixture(), phase: "awaiting_review", execution: { result: { verification_passed: true } } }
|
|
|
+ const posts: unknown[] = []
|
|
|
+ const server = Bun.serve({
|
|
|
+ hostname: "127.0.0.1",
|
|
|
+ port: 0,
|
|
|
+ async fetch(req) {
|
|
|
+ if (new URL(req.url).pathname.endsWith("/evidence"))
|
|
|
+ return Response.json({ id: state.id, revision: state.revision, token: "d".repeat(64), files: [] })
|
|
|
+ if (req.method === "POST") {
|
|
|
+ posts.push(await req.json())
|
|
|
+ return Response.json({ ...state, revision: 3, phase: "accepted_for_delivery" })
|
|
|
+ }
|
|
|
+ return Response.json(state)
|
|
|
+ },
|
|
|
+ })
|
|
|
+ const ui = await mountReview({
|
|
|
+ root: tmp.path,
|
|
|
+ width: 80,
|
|
|
+ height: 32,
|
|
|
+ keybinds: { input_submit: "return" },
|
|
|
+ connection: { url: `http://127.0.0.1:${server.port}`, apiKey: "test-key" },
|
|
|
+ })
|
|
|
+ try {
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("待质量审查"))
|
|
|
+ await selectMenu(ui, "质量审查")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("a 填写通过意见"))
|
|
|
+ ui.app.mockInput.pressKey("a")
|
|
|
+ await wait(() => ui.app.renderer.currentFocusedEditor instanceof TextareaRenderable)
|
|
|
+ ui.app.mockInput.pressEnter()
|
|
|
+ expect(posts).toHaveLength(0)
|
|
|
+ for (const [index, value] of ["拆分赋值与判断", "Check 覆盖正负值", "仍需集成检查"].entries()) {
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes(`${index + 1}/3`))
|
|
|
+ await wait(() => ui.app.renderer.currentFocusedEditor instanceof TextareaRenderable)
|
|
|
+ ui.app.renderer.currentFocusedEditor!.insertText(value)
|
|
|
+ ui.app.mockInput.pressEnter()
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes(index === 2 ? "核对意见,确认提交" : `${index + 2}/3`))
|
|
|
+ expect(posts).toHaveLength(0)
|
|
|
+ }
|
|
|
+ ui.app.mockInput.pressKey("b")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("a 填写通过意见"))
|
|
|
+ ui.app.mockInput.pressKey("a")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("核对意见,确认提交"))
|
|
|
+ // Edit only the risk field; the other inspected opinions must remain intact.
|
|
|
+ ui.app.mockInput.pressKey("3")
|
|
|
+ await wait(() => ui.app.renderer.currentFocusedEditor instanceof TextareaRenderable)
|
|
|
+ expect(ui.app.renderer.currentFocusedEditor!.plainText).toBe("仍需集成检查")
|
|
|
+ ui.app.renderer.currentFocusedEditor!.insertText(",未覆盖设备")
|
|
|
+ ui.app.mockInput.pressEnter()
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("核对意见,确认提交"))
|
|
|
+ expect(posts).toHaveLength(0)
|
|
|
+ ui.app.mockInput.pressKey("c")
|
|
|
+ await wait(() => ui.app.renderer.currentFocusedEditor instanceof TextareaRenderable)
|
|
|
+ ui.app.mockInput.pressEnter()
|
|
|
+ expect(posts).toHaveLength(0)
|
|
|
+ ui.app.renderer.currentFocusedEditor!.insertText("返回")
|
|
|
+ ui.app.mockInput.pressEnter()
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("核对意见,确认提交"))
|
|
|
+ ui.app.mockInput.pressKey("c")
|
|
|
+ await wait(() => ui.app.renderer.currentFocusedEditor instanceof TextareaRenderable)
|
|
|
+ ui.app.renderer.currentFocusedEditor!.insertText("通过")
|
|
|
+ ui.app.mockInput.pressEnter()
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("人工审查通过,待交付"))
|
|
|
+ expect(posts).toEqual([
|
|
|
+ {
|
|
|
+ revision: 2,
|
|
|
+ version: state.proposal.version,
|
|
|
+ token: "d".repeat(64),
|
|
|
+ decision: "accept",
|
|
|
+ notes: { benefits: "拆分赋值与判断", invariants: "Check 覆盖正负值", risks: "仍需集成检查,未覆盖设备" },
|
|
|
+ },
|
|
|
+ ])
|
|
|
+ } finally {
|
|
|
+ await ui.cleanup()
|
|
|
+ server.stop(true)
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+for (const [phase, title] of [
|
|
|
+ ["running", "刷新原任务进度"],
|
|
|
+ ["selected", "让智能体生成本批计划"],
|
|
|
+ ["accepted_for_delivery", "领取本批交付材料"],
|
|
|
+]) {
|
|
|
+ test(`stage navigation keeps ${phase} focused without issuing writes`, async () => {
|
|
|
+ await using tmp = await tmpdir()
|
|
|
+ const state = { ...fixture(), phase }
|
|
|
+ const calls: string[] = []
|
|
|
+ const server = Bun.serve({
|
|
|
+ hostname: "127.0.0.1",
|
|
|
+ port: 0,
|
|
|
+ fetch(req) {
|
|
|
+ calls.push(req.method)
|
|
|
+ return Response.json(state)
|
|
|
+ },
|
|
|
+ })
|
|
|
+ const ui = await mountReview({
|
|
|
+ root: tmp.path,
|
|
|
+ keybinds: { input_submit: "return" },
|
|
|
+ connection: { url: `http://127.0.0.1:${server.port}`, apiKey: "test" },
|
|
|
+ })
|
|
|
+ try {
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes(title))
|
|
|
+ const home = ui.app.captureCharFrame()
|
|
|
+ expect(home).not.toContain("从审定副本开始下一批")
|
|
|
+ expect(home).not.toContain("操作与审查历史")
|
|
|
+ expect(home).not.toContain("c 进入确认")
|
|
|
+ if (phase === "selected") {
|
|
|
+ await selectMenu(ui, title)
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("按 Esc 关闭面板"))
|
|
|
+ expect(ui.app.captureCharFrame()).toContain(state.id)
|
|
|
+ expect(ui.app.captureCharFrame()).toContain("只生成计划,不执行")
|
|
|
+ }
|
|
|
+ if (phase === "accepted_for_delivery") {
|
|
|
+ expect(home).toContain("本批审查已完成")
|
|
|
+ await selectMenu(ui, "其他操作")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("从审定副本开始下一批"))
|
|
|
+ expect(ui.app.captureCharFrame()).toContain("无需逐项完成")
|
|
|
+ expect(ui.app.captureCharFrame()).not.toContain("质量审查")
|
|
|
+ }
|
|
|
+ if (phase === "running") {
|
|
|
+ await selectMenu(ui, title)
|
|
|
+ await wait(() => calls.length === 2)
|
|
|
+ }
|
|
|
+ expect(calls.every((method) => method === "GET")).toBe(true)
|
|
|
+ } finally {
|
|
|
+ await ui.cleanup()
|
|
|
+ server.stop(true)
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+test("cancelling a partially filled quality opinion does not approve the task", async () => {
|
|
|
+ await using tmp = await tmpdir()
|
|
|
+ const state = { ...fixture(), phase: "awaiting_review", execution: {} }
|
|
|
+ const writes: string[] = []
|
|
|
+ const server = Bun.serve({
|
|
|
+ hostname: "127.0.0.1",
|
|
|
+ port: 0,
|
|
|
+ fetch(req) {
|
|
|
+ if (req.method !== "GET") writes.push(req.method)
|
|
|
+ if (new URL(req.url).pathname.endsWith("/evidence"))
|
|
|
+ return Response.json({ id: state.id, revision: state.revision, token: "d".repeat(64), files: [] })
|
|
|
+ return Response.json(state)
|
|
|
+ },
|
|
|
+ })
|
|
|
+ const ui = await mountReview({
|
|
|
+ root: tmp.path,
|
|
|
+ keybinds: { input_submit: "return" },
|
|
|
+ connection: { url: `http://127.0.0.1:${server.port}`, apiKey: "test" },
|
|
|
+ })
|
|
|
+ try {
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("待质量审查"))
|
|
|
+ await selectMenu(ui, "质量审查")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("a 填写通过意见"))
|
|
|
+ ui.app.mockInput.pressKey("a")
|
|
|
+ await wait(() => ui.app.renderer.currentFocusedEditor instanceof TextareaRenderable)
|
|
|
+ ui.app.renderer.currentFocusedEditor!.insertText("方法职责更清晰")
|
|
|
+ ui.app.mockInput.pressEnter()
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("2/3"))
|
|
|
+ ui.app.mockInput.pressKey("ESCAPE")
|
|
|
+ await ui.app.renderOnce()
|
|
|
+ expect(writes).toEqual([])
|
|
|
+ } finally {
|
|
|
+ await ui.cleanup()
|
|
|
+ server.stop(true)
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+test("planning request returns to a draft without sending or changing a review", async () => {
|
|
|
+ await using tmp = await tmpdir()
|
|
|
+ const state = { ...fixture(), phase: "selected" }
|
|
|
+ const requests: string[] = []
|
|
|
+ const drafts: string[] = []
|
|
|
+ const server = Bun.serve({
|
|
|
+ hostname: "127.0.0.1",
|
|
|
+ port: 0,
|
|
|
+ fetch(req) {
|
|
|
+ requests.push(req.method)
|
|
|
+ return Response.json(state)
|
|
|
+ },
|
|
|
+ })
|
|
|
+ const ui = await mountReview({
|
|
|
+ root: tmp.path,
|
|
|
+ width: 80,
|
|
|
+ height: 32,
|
|
|
+ keybinds: { input_submit: "return" },
|
|
|
+ connection: { url: `http://127.0.0.1:${server.port}`, apiKey: "test" },
|
|
|
+ onDraft: (text) => {
|
|
|
+ drafts.push(text)
|
|
|
+ },
|
|
|
+ })
|
|
|
+ try {
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("让智能体生成本批计划"))
|
|
|
+ expect(drafts).toHaveLength(0)
|
|
|
+ await selectMenu(ui, "让智能体生成本批计划")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("p 带回输入框"))
|
|
|
+ ui.app.mockInput.pressKey("p")
|
|
|
+ await wait(() => drafts.length === 1)
|
|
|
+ expect(drafts[0]).toContain(state.id)
|
|
|
+ expect(drafts[0]).toContain("只生成计划,不执行")
|
|
|
+ expect(drafts[0]).toContain("下游依赖契约")
|
|
|
+ expect(drafts[0]).toContain("impact、dependencies、unresolved")
|
|
|
+ expect(requests).toEqual(["GET"])
|
|
|
+ } finally {
|
|
|
+ await ui.cleanup()
|
|
|
+ server.stop(true)
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+test("long rejection reasons remain scrollable and require separate confirmation in a small terminal", async () => {
|
|
|
+ await using tmp = await tmpdir()
|
|
|
+ const state = { ...fixture(), phase: "awaiting_review", execution: {} }
|
|
|
+ const reason = "请补充测试依据与失败路径。".repeat(80) + "最终需要核对回滚行为"
|
|
|
+ const posts: unknown[] = []
|
|
|
+ const server = Bun.serve({
|
|
|
+ hostname: "127.0.0.1",
|
|
|
+ port: 0,
|
|
|
+ async fetch(req) {
|
|
|
+ if (new URL(req.url).pathname.endsWith("/evidence"))
|
|
|
+ return Response.json({ id: state.id, revision: state.revision, token: "d".repeat(64), files: [] })
|
|
|
+ if (req.method === "POST") {
|
|
|
+ posts.push(await req.json())
|
|
|
+ return Response.json({ ...state, phase: "changes_requested", revision: 3 })
|
|
|
+ }
|
|
|
+ return Response.json(state)
|
|
|
+ },
|
|
|
+ })
|
|
|
+ const ui = await mountReview({
|
|
|
+ root: tmp.path,
|
|
|
+ width: 80,
|
|
|
+ height: 32,
|
|
|
+ keybinds: { input_submit: "return" },
|
|
|
+ connection: { url: `http://127.0.0.1:${server.port}`, apiKey: "test" },
|
|
|
+ })
|
|
|
+ try {
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("待质量审查"))
|
|
|
+ await selectMenu(ui, "质量审查")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("a 填写通过意见"))
|
|
|
+ ui.app.mockInput.pressKey("r")
|
|
|
+ await wait(() => ui.app.renderer.currentFocusedEditor instanceof TextareaRenderable)
|
|
|
+ ui.app.renderer.currentFocusedEditor!.insertText(reason)
|
|
|
+ ui.app.mockInput.pressEnter()
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("核对意见,确认提交"))
|
|
|
+ expect(posts).toHaveLength(0)
|
|
|
+ ui.app.mockInput.pressKey("END")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("最终需要核对回滚行为"))
|
|
|
+ ui.app.mockInput.pressKey("c")
|
|
|
+ await wait(() => ui.app.renderer.currentFocusedEditor instanceof TextareaRenderable)
|
|
|
+ ui.app.mockInput.pressEnter()
|
|
|
+ expect(posts).toHaveLength(0)
|
|
|
+ ui.app.renderer.currentFocusedEditor!.insertText("退回")
|
|
|
+ ui.app.mockInput.pressEnter()
|
|
|
+ await wait(() => posts.length === 1)
|
|
|
+ expect(posts).toEqual([
|
|
|
+ { revision: 2, version: state.proposal.version, token: "d".repeat(64), decision: "revise", notes: { reason } },
|
|
|
+ ])
|
|
|
+ } finally {
|
|
|
+ await ui.cleanup()
|
|
|
+ server.stop(true)
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+test("an initial read failure leaves a working reconnect action", async () => {
|
|
|
+ await using tmp = await tmpdir()
|
|
|
+ const state = fixture()
|
|
|
+ const calls: string[] = []
|
|
|
+ const server = Bun.serve({
|
|
|
+ hostname: "127.0.0.1",
|
|
|
+ port: 0,
|
|
|
+ fetch(req) {
|
|
|
+ calls.push(req.method)
|
|
|
+ if (calls.length === 1) return new Response("temporarily unavailable", { status: 503 })
|
|
|
+ return Response.json(state)
|
|
|
+ },
|
|
|
+ })
|
|
|
+ const ui = await mountReview({
|
|
|
+ root: tmp.path,
|
|
|
+ keybinds: { input_submit: "return" },
|
|
|
+ connection: { url: `http://127.0.0.1:${server.port}`, apiKey: "test" },
|
|
|
+ })
|
|
|
+ try {
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("上次请求结果尚未核对"))
|
|
|
+ await selectMenu(ui, "同步原任务状态")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("查看完整计划与补丁"))
|
|
|
+ expect(calls).toEqual(["GET", "GET"])
|
|
|
+ } finally {
|
|
|
+ await ui.cleanup()
|
|
|
+ server.stop(true)
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+test("an uncertain execution response requires reading the original task before any further action", async () => {
|
|
|
+ await using tmp = await tmpdir()
|
|
|
+ const state = fixture()
|
|
|
+ const calls: string[] = []
|
|
|
+ const server = Bun.serve({
|
|
|
+ hostname: "127.0.0.1",
|
|
|
+ port: 0,
|
|
|
+ fetch(req) {
|
|
|
+ calls.push(req.method)
|
|
|
+ if (req.method === "POST") {
|
|
|
+ state.phase = "running"
|
|
|
+ state.revision = 3
|
|
|
+ return new Response("response unavailable", { status: 503 })
|
|
|
+ }
|
|
|
+ return Response.json(state)
|
|
|
+ },
|
|
|
+ })
|
|
|
+ const ui = await mountReview({
|
|
|
+ root: tmp.path,
|
|
|
+ keybinds: { input_submit: "return" },
|
|
|
+ connection: { url: `http://127.0.0.1:${server.port}`, apiKey: "test" },
|
|
|
+ })
|
|
|
+ try {
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("等待确认"))
|
|
|
+ await selectMenu(ui, "查看完整计划")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("c 进入确认"))
|
|
|
+ ui.app.mockInput.pressKey("c")
|
|
|
+ await wait(() => ui.app.renderer.currentFocusedEditor instanceof TextareaRenderable)
|
|
|
+ ui.app.renderer.currentFocusedEditor!.insertText("执行")
|
|
|
+ ui.app.mockInput.pressEnter()
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("上次请求结果尚未核对"))
|
|
|
+ expect(ui.app.captureCharFrame()).not.toContain("查看完整计划与补丁")
|
|
|
+ expect(calls).toEqual(["GET", "POST"])
|
|
|
+ await selectMenu(ui, "同步原任务状态")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("刷新原任务进度"))
|
|
|
+ expect(calls).toEqual(["GET", "POST", "GET"])
|
|
|
+ expect(ui.app.captureCharFrame()).toContain("执行中")
|
|
|
+ } finally {
|
|
|
+ await ui.cleanup()
|
|
|
+ server.stop(true)
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+test("synchronization after a failed save cannot silently discard local selections", async () => {
|
|
|
+ await using tmp = await tmpdir()
|
|
|
+ const state = { ...fixture(), phase: "awaiting_selection", proposal: null, decisions: {} }
|
|
|
+ const calls: string[] = []
|
|
|
+ const server = Bun.serve({
|
|
|
+ hostname: "127.0.0.1",
|
|
|
+ port: 0,
|
|
|
+ fetch(req) {
|
|
|
+ calls.push(req.method)
|
|
|
+ if (req.method === "POST") return new Response("save failed", { status: 503 })
|
|
|
+ return Response.json(state)
|
|
|
+ },
|
|
|
+ })
|
|
|
+ const ui = await mountReview({
|
|
|
+ root: tmp.path,
|
|
|
+ keybinds: { input_submit: "return" },
|
|
|
+ connection: { url: `http://127.0.0.1:${server.port}`, apiKey: "test" },
|
|
|
+ })
|
|
|
+ try {
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("等待选择"))
|
|
|
+ await selectMenu(ui, "候选清单")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("候选选择 ·"))
|
|
|
+ await selectMenu(ui, "Price#total")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("s 选中"))
|
|
|
+ ui.app.mockInput.pressKey("s")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("[✓]"))
|
|
|
+ await selectMenu(ui, "交给智能体规划")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("上次请求结果尚未核对"))
|
|
|
+ await selectMenu(ui, "同步原任务状态")
|
|
|
+ await wait(() => ui.app.renderer.currentFocusedEditor instanceof TextareaRenderable)
|
|
|
+ ui.app.mockInput.pressEnter()
|
|
|
+ expect(calls).toEqual(["GET", "POST"])
|
|
|
+ ui.app.renderer.currentFocusedEditor!.insertText("返回")
|
|
|
+ ui.app.mockInput.pressEnter()
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("同步原任务状态"))
|
|
|
+ expect(calls).toEqual(["GET", "POST"])
|
|
|
+ await selectMenu(ui, "同步原任务状态")
|
|
|
+ await wait(() => ui.app.renderer.currentFocusedEditor instanceof TextareaRenderable)
|
|
|
+ ui.app.renderer.currentFocusedEditor!.insertText("同步")
|
|
|
+ ui.app.mockInput.pressEnter()
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("查看候选清单并选择"))
|
|
|
+ expect(calls).toEqual(["GET", "POST", "GET"])
|
|
|
+ } finally {
|
|
|
+ await ui.cleanup()
|
|
|
+ server.stop(true)
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+test("closing a panel retains selection intent only for the same connection and revision", async () => {
|
|
|
+ await using tmp = await tmpdir()
|
|
|
+ const state = { ...fixture(), phase: "awaiting_selection", proposal: null, decisions: {} }
|
|
|
+ const calls: string[] = []
|
|
|
+ const server = Bun.serve({
|
|
|
+ hostname: "127.0.0.1",
|
|
|
+ port: 0,
|
|
|
+ fetch(req) {
|
|
|
+ calls.push(req.method)
|
|
|
+ return Response.json(state)
|
|
|
+ },
|
|
|
+ })
|
|
|
+ const input = {
|
|
|
+ root: tmp.path,
|
|
|
+ keybinds: { input_submit: "return" },
|
|
|
+ connection: { url: `http://127.0.0.1:${server.port}`, apiKey: "test" },
|
|
|
+ }
|
|
|
+ const first = await mountReview(input)
|
|
|
+ try {
|
|
|
+ await wait(() => first.app.captureCharFrame().includes("等待选择"))
|
|
|
+ await selectMenu(first, "候选清单")
|
|
|
+ await wait(() => first.app.captureCharFrame().includes("候选选择 ·"))
|
|
|
+ await selectMenu(first, "Price#total")
|
|
|
+ await wait(() => first.app.captureCharFrame().includes("s 选中"))
|
|
|
+ first.app.mockInput.pressKey("s")
|
|
|
+ await wait(() => first.app.captureCharFrame().includes("[✓]"))
|
|
|
+ first.app.mockInput.pressKey("ESCAPE")
|
|
|
+ await first.app.renderOnce()
|
|
|
+ } finally {
|
|
|
+ await first.cleanup()
|
|
|
+ }
|
|
|
+ const reopened = await mountReview(input)
|
|
|
+ try {
|
|
|
+ await wait(() => reopened.app.captureCharFrame().includes("已恢复本客户端未保存的选择"))
|
|
|
+ await selectMenu(reopened, "候选清单")
|
|
|
+ await wait(() => reopened.app.captureCharFrame().includes("[✓]"))
|
|
|
+ expect(reopened.app.captureCharFrame()).toContain("本地未保存草稿")
|
|
|
+ expect(calls.every((method) => method === "GET")).toBe(true)
|
|
|
+ } finally {
|
|
|
+ await reopened.cleanup()
|
|
|
+ }
|
|
|
+ state.revision = 3
|
|
|
+ const changed = await mountReview(input)
|
|
|
+ try {
|
|
|
+ await wait(() => changed.app.captureCharFrame().includes("未应用旧选择"))
|
|
|
+ await selectMenu(changed, "候选清单")
|
|
|
+ await wait(() => changed.app.captureCharFrame().includes("候选选择 ·"))
|
|
|
+ expect(changed.app.captureCharFrame()).not.toContain("[✓]")
|
|
|
+ expect(calls.every((method) => method === "GET")).toBe(true)
|
|
|
+ } finally {
|
|
|
+ await changed.cleanup()
|
|
|
+ server.stop(true)
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+test("reselecting the same candidate or toggling scope back does not revoke an existing proposal", async () => {
|
|
|
+ await using tmp = await tmpdir()
|
|
|
+ const state = fixture()
|
|
|
+ const calls: string[] = []
|
|
|
+ const server = Bun.serve({
|
|
|
+ hostname: "127.0.0.1",
|
|
|
+ port: 0,
|
|
|
+ fetch(req) {
|
|
|
+ calls.push(req.method)
|
|
|
+ return Response.json(state)
|
|
|
+ },
|
|
|
+ })
|
|
|
+ const ui = await mountReview({
|
|
|
+ root: tmp.path,
|
|
|
+ keybinds: { input_submit: "return" },
|
|
|
+ connection: { url: `http://127.0.0.1:${server.port}`, apiKey: "test" },
|
|
|
+ })
|
|
|
+ try {
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("等待确认"))
|
|
|
+ await selectMenu(ui, "材料与记录")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("按需查看"))
|
|
|
+ await selectMenu(ui, "候选清单")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("候选选择 ·"))
|
|
|
+ await selectMenu(ui, "Price#total")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("s 选中"))
|
|
|
+ ui.app.mockInput.pressKey("s")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("[✓]"))
|
|
|
+ expect(ui.app.captureCharFrame()).not.toContain("保存选择,交给智能体规划")
|
|
|
+ await selectMenu(ui, "范围:方法内修改")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("保存选择,交给智能体规划"))
|
|
|
+ await selectMenu(ui, "范围:结构重构")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("候选选择 · 沿用已保存的选择"))
|
|
|
+ expect(ui.app.captureCharFrame()).not.toContain("保存选择,交给智能体规划")
|
|
|
+ await selectMenu(ui, "返回菜单")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("查看完整计划与补丁"))
|
|
|
+ expect(calls).toEqual(["GET"])
|
|
|
+ } finally {
|
|
|
+ await ui.cleanup()
|
|
|
+ server.stop(true)
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+test("overlong quality opinions stay editable without a failed server submission", async () => {
|
|
|
+ await using tmp = await tmpdir()
|
|
|
+ const state = { ...fixture(), phase: "awaiting_review", execution: {} }
|
|
|
+ const writes: string[] = []
|
|
|
+ const server = Bun.serve({
|
|
|
+ hostname: "127.0.0.1",
|
|
|
+ port: 0,
|
|
|
+ fetch(req) {
|
|
|
+ if (req.method !== "GET") writes.push(req.method)
|
|
|
+ if (new URL(req.url).pathname.endsWith("/evidence"))
|
|
|
+ return Response.json({ id: state.id, revision: state.revision, token: "d".repeat(64), files: [] })
|
|
|
+ return Response.json(state)
|
|
|
+ },
|
|
|
+ })
|
|
|
+ const ui = await mountReview({
|
|
|
+ root: tmp.path,
|
|
|
+ keybinds: { input_submit: "return" },
|
|
|
+ connection: { url: `http://127.0.0.1:${server.port}`, apiKey: "test" },
|
|
|
+ })
|
|
|
+ try {
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("待质量审查"))
|
|
|
+ await selectMenu(ui, "质量审查")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("a 填写通过意见"))
|
|
|
+ ui.app.mockInput.pressKey("r")
|
|
|
+ await wait(() => ui.app.renderer.currentFocusedEditor instanceof TextareaRenderable)
|
|
|
+ const opinion = "需".repeat(8001)
|
|
|
+ ui.app.renderer.currentFocusedEditor!.insertText(opinion)
|
|
|
+ ui.app.mockInput.pressEnter()
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("最多 8000"))
|
|
|
+ expect(ui.app.renderer.currentFocusedEditor!.plainText).toBe(opinion)
|
|
|
+ expect(writes).toEqual([])
|
|
|
+ } finally {
|
|
|
+ await ui.cleanup()
|
|
|
+ server.stop(true)
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+test("evidence publication recovery does not submit another execution", async () => {
|
|
|
+ await using tmp = await tmpdir()
|
|
|
+ const state = { ...fixture(), phase: "evidence_failed" }
|
|
|
+ const writes: string[] = []
|
|
|
+ const server = Bun.serve({
|
|
|
+ hostname: "127.0.0.1",
|
|
|
+ port: 0,
|
|
|
+ async fetch(req) {
|
|
|
+ if (req.method === "POST") {
|
|
|
+ writes.push(new URL(req.url).pathname)
|
|
|
+ expect(await req.json()).toEqual({ revision: state.revision })
|
|
|
+ return Response.json({ ...state, revision: state.revision + 1, phase: "awaiting_review" })
|
|
|
+ }
|
|
|
+ return Response.json(state)
|
|
|
+ },
|
|
|
+ })
|
|
|
+ const ui = await mountReview({
|
|
|
+ root: tmp.path,
|
|
|
+ keybinds: { input_submit: "return" },
|
|
|
+ connection: { url: `http://127.0.0.1:${server.port}`, apiKey: "test" },
|
|
|
+ })
|
|
|
+ try {
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("重新整理已执行的证据"))
|
|
|
+ expect(ui.app.captureCharFrame()).not.toContain("查看完整计划与补丁")
|
|
|
+ await selectMenu(ui, "重新整理已执行的证据")
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("待质量审查"))
|
|
|
+ expect(writes).toEqual([`/api/v1/software-reviews/${state.id}/recover-evidence`])
|
|
|
+ } finally {
|
|
|
+ await ui.cleanup()
|
|
|
+ server.stop(true)
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+test("running review refreshes into quality review without resubmitting execution", async () => {
|
|
|
+ await using tmp = await tmpdir()
|
|
|
+ let state = { ...fixture(), phase: "running" }
|
|
|
+ const calls: string[] = []
|
|
|
+ const server = Bun.serve({
|
|
|
+ hostname: "127.0.0.1",
|
|
|
+ port: 0,
|
|
|
+ fetch(req) {
|
|
|
+ calls.push(req.method)
|
|
|
+ return Response.json(state)
|
|
|
+ },
|
|
|
+ })
|
|
|
+ const ui = await mountReview({
|
|
|
+ root: tmp.path,
|
|
|
+ keybinds: { input_submit: "return" },
|
|
|
+ connection: { url: `http://127.0.0.1:${server.port}`, apiKey: "test-key" },
|
|
|
+ })
|
|
|
+ try {
|
|
|
+ await wait(() => calls.length > 0)
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("执行中"))
|
|
|
+ state = { ...state, revision: 3, phase: "awaiting_review" }
|
|
|
+ await wait(() => ui.app.captureCharFrame().includes("待质量审查"), 5000)
|
|
|
+ expect(calls.length).toBeGreaterThan(1)
|
|
|
+ expect(calls.every((method) => method === "GET")).toBe(true)
|
|
|
+ } finally {
|
|
|
+ await ui.cleanup()
|
|
|
+ server.stop(true)
|
|
|
+ }
|
|
|
+})
|