You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

10 KiB

W2 T4 Review: Company Custom Checklist CRUD and Idempotency

Verdict

  • Spec compliance: PASS
  • Task quality: Approved
  • Overall: PASS
  • Base: 037ddc7
  • Head: 7370e51
  • Blocking findings: none
  • Deferred blocker: concurrent create / 50-item limit races are not resolved by this task and must be treated as mandatory T6 concurrency-gate obligations.

Verification

  • Read task-reviewer-prompt.md, writing-good-tests.md, task-briefs/T4.md, the execution contract, specs/onboarding-checklist/spec.md, the design, and the complete 037ddc7..7370e51 diff. Focused reads were used only where the diff output was truncated mid-function.
  • Scope check: the commit changes 9 files: the 7 files named by the T4 brief plus additive changes to ErrorCode.java and GlobalExceptionHandler.java.
  • Re-ran the T4 evidence command from mydreamjob-server with JAVA_HOME=D:\java and Maven 3.9.7.
  • Result: Tests run: 22, Failures: 0, Errors: 0, Skipped: 0, BUILD SUCCESS.
  • git diff --check 037ddc7..7370e51 reported no whitespace errors.
  • RED-before-GREEN ordering cannot be reconstructed from the final commit; the independently reproduced current HEAD is GREEN.

Spec Compliance

  • COMPANY ownership is enforced centrally: requireCompanyOwner checks the COMPANY role and resolves the company by ownerUserId, missing roles return FORBIDDEN, and missing company profiles return COMPANY_NOT_FOUND (OnboardingChecklistService.java:229-239). The role/profile tests are at OnboardingCompanyChecklistServiceTest.java:269-297.
  • Company listing is isolated to the current company and includes inactive rows: findAll() results are filtered by scopeType=COMPANY, scopeId, and companyId, then sorted (OnboardingChecklistService.java:121-131, :241-257). The test verifies own active/inactive items and excludes another company and a STANDARD item (OnboardingCompanyChecklistServiceTest.java:47-68).
  • Create validation covers title, description, category, sortOrder, required default, and idempotency-key length in both the request DTO and service (CompanyChecklistItemCreateRequest.java:9-33, OnboardingChecklistService.java:266-289). Invalid requests return BAD_REQUEST before repository access (OnboardingCompanyChecklistServiceTest.java:71-88).
  • Create normalization and hashing are deterministic: title/description/idempotency key are trimmed, blank optional strings become null, required defaults to false, sortOrder defaults to 1000, and the canonical request is SHA-256 hashed (OnboardingChecklistService.java:140-148, :315-349). The persistence test verifies the normalized entity and hash shape (OnboardingCompanyChecklistServiceTest.java:91-122).
  • Sequential idempotency replay and conflict behavior are correct: an existing (company, idempotencyKey) with the same hash returns the original row without a second save; a different hash raises 9704 (OnboardingChecklistService.java:150-163; tests at OnboardingCompanyChecklistServiceTest.java:125-166).
  • The 50-active-item boundary is enforced in the sequential path: active rows are counted, and 50 existing active rows produce 9703 before save (OnboardingChecklistService.java:165-170; test at OnboardingCompanyChecklistServiceTest.java:169-185).
  • Update applies every mutable field and checks the supplied version before mutation (OnboardingChecklistService.java:189-209). Stale versions return 9702 and do not save (OnboardingCompanyChecklistServiceTest.java:188-228).
  • Delete is a soft delete and repeated deletion is idempotent: the active flag is set false with the current version, then a second delete returns the already-inactive row without another save (OnboardingChecklistService.java:211-227; test at OnboardingCompanyChecklistServiceTest.java:231-251).
  • Cross-company mutation is blocked by the same ownership predicate used by update and delete; an item belonging to another company returns 9701 and remains unchanged (OnboardingChecklistService.java:252-264; delete isolation test at OnboardingCompanyChecklistServiceTest.java:254-266).
  • The response DTO is whitelist-only and exposes item identity, source, key, display fields, sort/active/version; it does not expose company IDs, owner IDs, idempotency keys, request hashes, timestamps, or internal audit fields (CompanyChecklistItemView.java:14-23).
  • Error mappings are correct: 9702, 9703, and 9704 are defined (ErrorCode.java:175-179) and map to HTTP 409 (GlobalExceptionHandler.java:167-172); 9701 remains mapped to 404. Controller tests cover 409 for 9702/9704 and 404 for 9701 (OnboardingCompanyChecklistControllerTest.java:120-132, :167-189, :211-233).
  • Controller wiring matches the fixed endpoints and role-neutral transport contract: all four company CRUD methods resolve the authenticated user and delegate to the service (OnboardingChecklistController.java:65-108). Authentication is covered at OnboardingCompanyChecklistControllerTest.java:235-250.

