Skip to content

Payroll Settlement Sheet — Test Coverage (batch2/sheet) ​

Status: design approved 2026-07-01. Feeds an implementation plan (writing-plans). Branch: test/batch2-sheet off develop (revised 2026-07-03 — test/unit-coverage-batch1 was found to be 423 commits behind develop and missing the sheets/workbook/ submodule split entirely; only its overtime-adapter fix commit is cherry-picked forward).

1. Goal ​

Add frontend unit tests and Playwright e2e tests for the 급여정산 (payroll settlement) sheet feature, targeting silent-breakage logic (money, dates, sort, domain→sheet mapping). Fix the adapter bugs found during investigation and pin the correct behavior with tests. Backend work is explicitly out of scope for this batch.

2. Scope ​

In

  • Frontend unit tests (Vitest + happy-dom, co-located *.test.ts) across four areas: money compute (frontend), domain adapters (5), interaction, engine.
  • New Playwright e2e specs for journeys not covered by the existing 01–20.
  • Fix the 5 read-only domain adapters' drift bugs + correct-behavior tests.
  • Small behavior-preserving refactors to make private logic testable.

Out (handoff / later)

  • Backend unit tests (bankersRound, evaluateFormula, commit-split) — not this author's role. Backend money correctness is only indirectly exercised by e2e #23.
  • The multi-tenancy leak fix in resolver.ts / sheet-cell.router.ts / sheet-row.router.ts — report only (Section 8), handed to a backend owner.

3. Background (subsystems + current coverage) ​

