All checks were successful
CI / Unit & Component Tests (push) Successful in 5m10s
CI / OCR Service Tests (push) Successful in 24s
CI / Backend Unit Tests (push) Successful in 5m14s
CI / fail2ban Regex (push) Successful in 50s
CI / Semgrep Security Scan (push) Successful in 27s
CI / Compose Bucket Idempotency (push) Successful in 1m5s
Closes #837 Makes `PersonRelationship` fully editable (type, related person, dates, notes), migrates its dates from `Integer fromYear/toYear` to `LocalDate + DatePrecision` (mirroring the #773 person pattern, ADR-039 / V76), activates the previously-dead `notes` column, and gives the Zeitstrahl's derived **Heirat** events full date precision for free. Both Open Decisions confirmed as adopted: **no `@Version`** (last-write-wins, single-writer archive) and **`DELETE` ownership-mismatch aligned 403 → 404** (anti-enumeration, matching the new `PUT`). ## What's in it - **V78** migrates `person_relationships.from_year/to_year` → `from_date`/`to_date` + NOT-NULL `*_date_precision` (default `UNKNOWN`); pre-check abort on corrupt years, `YYYY-01-01`/`YEAR` backfill, 5 named CHECK constraints, year columns dropped. - **`PUT /api/persons/{id}/relationships/{relId}`** (`@RequirePermission(WRITE_ALL)`) re-runs every create invariant (self / coherence / order / reverse-PARENT_OF / duplicate) and re-flags family membership; orientation preserved per viewpoint. - New `ErrorCode.INVALID_RELATIONSHIP_DATES` registered in all four sites (§3.6). - `TimelineEventService` sources the derived marriage date from `SPOUSE_OF.fromDate` + precision. - Frontend: `RelationshipDateField` (DAY/MONTH/YEAR), upsert-capable `AddRelationshipForm` (pre-fill + notes + in-flight submit lock), `RelationshipChip` Edit affordance, `updateRelationship` server action, read-view date range + notes, `formatRelationshipDateRange` helper. `api.ts` regenerated. - Docs: ADR-044, db-orm/db-relationships diagrams, DEPLOYMENT §5 deploy note, RTM REQ-001…REQ-019. ## Requirements All 19 EARS requirements implemented red/green and marked `Done` in `.specify/rtm.md`. ## Test plan - **Backend** (targeted, green): `RelationshipMigrationTest` (Testcontainers pg16, 8), `RelationshipServiceTest` (22), `RelationshipControllerTest` (15), `RelationshipServiceIntegrationTest` (real DB, 10), `DerivedEventsAssemblyTest` (17), `ArchitectureTest` (14); `clean package` builds. - **Frontend** (green): `relationshipDates.spec.ts`, `AddRelationshipForm.svelte.spec.ts`, `RelationshipChip.svelte.spec.ts`, `PersonRelationshipsCard.svelte.test.ts`, `page.server.spec.ts`, `messages.spec.ts`. `npm run check` = 798 (below the ~834 baseline); `npm run lint` clean. ## Notes for reviewers - **Spec deviation:** the edit form was built by making `AddRelationshipForm` upsert-capable rather than a duplicate `EditRelationshipForm` (DRY); RTM rows reference `AddRelationshipForm.svelte.spec.ts`. - `api.ts` regenerated from the live spec; only relationship-relevant hunks remain (one springdoc `PageableObject` field-reorder pruned). - **Deploy:** V78 is one-way and not rolling-deploy-safe — stop old JAR → start new JAR (Flyway runs first); targeted `pg_restore -t person_relationships` for rollback. No maintenance window. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Marcel <marcel@familienarchiv> Reviewed-on: #841
87 lines
2.6 KiB
TypeScript
87 lines
2.6 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest';
|
|
import { render } from 'vitest-browser-svelte';
|
|
import StammbaumConnectors from './StammbaumConnectors.svelte';
|
|
import { NODE_H } from './layout/buildLayout';
|
|
import type { components } from '$lib/generated/api';
|
|
|
|
type RelationshipDTO = components['schemas']['RelationshipDTO'];
|
|
|
|
const P = '00000000-0000-0000-0000-0000000000c1';
|
|
const C = '00000000-0000-0000-0000-0000000000c2';
|
|
const H = '00000000-0000-0000-0000-0000000000c3';
|
|
const W = '00000000-0000-0000-0000-0000000000c4';
|
|
|
|
function parentEdge(parentId: string, childId: string): RelationshipDTO {
|
|
return {
|
|
id: `${parentId}>${childId}`,
|
|
personId: parentId,
|
|
relatedPersonId: childId,
|
|
personDisplayName: '',
|
|
relatedPersonDisplayName: '',
|
|
relationType: 'PARENT_OF',
|
|
fromDatePrecision: 'UNKNOWN',
|
|
toDatePrecision: 'UNKNOWN'
|
|
};
|
|
}
|
|
|
|
function endedSpouseEdge(a: string, b: string): RelationshipDTO {
|
|
return {
|
|
id: `${a}~${b}`,
|
|
personId: a,
|
|
relatedPersonId: b,
|
|
personDisplayName: '',
|
|
relatedPersonDisplayName: '',
|
|
relationType: 'SPOUSE_OF',
|
|
fromDatePrecision: 'UNKNOWN',
|
|
toDate: '1950-01-01',
|
|
toDatePrecision: 'YEAR'
|
|
};
|
|
}
|
|
|
|
const positions = new Map([
|
|
[P, { x: 0, y: 0 }],
|
|
[C, { x: 400, y: NODE_H + 80 }],
|
|
[H, { x: 0, y: 400 }],
|
|
[W, { x: 300, y: 400 }]
|
|
]);
|
|
|
|
const dashesOf = (selector: string) =>
|
|
Array.from(document.querySelectorAll(selector))
|
|
.map((el) => el.getAttribute('stroke-dasharray'))
|
|
.filter((d): d is string => d !== null);
|
|
|
|
describe('StammbaumConnectors — cross-link cadence (#724)', () => {
|
|
it('renders a cross-level link with the distinct 2 6 dash, never the 4 4 ended-marriage dash', async () => {
|
|
render(StammbaumConnectors, {
|
|
edges: [parentEdge(P, C), endedSpouseEdge(H, W)],
|
|
positions,
|
|
crossLinks: [{ parentId: P, childId: C }]
|
|
});
|
|
|
|
await vi.waitFor(() => {
|
|
const dashes = dashesOf('line');
|
|
// The cross-link cadence is present …
|
|
expect(dashes).toContain('2 6');
|
|
// … the ended-marriage cadence is present …
|
|
expect(dashes).toContain('4 4');
|
|
// … and the two are genuinely different cadences (WCAG 1.4.1: not by
|
|
// stroke alone, but they must not collapse into the same pattern).
|
|
expect('2 6').not.toBe('4 4');
|
|
});
|
|
});
|
|
|
|
it('draws a normal parent→child connector solid when it is NOT a cross-link', async () => {
|
|
render(StammbaumConnectors, {
|
|
edges: [parentEdge(P, C)],
|
|
positions,
|
|
crossLinks: []
|
|
});
|
|
|
|
await vi.waitFor(() => {
|
|
// No dashed parent lines at all when nothing is a cross-link.
|
|
expect(dashesOf('line')).not.toContain('2 6');
|
|
expect(document.querySelectorAll('line').length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
});
|