Test Falsifiability

  • The service tests assert observable results and side effects: persisted scope/company/defaults, replay returning the original ID with one save, conflict preventing a save, stale version preventing mutation, and repeat deletion not saving twice. Each has a plausible production mutation that would fail it.
  • Controller tests exercise real MockMvc security, validation, serialization, and exception mapping with a mocked service. They would catch wrong paths, missing role passthrough, weak request validation, and incorrect 409/404 mappings.
  • The 50-item test catches blocking at exactly 50, but does not independently prove that 49 active items are allowed or that inactive items do not count toward the limit. The implementation is correct by inspection, but the lower boundary/filtering semantics are not fully falsifiable from the current test.
  • Cross-company isolation is directly tested for delete only. Update and list use the same ownership predicate, and the controller test mocks the cross-company update result, so the service-level update isolation path is not independently exercised.
  • The HTTP mapping for 9703 is statically present but has no focused MockMvc assertion; only the service error code is tested. This is non-blocking because T6 owns the full HTTP/concurrency integration gate.

Concurrent Create / Limit Race Disposition

The reported race is real, but it is not a blocking finding for T4.

  • The current create path reads findAll() and checks idempotency/active count before inserting (OnboardingChecklistService.java:150-186). There is no row lock or atomic counter.
  • V38 does have a unique key on (company_id, idempotency_key) (V38__onboarding_checklist_and_company_knowledge.sql/equivalent migration line 27; entity declaration OnboardingChecklistItem.java:29-35). Simultaneous identical creates can therefore make one request fail with DataIntegrityViolationException; saveCompanyItem only translates optimistic-lock failures (OnboardingChecklistService.java:366-373), so the loser can surface as a 500 instead of a safe replay.
  • The 50-item limit is also non-atomic: two concurrent creates at 49 active items can both observe 49 and insert, producing 51 active items.
  • T4's brief and Done Definition require sequential validation, replay/conflict, and the 50-item boundary, but do not assign concurrent execution to T4. The execution contract maps concurrency to B3, and the T6 brief explicitly requires concurrent/idempotent conflict behavior, error mapping, no partial writes, and the concurrency integration test (task-briefs/T6.md:12, :18, :21, :30-33, :51).

Mandatory T6 test obligations:

  1. Fire two concurrent creates with the same company, key, and normalized body: assert exactly one row, both callers receive the same successful item (or the loser receives the same successful replay), and neither receives an internal/500 response.
  2. Fire two concurrent creates with the same key and differing bodies: assert exactly one row, one successful create, and one 409 9704, not a unique-constraint 500.
  3. Start with 49 active items and concurrently create two distinct items: assert the final active count is 50, one request succeeds, one returns 409 9703, and no partial/extra row remains.
  4. Start at 50 active items and verify new creates return 409 9703; verify replaying an existing idempotency key still returns the original item without counting as a new item.
  5. Repeat the key test across two companies to prove (company_id, idempotency_key) isolation and no cross-company replay.

Issues

Critical (Must Fix)

None.

Important (Should Fix)

None for T4. The concurrent create/limit race is deferred by the approved execution plan to T6, but it must not be closed without the obligations above.

Minor (Nice to Have)

  • Strengthen OnboardingCompanyChecklistServiceTest.java:169-185 with a 49-active success case and an inactive-item-does-not-count case; the current test proves the upper rejection only.
  • Add a service-level cross-company update test alongside the existing delete isolation test, or explicitly retain this as a T6 integration obligation.
  • Add a focused MockMvc assertion for 9703 -> HTTP 409, since only the service error code is currently exercised for the limit path.
  • ErrorCode.java and GlobalExceptionHandler.java are outside the seven files named by task-briefs/T4.md:14-23. The additions are limited to the T4 error codes and are necessary for the required 409 contract; reconcile ownership in the task ledger so T6 does not duplicate or renumber them.

Residual Verification Warning

  • Real MySQL 8.4 execution and true concurrent JPA behavior were not exercised by this T4 unit/MockMvc suite. H2-backed MockMvc tests passed; the remaining database/concurrency proof belongs to the B3/T6 verification gate.

Assessment

Task quality: Approved

Reasoning: T4 implements the approved sequential company CRUD, ownership isolation, validation, deterministic idempotency replay/conflict, optimistic update, soft-delete idempotency, DTO minimization, and error-code mappings. The only blocking-risk item is the explicitly deferred concurrent create/limit race, which must be proven and fixed at T6.

Controller receipt command when ssf is available:

ssf execution review changes/n04-onboard --wave W2 --base 037ddc7 --head 7370e51 --report changes/n04-onboard/.superpowers/sdd/reviews/W2-T4.md --verdict pass