The "sheet" is three subsystems:

  1. Read-only domain tabs — apps/hr-system/src/sheets/*.adapter.ts (overtime, education, resignation, sports, yearend) built via makeReadOnlySheet + a pure SheetAdapter. CLAUDE.md flags these as prime silent-breakage targets.
  2. Workbook (settlement core) — sheets/workbook/ + workbook-adapter.ts (102KB): formula/summary/subtotal/sort/filter/commit.
  3. Backend resolver — packages/api/src/routers/sheet-rows/ (out of scope).

Existing coverage: 20 Playwright e2e specs, 9 SDK→API integration tests, ~8 frontend unit tests, 1 backend unit test. Money/date/sort/mapping logic is largely uncovered.

4. Guardrails ​

  • Silent-breakage first — test logic that produces a wrong-but-plausible result without throwing. Not coverage numbers.
  • Behavior, not implementation — assert return values / rendered strings / emitted events. No asserting private state or "was called".
  • Runner AND build must be green — every file passes both pnpm --filter @hereby/hr-system exec vitest run <file> andpnpm --filter @hereby/hr-system build (tsc). Vitest does not type-check.
  • cellRenderers are (row, col, rowIdx) => string — always call with the full 3-arg signature in adapter tests, or the build breaks even when the runner is green.
  • Real enums — adapter label maps must be asserted against the real @hereby/core enums (OvertimeCategory, OvertimeStatus, SubsidyStatus, ResignationStatus, SettlementStatus, IncentiveCategory), not hand-rolled string maps.

5. Prerequisite refactors (behavior-preserving, land first in PR-A) ​

  1. export the private pure fns in workbook-adapter.ts: evalColumnFilter, pasteNormalizeIsoDate, projectRowAmount, buildColumnsFromLayout.
  2. Consolidate date normalization — one pure helper sheets/workbook/date-normalize.ts; pasteNormalizeIsoDate and drafts.ts::_normalizeToIsoDate delegate to it (removes a 3-way duplication; resolver's copy stays backend/out-of-scope but is noted).
  3. Extract pure cores so slice methods delegate (and become directly testable):
    • compareByColumn(a, b, col, dir) ← filters-sort.ts::sortByColumn
    • summaryValue(col, rows) ← payroll.ts::summaryFor
    • detectFillSeries(values) ← fill-handle.ts::_detectFillSeries → fill-series.ts

Each refactor is verified by running the existing tests + build before adding new tests.

6. Deliverables — unit tests ​

PR-A — Money (frontend) + Domain adapters (+ E1 adapter fixes) ​

Tier 1 — money (frontend):

FileTargetKey cases
sheets/workbook-adapter.filter.test.tsevalColumnFilter11 operators × amount/date/string coercion; between boundaries; isEmpty/isNotEmpty; null/undefined per operator
sheets/workbook/date-normalize.test.tsconsolidated date helperyyyy-mm-dd, yyyy/m/d, m/d/yyyy (auto-flip >12), m/d/yy pivot (00–69→2000s, 70–99→1900s); zero-pad; invalid (Feb 30, month 13)
sheets/workbook-adapter.amount.test.tsprojectRowAmount + subtotal genovertime/deduction hook vs row.amount fallback; NaN; multi-config subtotal sums; >=2 threshold
sheets/workbook/filters-sort.test.tscompareByColumnasc/desc sign; null-last; Korean locale (가/나/다); 'abc'→NaN coercion
sheets/workbook/payroll.test.tssummaryValue, isPayrollLedger, aggregateSourceLabelper-column sum/count/distinct rule; subtotal-row exclusion; thousands format; ledger 3-way heuristic
sheets/workbook-adapter.columns.test.tsbuildColumnsFromLayoutformat/kind override; render heuristic; width default; identity field path

Tier 2 — domain adapters (5). Each file asserts: buildDefaultColumns field bindings, cellRenderers (3-arg), searchPredicate, label maps vs real @hereby/core enums.

  • sheets/overtime.adapter.test.ts (extend batch1's), education.adapter.test.ts, resignation.adapter.test.ts, sports.adapter.test.ts, yearend.adapter.test.ts.

E1 fixes shipped in PR-A (with the tests above proving correctness):

  • overtime: CAT_LABEL (weekend→real saturday; add HOLIDAY_NIGHT); STATUS_LABEL (add rejected/report_*; drop fictional draft/paid); fix 시급/할증률/수당액 bound to non-existent hourlyRate/multiplier/calculatedPay.
  • yearend: STATUS_LABEL add calculated/applied.
  • sports: sdk.sportsIncentive.list does not exist (only fixedIncentive); TYPE_LABEL vs real IncentiveCategory. Repoint to the real service or mark clearly if the tab is dead; fix under a FIXME(타입 복구 중 발견) if the server proc is missing.
  • education/resignation: date field typed string but SDK returns Date — fix typing.
  • Prefer removing type Sdk = any where it hid these; keep SDK live types.

PR-B — Interaction + Engine (frontend) ​

Tier 3 — interaction:

FileTargetKey cases
sheets/workbook/fill-series.test.tsdetectFillSeriesnumeric step; day-by-day date; single value; non-constant step; comma numbers; null
sheets/workbook/import-export.test.ts_parseTsvImport, csv escapeexplicit 0 unitCount (bug: coerced to 1); non-numeric totalAmount; RFC-4180 quoting
sheets/workbook/cell-edit.test.ts_isCellEditableForRowcolumn-kind gate; legacy field heuristics (shared by bulk-edit)
sheets/workbook/clipboard.test.ts_resolveEmployeeForPasteemployeeNumber exact → name case-insensitive fallback
sheets/workbook/treatment.test.tstransposedRows/matrixRows, session splitper-employee subtotal; matrix grid; Math.round therapist split; latest-date tie-break

Tier 4 — engine:

FileTargetKey cases
sheet/virtualizer.test.ts (new)computeWindow, shouldVirtualize, scrollOffsetForRowfirst/last off-by-one; spacer heights; threshold=500 boundary; totalRows<=0
sheet/render.test.ts (new)buildColumnStyleCache, defaultRenderers, formatAmountfrozen-left cumulative math; 1.5h→'2' rounding regression; formula-null N/A vs 0; escapeHtml present
sheet/selection.test.ts (extend)extendToRange, selectionToTsv, selectionRectrange math; TSV shape
sheet/engine.test.ts (new, selective)moveSelection, fillSelectionedge clamp; axis (down vs right); no-op dedup preserves undo

7. Deliverables — Playwright e2e (PR-C, packages/test/e2e/tests/21+) ​

Critical journeys only (avoid the ice-cream-cone). Seed via API, isolate per spec, clean up via deleteSheetsByNamePrefix, prefer waitForResponse over timeouts.

  1. 21-payroll-confirm-cancel — 급여대장 row → 💰 급여 확정 → breakdown modal → confirm → locked → 🔓 확정 취소.
  2. 22-filter-sort — column filter rule → live filter → header sort → persists across reload.
  3. 23-formula-commit-amount — formula column → runCompute → commit → assert rendered amount against a hand-computed value (transitively covers backend rounding/eval).
  4. 24-paste-fill — paste a TSV block → fill-down.
  5. 25-carry-forward-apply — spec 14 covers preview/empty; add the apply path.
  6. 26-export — eCount XLSX / CSV download assertion.

8. Handoff report (backend — NOT fixed in this batch) ​

Multi-tenancy leak. resolver.ts::loadSourceRecords filters by organizationId for only e_sign_records; the other 14 source kinds (allowance/deduction/overtime/ fixed_incentive/leave/resignation/attendance/offsite/ward_daily/salary_grade/ grade_promotion/incentive_preset/employment_contract/year_end_settlement) are fetched by bare id = ANY($1). sheet-cell.router.ts::set and sheet-row.router.ts::create/ batchCreate accept client-supplied ref*RecordId without ownership validation → an org-A admin with an org-B record UUID can render org-B amounts into an org-A sheet. resolver.test.ts already pins this for e_sign only. Recommended fix (separate security PR): validate ref ownership at write-time in the routers AND add organizationId to all 14 read-time loaders; extend resolver.test.ts one case per kind.

9. Verification ​

  • Per file: pnpm --filter @hereby/hr-system exec vitest run <file> + pnpm --filter @hereby/hr-system build.
  • Batch close: pnpm lint:check && pnpm build.
  • e2e: make db-start && make db-seed, then pnpm test:e2e (Playwright auto-launches API + hr-system).

10. Sequencing ​

PR-A (refactors §5 + Tier 1 + Tier 2 + E1 fixes) → PR-B (Tier 3 + Tier 4) → PR-C (e2e 21–26). Multi-tenancy = separate security PR by backend owner.

11. Open risks ​

  • Some interaction helpers read this; if a pure core cannot be cleanly extracted, test by instantiating the slice with a minimal stubbed this (documented per file).
  • e2e #23's hand-computed amount must use seeded contract values; if backend rounding is wrong, the spec fails → that is a backend handoff, not a spec bug.
  • sports adapter may be dead code (no real SDK service); confirm before "fixing" vs removing the tab.