plan: copy-close on case page
Bite-sized implementation plan for spec 0002 (case-page half of #14): add a gated "Kopieren + Schließen" control below the document (Task 1, with a shared copyDocText() refactor), a bare "Schließen" for documentless open cases (Task 2), then remove the H1 trash-can close (Task 3), and a whole-suite + clippy + fmt gate (Task 4). Ordered so each task leaves the suite green. refs #14
This commit is contained in:
@@ -0,0 +1,487 @@
|
|||||||
|
# Copy + Close on the Case Page — Implementation Plan
|
||||||
|
|
||||||
|
> **Parent spec:** `docs/specs/0002-copy-close-on-case-page.md`
|
||||||
|
>
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement`
|
||||||
|
> skill to run this plan. Steps use `- [ ]` checkboxes for tracking.
|
||||||
|
|
||||||
|
**Goal:** Replace the case page's icon-only trash-can close with a text
|
||||||
|
"Kopieren + Schließen" control below the document that copies the
|
||||||
|
document text then closes the case (landing on `/cases`), keeping a bare
|
||||||
|
"Schließen" control for documentless open cases.
|
||||||
|
|
||||||
|
**Architecture:** Frontend-only. One Askama template
|
||||||
|
(`server/templates/case_page.html`) and its inline `<script>`; no Rust
|
||||||
|
route/handler/struct change — the existing `POST /cases/{id}/close`
|
||||||
|
already strips the case-page `Referer` to `/cases`. Tests render the
|
||||||
|
page and assert on the HTML (`server/tests/case_page_test.rs`).
|
||||||
|
|
||||||
|
**Tech Stack:** Askama template + vanilla inline JS; Rust integration
|
||||||
|
tests via `tower::ServiceExt::oneshot`.
|
||||||
|
|
||||||
|
**Task ordering note (load-bearing):** the new close affordances
|
||||||
|
(Tasks 1 + 2) are added *before* the H1 trash-can is removed (Task 3).
|
||||||
|
Removing the trash-can first would leave `case_page_empty_case_shows_placeholder`
|
||||||
|
(asserts a close action on the empty case) RED until a later task; this
|
||||||
|
ordering keeps the whole suite green at every task boundary.
|
||||||
|
|
||||||
|
**Template control-flow recap (from recon):** the document area is
|
||||||
|
`{% match document_html %}`. The `{% when Some with (html) %}` arm holds
|
||||||
|
`#doc-content`, `#copy-btn`, `#doc-body`, and the copy `<script>` — here
|
||||||
|
`has_document` is always true. The `{% when None %}` arm (no document)
|
||||||
|
nests `{% match analysis_failed %}` (failure-banner vs.
|
||||||
|
analyzing/empty/status). The H1 title row has a separate
|
||||||
|
`{% if is_closed %}` reopen / `{% else %}` close (trash-can) / `{% endif %}`.
|
||||||
|
|
||||||
|
**Parse gate (self-review §9):** the project profile declares no
|
||||||
|
`spec_validation` parsers, and the inlined bodies are Askama/HTML (no
|
||||||
|
configured parser) plus Rust test code (caught by the `implement`
|
||||||
|
compile gate). Parse-the-bytes gate is a documented no-op here.
|
||||||
|
|
||||||
|
**Files this plan creates or modifies:**
|
||||||
|
|
||||||
|
- Modify: `server/templates/case_page.html` — H1 close arm removed;
|
||||||
|
two new `doc-footer-actions` close forms; copy `<script>` refactored.
|
||||||
|
- Test: `server/tests/case_page_test.rs` — two new tests + one extended.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Gated "Kopieren + Schließen" control + copy-script refactor
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `server/templates/case_page.html` (after `#doc-body` ~625; script ~626–661)
|
||||||
|
- Test: `server/tests/case_page_test.rs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test (and a closed-case guard)**
|
||||||
|
|
||||||
|
Append both functions at the end of `server/tests/case_page_test.rs`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Property: an open case WITH a document renders the combined
|
||||||
|
/// "Kopieren + Schließen" control below the document — a single form
|
||||||
|
/// that POSTs the close action and carries the CSRF token.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn case_page_open_document_has_copy_close_button() {
|
||||||
|
let cfg = TestConfig::new()
|
||||||
|
.with_label("cp-copyclose")
|
||||||
|
.with_user(test_user("dr_a"))
|
||||||
|
.with_llm("http://unused")
|
||||||
|
.build();
|
||||||
|
let case_id = "11111111-1111-1111-1111-111111111111";
|
||||||
|
let case_dir = seed_case(&cfg.data_path, "dr_a", case_id);
|
||||||
|
write_document_for_test(&case_dir, "fertiger Brief", "1970-01-01T00:00:00Z");
|
||||||
|
|
||||||
|
let app = doctate_server::create_router(cfg);
|
||||||
|
let cookie = login_dr_a(&app).await;
|
||||||
|
|
||||||
|
let resp = app
|
||||||
|
.oneshot(get_with_cookie(paths::case_detail(case_id), &cookie))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
|
let body = body_string(resp).await;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
body.contains(r#"id="copy-close-form""#),
|
||||||
|
"combined copy-close form missing"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
body.contains("Kopieren + Schließen"),
|
||||||
|
"copy-close button label missing"
|
||||||
|
);
|
||||||
|
// The combined form must POST the close action...
|
||||||
|
let form_start = body
|
||||||
|
.find(r#"id="copy-close-form""#)
|
||||||
|
.expect("copy-close form present");
|
||||||
|
let rest = &body[form_start..];
|
||||||
|
let form_end = rest.find("</form>").expect("copy-close form closes");
|
||||||
|
let form_html = &rest[..form_end];
|
||||||
|
assert!(
|
||||||
|
form_html.contains(&format!(r#"action="{}""#, paths::case_close(case_id))),
|
||||||
|
"copy-close form must POST the close action"
|
||||||
|
);
|
||||||
|
// ...and carry the CSRF token inside that same form.
|
||||||
|
assert!(
|
||||||
|
form_html.contains(r#"name="csrf_token""#),
|
||||||
|
"copy-close form must carry the CSRF token"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Guard: a CLOSED case (even with a document, rendered read-only) shows
|
||||||
|
/// no copy-close control — closing an already-closed case is impossible.
|
||||||
|
/// The reopen affordance is the only state-changing control.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn case_page_closed_document_hides_copy_close() {
|
||||||
|
let cfg = TestConfig::new()
|
||||||
|
.with_label("cp-closed-copyclose")
|
||||||
|
.with_user(test_user("dr_a"))
|
||||||
|
.with_llm("http://unused")
|
||||||
|
.build();
|
||||||
|
let case_id = "11111111-1111-1111-1111-111111111111";
|
||||||
|
let case_dir = seed_case(&cfg.data_path, "dr_a", case_id);
|
||||||
|
write_document_for_test(&case_dir, "fertiger Brief", "1970-01-01T00:00:00Z");
|
||||||
|
write_closed_marker(&case_dir, "2026-04-15T12:00:00Z");
|
||||||
|
|
||||||
|
let app = doctate_server::create_router(cfg);
|
||||||
|
let cookie = login_dr_a(&app).await;
|
||||||
|
|
||||||
|
let resp = app
|
||||||
|
.oneshot(get_with_cookie(paths::case_detail(case_id), &cookie))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
|
let body = body_string(resp).await;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!body.contains(r#"id="copy-close-form""#),
|
||||||
|
"closed case must not expose the copy-close control"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
body.contains(&format!(r#"action="{}""#, paths::case_reopen(case_id))),
|
||||||
|
"closed case must still offer reopen"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run tests to verify the RED driver fails**
|
||||||
|
|
||||||
|
Run: `cargo test --manifest-path server/Cargo.toml -j 4 case_page_open_document_has_copy_close_button`
|
||||||
|
Expected: FAIL with "combined copy-close form missing" (the form does not exist yet). The crate still compiles — `case_page_closed_document_hides_copy_close` only asserts absence, so it compiles and would pass; it becomes load-bearing once Step 3 adds the gated form, and is verified green in Step 5.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Insert the gated copy-close form below the document**
|
||||||
|
|
||||||
|
In `server/templates/case_page.html`, find this exact block (the
|
||||||
|
`#doc-body` line and the `</div>` closing `#doc-content`):
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div id="doc-body">{{ html|safe }}</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace it with:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div id="doc-body">{{ html|safe }}</div>
|
||||||
|
</div>
|
||||||
|
{% if !is_closed %}
|
||||||
|
<div class="doc-footer-actions">
|
||||||
|
<form id="copy-close-form" method="post" action="/cases/{{ case_id }}/close">
|
||||||
|
{% call csrf::field(csrf_token) %}
|
||||||
|
<button type="submit" class="copy-close-btn">Kopieren + Schließen</button>
|
||||||
|
</form>
|
||||||
|
<p id="copy-close-error" class="copy-error" role="alert" hidden>
|
||||||
|
Kopieren fehlgeschlagen — Fall nicht geschlossen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Refactor the copy `<script>` to share the clipboard write and wire the close form**
|
||||||
|
|
||||||
|
In the same file, find this exact `<script>` IIFE (immediately after
|
||||||
|
the block edited in Step 3):
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
const btn = document.getElementById("copy-btn");
|
||||||
|
if (!btn) return;
|
||||||
|
btn.hidden = false;
|
||||||
|
btn.addEventListener("click", async () => {
|
||||||
|
const text = document.getElementById("doc-body").innerText;
|
||||||
|
let ok = false;
|
||||||
|
try {
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
ok = true;
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
/* fall through */
|
||||||
|
}
|
||||||
|
if (!ok) {
|
||||||
|
const ta = document.createElement("textarea");
|
||||||
|
ta.value = text;
|
||||||
|
ta.setAttribute("readonly", "");
|
||||||
|
ta.style.position = "fixed";
|
||||||
|
ta.style.top = "-1000px";
|
||||||
|
document.body.appendChild(ta);
|
||||||
|
ta.select();
|
||||||
|
try {
|
||||||
|
ok = document.execCommand("copy");
|
||||||
|
} catch (_) {}
|
||||||
|
ta.remove();
|
||||||
|
}
|
||||||
|
if (ok) {
|
||||||
|
btn.classList.add("copied");
|
||||||
|
setTimeout(() => btn.classList.remove("copied"), 1500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace it with:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
// Shared two-path clipboard write: async Clipboard API in
|
||||||
|
// secure contexts, textarea + execCommand fallback otherwise.
|
||||||
|
// Returns whether the copy actually succeeded so callers can
|
||||||
|
// gate the follow-up close on it.
|
||||||
|
async function copyDocText() {
|
||||||
|
const body = document.getElementById("doc-body");
|
||||||
|
if (!body) return false; // no document → nothing to copy
|
||||||
|
const text = body.innerText;
|
||||||
|
let ok = false;
|
||||||
|
try {
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
ok = true;
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
/* fall through to execCommand */
|
||||||
|
}
|
||||||
|
if (!ok) {
|
||||||
|
const ta = document.createElement("textarea");
|
||||||
|
ta.value = text;
|
||||||
|
ta.setAttribute("readonly", "");
|
||||||
|
ta.style.position = "fixed";
|
||||||
|
ta.style.top = "-1000px";
|
||||||
|
document.body.appendChild(ta);
|
||||||
|
ta.select();
|
||||||
|
try {
|
||||||
|
ok = document.execCommand("copy");
|
||||||
|
} catch (_) {}
|
||||||
|
ta.remove();
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyBtn = document.getElementById("copy-btn");
|
||||||
|
if (copyBtn) {
|
||||||
|
copyBtn.hidden = false;
|
||||||
|
copyBtn.addEventListener("click", async () => {
|
||||||
|
if (await copyDocText()) {
|
||||||
|
copyBtn.classList.add("copied");
|
||||||
|
setTimeout(() => copyBtn.classList.remove("copied"), 1500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combined "copy then close": the clipboard write must finish
|
||||||
|
// inside the user-gesture context before the form navigates,
|
||||||
|
// and a failed copy aborts the close so the user never loses a
|
||||||
|
// document they believed was on the clipboard.
|
||||||
|
const closeForm = document.getElementById("copy-close-form");
|
||||||
|
if (closeForm) {
|
||||||
|
closeForm.addEventListener("submit", async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (await copyDocText()) {
|
||||||
|
closeForm.submit(); // close → 303 → /cases
|
||||||
|
} else {
|
||||||
|
document.getElementById("copy-close-error").hidden = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run tests to verify they pass**
|
||||||
|
|
||||||
|
Run: `cargo test --manifest-path server/Cargo.toml -j 4 --test case_page_test`
|
||||||
|
Expected: the whole `case_page_test` target PASSes — in particular the new
|
||||||
|
`case_page_open_document_has_copy_close_button` (form present on open+doc),
|
||||||
|
`case_page_closed_document_hides_copy_close` (absent on closed), and the
|
||||||
|
pre-existing `case_page_document_has_copy_button` (icon `#copy-btn` +
|
||||||
|
`#doc-content` survive the refactor).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Bare "Schließen" control for documentless open cases
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `server/templates/case_page.html` (the `{% when None %}` arm of `{% match document_html %}`, ~line 746)
|
||||||
|
- Test: `server/tests/case_page_test.rs:270` (`case_page_empty_case_shows_placeholder`, extended)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Extend the empty-case test with the label assertion**
|
||||||
|
|
||||||
|
In `server/tests/case_page_test.rs`, in `case_page_empty_case_shows_placeholder`,
|
||||||
|
after the existing close-form assertion (the block ending at the
|
||||||
|
`"close form missing on empty case"` message), add:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
assert!(
|
||||||
|
body.contains(r#">Schließen</button>"#),
|
||||||
|
"documentless open case must offer a bare Schließen button"
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `cargo test --manifest-path server/Cargo.toml -j 4 case_page_empty_case_shows_placeholder`
|
||||||
|
Expected: FAIL with "documentless open case must offer a bare Schließen button" (today the empty case has only the icon trash-can in the H1, no text `>Schließen</button>`).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Insert the bare close form in the no-document branch**
|
||||||
|
|
||||||
|
In `server/templates/case_page.html`, find this exact line (the two
|
||||||
|
`{% endmatch %}` that close the inner `analysis_failed` match and the
|
||||||
|
outer `document_html` match):
|
||||||
|
|
||||||
|
```html
|
||||||
|
{% endif %} {% endmatch %} {% endmatch %}
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace it with (split: bare form lands inside the `document_html`
|
||||||
|
`None` arm, after the inner match resolves, before the outer match
|
||||||
|
closes — so it renders for both the failure-banner and the
|
||||||
|
placeholder/analyzing sub-cases of a documentless open case):
|
||||||
|
|
||||||
|
```html
|
||||||
|
{% endif %} {% endmatch %}
|
||||||
|
{% if !is_closed %}
|
||||||
|
<div class="doc-footer-actions">
|
||||||
|
<form method="post" action="/cases/{{ case_id }}/close">
|
||||||
|
{% call csrf::field(csrf_token) %}
|
||||||
|
<button type="submit" class="copy-close-btn">Schließen</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endmatch %}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `cargo test --manifest-path server/Cargo.toml -j 4 case_page_empty_case_shows_placeholder`
|
||||||
|
Expected: PASS (placeholder text, the close action, and the bare `>Schließen</button>` are all present).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Remove the H1 trash-can close button (open-case arm)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `server/templates/case_page.html:439–473` (the `{% else %}` close arm of the H1 title-row form)
|
||||||
|
- Test: `server/tests/case_page_test.rs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Append at the end of `server/tests/case_page_test.rs`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Property: the icon-only trash-can close button in the H1 title row is
|
||||||
|
/// gone — closing now happens via the text control below the document.
|
||||||
|
/// (The closed-case reopen affordance in the same slot is untouched.)
|
||||||
|
#[tokio::test]
|
||||||
|
async fn case_page_open_case_has_no_trashcan_close() {
|
||||||
|
let cfg = TestConfig::new()
|
||||||
|
.with_label("cp-no-trash")
|
||||||
|
.with_user(test_user("dr_a"))
|
||||||
|
.with_llm("http://unused")
|
||||||
|
.build();
|
||||||
|
let case_id = "11111111-1111-1111-1111-111111111111";
|
||||||
|
let case_dir = seed_case(&cfg.data_path, "dr_a", case_id);
|
||||||
|
write_document_for_test(&case_dir, "fertiger Brief", "1970-01-01T00:00:00Z");
|
||||||
|
|
||||||
|
let app = doctate_server::create_router(cfg);
|
||||||
|
let cookie = login_dr_a(&app).await;
|
||||||
|
|
||||||
|
let resp = app
|
||||||
|
.oneshot(get_with_cookie(paths::case_detail(case_id), &cookie))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
|
let body = body_string(resp).await;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!body.contains(r#"title="Fall schließen""#),
|
||||||
|
"the H1 trash-can close button must be gone"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `cargo test --manifest-path server/Cargo.toml -j 4 case_page_open_case_has_no_trashcan_close`
|
||||||
|
Expected: FAIL with "the H1 trash-can close button must be gone" (the title-row close form still carries `title="Fall schließen"`).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Delete the H1 close arm**
|
||||||
|
|
||||||
|
In `server/templates/case_page.html`, find this exact block (the
|
||||||
|
`{% else %}` close arm and the `{% endif %}` that closes the H1
|
||||||
|
reopen/close conditional):
|
||||||
|
|
||||||
|
```html
|
||||||
|
{% else %}
|
||||||
|
<form
|
||||||
|
class="delete-form"
|
||||||
|
method="post"
|
||||||
|
action="/cases/{{ case_id }}/close"
|
||||||
|
>
|
||||||
|
{% call csrf::field(csrf_token) %}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="delete-btn"
|
||||||
|
title="Fall schließen"
|
||||||
|
aria-label="Fall schließen"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M3 6h18"></path>
|
||||||
|
<path
|
||||||
|
d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"
|
||||||
|
></path>
|
||||||
|
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
||||||
|
<line x1="10" x2="10" y1="11" y2="17"></line>
|
||||||
|
<line x1="14" x2="14" y1="11" y2="17"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace it with (drop the entire `{% else %}` arm; keep `{% endif %}`):
|
||||||
|
|
||||||
|
```html
|
||||||
|
{% endif %}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `cargo test --manifest-path server/Cargo.toml -j 4 case_page_open_case_has_no_trashcan_close`
|
||||||
|
Expected: PASS (no `title="Fall schließen"` on the open case page).
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run the full case-page + close-redirect suites to confirm no regression**
|
||||||
|
|
||||||
|
Run: `cargo test --manifest-path server/Cargo.toml -j 4 --test case_page_test`
|
||||||
|
Expected: PASS — in particular `case_page_empty_case_shows_placeholder` (close action now served by the Task 2 bare form), `case_page_renders_for_closed_case_without_show_closed`, and `case_page_hides_mutation_actions_when_closed` (H1 reopen arm untouched) all stay green.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Whole-suite + lint/format gate
|
||||||
|
|
||||||
|
**Files:** (none — verification only)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Run the full server test suite**
|
||||||
|
|
||||||
|
Run: `cargo test --manifest-path server/Cargo.toml -j 4`
|
||||||
|
Expected: PASS (no failures, no regressions).
|
||||||
|
|
||||||
|
- [ ] **Step 2: Clippy**
|
||||||
|
|
||||||
|
Run: `cargo clippy --manifest-path server/Cargo.toml --all-targets`
|
||||||
|
Expected: no warnings (CLAUDE.md: "cargo clippy hat immer Recht").
|
||||||
|
|
||||||
|
- [ ] **Step 3: Format**
|
||||||
|
|
||||||
|
Run: `cargo fmt --manifest-path server/Cargo.toml`
|
||||||
|
Expected: no diff (or only the newly added test code reflowed).
|
||||||
Reference in New Issue
Block a user