diff --git a/.gitignore b/.gitignore index 023f5cd9..4ae075d8 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ debug.log !/dist/css/simple-comment-style.css !/cypress/**/*.js deno.lock +.codex diff --git a/docs/archive/Priority5AuthServiceSlice10Checklist.md b/docs/archive/Priority5AuthServiceSlice10Checklist.md new file mode 100644 index 00000000..41a0cbf4 --- /dev/null +++ b/docs/archive/Priority5AuthServiceSlice10Checklist.md @@ -0,0 +1,92 @@ +# Priority 5 Auth Service Slice 10 Checklist + +Status: approved + +Classification: approved implementation checklist + +Source plan: `docs/plans/Priority5AuthServiceSlice10Plan.md` + +Parent plan: `docs/plans/Priority5Completion.md` (Item 10: Draft a slice for removing `dispatchableStore` / `loginStateStore` logout relay behavior from `SelfDisplay.svelte`) + +## Scope Lock + +In scope: + +- add an explicit `authService` prop to `SelfDisplay.svelte` +- pass the widget-scoped `authService` from `SimpleComment.svelte` to `SelfDisplay.svelte` +- remove `SelfDisplay.svelte` use of `dispatchableStore` +- remove `SelfDisplay.svelte` subscription to `loginStateStore` +- use `authService.authRuntimeSnapshot` for processing state and logout-button visibility +- call `authService.logout()` directly from `SelfDisplay.svelte` +- keep test-writing passes separate from production implementation passes + +Out of scope: + +- removing `dispatchableStore` from `Login.svelte` +- removing `loginStateStore` selected-tab publication from `Login.svelte` +- removing legacy store definitions +- removing the temporary Slice 8 auth bridge +- changing `CommentInput.svelte` +- changing backend/API contracts +- choosing the final direct-props versus thin auth-service-backed store architecture + +## Slice Intent + +This slice removes the logout relay from `SelfDisplay.svelte`. After the slice, the visible user panel should observe logout availability through the injected widget-scoped `authService` and call `authService.logout()` directly. `SelfDisplay.svelte` should no longer depend on `dispatchableStore` or `loginStateStore` for logout behavior. + +## Atomic Checklist Items + +- [x] T01 `[tests]` Add fail-first `SelfDisplay.svelte` component tests for auth-service-driven logout behavior in `src/tests/frontend/components/SelfDisplay.auth-service.test.ts`. + - Depends on: none. + - Required coverage: + - when `authService.authRuntimeSnapshot.nextEvents` includes `LOGOUT`, the logout button is visible for a current user. + - clicking the logout button calls `authService.logout()` directly instead of dispatching `logoutIntent`. + - when auth runtime state is processing, the skeleton display remains visible. + - source guard proves `SelfDisplay.svelte` does not import `dispatchableStore` or `loginStateStore`. + - Trace: + - "Add fail-first component tests proving `SelfDisplay.svelte` reads auth runtime state from `authService`, calls `authService.logout()` directly, and does not import legacy relay stores." (`docs/plans/Priority5AuthServiceSlice10Plan.md`, Approach) + - "Pass: `SelfDisplay.svelte` component tests prove logout button visibility comes from `authService.authRuntimeSnapshot`, clicking Log out calls `authService.logout()`, and legacy relay stores are not imported." (`docs/plans/Priority5AuthServiceSlice10Plan.md`, Validation Strategy) + - "Clicking the logout button calls `authService.logout()` directly." (`docs/plans/Priority5AuthServiceSlice10Plan.md`, Acceptance Criteria) + +- [x] C01 `[frontend]` Refactor `src/components/SelfDisplay.svelte` to use injected `authService` runtime state and direct `authService.logout()` instead of legacy logout relay stores. + - Depends on: T01. + - Validated by: T01. + - Trace: + - "add `export let authService: AuthService`" (`docs/plans/Priority5AuthServiceSlice10Plan.md`, Detailed File Impact) + - "subscribe to `authService.authRuntimeSnapshot`" (`docs/plans/Priority5AuthServiceSlice10Plan.md`, Detailed File Impact) + - "call `authService.logout()` directly in `onLogoutClick`" (`docs/plans/Priority5AuthServiceSlice10Plan.md`, Detailed File Impact) + - "remove `dispatchableStore` and `loginStateStore` imports." (`docs/plans/Priority5AuthServiceSlice10Plan.md`, Detailed File Impact) + +- [x] C02 `[frontend]` Pass the widget-scoped `authService` from `src/components/SimpleComment.svelte` to `SelfDisplay.svelte`. + - Depends on: C01. + - Validated by: `yarn typecheck`. + - Trace: + - "Pass the existing widget-scoped `authService` from `SimpleComment.svelte` to `SelfDisplay.svelte`." (`docs/plans/Priority5AuthServiceSlice10Plan.md`, In Scope) + - "pass `{authService}` to `SelfDisplay.svelte`." (`docs/plans/Priority5AuthServiceSlice10Plan.md`, Detailed File Impact) + - "`SimpleComment.svelte` passes the widget-scoped `authService` to `SelfDisplay.svelte`." (`docs/plans/Priority5AuthServiceSlice10Plan.md`, Acceptance Criteria) + +## Behavior Slices + +### Slice 10A + +Goal: prove and implement direct logout behavior in `SelfDisplay.svelte`. + +Items: T01, C01 + +Type: behavior + +### Slice 10B + +Goal: wire the composition root to provide the widget-scoped auth service to `SelfDisplay.svelte`. + +Items: C02 + +Type: mechanical + +## Conformance QC (Checklist) + +- Missing from plan: none. +- Extra beyond plan: none; each item maps to the plan's file impacts, approach, and validation strategy. +- Atomicity fixes needed: none; each item can be checked and committed independently. +- Validation mapping gaps: none; implementation items are covered by fail-first component tests or typecheck. +- Pass/Fail: checklist achieves plan goals — **Pass**. diff --git a/docs/archive/Priority5AuthServiceSlice10Plan.md b/docs/archive/Priority5AuthServiceSlice10Plan.md new file mode 100644 index 00000000..cd7a93c9 --- /dev/null +++ b/docs/archive/Priority5AuthServiceSlice10Plan.md @@ -0,0 +1,184 @@ +# Priority 5 Auth Service Slice 10 Plan + +Status: approved + +Source backlog: `docs/RepoHealthImprovementBacklog.md` (`Priority 5`) + +Parent plan: `docs/plans/Priority5Completion.md` (Item 10) + +Related artifacts: + +- `docs/archive/Priority5AuthServiceSlice9Plan.md` +- `docs/archive/Priority5AuthServiceSlice9Checklist.md` + +## Goal + +Remove the logout relay dependency from `SelfDisplay.svelte` so logout is triggered through the widget-scoped `auth-service` instead of through `dispatchableStore` and `loginStateStore`. + +## Intent + +This slice is about letting the visible user panel perform logout directly through the shared auth service. + +Today, `SelfDisplay.svelte` uses `loginStateStore` to decide whether auth is processing and whether logout is allowed. When the user clicks the logout button, it dispatches a `logoutIntent` event through `dispatchableStore`, and `Login.svelte` receives that event and calls `authService.logout()`. That keeps logout behavior coupled to a legacy global relay and to the login form component. + +After this slice, `SelfDisplay.svelte` should no longer import or use those legacy stores. It should receive the widget-scoped `authService`, observe auth runtime state from that service, and call `authService.logout()` directly when the user clicks Log out. + +In plain terms: the user panel should log out through the auth service, not ask the login form to do it by shouting through a shared store. + +## In Scope + +- Add an `authService` prop to `SelfDisplay.svelte`. +- Pass the existing widget-scoped `authService` from `SimpleComment.svelte` to `SelfDisplay.svelte`. +- Remove `SelfDisplay.svelte` imports and use of `dispatchableStore`. +- Remove `SelfDisplay.svelte` imports and subscription to `loginStateStore`. +- Have `SelfDisplay.svelte` observe `authService.authRuntimeSnapshot` for auth processing state and `nextEvents`. +- Have `SelfDisplay.svelte` call `authService.logout()` directly from the logout button. +- Preserve the current visible user display and skeleton/processing behavior. +- Add fail-first component tests before production changes. + +## Out of Scope + +- Removing `dispatchableStore` from `Login.svelte`. +- Removing `loginStateStore` selected-tab publication from `Login.svelte`. +- Removing `loginStateStore`, `dispatchableStore`, or `currentUserStore` definitions. +- Removing the temporary Slice 8 auth store bridge. +- Changing `CommentInput.svelte`. +- Changing backend/API contracts. +- Choosing the final direct-props versus thin auth-service-backed store architecture for the whole widget. + +## Constraints + +- Keep `auth-service` widget-scoped; do not introduce a singleton auth-service import. +- Keep the implementation narrow to `SelfDisplay.svelte` logout behavior and its composition-root prop wiring. +- Keep test-writing passes separate from production implementation passes. +- Do not edit tests during implementation unless the implementation stops and explains why a test is wrong. +- Preserve current behavior where the logout button is visible only when auth runtime state allows `LOGOUT`. + +## Current State + +At the start of this slice: + +- `SelfDisplay.svelte` receives `currentUser` as a prop. +- `SelfDisplay.svelte` imports `dispatchableStore` and dispatches `logoutIntent` from its logout button. +- `SelfDisplay.svelte` imports `loginStateStore` to observe `state` and `nextEvents`. +- `Login.svelte` still subscribes to `dispatchableStore` for `logoutIntent`. +- `SimpleComment.svelte` creates the widget-scoped `authService` but does not pass it to `SelfDisplay.svelte`. +- `auth-service.ts` already owns `logout()` and exposes `authRuntimeSnapshot`. + +## Detailed File Impact + +### `src/components/SelfDisplay.svelte` + +Expected role: user panel that observes auth state and delegates logout directly to `authService`. + +Expected changes: + +- add `export let authService: AuthService`, +- subscribe to `authService.authRuntimeSnapshot`, +- derive processing state from `authRuntimeSnapshot.state`, +- derive logout-button visibility from `authRuntimeSnapshot.nextEvents`, +- call `authService.logout()` directly in `onLogoutClick`, +- clean up the auth-service subscription in `onDestroy`, +- remove `dispatchableStore` and `loginStateStore` imports. + +Non-goals: + +- do not change avatar/user-display markup except as required by auth-service wiring. + +### `src/components/SimpleComment.svelte` + +Expected role: current composition root that owns and passes the widget-scoped auth service. + +Expected changes: + +- pass `{authService}` to `SelfDisplay.svelte`. + +Non-goals: + +- do not remove the temporary auth store bridge in this slice. + +### `src/components/Login.svelte` + +Expected role: unchanged during Slice 10. + +Expected changes: + +- no production changes expected. + +Non-goals: + +- do not remove the now-obsolete `logoutIntent` handler in this slice; leave dead relay cleanup to Slice 11. + +### `src/lib/svelte-stores.ts` + +Expected role: unchanged legacy compatibility surface. + +Expected changes: + +- none. + +## Approach + +1. Add fail-first component tests proving `SelfDisplay.svelte` reads auth runtime state from `authService`, calls `authService.logout()` directly, and does not import legacy relay stores. +2. Wire `SelfDisplay.svelte` to the injected auth service and remove legacy store usage. +3. Pass `authService` from `SimpleComment.svelte` to `SelfDisplay.svelte`. +4. Stop there. Do not remove the legacy store definitions, the Slice 8 bridge, or `Login.svelte` dead relay handling. + +## Risks and Mitigations + +- Risk: this slice quietly becomes the cleanup slice by deleting legacy stores or bridge code. + - Mitigation: keep all legacy-store deletion and bridge cleanup deferred to Slice 11. + +- Risk: logout button visibility changes because `SelfDisplay.svelte` observes auth-service state instead of `loginStateStore`. + - Mitigation: derive visibility from the same `nextEvents` shape already bridged from `authService.authRuntimeSnapshot`. + +- Risk: processing skeleton behavior changes unintentionally. + - Mitigation: preserve the existing processing-state vocabulary and cover it with component tests. + +## Acceptance Criteria + +1. `SelfDisplay.svelte` receives `authService` as an explicit prop. +2. `SimpleComment.svelte` passes the widget-scoped `authService` to `SelfDisplay.svelte`. +3. `SelfDisplay.svelte` no longer imports or uses `dispatchableStore`. +4. `SelfDisplay.svelte` no longer imports or subscribes to `loginStateStore`. +5. Clicking the logout button calls `authService.logout()` directly. +6. Logout button visibility still follows whether auth runtime `nextEvents` includes `LOGOUT`. +7. Processing skeleton behavior remains based on auth runtime state. +8. `Login.svelte` logout relay cleanup is deferred to Slice 11. + +## Validation Strategy + +Required evidence types for Slice 10: + +- **Component evidence** + - Pass: `SelfDisplay.svelte` component tests prove logout button visibility comes from `authService.authRuntimeSnapshot`, clicking Log out calls `authService.logout()`, and legacy relay stores are not imported. + - Fail: `SelfDisplay.svelte` still dispatches `logoutIntent` or reads `loginStateStore` for logout behavior. + +- **Type/build evidence** + - Pass: frontend typecheck succeeds after passing `authService` into `SelfDisplay.svelte`. + - Fail: component composition or auth-service prop typing introduces type errors. + +## Open Questions / Assumptions + +- Assumption: `SelfDisplay.svelte` should use `authService.authRuntimeSnapshot` directly rather than a new auth-state store, because the final direct-props versus thin-store architecture decision remains deferred. +- Assumption: leaving the obsolete `Login.svelte` logout relay listener in place until Slice 11 is acceptable because this slice only removes `SelfDisplay.svelte` as a relay producer/consumer. +- Assumption: the temporary Slice 8 auth bridge remains until Slice 11 cleanup. + +## Scope Guard + +The following work is explicitly deferred and must not be folded into Slice 10 without a separate approved plan/checklist update: + +- removing `dispatchableStore` handling from `Login.svelte`, +- deleting `loginStateStore`, `currentUserStore`, or `dispatchableStore`, +- removing the temporary Slice 8 auth bridge, +- changing `CommentInput.svelte`, +- redesigning the frontend auth architecture beyond explicit widget-scoped `authService` props. + +## Conformance QC (Plan) + +- Intent clarity issues: none; the plan states the user-facing goal and the narrow replacement path. +- Missing required sections: none. +- Ambiguities/assumptions to resolve: none blocking; cleanup work is explicitly deferred. +- Validation strategy gaps: none; component and type/build evidence are defined. +- Traceability readiness: ready; scope, acceptance criteria, and validation statements are quoteable under stable headings. +- Pass/Fail: ready for checklist authoring — **Pass**. diff --git a/docs/archive/Priority5AuthServiceSlice11Checklist.md b/docs/archive/Priority5AuthServiceSlice11Checklist.md new file mode 100644 index 00000000..38540d3c --- /dev/null +++ b/docs/archive/Priority5AuthServiceSlice11Checklist.md @@ -0,0 +1,138 @@ +# Priority 5 Auth Service Slice 11 Checklist + +Status: archived, completed + +Classification: approved implementation checklist + +Source plan: `docs/archive/Priority5AuthServiceSlice11Plan.md` + +Parent plan: `docs/plans/Priority5Completion.md` (Item 11: Draft a cleanup slice for removing the temporary auth-service bridge and any legacy auth/session store paths made obsolete by slices 8-10) + +## Scope Lock + +In scope: + +- remove `Login.svelte` use of `dispatchableStore` +- remove `Login.svelte` use of `loginStateStore` +- remove `Login.svelte` handling for `loginIntent` and `logoutIntent` +- keep `Login.svelte` selected-tab UI, selected-tab binding, and `simple_comment_login_tab` persistence +- remove `SimpleComment.svelte` use of `createAuthStoreBridge` +- remove `SimpleComment.svelte` use of `currentUserStore` +- subscribe `SimpleComment.svelte` directly to `authService.currentUser` +- remove migrated component-test imports of `dispatchableStore` that exist only for negative relay spies +- remove obsolete bridge/store unit tests +- delete `src/lib/auth-store-bridge.ts` +- delete `src/lib/svelte-stores.ts` after imports are removed +- keep test-writing passes separate from production implementation passes + +Out of scope: + +- changing `auth-service.ts` command behavior or public service API +- changing `CommentInput.svelte` auth request behavior +- changing `SelfDisplay.svelte` logout behavior +- removing `Login.svelte` selected-tab binding to `CommentInput.svelte` +- removing `simple_comment_login_tab` localStorage persistence +- deciding the broader direct-props versus thin auth-service-backed store architecture +- adding a new event bus, auth controller, `AuthRuntime.svelte`, or broad auth workflow module +- splitting `Login.svelte` into smaller form components + +## Slice Intent + +This slice removes the temporary migration scaffolding left after slices 8-10. After the slice, runtime auth UI should continue to work through the widget-scoped `authService`, but `dispatchableStore`, `loginStateStore`, `currentUserStore`, and `createAuthStoreBridge` should no longer be part of runtime or test code. + +## Atomic Checklist Items + +- [x] T01 `[tests]` Add fail-first cleanup/source-guard component tests and remove migrated component-test negative relay spies. + - Depends on: none. + - Required coverage: + - `Login.svelte` source must not import `dispatchableStore` or `loginStateStore`. + - `Login.svelte` source must not handle `loginIntent` or `logoutIntent`. + - `SimpleComment.svelte` source must not import or install `createAuthStoreBridge`. + - `SimpleComment.svelte` source must not import or subscribe to `currentUserStore`. + - `CommentInput.svelte` tests must keep positive `authService.requestAuth()` coverage without importing `dispatchableStore` for a negative spy. + - `SelfDisplay.svelte` tests must keep positive `authService.logout()` coverage without importing `dispatchableStore` for a negative spy. + - Existing direct auth-service delegation coverage must remain in place. + - Trace: + - "Add or revise fail-first component/source tests for the desired cleanup state." (`docs/plans/Priority5AuthServiceSlice11Plan.md`, Approach) + - "Replace migrated component-test negative relay spies with source guards or direct auth-service assertions that do not import `dispatchableStore`." (`docs/plans/Priority5AuthServiceSlice11Plan.md`, In Scope) + - "Do not keep `dispatchableStore` solely so tests can spy on events that no runtime component should dispatch." (`docs/plans/Priority5AuthServiceSlice11Plan.md`, Constraints) + - "Keep positive behavior assertions against `authService` (`requestAuth()` for comments, `logout()` for self display)." (`docs/plans/Priority5AuthServiceSlice11Plan.md`, DispatchableStore Test Caveat) + +- [x] C01 `[frontend]` Remove dead legacy relay handling and selected-tab store publication from `src/components/Login.svelte`. + - Depends on: T01. + - Validated by: T01 and existing `Login.svelte` auth-service component tests. + - Trace: + - "Remove `Login.svelte` imports and use of `dispatchableStore`." (`docs/plans/Priority5AuthServiceSlice11Plan.md`, In Scope) + - "Remove `Login.svelte` imports and use of `loginStateStore`." (`docs/plans/Priority5AuthServiceSlice11Plan.md`, In Scope) + - "Remove `Login.svelte` handling for `loginIntent` and `logoutIntent`." (`docs/plans/Priority5AuthServiceSlice11Plan.md`, In Scope) + - "keep `bind:selectedTab` support through the existing `selectedTab` export and `$: selectedTab = selectedIndex`" (`docs/plans/Priority5AuthServiceSlice11Plan.md`, Detailed File Impact) + - "keep `simple_comment_login_tab` localStorage persistence." (`docs/plans/Priority5AuthServiceSlice11Plan.md`, Detailed File Impact) + +- [x] C02 `[frontend]` Replace `src/components/SimpleComment.svelte` bridge/global-store plumbing with a direct `authService.currentUser` subscription. + - Depends on: T01. + - Validated by: T01 and `yarn typecheck`. + - Trace: + - "Remove `SimpleComment.svelte` installation of `createAuthStoreBridge`." (`docs/plans/Priority5AuthServiceSlice11Plan.md`, In Scope) + - "Remove `SimpleComment.svelte` subscription to `currentUserStore`." (`docs/plans/Priority5AuthServiceSlice11Plan.md`, In Scope) + - "Have `SimpleComment.svelte` subscribe directly to `authService.currentUser` to keep its local `currentUser` prop flow updated." (`docs/plans/Priority5AuthServiceSlice11Plan.md`, In Scope) + - "`SimpleComment.svelte` subscribes directly to `authService.currentUser`, preserving the existing local `currentUser` prop flow without the temporary bridge." (`docs/plans/Priority5AuthServiceSlice11Plan.md`, Risks and Mitigations) + +- [x] T02 `[tests]` Remove obsolete bridge/store test dependencies before deleting their modules. + - Depends on: C01, C02. + - Required cleanup: + - remove `src/tests/frontend/auth-store-bridge.test.ts`, + - remove `src/tests/frontend/svelte-stores.test.ts`, + - remove `currentUserStore`, `loginStateStore`, and `dispatchableStore` imports/resets from `src/tests/frontend/components/vitest.setup.ts`, + - verify component tests still cover auth-service behavior without legacy store imports. + - Trace: + - "Remove or revise tests that exist only to exercise the deleted bridge/store modules." (`docs/plans/Priority5AuthServiceSlice11Plan.md`, In Scope) + - "remove `src/tests/frontend/auth-store-bridge.test.ts`" (`docs/plans/Priority5AuthServiceSlice11Plan.md`, Detailed File Impact) + - "remove `src/tests/frontend/svelte-stores.test.ts`" (`docs/plans/Priority5AuthServiceSlice11Plan.md`, Detailed File Impact) + - "remove legacy store resets from component test setup once no component tests import those stores." (`docs/plans/Priority5AuthServiceSlice11Plan.md`, Detailed File Impact) + +- [x] C03 `[cleanup]` Delete obsolete bridge/store modules and prove no runtime or test imports remain. + - Depends on: T02. + - Validated by: repository import search, `yarn typecheck`, and `yarn run ci:local`. + - Required cleanup: + - delete `src/lib/auth-store-bridge.ts`, + - delete `src/lib/svelte-stores.ts`, + - verify no runtime or test imports remain for `auth-store-bridge` or `svelte-stores`. + - Trace: + - "Delete the obsolete `src/lib/auth-store-bridge.ts` module." (`docs/plans/Priority5AuthServiceSlice11Plan.md`, In Scope) + - "Delete obsolete `src/lib/svelte-stores.ts` relay/store definitions if no runtime or test imports remain." (`docs/plans/Priority5AuthServiceSlice11Plan.md`, In Scope) + - "Pass: repository search finds no runtime or test imports of `src/lib/auth-store-bridge.ts` or `src/lib/svelte-stores.ts` after deletion." (`docs/plans/Priority5AuthServiceSlice11Plan.md`, Validation Strategy) + - "Fail: deleted modules still have importers or must be kept alive for tests." (`docs/plans/Priority5AuthServiceSlice11Plan.md`, Validation Strategy) + +## Behavior Slices + +### Slice 11A + +Goal: prove and remove the dead Login relay path. + +Items: T01, C01 + +Type: behavior + +### Slice 11B + +Goal: replace the temporary composition-root bridge with direct auth-service current-user observation. + +Items: C02 + +Type: behavior + +### Slice 11C + +Goal: delete obsolete bridge/store test scaffolding and modules after all imports are gone. + +Items: T02, C03 + +Type: mechanical + +## Conformance QC (Checklist) + +- Missing from plan: none. +- Extra beyond plan: none; each item maps to the plan's scope, file impacts, approach, acceptance criteria, or validation strategy. +- Atomicity fixes needed: none; each item can be checked and committed independently. +- Validation mapping gaps: none; implementation items are covered by source/component tests, import search, typecheck, and local CI. +- Pass/Fail: checklist achieves plan goals — **Pass**. diff --git a/docs/archive/Priority5AuthServiceSlice11Plan.md b/docs/archive/Priority5AuthServiceSlice11Plan.md new file mode 100644 index 00000000..31d7071a --- /dev/null +++ b/docs/archive/Priority5AuthServiceSlice11Plan.md @@ -0,0 +1,267 @@ +# Priority 5 Auth Service Slice 11 Plan + +Status: archived, completed + +Source backlog: `docs/RepoHealthImprovementBacklog.md` (`Priority 5`) + +Parent plan: `docs/plans/Priority5Completion.md` (Item 11) + +Related artifacts: + +- `docs/archive/Priority5AuthServiceSlice8Plan.md` +- `docs/archive/Priority5AuthServiceSlice9Plan.md` +- `docs/archive/Priority5AuthServiceSlice10Plan.md` + +## Goal + +Remove the temporary auth-service bridge and the legacy auth/session relay stores made obsolete by slices 8-10. + +## Intent + +This slice is about finishing the cleanup after the careful auth-service extraction work. + +`CommentInput.svelte` no longer asks `Login.svelte` to log in through `dispatchableStore`, and `SelfDisplay.svelte` no longer asks `Login.svelte` to log out through `dispatchableStore`. Both now work through the widget-scoped `authService`. + +That leaves a few old support pieces behind: `Login.svelte` still listens for relay events that no runtime component sends anymore, `SimpleComment.svelte` still installs the temporary bridge from `authService` into legacy global stores, and tests still import `dispatchableStore` only to prove the old path is not called. + +After this slice, the widget should keep the same visible auth behavior, but the old relay path should be gone. `SimpleComment.svelte` should observe `authService.currentUser` directly, `Login.svelte` should no longer import or publish through legacy stores, and the obsolete bridge/store modules should be deleted. + +In plain terms: now that the components talk to the auth service directly, remove the scaffolding that helped us migrate there. + +## Motivation + +Slice 8 intentionally introduced `src/lib/auth-store-bridge.ts` as temporary migration scaffolding so existing consumers could keep working while ownership moved to `auth-service`. + +Slices 9 and 10 removed the two runtime consumers that needed that scaffolding: + +- `CommentInput.svelte` now requests auth through `authService`. +- `SelfDisplay.svelte` now reads logout state and performs logout through `authService`. + +Keeping the bridge and relay stores after that point creates avoidable confusion: future work could accidentally revive `dispatchableStore` / `loginStateStore`, and tests could keep the dead relay alive by importing `dispatchableStore` only for negative assertions. + +## In Scope + +- Remove `Login.svelte` imports and use of `dispatchableStore`. +- Remove `Login.svelte` imports and use of `loginStateStore`. +- Remove `Login.svelte` handling for `loginIntent` and `logoutIntent`. +- Preserve `Login.svelte` form-local state, validation, selected-tab UI, selected-tab binding, and `simple_comment_login_tab` localStorage behavior. +- Remove `SimpleComment.svelte` installation of `createAuthStoreBridge`. +- Remove `SimpleComment.svelte` subscription to `currentUserStore`. +- Have `SimpleComment.svelte` subscribe directly to `authService.currentUser` to keep its local `currentUser` prop flow updated. +- Delete the obsolete `src/lib/auth-store-bridge.ts` module. +- Delete obsolete `src/lib/svelte-stores.ts` relay/store definitions if no runtime or test imports remain. +- Remove or revise tests that exist only to exercise the deleted bridge/store modules. +- Replace migrated component-test negative relay spies with source guards or direct auth-service assertions that do not import `dispatchableStore`. + +## Out of Scope + +- Changing `auth-service.ts` command behavior or public service API. +- Changing backend/API contracts. +- Changing `CommentInput.svelte` auth request behavior. +- Changing `SelfDisplay.svelte` logout behavior. +- Removing `Login.svelte` selected-tab binding to `CommentInput.svelte`. +- Removing `simple_comment_login_tab` localStorage persistence. +- Deciding the broader direct-props versus thin auth-service-backed store architecture for future auth state. +- Adding a new event bus, auth controller, `AuthRuntime.svelte`, or broad auth workflow module. +- Splitting `Login.svelte` into smaller form components. + +## Constraints + +- Keep test-writing and production implementation passes separate. +- Tests must move from failing to passing through production code changes only. +- If implementation discovers an actual runtime consumer of `dispatchableStore`, `loginStateStore`, `currentUserStore`, or `createAuthStoreBridge` outside the expected cleanup surfaces, stop and revise the plan. +- Do not keep `dispatchableStore` solely so tests can spy on events that no runtime component should dispatch. +- Do not replace the old relay with another ad-hoc event bus. + +## Current State + +At the start of this slice: + +- `Login.svelte` imports `dispatchableStore` and `loginStateStore`. +- `Login.svelte` subscribes to `dispatchableStore` and handles `loginIntent` / `logoutIntent`. +- `Login.svelte` publishes selected-tab state through `loginStateStore`. +- `SimpleComment.svelte` imports and installs `createAuthStoreBridge(authService)`. +- `SimpleComment.svelte` subscribes to `currentUserStore` to keep local `currentUser` updated. +- `src/lib/auth-store-bridge.ts` publishes `authService.currentUser` and `authService.authRuntimeSnapshot` to legacy stores. +- `src/lib/svelte-stores.ts` defines `dispatchableStore`, `loginStateStore`, and `currentUserStore`. +- `CommentInput.svelte` and `SelfDisplay.svelte` no longer import or use those legacy stores. +- Some component tests still import `dispatchableStore` only to prove migrated components no longer dispatch legacy relay events. + +## Detailed File Impact + +### `src/components/Login.svelte` + +Expected role: form-local auth UI that delegates auth commands to `authService`. + +Expected changes: + +- remove the `dispatchableStore` / `loginStateStore` import, +- remove the `dispatchableStore.subscribe(...)` relay listener, +- remove `unsubscribeDispatchableStore()` from `onDestroy`, +- remove `$: loginStateStore.set({ select: selectedIndex })`, +- keep `bind:selectedTab` support through the existing `selectedTab` export and `$: selectedTab = selectedIndex`, +- keep `simple_comment_login_tab` localStorage persistence. + +### `src/components/SimpleComment.svelte` + +Expected role: widget composition root that owns the widget-scoped `authService`. + +Expected changes: + +- remove `createAuthStoreBridge` import and setup, +- remove `currentUserStore` import and subscription, +- subscribe directly to `authService.currentUser`, +- clean up that subscription in `onDestroy`, +- keep passing the same widget-scoped `authService` to child components. + +### `src/lib/auth-store-bridge.ts` + +Expected role: obsolete temporary migration helper. + +Expected changes: + +- delete the file after `SimpleComment.svelte` no longer imports it. + +### `src/lib/svelte-stores.ts` + +Expected role: obsolete relay/global-store module after bridge and relay consumers are removed. + +Expected changes: + +- delete the file after runtime and test imports are removed. + +### `src/tests/frontend/components/Login.auth-service.test.ts` + +Expected role: component coverage for `Login.svelte` auth-service delegation. + +Expected changes: + +- remove tests that assert legacy `dispatchableStore` relay behavior, +- remove tests that assert selected-tab publication to `loginStateStore`, +- add or keep source guards proving `Login.svelte` no longer imports legacy relay stores, +- preserve tests for direct auth-service delegation, form-local validation, selected-tab UI behavior, and selected-tab binding. + +### `src/tests/frontend/components/CommentInput.auth-service.test.ts` + +Expected role: component coverage for `CommentInput.svelte` auth-service auth requests. + +Expected changes: + +- remove the `dispatchableStore` import used only for negative spying, +- keep direct assertions that `authService.requestAuth()` is called, +- keep source guards proving `CommentInput.svelte` does not import legacy relay stores. + +### `src/tests/frontend/components/SelfDisplay.auth-service.test.ts` + +Expected role: component coverage for `SelfDisplay.svelte` auth-service logout. + +Expected changes: + +- remove the `dispatchableStore` import used only for negative spying, +- keep direct assertions that `authService.logout()` is called, +- keep source guards proving `SelfDisplay.svelte` does not import legacy relay stores. + +### Obsolete Bridge/Store Tests + +Expected role: removed along with deleted modules. + +Expected changes: + +- remove `src/tests/frontend/auth-store-bridge.test.ts`, +- remove `src/tests/frontend/svelte-stores.test.ts`, +- remove legacy store resets from component test setup once no component tests import those stores. + +## DispatchableStore Test Caveat + +Slice 10 left a test caveat: some migrated component tests still import `dispatchableStore` only to assert that `loginIntent` or `logoutIntent` is not dispatched. + +The minimal solution is not to keep `dispatchableStore` alive for those tests. Instead: + +1. Keep positive behavior assertions against `authService` (`requestAuth()` for comments, `logout()` for self display). +2. Keep source guards proving migrated runtime components do not import `dispatchableStore` or `loginStateStore`. +3. Remove the negative relay spies and then delete `dispatchableStore` with the rest of `src/lib/svelte-stores.ts`. + +This preserves regression coverage without retaining a dead event bus as test scaffolding. + +## Approach + +1. Add or revise fail-first component/source tests for the desired cleanup state. +2. Remove dead relay handling and legacy selected-tab publication from `Login.svelte`. +3. Replace `SimpleComment.svelte` bridge/global-store subscription with a direct `authService.currentUser` subscription. +4. Remove obsolete test dependencies on the soon-to-be-deleted bridge/store modules. +5. Delete the obsolete bridge/store modules. +6. Validate no runtime or test imports remain for `auth-store-bridge` or `svelte-stores`. + +## Risks and Mitigations + +- Risk: removing `currentUserStore` breaks current-user propagation from auth commands to discussion/comment/self-display components. + - Mitigation: `SimpleComment.svelte` subscribes directly to `authService.currentUser`, preserving the existing local `currentUser` prop flow without the temporary bridge. + +- Risk: deleting `loginStateStore` removes selected-tab coordination needed by `CommentInput.svelte`. + - Mitigation: `CommentInput.svelte` already uses `bind:selectedTab` on `Login.svelte`; keep that direct binding and preserve selected-tab UI/localStorage behavior. + +- Risk: deleting `dispatchableStore` hides a regression where components silently stop requesting auth/logout. + - Mitigation: rely on positive auth-service component assertions and source guards rather than negative relay spies. + +- Risk: cleanup grows into a frontend state architecture decision. + - Mitigation: use existing `authService` stores and explicit props only; do not introduce a new store or event bus. + +## Acceptance Criteria + +1. `Login.svelte` no longer imports `dispatchableStore` or `loginStateStore`. +2. `Login.svelte` no longer subscribes to or handles `loginIntent` / `logoutIntent`. +3. `Login.svelte` still preserves form-local validation, selected-tab UI, selected-tab binding, and `simple_comment_login_tab` localStorage behavior. +4. `SimpleComment.svelte` no longer imports or installs `createAuthStoreBridge`. +5. `SimpleComment.svelte` no longer imports or subscribes to `currentUserStore`. +6. `SimpleComment.svelte` directly subscribes to `authService.currentUser` and cleans up that subscription on destroy. +7. `src/lib/auth-store-bridge.ts` is removed. +8. `src/lib/svelte-stores.ts` is removed if no imports remain. +9. Migrated component tests do not import `dispatchableStore` solely for negative relay assertions. +10. Obsolete bridge/store unit tests are removed with the modules they covered. +11. No runtime or test imports remain for `auth-store-bridge` or `svelte-stores`. + +## Validation Strategy + +Required evidence types for Slice 11: + +- **Component/source evidence** + - Pass: component/source tests prove `Login.svelte`, `CommentInput.svelte`, `SelfDisplay.svelte`, and `SimpleComment.svelte` no longer import or use legacy auth relay stores or bridge plumbing. + - Fail: any migrated runtime component still imports `dispatchableStore`, `loginStateStore`, `currentUserStore`, or `createAuthStoreBridge`. + +- **Behavior evidence** + - Pass: existing component tests still prove `Login.svelte` delegates auth commands to `authService`, `CommentInput.svelte` requests auth through `authService`, and `SelfDisplay.svelte` logs out through `authService`. + - Fail: cleanup removes or weakens direct auth-service behavior coverage. + +- **Import cleanup evidence** + - Pass: repository search finds no runtime or test imports of `src/lib/auth-store-bridge.ts` or `src/lib/svelte-stores.ts` after deletion. + - Fail: deleted modules still have importers or must be kept alive for tests. + +- **Type/build evidence** + - Pass: `yarn typecheck` and `yarn run ci:local` pass. + - Fail: deleting the bridge/store modules causes unresolved imports, type errors, or test regressions. + +## Open Questions / Assumptions + +- Assumption: `bind:selectedTab` is now the only needed selected-tab coordination path between `Login.svelte` and `CommentInput.svelte`. +- Assumption: no runtime component outside the expected surfaces still depends on `dispatchableStore`, `loginStateStore`, `currentUserStore`, or `createAuthStoreBridge`. +- Assumption: deleting `src/lib/svelte-stores.ts` is acceptable once runtime and test imports are gone; if an unexpected non-auth consumer appears, stop and revise the plan. + +## Scope Guard + +The following work is explicitly deferred and must not be folded into Slice 11 without a separate approved plan/checklist update: + +- broader auth state architecture decisions, +- replacing explicit auth-service props with a new store, +- deleting selected-tab localStorage persistence, +- changing auth-service command semantics, +- changing comment submission behavior, +- splitting or redesigning `Login.svelte`. + +## Conformance QC (Plan) + +- Intent clarity issues: none; the plan states the cleanup goal and why the temporary bridge/stores are now obsolete. +- Missing required sections: none. +- Ambiguities/assumptions to resolve: none blocking; expected cleanup surfaces and stop conditions are explicit. +- Validation strategy gaps: none; source/component, behavior, import-cleanup, and type/build evidence are defined. +- Traceability readiness: ready; scope, acceptance criteria, and validation statements are quoteable under stable headings. +- Pass/Fail: ready for checklist authoring — **Pass**. diff --git a/docs/archive/Priority5AuthServiceSlice1Checklist.md b/docs/archive/Priority5AuthServiceSlice1Checklist.md new file mode 100644 index 00000000..0438d0c2 --- /dev/null +++ b/docs/archive/Priority5AuthServiceSlice1Checklist.md @@ -0,0 +1,90 @@ +# Priority 5 Auth Service Slice 1 Checklist + +Status: archived, completed + +Archived after all slice-1 checklist items were completed. + +Classification: proposed implementation checklist draft (not approved) + +Source plan: `docs/RepoHealthImprovementBacklog.md` (Priority 5: Frontend Architecture Decoupling) + +## Scope Lock (from Source Plan) + +In scope: + +- reduce component-bound behavior coupling where the repo already flags it as a concern +- clarify boundaries between view components, state machines, and auth/workflow logic + +Success signals to satisfy: + +- auth and identity flows are less dependent on component presence +- UI modules are easier to test at the right boundary +- login-related state, side effects, and rendering responsibilities are easier to explain as separate concerns + +Out of scope: + +- changing backend/API contracts +- introducing auth feature behavior changes beyond boundary ownership refactor +- request/outcome coordination for `CommentInput.svelte` and reply flows +- moving login form-local field state or field-level validation UI out of `Login.svelte` +- extracting auth API workflows in this slice + +## Slice Intent + +This slice establishes only the minimal contract for a widget-scoped `auth-service` that owns login-machine runtime lifecycle outside `Login.svelte`. + +Execution note: + +- maintainer preference requires three separate sessions for this slice: + - export scaffold + - fail-first tests + - implementation to green +- if implementation cannot reach green without changing the tests, stop and discuss rather than modifying tests in the implementation session + +## Atomic Checklist Items + +- [x] C01 `[frontend]` Add the minimal slice-1 runtime scaffold to `src/lib/auth-service.ts` by defining `AuthSessionState` from `src/lib/login.xstate.ts`, adding `destroy` to the `AuthService` lifecycle contract, and preserving exploratory request/outcome, current-user, and auth-command surfaces unless they directly conflict with the slice-1 runtime contract. + - Depends on: none. + - Trace: + - "clarify boundaries between view components, state machines, and auth/workflow logic" (Priority 5) + - "auth/session behavior is coupled to component presence and lifecycle rather than being owned by a smaller dedicated workflow/service boundary" (Priority 5) + +- [x] T01 `[tests]` Add fail-first frontend tests for `src/lib/auth-service.ts` in `src/tests/frontend/auth-service.test.ts` that prove `createAuthService()` creates and owns a live interpreted `src/lib/login.xstate.ts` runtime, publishes `sessionState` from that runtime, and disposes the runtime when `destroy()` is called. + - Depends on: C01. + - Trace: + - "UI modules are easier to test at the right boundary" (Priority 5) + - "clarify boundaries between view components, state machines, and auth/workflow logic" (Priority 5) + +- [x] C02 `[frontend]` Implement slice-1 runtime ownership in `src/lib/auth-service.ts` so `createAuthService()` owns a live interpreted instance of `src/lib/login.xstate.ts`, publishes session state from that runtime rather than manual string stores, and disposes the runtime through `destroy()`. + - Depends on: T01. + - Validated by: T01. + - Trace: + - "it drives the `loginMachine` state machine" (Priority 5) + - "auth/session behavior is coupled to component presence and lifecycle rather than being owned by a smaller dedicated workflow/service boundary" (Priority 5) + - success target: "auth and identity flows are less dependent on component presence" (Priority 5) + +## Behavior Slices + +### Slice 1A + +Goal: define the minimal public contract for auth-service runtime ownership while preserving non-conflicting exploratory auth-service surfaces for later slices. + +Items: C01 + +Type: behavior + +### Slice 1B + +Goal: lock the slice-1 runtime contract in tests before runtime implementation begins. + +Items: T01 + +Type: behavior + +### Slice 1C + +Goal: move login-machine runtime lifecycle ownership into auth-service and expose it through the tested contract. + +Items: C02 + +Type: behavior diff --git a/docs/archive/Priority5AuthServiceSlice2Checklist.md b/docs/archive/Priority5AuthServiceSlice2Checklist.md new file mode 100644 index 00000000..24a836f0 --- /dev/null +++ b/docs/archive/Priority5AuthServiceSlice2Checklist.md @@ -0,0 +1,92 @@ +# Priority 5 Auth Service Slice 2 Checklist + +Status: archived, completed + +Archived after all slice-2 checklist items were completed. + +Classification: proposed implementation checklist draft (not approved) + +Source plan: `docs/RepoHealthImprovementBacklog.md` (Priority 5: Frontend Architecture Decoupling) + +## Scope Lock (from Source Plan) + +In scope: + +- reduce component-bound behavior coupling where the repo already flags it as a concern +- clarify boundaries between view components, state machines, and auth/workflow logic + +Success signals to satisfy: + +- auth and identity flows are less dependent on component presence +- UI modules are easier to test at the right boundary +- login-related state, side effects, and rendering responsibilities are easier to explain as separate concerns + +Out of scope: + +- changing backend/API contracts +- introducing auth feature behavior changes beyond boundary ownership refactor +- login, signup, guest-login, and logout command implementation +- request/outcome coordination for `CommentInput.svelte` and reply flows +- login-tab ownership and form-local field/validation UI concerns +- localStorage/session persistence extraction + +## Slice Intent + +This slice moves initial auth verification and authenticated-user publication into `auth-service` so session bootstrap no longer depends on `Login.svelte` side effects. + +Execution note: + +- maintainer preference requires three separate sessions for this slice: + - export scaffold + - fail-first tests + - implementation to green +- if implementation cannot reach green without changing the tests, stop and discuss rather than modifying tests in the implementation session + +## Atomic Checklist Items + +- [x] C01 `[frontend]` Tighten the slice-2 public contract in `src/lib/auth-service.ts` so `currentUser`, `init`, and `CreateAuthServiceOptions.initialUser` are the explicit public surface for initial auth verification and authenticated-user publication, while preserving non-conflicting exploratory auth-service command and request/outcome surfaces unchanged. + - Depends on: none. + - Validated by: T01. + - Trace: + - "clarify boundaries between view components, state machines, and auth/workflow logic" (Priority 5) + - "auth/session behavior is coupled to component presence and lifecycle rather than being owned by a smaller dedicated workflow/service boundary" (Priority 5) + +- [x] T01 `[tests]` Add fail-first frontend tests for slice-2 init behavior in `src/tests/frontend/auth-service.init.test.ts` that prove `init()` drives auth bootstrap through `auth-service` rather than `Login.svelte`, calls `verifySelf()` when no `initialUser` is provided, transitions `sessionState` to `loggedIn` and publishes `currentUser` on successful verification, leaves `currentUser` undefined and transitions to `loggedOut` on `401` verification failure, leaves `currentUser` undefined and transitions to `error` on non-`401` verification failure, and defines the `initialUser` contract by proving whether `init()` skips `verifySelf()` when `initialUser` is supplied or still verifies and replaces it. + - Depends on: C01. + - Trace: + - "UI modules are easier to test at the right boundary" (Priority 5) + - "auth and identity flows are less dependent on component presence" (Priority 5) + +- [x] C02 `[frontend]` Implement slice-2 bootstrap behavior in `src/lib/auth-service.ts` so `init()` owns the `verifySelf` auth API side effect, maps verification outcomes onto the live interpreted `src/lib/login.xstate.ts` runtime, and publishes `currentUser` from the service rather than from `Login.svelte`. + - Depends on: T01. + - Validated by: T01. + - Trace: + - "it performs auth-related API calls such as verify" (Priority 5) + - "it performs mount/unmount side effects that influence app-level auth behavior" (Priority 5) + - success target: "login-related state, side effects, and rendering responsibilities are easier to explain as separate concerns" (Priority 5) + +## Behavior Slices + +### Slice 2A + +Goal: make auth-service's public bootstrap contract explicit without expanding into login submission flows. + +Items: C01 + +Type: behavior + +### Slice 2B + +Goal: lock the init verification and current-user publication contract in fail-first tests. + +Items: T01 + +Type: behavior + +### Slice 2C + +Goal: move initial auth verification and current-user publication into auth-service runtime ownership. + +Items: C02 + +Type: behavior diff --git a/docs/archive/Priority5AuthServiceSlice3Checklist.md b/docs/archive/Priority5AuthServiceSlice3Checklist.md new file mode 100644 index 00000000..f52ac56e --- /dev/null +++ b/docs/archive/Priority5AuthServiceSlice3Checklist.md @@ -0,0 +1,105 @@ +# Priority 5 Auth Service Slice 3 Checklist + +Status: archived, completed + +Archived after all slice-3 checklist items were completed and focused +auth-service validation passed. + +Classification: proposed implementation checklist draft (not approved) + +Source plan: `docs/RepoHealthImprovementBacklog.md` (Priority 5: Frontend Architecture Decoupling) + +## Scope Lock (from Source Plan) + +In scope: + +- reduce component-bound behavior coupling where the repo already flags it as a concern +- clarify boundaries between view components, state machines, and auth/workflow logic + +Success signals to satisfy: + +- auth and identity flows are less dependent on component presence +- UI modules are easier to test at the right boundary +- login-related state, side effects, and rendering responsibilities are easier to explain as separate concerns + +Out of scope: + +- changing backend/API contracts +- introducing auth feature behavior changes beyond boundary ownership refactor +- signup and guest-login command implementation +- request/outcome coordination for `CommentInput.svelte` and reply flows +- login-tab ownership and form-local field/validation UI concerns +- localStorage/session persistence extraction + +## Slice Intent + +This slice moves `login` and `logout` command ownership into `auth-service` so those auth API side effects no longer depend on `Login.svelte`. + +Execution note: + +- maintainer preference requires three separate sessions for this slice: + - export scaffold + - fail-first tests + - implementation to green +- if implementation cannot reach green without changing the tests, stop and discuss rather than modifying tests in the implementation session + +## Atomic Checklist Items + +- [x] C01 `[frontend]` Tighten the slice-3 public contract in `src/lib/auth-service.ts` so `login` and `logout` are the explicit slice-owned command surfaces for authenticated-session command ownership, while preserving non-conflicting exploratory `signup`, `loginGuest`, and request/outcome surfaces unchanged. + - Depends on: none. + - Validated by: T01, T02. + - Trace: + - "clarify boundaries between view components, state machines, and auth/workflow logic" (Priority 5) + - "auth/session behavior is coupled to component presence and lifecycle rather than being owned by a smaller dedicated workflow/service boundary" (Priority 5) + +- [x] T01 `[tests]` Add fail-first frontend tests for `login` command ownership in `src/tests/frontend/auth-service.login.test.ts` that prove `auth-service.login()` owns the `postAuth` side effect, drives the live `src/lib/login.xstate.ts` runtime through the login success/error path rather than leaving the behavior in `Login.svelte`, publishes authenticated user state on successful login, and leaves `currentUser` undefined while transitioning to `error` on failed login. + - Depends on: C01. + - Trace: + - "it performs auth-related API calls such as ... login" (Priority 5) + - "UI modules are easier to test at the right boundary" (Priority 5) + +- [x] C02 `[frontend]` Implement `login` command ownership in `src/lib/auth-service.ts` so `login()` owns the `postAuth` side effect, maps success/error onto the live interpreted `src/lib/login.xstate.ts` runtime, and publishes authenticated user state from the service rather than from `Login.svelte`. + - Depends on: T01. + - Validated by: T01. + - Trace: + - "it performs auth-related API calls such as ... login" (Priority 5) + - success target: "login-related state, side effects, and rendering responsibilities are easier to explain as separate concerns" (Priority 5) + +- [x] T02 `[tests]` Add fail-first frontend tests for `logout` command ownership in `src/tests/frontend/auth-service.logout.test.ts` that prove `auth-service.logout()` owns the `deleteAuth` side effect, drives the live `src/lib/login.xstate.ts` runtime through the logout success/error path rather than leaving the behavior in `Login.svelte`, clears `currentUser` on successful logout, and transitions to `error` on failed logout. + - Depends on: C01, C02. + - Trace: + - "it performs auth-related API calls such as ... logout" (Priority 5) + - "UI modules are easier to test at the right boundary" (Priority 5) + +- [x] C03 `[frontend]` Implement `logout` command ownership in `src/lib/auth-service.ts` so `logout()` owns the `deleteAuth` side effect, maps success/error onto the live interpreted `src/lib/login.xstate.ts` runtime, and clears authenticated user state from the service rather than from `Login.svelte`. + - Depends on: T02. + - Validated by: T02. + - Trace: + - "it performs auth-related API calls such as ... logout" (Priority 5) + - success target: "login-related state, side effects, and rendering responsibilities are easier to explain as separate concerns" (Priority 5) + +## Behavior Slices + +### Slice 3A + +Goal: make `login` and `logout` explicit auth-service command surfaces without expanding into signup, guest login, or consumer request/outcome seams. + +Items: C01 + +Type: behavior + +### Slice 3B + +Goal: lock `login` command ownership in fail-first tests and then move the `postAuth` side effect into auth-service. + +Items: T01, C02 + +Type: behavior + +### Slice 3C + +Goal: lock `logout` command ownership in fail-first tests and then move the `deleteAuth` side effect into auth-service. + +Items: T02, C03 + +Type: behavior diff --git a/docs/archive/Priority5AuthServiceSlice4Checklist.md b/docs/archive/Priority5AuthServiceSlice4Checklist.md new file mode 100644 index 00000000..4a4c3867 --- /dev/null +++ b/docs/archive/Priority5AuthServiceSlice4Checklist.md @@ -0,0 +1,76 @@ +# Priority 5 Auth Service Slice 4 Checklist + +Status: archived, completed + +Classification: proposed implementation checklist draft (not approved) + +Source plan: `docs/plans/Priority5Completion.md` (Item 4: Draft a slice for `signup()` command ownership in `auth-service`) + +## Scope Lock + +In scope: + +- move `signup()` command ownership into `src/lib/auth-service.ts` +- make `auth-service.signup()` own the `createUser` auth/account side effect currently handled by `Login.svelte` +- compose existing `src/apiClient.ts` exports (`createUser`, and the existing service-owned login/session verification path through `postAuth` / `verifySelf`) rather than duplicating HTTP transport or request-body encoding logic +- preserve the existing post-signup behavior path: successful signup proceeds through the login/auth verification path and publishes authenticated `currentUser` +- validate the service boundary with fail-first tests before implementation + +Out of scope: + +- changing backend/API contracts +- implementing custom fetch, auth header, response-resolution, or user transport logic inside `auth-service` +- adding new auth/user API helpers outside `src/apiClient.ts` +- changing signup form-local field state or field-level validation UI in `Login.svelte` +- rewiring `Login.svelte` to call `auth-service.signup()` +- moving guest-login, guest-profile-update, localStorage, or shared-store behavior +- modifying `CommentInput.svelte`, `SelfDisplay.svelte`, or relay-store behavior +- editing tests during the implementation pass; if the fail-first tests cannot be made green through production-code changes only, stop and discuss + +## Slice Intent + +This slice moves signup command ownership into `auth-service` so user creation and the follow-on authenticated-session transition no longer depend on `Login.svelte` state handlers. + +The existing `Login.svelte` behavior creates the user, transitions the login machine through the signup success path, then continues into login/session verification. Slice 4 should preserve that behavior at the service boundary while keeping form-local validation and component rewiring out of scope. + +`src/apiClient.ts` already exposes the API primitives needed for this slice: +`createUser` for account creation, `postAuth` for login/authentication, and +`verifySelf` for publishing the authenticated user. `auth-service` should +compose those primitives. If implementation discovers a missing reusable user +or auth API primitive, stop and propose adding it to `src/apiClient.ts` rather +than creating a one-off client inside `auth-service`. + +## Atomic Checklist Items + +- [x] T01 `[tests]` Add fail-first frontend tests for `signup` command ownership in `src/tests/frontend/auth-service.signup.test.ts` proving `auth-service.signup()` owns the `createUser` side effect via `src/apiClient.ts`, maps the service payload to the existing `createUser` input shape, drives the live `src/lib/login.xstate.ts` runtime through the signup success path, performs the existing post-signup authentication/verification continuation through existing `apiClient.ts` primitives, publishes authenticated `currentUser` on successful signup, and leaves `currentUser` undefined while transitioning to `error` on signup failure. + - Depends on: none. + - Trace: + - "`Login.svelte` still contains legacy direct calls ... signup uses `createUser(userInfo)` from the `signingUp` state handler." (`docs/plans/Priority5Completion.md`, Item 3 findings) + - "Draft a slice for `signup()` command ownership in `auth-service`, with fail-first tests first and implementation in a separate pass." (`docs/plans/Priority5Completion.md`, Item 4) + - "UI modules are easier to test at the right boundary" (`docs/RepoHealthImprovementBacklog.md`, Priority 5) + +- [x] C01 `[frontend]` Implement `signup` command ownership in `src/lib/auth-service.ts` so `signup()` owns the `createUser` side effect by importing it from `src/apiClient.ts`, maps success/error onto the live interpreted `src/lib/login.xstate.ts` runtime, reuses the existing service-owned login/session verification path after successful signup, and publishes authenticated user state from the service rather than from `Login.svelte`. + - Depends on: T01. + - Validated by: T01. + - Trace: + - "`Login.svelte` still contains legacy direct calls ... signup uses `createUser(userInfo)` from the `signingUp` state handler." (`docs/plans/Priority5Completion.md`, Item 3 findings) + - "Draft a slice for `signup()` command ownership in `auth-service`, with fail-first tests first and implementation in a separate pass." (`docs/plans/Priority5Completion.md`, Item 4) + - "login-related state, side effects, and rendering responsibilities are easier to explain as separate concerns" (`docs/RepoHealthImprovementBacklog.md`, Priority 5) + +## Behavior Slices + +### Slice 4A + +Goal: lock the signup command ownership contract in fail-first tests before implementation. + +Items: T01 + +Type: behavior + +### Slice 4B + +Goal: move the `createUser` signup side effect and post-signup auth/session continuation into `auth-service`. + +Items: C01 + +Type: behavior diff --git a/docs/archive/Priority5AuthServiceSlice5Checklist.md b/docs/archive/Priority5AuthServiceSlice5Checklist.md new file mode 100644 index 00000000..256ff46d --- /dev/null +++ b/docs/archive/Priority5AuthServiceSlice5Checklist.md @@ -0,0 +1,97 @@ +# Priority 5 Auth Service Slice 5 Checklist + +Status: archived, completed + +Classification: completed implementation checklist + +Source plan: `docs/plans/Priority5Completion.md` (Item 5: Draft a slice for `loginGuest()` command ownership in `auth-service`) + +## Scope Lock + +In scope: + +- move `loginGuest()` command ownership into `src/lib/auth-service.ts` +- make `auth-service.loginGuest()` own the guest-auth side effects currently handled by `Login.svelte` +- preserve the existing guest reuse flow: when a stored guest id/challenge is available, authenticate it with `postAuth`, verify it with `verifyUser`, and update the guest profile when display name or email changed +- preserve the existing new-guest flow: fetch a guest token with `getGuestToken`, verify it with `verifyUser`, create the guest user with `createGuestUser`, then publish authenticated `currentUser` +- compose existing `src/apiClient.ts` exports (`postAuth`, `verifyUser`, `getGuestToken`, `createGuestUser`, `updateUser`, and `verifySelf`) rather than duplicating HTTP transport or response-resolution logic +- validate the service boundary with fail-first tests before implementation + +Out of scope: + +- changing backend/API contracts +- implementing custom fetch, auth header, guest-token, response-resolution, or user transport logic inside `auth-service` +- adding new auth/user API helpers outside `src/apiClient.ts` +- changing guest form-local field state or field-level validation UI in `Login.svelte` +- rewiring `Login.svelte` to call `auth-service.loginGuest()` +- moving `localStorage` reads/writes into `auth-service` +- changing `simple_comment_user` persistence semantics +- modifying `CommentInput.svelte`, `SelfDisplay.svelte`, or relay-store behavior +- editing tests during the implementation pass; if the fail-first tests cannot be made green through production-code changes only, stop and discuss + +## Slice Intent + +This slice moves guest-login command ownership into `auth-service` so guest authentication, guest creation, and guest profile update side effects no longer depend on `Login.svelte` state handlers. + +The current `Login.svelte` flow reads stored guest identity from `localStorage`, then either reuses the stored guest credentials or creates a new guest. This slice should not silently move storage ownership into `auth-service`. Instead, the service contract should accept any reusable stored guest identity explicitly as command input/dependency, leaving storage extraction to a later slice if still needed. + +`src/apiClient.ts` already exposes the API primitives needed for this slice: +`postAuth`, `verifyUser`, `getGuestToken`, `createGuestUser`, `updateUser`, and +`verifySelf`. `auth-service` should compose those primitives. If implementation +discovers a missing reusable guest/user API primitive, stop and propose adding it +to `src/apiClient.ts` rather than creating a one-off client inside +`auth-service`. + +## Atomic Checklist Items + +- [x] C01 `[frontend]` Tighten the `loginGuest` public contract in `src/lib/auth-service.ts` so `GuestLoginPayload` can carry the submitted guest display name/email plus optional reusable stored guest identity (`id`, `challenge`, stored `name`, stored `email`) without making `auth-service` read from `localStorage`. + - Depends on: none. + - Validated by: T01. + - Trace: + - "guest login flow reads stored guest credentials, attempts `postAuth(storedId, storedChallenge)`, calls `verifyUser()`, falls back to `getGuestToken()`, calls `verifyUser()` again, creates a guest with `createGuestUser(...)`, and sends success/error machine events." (`docs/plans/Priority5Completion.md`, Item 3 findings) + - "guest profile update uses `updateUser({ id: storedId, name: displayName, email: userEmail })` when stored guest identity differs from the submitted guest form values." (`docs/plans/Priority5Completion.md`, Item 3 findings) + - "Draft a slice for `loginGuest()` command ownership in `auth-service`, including existing guest-token, verify, create guest, and update-if-changed behavior." (`docs/plans/Priority5Completion.md`, Item 5) + +- [x] T01 `[tests]` Add fail-first frontend tests for `loginGuest` command ownership in `src/tests/frontend/auth-service.login-guest.test.ts` proving `auth-service.loginGuest()` owns the stored-guest reuse path, fallback new-guest path, update-if-changed behavior, authenticated `currentUser` publication, and error handling. + - Depends on: C01. + - Required coverage: + - stored guest with valid `id`/`challenge` calls `postAuth(storedId, storedChallenge)` and `verifyUser()`, does not call `getGuestToken()` or `createGuestUser()`, and publishes authenticated `currentUser` + - stored guest with changed display name or email calls `updateUser({ id: storedId, name: displayName, email })` + - missing stored guest credentials, or failed stored guest authentication, falls back to `getGuestToken()`, `verifyUser()`, and `createGuestUser({ id, name: displayName, email })` + - guest-login failure leaves `currentUser` undefined and transitions to `error` + - Trace: + - "guest login flow reads stored guest credentials, attempts `postAuth(storedId, storedChallenge)`, calls `verifyUser()`, falls back to `getGuestToken()`, calls `verifyUser()` again, creates a guest with `createGuestUser(...)`, and sends success/error machine events." (`docs/plans/Priority5Completion.md`, Item 3 findings) + - "UI modules are easier to test at the right boundary" (`docs/RepoHealthImprovementBacklog.md`, Priority 5) + +- [x] C02 `[frontend]` Implement `loginGuest` command ownership in `src/lib/auth-service.ts` so `loginGuest()` composes existing `src/apiClient.ts` guest/auth/user primitives, maps success/error onto the live interpreted `src/lib/login.xstate.ts` runtime, and publishes authenticated user state from the service rather than from `Login.svelte`. + - Depends on: T01. + - Validated by: T01. + - Trace: + - "guest login flow reads stored guest credentials, attempts `postAuth(storedId, storedChallenge)`, calls `verifyUser()`, falls back to `getGuestToken()`, calls `verifyUser()` again, creates a guest with `createGuestUser(...)`, and sends success/error machine events." (`docs/plans/Priority5Completion.md`, Item 3 findings) + - "login-related state, side effects, and rendering responsibilities are easier to explain as separate concerns" (`docs/RepoHealthImprovementBacklog.md`, Priority 5) + +## Behavior Slices + +### Slice 5A + +Goal: define the guest-login command input contract without moving storage ownership into `auth-service`. + +Items: C01 + +Type: behavior + +### Slice 5B + +Goal: lock guest-login command ownership and fallback/update behavior in fail-first tests before implementation. + +Items: T01 + +Type: behavior + +### Slice 5C + +Goal: move stored-guest reuse, new-guest creation, and update-if-changed side effects into `auth-service`. + +Items: C02 + +Type: behavior diff --git a/docs/archive/Priority5AuthServiceSlice6Checklist.md b/docs/archive/Priority5AuthServiceSlice6Checklist.md new file mode 100644 index 00000000..7e6e05d2 --- /dev/null +++ b/docs/archive/Priority5AuthServiceSlice6Checklist.md @@ -0,0 +1,139 @@ +# Priority 5 Auth Service Slice 6 Checklist + +Status: approved + +Classification: approved implementation checklist + +Source plan: `docs/plans/Priority5AuthServiceSlice6Plan.md` + +Parent plan: `docs/plans/Priority5Completion.md` (Item 6: Draft a slice for moving session/localStorage persistence out of `Login.svelte`) + +## Scope Lock + +In scope: + +- move `simple_comment_user` session/guest persistence ownership out of `Login.svelte` +- create a small auth persistence boundary, such as `src/lib/auth-persistence.ts`, for `simple_comment_user` reads/writes +- expose explicit persistence operations for stored auth user data: + - `loadStoredUser` + - `saveStoredUser` + - `clearStoredUser` + - `loadStoredGuestIdentity` +- make the persistence boundary tolerate missing storage, malformed JSON, and incomplete stored data without throwing during normal auth flows +- let `auth-service` use an injectable persistence dependency so tests and non-browser clients can provide their own storage behavior +- have `auth-service` save verified/authenticated users and clear stored session data on confirmed logout or unauthenticated initial verification +- have `auth-service.loginGuest()` use stored guest identity from the persistence dependency when the caller does not pass `storedGuest` explicitly +- have explicit `storedGuest` command input take precedence over persisted guest identity +- replace `Login.svelte` direct `simple_comment_user` `localStorage` reads/writes with the shared persistence boundary while preserving existing form hydration behavior +- stop `Login.svelte` from saving authenticated users or reading stored guest identity for command submission +- validate persistence behavior with fail-first tests before implementation + +Out of scope: + +- moving `simple_comment_login_tab` persistence out of `Login.svelte` +- changing login tab selection behavior or selected-tab UI persistence semantics +- treating `localStorage` as authoritative authentication state; server verification remains the source of truth for session validity +- adding a broad auth controller, runtime component, workflow module, or event bus +- changing backend/API contracts +- changing `src/apiClient.ts` HTTP transport behavior +- rewiring `Login.svelte` to call `auth-service` commands; Slice 7 already completed that work +- replacing `currentUserStore`, `loginStateStore`, or `dispatchableStore` +- modifying `CommentInput.svelte` or `SelfDisplay.svelte` +- editing tests during an implementation pass; if fail-first tests cannot be made green through production-code changes only, stop and discuss + +## Slice Intent + +This slice resolves the Item 6 conditional in favor of moving session and guest persistence ownership out of `Login.svelte`. Auth/session continuity should not depend on the `Login.svelte` component being mounted, especially for future auth checks or guest reuse flows that may be initiated by `CommentInput.svelte`, `SelfDisplay.svelte`, or another auth-aware surface. + +The safe version is intentionally small: introduce a persistence adapter for `simple_comment_user`, then let `auth-service` depend on that adapter rather than `Login.svelte` reading browser storage for session/guest command behavior. This keeps raw `localStorage` isolated, keeps auth-service testable, and avoids reintroducing the broad frontend architecture churn that Priority 5 has been trying to avoid. + +After Slice 7, `Login.svelte` already delegates auth commands to `auth-service`. This slice should not redo that wiring. It should remove the remaining `simple_comment_user` storage ownership from the component while preserving form hydration from stored user data through the shared boundary. + +`simple_comment_login_tab` should stay in `Login.svelte` for this slice. It is a UI preference, not session/auth persistence, and moving it now would make the slice look cleaner while expanding its true responsibility. + +## Atomic Checklist Items + +- [x] T01 `[tests]` Add fail-first frontend unit tests for the auth persistence boundary in `src/tests/frontend/auth-persistence.test.ts`. + - Depends on: none. + - Required coverage: + - missing `simple_comment_user` returns `undefined` + - malformed `simple_comment_user` returns `undefined` without throwing + - saved users round-trip through `saveStoredUser` and `loadStoredUser` + - `clearStoredUser` removes the stored user + - `loadStoredGuestIdentity` returns only the reusable guest identity fields needed by `auth-service.loginGuest()` + - Trace: + - "Add a small `simple_comment_user` persistence boundary, expected as `src/lib/auth-persistence.ts`." (`docs/plans/Priority5AuthServiceSlice6Plan.md`, In Scope) + - "The persistence boundary safely handles missing storage, malformed JSON, incomplete stored data, and non-browser environments." (`docs/plans/Priority5AuthServiceSlice6Plan.md`, Acceptance Criteria) + - "Pass: tests prove missing/malformed/incomplete stored data is handled safely and valid stored users/guest identity load as expected." (`docs/plans/Priority5AuthServiceSlice6Plan.md`, Validation Strategy) + +- [x] C01 `[frontend]` Implement `src/lib/auth-persistence.ts` as a small `simple_comment_user` persistence boundary with typed exports for `loadStoredUser`, `saveStoredUser`, `clearStoredUser`, and `loadStoredGuestIdentity`. + - Depends on: T01. + - Validated by: T01. + - Trace: + - "Expose explicit operations for stored auth user data" (`docs/plans/Priority5AuthServiceSlice6Plan.md`, In Scope) + - "Make the persistence boundary tolerate missing storage, malformed JSON, incomplete stored data, and non-browser environments without throwing during normal auth flows." (`docs/plans/Priority5AuthServiceSlice6Plan.md`, In Scope) + - "`src/lib/auth-persistence.ts` exposes typed persistence operations for `simple_comment_user`." (`docs/plans/Priority5AuthServiceSlice6Plan.md`, Acceptance Criteria) + +- [x] T02 `[tests]` Add fail-first frontend tests for `auth-service` persistence integration using an injected persistence dependency rather than browser `localStorage`. + - Depends on: C01. + - Required coverage: + - successful `init()`, `login()`, `signup()`, and `loginGuest()` save the verified authenticated user + - unauthenticated initial verification clears stored user data + - successful `logout()` clears stored user data + - failed auth commands do not save a new stored user + - `loginGuest()` uses persisted guest identity when `storedGuest` is omitted + - explicit `storedGuest` command input takes precedence over persisted guest identity + - Trace: + - "Let `auth-service` receive an injectable persistence dependency so tests and non-browser clients can provide their own storage behavior." (`docs/plans/Priority5AuthServiceSlice6Plan.md`, In Scope) + - "Make `auth-service.loginGuest()` use persisted guest identity when command input does not include `storedGuest`." (`docs/plans/Priority5AuthServiceSlice6Plan.md`, In Scope) + - "Pass: tests with an injected fake persistence dependency prove save, clear, failed-command, persisted-guest, and explicit-guest precedence behavior." (`docs/plans/Priority5AuthServiceSlice6Plan.md`, Validation Strategy) + +- [x] C02 `[frontend]` Integrate the auth persistence boundary into `src/lib/auth-service.ts` through an injectable persistence dependency, preserving server verification as the source of truth while saving, clearing, and reading stored guest identity through the adapter. + - Depends on: T02. + - Validated by: T02. + - Trace: + - "Make `auth-service` save verified/authenticated users after successful `init()`, `login()`, `signup()`, and `loginGuest()` flows." (`docs/plans/Priority5AuthServiceSlice6Plan.md`, In Scope) + - "Make `auth-service` clear stored session data on unauthenticated initial verification and confirmed logout." (`docs/plans/Priority5AuthServiceSlice6Plan.md`, In Scope) + - "`auth-service` accepts an injectable persistence dependency." (`docs/plans/Priority5AuthServiceSlice6Plan.md`, Acceptance Criteria) + +- [x] T03 `[tests]` Update the `Login.svelte` component-boundary guest submission test so stored guest identity is not passed from the component to `authService.loginGuest()`; persisted guest reuse is covered at the `auth-service` boundary instead. + - Depends on: C02. + - Trace: + - "Make `auth-service.loginGuest()` use persisted guest identity when command input does not include `storedGuest`." (`docs/plans/Priority5AuthServiceSlice6Plan.md`, In Scope) + - "Preserve `Login.svelte` form hydration from stored user data without letting the component own session saving, clearing, or guest reuse." (`docs/plans/Priority5AuthServiceSlice6Plan.md`, In Scope) + - "Fail: `auth-service` requires browser `localStorage` or guest reuse still depends on `Login.svelte`." (`docs/plans/Priority5AuthServiceSlice6Plan.md`, Validation Strategy) + +- [x] C03 `[frontend]` Remove `src/components/Login.svelte` direct `simple_comment_user` `localStorage` access by using the shared auth persistence boundary only for stored-user form hydration, leaving session save/clear and stored-guest command reuse owned by `auth-service`. + - Depends on: T03. + - Validated by: `yarn typecheck` and existing `src/tests/frontend/components/Login.auth-service.test.ts`. + - Trace: + - "Replace `Login.svelte` direct `simple_comment_user` `localStorage` access with the shared persistence boundary." (`docs/plans/Priority5AuthServiceSlice6Plan.md`, In Scope) + - "Preserve `Login.svelte` form hydration from stored user data without letting the component own session saving, clearing, or guest reuse." (`docs/plans/Priority5AuthServiceSlice6Plan.md`, In Scope) + - "`Login.svelte` no longer directly calls `localStorage` for `simple_comment_user`." (`docs/plans/Priority5AuthServiceSlice6Plan.md`, Acceptance Criteria) + - "`Login.svelte` still preserves form hydration from stored user data and still owns `simple_comment_login_tab` UI preference persistence." (`docs/plans/Priority5AuthServiceSlice6Plan.md`, Acceptance Criteria) + +## Behavior Slices + +### Slice 6A + +Goal: define and implement the small persistence adapter without touching auth command behavior. + +Items: T01, C01 + +Type: behavior + +### Slice 6B + +Goal: let `auth-service` own session persistence through an injected adapter while preserving server verification as the source of truth. + +Items: T02, C02 + +Type: behavior + +### Slice 6C + +Goal: remove direct `simple_comment_user` storage access from `Login.svelte` without moving UI preference persistence, rewiring auth commands, or keeping guest reuse in the component. + +Items: T03, C03 + +Type: mechanical diff --git a/docs/archive/Priority5AuthServiceSlice6Plan.md b/docs/archive/Priority5AuthServiceSlice6Plan.md new file mode 100644 index 00000000..61d8cabc --- /dev/null +++ b/docs/archive/Priority5AuthServiceSlice6Plan.md @@ -0,0 +1,157 @@ +# Priority 5 Auth Service Slice 6 Plan + +Status: approved + +Source backlog: `docs/RepoHealthImprovementBacklog.md` (`Priority 5`) + +Parent plan: `docs/plans/Priority5Completion.md` (Item 6) + +Related artifacts: + +- `docs/plans/Priority5AuthServiceSlice6Checklist.md` +- `docs/archive/Priority5AuthServiceSlice7Plan.md` + +## Goal + +Move `simple_comment_user` session and guest persistence out of `Login.svelte` and behind a small auth persistence boundary that `auth-service` can use directly. + +## Intent + +This slice is about making stored auth/session data belong to the auth layer, not the login form. + +After Slice 7, `Login.svelte` delegates auth commands to `auth-service`, but it still reads and writes `simple_comment_user` itself. That means guest reuse and saved session data still depend on the login component being mounted and still require the component to know the raw browser storage format. + +For this slice, success means: + +- `auth-service` saves verified authenticated users after successful auth flows, +- `auth-service` clears stored session data when server verification says there is no valid session or when logout succeeds, +- `auth-service.loginGuest()` can reuse stored guest identity through an injected persistence dependency when the caller does not provide one, +- `Login.svelte` stops saving authenticated users and stops reading stored guest identity for command submission, +- `Login.svelte` may still read persisted user data through the shared boundary only to prefill local form fields. + +In plain terms: the login form may remember what to show in its fields, but the auth service owns what counts as stored auth/session data. + +## In Scope + +- Add a small `simple_comment_user` persistence boundary, expected as `src/lib/auth-persistence.ts`. +- Expose explicit operations for stored auth user data: + - `loadStoredUser` + - `saveStoredUser` + - `clearStoredUser` + - `loadStoredGuestIdentity` +- Make the persistence boundary tolerate missing storage, malformed JSON, incomplete stored data, and non-browser environments without throwing during normal auth flows. +- Let `auth-service` receive an injectable persistence dependency so tests and non-browser clients can provide their own storage behavior. +- Make `auth-service` save verified/authenticated users after successful `init()`, `login()`, `signup()`, and `loginGuest()` flows. +- Make `auth-service` clear stored session data on unauthenticated initial verification and confirmed logout. +- Make `auth-service.loginGuest()` use persisted guest identity when command input does not include `storedGuest`. +- Make explicit `storedGuest` command input take precedence over persisted guest identity. +- Replace `Login.svelte` direct `simple_comment_user` `localStorage` access with the shared persistence boundary. +- Preserve `Login.svelte` form hydration from stored user data without letting the component own session saving, clearing, or guest reuse. + +## Out of Scope + +- Moving `simple_comment_login_tab` persistence out of `Login.svelte`. +- Changing login tab selection behavior or selected-tab UI persistence semantics. +- Treating local persistence as authoritative authentication state; server verification remains the source of truth for session validity. +- Rewiring `Login.svelte` auth commands to `auth-service`; Slice 7 already completed that work. +- Replacing `currentUserStore`, `loginStateStore`, or `dispatchableStore`. +- Removing login relay behavior from `CommentInput.svelte`. +- Removing logout relay behavior from `SelfDisplay.svelte`. +- Modifying `CommentInput.svelte` or `SelfDisplay.svelte`. +- Adding a broad auth controller, runtime component, workflow module, singleton service, or event bus. +- Changing backend/API contracts or `src/apiClient.ts` HTTP transport behavior. + +## Constraints + +- `auth-service` remains widget-scoped and injectable; do not introduce a singleton auth-service. +- Persistence is cache/form/guest-reuse support only, not proof of authentication. +- Keep the slice narrow and reversible; do not fold in Slice 8, 9, or 10 relay/store cleanup. +- Test and implementation passes remain separate per current team convention. + +## Current State + +At the start of this slice: + +- `auth-service` owns auth command execution and the live auth runtime. +- `Login.svelte` receives an `AuthService` and delegates login, signup, guest login, logout, and init behavior to it. +- `Login.svelte` still directly reads `simple_comment_user` to hydrate form fields. +- `Login.svelte` still directly reads `simple_comment_user` to pass stored guest identity into `authService.loginGuest()`. +- `Login.svelte` still directly writes `simple_comment_user` when `authService.currentUser` changes. +- No shared auth persistence boundary exists. + +## Approach + +1. Add fail-first unit tests for the new persistence boundary. +2. Implement the persistence boundary as a small module that hides raw browser storage access and parsing. +3. Add fail-first unit tests for `auth-service` persistence integration using an injected persistence dependency. +4. Integrate persistence into `auth-service` so the service owns save/clear/guest-reuse behavior. +5. Update `Login.svelte` so it no longer reads/writes raw `simple_comment_user`; it may only use the shared boundary to hydrate local form fields. +6. Stop there. Do not remove relay stores, redesign auth-state distribution, or move UI preference persistence. + +## Risks and Mitigations + +- Risk: persisted user data becomes treated as logged-in truth. + - Mitigation: `auth-service.init()` must still verify with the server; persistence only stores cache/form/guest-reuse data. + +- Risk: moving persistence accidentally changes tab or form behavior. + - Mitigation: keep `simple_comment_login_tab` and form-local validation/UI behavior in `Login.svelte`. + +- Risk: `Login.svelte` keeps owning guest reuse by calling the new persistence boundary before `loginGuest()`. + - Mitigation: require `auth-service.loginGuest()` to read persisted guest identity when command input omits `storedGuest`. + +- Risk: tests become browser-storage-coupled and hard to run outside JSDOM. + - Mitigation: inject persistence into `auth-service` and test service behavior with a fake dependency. + +## Acceptance Criteria + +1. `src/lib/auth-persistence.ts` exposes typed persistence operations for `simple_comment_user`. +2. The persistence boundary safely handles missing storage, malformed JSON, incomplete stored data, and non-browser environments. +3. `auth-service` accepts an injectable persistence dependency. +4. Successful `init()`, `login()`, `signup()`, and `loginGuest()` flows save the verified authenticated user. +5. Unauthenticated initial verification and successful logout clear stored user data. +6. Failed auth commands do not save a new stored user. +7. `authService.loginGuest()` uses persisted guest identity when command input omits `storedGuest`. +8. Explicit `storedGuest` command input takes precedence over persisted guest identity. +9. `Login.svelte` no longer directly calls `localStorage` for `simple_comment_user`. +10. `Login.svelte` still preserves form hydration from stored user data and still owns `simple_comment_login_tab` UI preference persistence. + +## Validation Strategy + +Required evidence types for Slice 6: + +- **Unit evidence for persistence boundary** + - Pass: tests prove missing/malformed/incomplete stored data is handled safely and valid stored users/guest identity load as expected. + - Fail: malformed storage throws during normal auth flows or the boundary leaks raw storage assumptions to callers. + +- **Unit evidence for auth-service integration** + - Pass: tests with an injected fake persistence dependency prove save, clear, failed-command, persisted-guest, and explicit-guest precedence behavior. + - Fail: `auth-service` requires browser `localStorage` or guest reuse still depends on `Login.svelte`. + +- **Component/type evidence** + - Pass: `Login.svelte` typechecks after raw `simple_comment_user` access is removed, and existing component delegation tests continue to pass. + - Fail: `Login.svelte` keeps direct `simple_comment_user` localStorage reads/writes or loses existing form hydration behavior. + +## Open Questions / Assumptions + +- Assumption: `Login.svelte` may import the persistence boundary to load stored user data for form hydration only. +- Assumption: keeping `simple_comment_login_tab` in `Login.svelte` is still correct because it is UI preference state, not auth/session persistence. +- Assumption: preserving the explicit `storedGuest` command input remains useful for tests and non-browser clients even after service-side persistence is added. + +## Scope Guard + +The following work is explicitly deferred and must not be folded into Slice 6 without a separate approved plan/checklist update: + +- Slice 8 shared-store publication replacement +- Slice 9 login relay removal from `CommentInput.svelte` +- Slice 10 logout relay removal from `SelfDisplay.svelte` +- Final project-wide auth-state distribution redesign +- Login form component splitting or visual redesign + +## Conformance QC (Plan) + +- Intent clarity issues: none observed; intent distinguishes auth/session persistence ownership from form-field hydration. +- Missing required sections: none (`Goal`, `Intent`, `In Scope`, `Out of Scope`, `Acceptance Criteria`, and `Validation Strategy` are present). +- Ambiguities/assumptions to resolve: none blocking implementation; the allowed `Login.svelte` form-hydration use of the persistence boundary is explicit. +- Validation strategy gaps: none for persistence-boundary, service-integration, and Login raw-storage-removal scope. +- Traceability readiness: ready; headings and acceptance criteria are stable and quoteable for checklist refinement. +- Pass/Fail: structurally ready for checklist execution after checklist reconciliation — **Pass**. diff --git a/docs/archive/Priority5AuthServiceSlice7Checklist.md b/docs/archive/Priority5AuthServiceSlice7Checklist.md new file mode 100644 index 00000000..32b1527f --- /dev/null +++ b/docs/archive/Priority5AuthServiceSlice7Checklist.md @@ -0,0 +1,117 @@ +# Priority 5 Auth Service Slice 7 Checklist + +Status: approved + +Classification: approved implementation checklist + +Source plan: `docs/plans/Priority5AuthServiceSlice7Plan.md` + +Parent plan: `docs/plans/Priority5Completion.md` (Item 7: Draft a slice for wiring `Login.svelte` to call `auth-service` commands) + +## Scope Lock + +In scope: + +- wire `Login.svelte` auth actions through `auth-service` command methods instead of direct `src/apiClient.ts` calls +- keep form-local field state, field validation, selected-tab UI, selected-tab persistence, and status-message formatting in `Login.svelte` +- create a widget-scoped `AuthService` instance at the current widget composition root and thread it explicitly down the current render path to `Login.svelte` +- replace `Login.svelte` local auth-machine runtime ownership with reads from the service-owned auth runtime +- expose the minimal service-owned auth state metadata needed for `Login.svelte` to preserve the existing `loginStateStore` publication shape during the transition +- keep `CommentInput.svelte` and `SelfDisplay.svelte` working through the existing relay stores for now +- validate command delegation with fail-first tests before implementation + +Out of scope: + +- moving `simple_comment_user` persistence out of `Login.svelte` +- replacing `loginStateStore`, `dispatchableStore`, or `currentUserStore` +- removing relay behavior from `CommentInput.svelte` or `SelfDisplay.svelte` +- deciding the final broader auth-state exposure strategy for the whole widget beyond this explicit prop-threaded seam +- introducing a singleton auth-service import, a new event bus, an auth controller, or a runtime wrapper component +- splitting `Login.svelte` into smaller components +- changing backend/API contracts +- editing tests during an implementation pass; if fail-first tests cannot be made green through production-code changes only, stop and discuss + +## Slice Intent + +This slice removes the remaining direct auth command side effects from `Login.svelte` without reopening the large architecture churn that earlier Priority 5 planning flirted with. `auth-service` already owns the live auth runtime and the auth commands. `Login.svelte` should become the place that validates fields, gathers form input, chooses which auth command to invoke, and renders the resulting auth state, not the place that talks to auth endpoints directly. + +The safe implementation path is to create a widget-scoped `AuthService` instance at the current composition root, thread it explicitly down the existing render path, and have `Login.svelte` delegate to it. That is narrower and safer than introducing a singleton or broad new store, and it does not decide the later direct-props versus thin-store question for every other auth-aware component. + +Because `CommentInput.svelte` and `SelfDisplay.svelte` still depend on `loginStateStore`, this slice should preserve that store shape for now. The key constraint is to avoid two authoritative auth runtimes: `Login.svelte` should observe the service-owned auth state rather than continue interpreting its own auth machine. + +## Atomic Checklist Items + +- T01 `[tests]` Add fail-first frontend component tests for `Login.svelte` auth-service delegation in `src/tests/frontend/components/Login.auth-service.test.ts`. + - Depends on: none. + - [x] T01.01 Add a fail-first test proving mount/init delegates the initial auth check through `authService.init()` rather than running direct `verifySelf()` logic inside `Login.svelte`. + - [x] T01.02 Add a fail-first test proving a valid login submission calls `authService.login({ userId, password })` with the current form values. + - [x] T01.03 Add a fail-first test proving a valid signup submission calls `authService.signup({ userId, password, displayName, email })` with the current form values. + - [x] T01.04 Add fail-first tests proving a valid guest submission calls `authService.loginGuest({ displayName, email })` when no stored guest data exists and passes the stored guest identity through the service payload when stored guest data is present. + - [x] T01.05 Add fail-first tests proving local validation failures still surface component-local errors and do not call `authService.login()`, `authService.signup()`, or `authService.loginGuest()`. + - [x] T01.06 Add a fail-first test proving logout intent delegates through `authService.logout()` only when logout is currently allowed by the observed auth state. + - [x] T01.07 Add fail-first tests proving `Login.svelte` no longer performs direct auth command calls to `verifySelf`, `verifyUser`, `postAuth`, `createUser`, `getGuestToken`, `createGuestUser`, `updateUser`, or `deleteAuth()` for the delegated flows covered by this slice. + - [x] T01.08 Add a fail-first test proving `Login.svelte` publishes the existing `loginStateStore` compatibility shape from observed service-owned auth state rather than from a second authoritative local auth runtime. + - Trace: + - "Add fail-first component tests that treat `Login.svelte` as the boundary under test and assert that it delegates auth actions to an injected `AuthService`." (`docs/plans/Priority5AuthServiceSlice7Plan.md`, Approach) + - "Login.svelte delegation behavior is tested at the component boundary." (`docs/plans/Priority5AuthServiceSlice7Plan.md`, Validation Strategy) + - "Pass: tests show valid user actions call the appropriate `auth-service` methods, and local validation failures do not call service commands." (`docs/plans/Priority5AuthServiceSlice7Plan.md`, Validation Strategy) + - "Fail: `Login.svelte` still calls auth APIs directly for covered command paths, or component validation behavior is lost." (`docs/plans/Priority5AuthServiceSlice7Plan.md`, Validation Strategy) + +- [x] C01 `[frontend]` Create a widget-scoped `AuthService` instance with `createAuthService()` in `src/components/SimpleComment.svelte`, then thread it explicitly through `src/components/DiscussionDisplay.svelte` and `src/components/CommentInput.svelte` into `src/components/Login.svelte`. + - Depends on: T01. + - Validated by: `yarn typecheck`. + - Trace: + - "Create a widget-scoped `AuthService` instance at the current composition root and thread it explicitly through the current component path into `Login.svelte`." (`docs/plans/Priority5AuthServiceSlice7Plan.md`, In Scope) + - "`auth-service` remains widget-scoped; do not introduce a singleton service instance." (`docs/plans/Priority5AuthServiceSlice7Plan.md`, Constraints) + - "The new widget-scoped service seam composes cleanly through the current component tree." (`docs/plans/Priority5AuthServiceSlice7Plan.md`, Validation Strategy) + +- [x] C02 `[frontend]` Extend `src/lib/auth-service.ts` with one readable auth-runtime snapshot store that exposes the service-owned machine state needed by `Login.svelte` to preserve the existing `loginStateStore` compatibility contract (`state`, `nextEvents`, and any required error context), without running a second interpreted auth machine inside the component. + - Depends on: T01. + - Validated by: T01.08. + - Trace: + - "Make `Login.svelte` observe service-owned auth state rather than interpret its own auth machine as a second authority." (`docs/plans/Priority5AuthServiceSlice7Plan.md`, In Scope) + - "The slice must not create two authoritative auth runtimes after wiring is complete." (`docs/plans/Priority5AuthServiceSlice7Plan.md`, Constraints) + - "Preserve compatibility with the current relay/store consumers by continuing to publish the existing `loginStateStore` shape during this slice." (`docs/plans/Priority5AuthServiceSlice7Plan.md`, In Scope) + +- [x] C03 `[frontend]` Replace direct auth API calls and local auth-machine ownership in `src/components/Login.svelte` with `auth-service` command delegation and subscriptions to the auth-runtime snapshot store added in `C02`, then publish the existing `loginStateStore` compatibility shape from that observed service state while preserving component-local validation/UI behavior for unreworked consumers. + - Depends on: C01, C02. + - Validated by: T01. + - Trace: + - "Replace direct auth API command calls in `Login.svelte` with calls to `auth-service` methods" (`docs/plans/Priority5AuthServiceSlice7Plan.md`, In Scope) + - "`Login.svelte` delegates auth command execution to a widget-scoped `AuthService`." (`docs/plans/Priority5AuthServiceSlice7Plan.md`, Acceptance Criteria) + - "`Login.svelte` does not continue to run a second authoritative interpreted auth runtime after the slice is complete." (`docs/plans/Priority5AuthServiceSlice7Plan.md`, Acceptance Criteria) + - "The slice does not introduce a singleton auth-service, a new event bus, or a broader auth-state architecture redesign." (`docs/plans/Priority5AuthServiceSlice7Plan.md`, Acceptance Criteria) + +## Behavior Slices + +### Slice 7A + +Goal: lock the `Login.svelte` command-delegation boundary in fail-first tests before implementation. + +Items: T01 + +Type: behavior + +### Slice 7B + +Goal: make a widget-scoped auth-service instance explicitly available to `Login.svelte` without introducing a new global auth mechanism. + +Items: C01 + +Type: mechanical + +### Slice 7C + +Goal: expose just enough service-owned runtime metadata to avoid a second authoritative auth machine inside `Login.svelte`. + +Items: C02 + +Type: behavior + +### Slice 7D + +Goal: make `Login.svelte` a command-delegating/view component for auth while preserving its current validation/UI responsibilities and legacy store publication. + +Items: C03 + +Type: behavior diff --git a/docs/archive/Priority5AuthServiceSlice7Plan.md b/docs/archive/Priority5AuthServiceSlice7Plan.md new file mode 100644 index 00000000..996edcab --- /dev/null +++ b/docs/archive/Priority5AuthServiceSlice7Plan.md @@ -0,0 +1,174 @@ +# Priority 5 Auth Service Slice 7 Plan + +Status: planning + +Source backlog: `docs/RepoHealthImprovementBacklog.md` (`Priority 5`) + +Parent plan: `docs/plans/Priority5Completion.md` (Item 7) + +Related artifacts: +- `docs/plans/Priority5AuthServiceSlice7Checklist.md` (pre-plan draft checklist input; must be reconciled to this plan before implementation use) + +## Goal + +Wire `Login.svelte` to use the existing widget-scoped `auth-service` commands and auth runtime so the component stops performing direct auth API work while still owning form-local input, validation, and rendering behavior. + +## Intent + +This slice is about making `Login.svelte` simpler and less risky without turning the frontend inside out. + +Today, `Login.svelte` still does two jobs at once: + +- it behaves like a form component, +- and it also behaves like an auth controller that talks to the backend directly and runs its own auth state machine. + +For this slice, success means: + +- `Login.svelte` still owns its own fields, validation messages, selected tab, and UI rendering, +- but when the user tries to log in, sign up, continue as a guest, or log out, the component calls `auth-service` instead of calling auth APIs directly, +- and the component reads auth state from the service-owned runtime instead of running a second authoritative auth runtime locally. + +In plain terms: the login form should stay a form, and the auth service should stay the place that owns auth behavior. + +## Motivation + +Slice 7 exists because Priority 5 is not only about extracting auth commands into `auth-service`; it is also about removing the remaining hidden dependence on `Login.svelte` as the place where auth actually happens. + +The command extraction slices already moved `init()`, `login()`, `logout()`, `signup()`, and `loginGuest()` into `auth-service`. That work lowered risk at the service boundary, but `Login.svelte` still contains direct auth-side-effect code and still interprets its own auth machine. As a result, the codebase still has two competing centers of auth responsibility: + +- `auth-service`, which is supposed to own auth behavior, +- and `Login.svelte`, which still behaves like a second auth runtime and command executor. + +That split keeps the current relay/store setup harder to reason about, and it makes later decoupling work in `CommentInput.svelte` and `SelfDisplay.svelte` more fragile than it needs to be. + +Slice 7 is the smallest reasonable next step because it removes direct command execution from `Login.svelte` without also trying to solve persistence extraction, relay removal, or the final auth-state distribution strategy all at once. + +## In Scope + +- Replace direct auth API command calls in `Login.svelte` with calls to `auth-service` methods: + - `init()` + - `login()` + - `signup()` + - `loginGuest()` + - `logout()` +- Create a widget-scoped `AuthService` instance at the current composition root and thread it explicitly through the current component path into `Login.svelte`. +- Make `Login.svelte` observe service-owned auth state rather than interpret its own auth machine as a second authority. +- Preserve `Login.svelte` ownership of: + - field values, + - field-level validation, + - selected-tab UI, + - selected-tab persistence, + - user-facing status/error rendering. +- Preserve compatibility with the current relay/store consumers by continuing to publish the existing `loginStateStore` shape during this slice. +- Add validation that proves `Login.svelte` delegates auth commands to `auth-service` and does not call auth APIs directly for those flows. + +## Out of Scope + +- Moving `simple_comment_user` persistence out of `Login.svelte`. +- Removing `loginStateStore`, `dispatchableStore`, or `currentUserStore`. +- Removing login relay behavior from `CommentInput.svelte`. +- Removing logout relay behavior from `SelfDisplay.svelte`. +- Choosing the final project-wide answer to direct props versus a thin auth-service-backed store. +- Introducing a singleton auth-service, new event bus, auth controller, runtime wrapper component, or broader frontend state redesign. +- Splitting `Login.svelte` into multiple components. +- Changing backend/API contracts. +- Broad visual or UX redesign of login/signup/guest forms. + +## Constraints + +- `auth-service` remains widget-scoped; do not introduce a singleton service instance. +- Server verification remains the source of truth for session validity. +- The slice must not create two authoritative auth runtimes after wiring is complete. +- The slice must remain reviewable and reversible; do not fold in slices 6, 8, 9, or 10. +- Test and implementation passes remain separate per current team convention. + +## Current State + +At the start of this slice: + +- `auth-service` already owns the extracted auth commands and the live interpreted auth runtime. +- `Login.svelte` still imports and calls auth APIs directly for init/login/signup/guest/logout flows. +- `Login.svelte` still uses `useMachine(loginMachine)` and publishes login state outward through `loginStateStore`. +- `CommentInput.svelte` and `SelfDisplay.svelte` still rely on the relay-store contract and are not being cleaned up in this slice. + +## Approach + +1. Add fail-first component tests that treat `Login.svelte` as the boundary under test and assert that it delegates auth actions to an injected `AuthService`. +2. Introduce a widget-scoped service seam at the current composition root and thread it explicitly down to `Login.svelte`. +3. Extend the service boundary only as much as needed for `Login.svelte` to observe service-owned auth runtime state and preserve the existing `loginStateStore` publication contract for unreworked consumers. +4. Replace direct auth API calls and local auth runtime ownership in `Login.svelte` with service delegation plus service-state observation. +5. Stop there. Do not fold relay removal, persistence extraction, or a broader auth-state redesign into this slice. + +## Risks and Mitigations + +- Risk: slice 7 quietly absorbs slice 8 by redesigning auth-state publication. + - Mitigation: preserve the current `loginStateStore` contract for now and limit new service state exposure to the minimum needed by `Login.svelte`. + +- Risk: slice 7 quietly absorbs slice 9 or 10 by changing `CommentInput.svelte` or `SelfDisplay.svelte` behavior. + - Mitigation: keep those components working through the existing relay/store contract and treat their cleanup as later slices only. + +- Risk: the implementation leaves two active auth runtimes, one in `auth-service` and one in `Login.svelte`. + - Mitigation: require the plan to treat service-owned runtime state as the only authority after wiring is complete. + +- Risk: wiring the service through components encourages a future singleton shortcut. + - Mitigation: explicitly scope the service per widget instance and pass it explicitly through the current tree. + +- Risk: UI behavior regresses because form validation and auth delegation are mixed together carelessly. + - Mitigation: keep validation/UI behavior explicitly in scope for preservation, and test the component boundary before implementation. + +## Acceptance Criteria + +1. `Login.svelte` no longer performs direct auth command calls to backend auth APIs for init/login/signup/guest/logout flows. +2. `Login.svelte` delegates auth command execution to a widget-scoped `AuthService`. +3. `Login.svelte` does not continue to run a second authoritative interpreted auth runtime after the slice is complete. +4. `Login.svelte` still owns form-local field state, validation behavior, selected-tab UI, selected-tab persistence, and user-facing status/error presentation. +5. Existing unreworked consumers that depend on the current `loginStateStore` contract continue to function during this slice. +6. The slice does not introduce a singleton auth-service, a new event bus, or a broader auth-state architecture redesign. +7. The resulting changes are narrow enough that later slices can still independently address persistence extraction and relay removal. + +## Validation Strategy + +Required evidence types for Slice 7: + +- **Unit/component evidence** + - `Login.svelte` delegation behavior is tested at the component boundary. + - Pass: tests show valid user actions call the appropriate `auth-service` methods, and local validation failures do not call service commands. + - Fail: `Login.svelte` still calls auth APIs directly for covered command paths, or component validation behavior is lost. + +- **Contract/parity evidence** + - Transitional relay/store behavior remains intact for unreworked consumers. + - Pass: the current `loginStateStore` publication shape required by existing consumers is still produced during this slice. + - Fail: slice 7 breaks current relay/store consumers or silently changes the state contract they rely on. + +- **Type/build evidence** + - The new widget-scoped service seam composes cleanly through the current component tree. + - Pass: frontend typecheck succeeds after the service seam and `Login.svelte` delegation changes. + - Fail: type errors or composition mismatches are introduced in the component tree. + +## Open Questions / Assumptions + +- Assumption: a widget-scoped service instance threaded through the current component path is acceptable as an intermediate step even if the later auth-state distribution choice is still open. +- Assumption: `auth-service` can expose the minimal runtime metadata needed by `Login.svelte` without prematurely deciding the broader state-distribution design. +- Open question: should the later direct-props versus thin-store decision happen before slices 9 and 10, or immediately after them? +- Open question: is the existing `loginStateStore` shape sufficient as a temporary compatibility contract once `Login.svelte` stops owning the runtime? + +## Scope Guard + +The following work is explicitly deferred and must not be folded into Slice 7 without a separate approved plan/checklist update: + +- Slice 6 persistence extraction work +- Slice 8 shared-store publication replacement +- Slice 9 login relay removal from `CommentInput.svelte` +- Slice 10 logout relay removal from `SelfDisplay.svelte` +- Final project-wide auth-state distribution redesign + +If the implementation appears to require one of those to succeed, stop and revise planning rather than silently expanding scope. + +## Conformance QC (Plan) + +- Intent clarity issues: none observed; intent is stated in plain language and distinguishes form responsibilities from auth responsibilities. +- Missing required sections: none (`Goal`, `Intent`, `In Scope`, `Out of Scope`, `Acceptance Criteria`, and `Validation Strategy` are present). +- Ambiguities/assumptions to resolve: whether the current `loginStateStore` shape is sufficient as a temporary compatibility layer should be validated during checklist authoring. +- Validation strategy gaps: none for this slice’s delegated-command and transitional-compatibility scope. +- Traceability readiness: ready; headings and acceptance criteria are stable and quoteable for checklist authoring. +- Pass/Fail: structurally ready for collaborative review and checklist reconciliation — **Pass**. diff --git a/docs/archive/Priority5AuthServiceSlice8Checklist.md b/docs/archive/Priority5AuthServiceSlice8Checklist.md new file mode 100644 index 00000000..3891d826 --- /dev/null +++ b/docs/archive/Priority5AuthServiceSlice8Checklist.md @@ -0,0 +1,118 @@ +# Priority 5 Auth Service Slice 8 Checklist + +Status: approved + +Classification: approved implementation checklist + +Source plan: `docs/plans/Priority5AuthServiceSlice8Plan.md` + +Parent plan: `docs/plans/Priority5Completion.md` (Item 8: Draft a slice for replacing `Login.svelte` shared-store publication with auth-service state subscriptions) + +## Scope Lock + +In scope: + +- remove `Login.svelte` publication of auth/session state to `currentUserStore` and `loginStateStore` +- keep `Login.svelte` selected-tab UI publication for this slice +- add a temporary widget-scoped compatibility bridge from `AuthService` state to the existing legacy stores +- install that bridge at the current composition root +- preserve the existing `loginStateStore` auth/session shape for unreworked `CommentInput.svelte` and `SelfDisplay.svelte` consumers +- preserve current user flow through the existing component tree +- validate the bridge with fail-first tests before implementation + +Out of scope: + +- removing `dispatchableStore` +- removing `loginStateStore` +- removing `currentUserStore` +- removing login relay behavior from `CommentInput.svelte` +- removing logout relay behavior from `SelfDisplay.svelte` +- moving selected-tab UI state out of `Login.svelte` +- choosing the final direct-props versus thin auth-service-backed store architecture +- changing backend/API contracts + +## Slice Intent + +This slice removes auth/session store publication from `Login.svelte` without removing the legacy stores yet. The temporary compatibility bridge should publish the current auth-service state into the existing stores so unreworked consumers keep functioning. `Login.svelte` remains responsible for form-local UI and selected-tab publication only. + +## Atomic Checklist Items + +- [x] T01 `[tests]` Add fail-first frontend unit tests for a temporary auth store bridge in `src/tests/frontend/auth-store-bridge.test.ts`. + - Depends on: none. + - Required coverage: + - `authService.currentUser` publishes to `currentUserStore`. + - `authService.authRuntimeSnapshot` publishes `{ state, nextEvents }` to `loginStateStore`. + - bridge cleanup unsubscribes from auth-service stores so later service updates do not keep publishing. + - Trace: + - "Add fail-first tests proving auth-state store publication comes from the widget-scoped `authService` bridge and does not require `Login.svelte` to publish auth/session state." (`docs/plans/Priority5AuthServiceSlice8Plan.md`, Approach) + - "Pass: tests prove auth/session store updates can be produced from `authService` without relying on `Login.svelte` as publisher." (`docs/plans/Priority5AuthServiceSlice8Plan.md`, Validation Strategy) + - "This helper keeps bridge behavior testable without making `auth-service.ts` import global legacy stores directly." (`docs/plans/Priority5AuthServiceSlice8Plan.md`, Detailed File Impact) + +- [x] C01 `[frontend]` Implement `src/lib/auth-store-bridge.ts` as the temporary compatibility bridge from an injected widget-scoped `AuthService` to the legacy `currentUserStore` and `loginStateStore`, returning a cleanup function. + - Depends on: T01. + - Validated by: T01. + - Trace: + - "Add a temporary compatibility bridge from the widget-scoped `AuthService` to the existing shared stores, expected as a small helper such as `src/lib/auth-store-bridge.ts`." (`docs/plans/Priority5AuthServiceSlice8Plan.md`, In Scope) + - "publish `{ state, nextEvents }` from `authRuntimeSnapshot` to `loginStateStore`" (`docs/plans/Priority5AuthServiceSlice8Plan.md`, Detailed File Impact) + - "publish service-owned current user values to `currentUserStore`" (`docs/plans/Priority5AuthServiceSlice8Plan.md`, Detailed File Impact) + +- [x] C02 `[frontend]` Install the temporary auth store bridge in `src/components/SimpleComment.svelte` using the existing widget-scoped `authService`, and clean up the bridge in `onDestroy`. + - Depends on: C01. + - Validated by: `yarn typecheck`. + - Trace: + - "Because `SimpleComment.svelte` creates the widget-scoped `authService`, it is the safest current place to install and clean up transitional subscriptions from `authService` to the existing shared stores." (`docs/plans/Priority5AuthServiceSlice8Plan.md`, Detailed File Impact) + - "call the temporary bridge helper with the widget-scoped `authService`" (`docs/plans/Priority5AuthServiceSlice8Plan.md`, Detailed File Impact) + - "Keep the bridge widget-scoped and lifecycle-cleaned; do not introduce a singleton auth-service or a broad new event bus." (`docs/plans/Priority5AuthServiceSlice8Plan.md`, In Scope) + +- [x] T02 `[tests]` Update `src/tests/frontend/components/Login.auth-service.test.ts` so `Login.svelte` is expected not to publish auth/session state to legacy stores while still publishing selected-tab UI state. + - Depends on: C02. + - Required coverage: + - observed auth runtime state from `Login.svelte` does not update `loginStateStore` with `{ state, nextEvents }`. + - `Login.svelte` still publishes `{ select }` when the selected tab changes. + - Trace: + - "`Login.svelte` stops publishing auth/session state to `currentUserStore` and `loginStateStore`" (`docs/plans/Priority5AuthServiceSlice8Plan.md`, Intent) + - "`Login.svelte` may still publish selected-tab UI state because that is local UI state, not shared auth/session state" (`docs/plans/Priority5AuthServiceSlice8Plan.md`, Intent) + - "`Login.svelte` still publishes selected-tab UI state for current unreworked consumers." (`docs/plans/Priority5AuthServiceSlice8Plan.md`, Acceptance Criteria) + +- [x] C03 `[frontend]` Remove auth/session legacy-store publication from `src/components/Login.svelte` while preserving selected-tab publication and all form-local UI behavior. + - Depends on: T02. + - Validated by: T02 and `yarn typecheck`. + - Trace: + - "remove `currentUserStore.set(self)` on destroy" (`docs/plans/Priority5AuthServiceSlice8Plan.md`, Detailed File Impact) + - "remove reactive `currentUserStore.set(self)`" (`docs/plans/Priority5AuthServiceSlice8Plan.md`, Detailed File Impact) + - "remove `loginStateStore.set({ state, nextEvents })` from the auth runtime snapshot handler" (`docs/plans/Priority5AuthServiceSlice8Plan.md`, Detailed File Impact) + - "keep `loginStateStore.set({ select: selectedIndex })` unless a later slice replaces selected-tab coupling." (`docs/plans/Priority5AuthServiceSlice8Plan.md`, Detailed File Impact) + +## Behavior Slices + +### Slice 8A + +Goal: add a testable temporary compatibility bridge from widget-scoped auth-service state to legacy stores. + +Items: T01, C01 + +Type: behavior + +### Slice 8B + +Goal: install the compatibility bridge at the current composition root without changing relay consumers. + +Items: C02 + +Type: mechanical + +### Slice 8C + +Goal: remove auth/session shared-store publication from `Login.svelte` while preserving selected-tab UI publication. + +Items: T02, C03 + +Type: behavior + +## Conformance QC (Checklist) + +- Missing from plan: none. +- Extra beyond plan: none; the helper seam is explicitly named in the plan. +- Atomicity fixes needed: none; each item can be checked and committed independently. +- Validation mapping gaps: none; implementation items are covered by fail-first tests and typecheck. +- Pass/Fail: checklist achieves plan goals — **Pass**. diff --git a/docs/archive/Priority5AuthServiceSlice8Plan.md b/docs/archive/Priority5AuthServiceSlice8Plan.md new file mode 100644 index 00000000..be240868 --- /dev/null +++ b/docs/archive/Priority5AuthServiceSlice8Plan.md @@ -0,0 +1,268 @@ +# Priority 5 Auth Service Slice 8 Plan + +Status: approved + +Source backlog: `docs/RepoHealthImprovementBacklog.md` (`Priority 5`) + +Parent plan: `docs/plans/Priority5Completion.md` (Item 8) + +Related artifacts: + +- `docs/archive/Priority5AuthServiceSlice6Plan.md` +- `docs/archive/Priority5AuthServiceSlice7Plan.md` + +## Goal + +Move legacy auth-state store publication out of `Login.svelte` so auth state is bridged from the widget-scoped `auth-service` instead of from the login form component. + +## Intent + +This slice is about removing the last place where `Login.svelte` acts like the owner of shared auth state. + +After Slices 6 and 7, `Login.svelte` no longer owns auth commands, auth persistence, or the live auth runtime. It still publishes auth/session state outward through shared Svelte stores: + +- `currentUserStore` +- `loginStateStore` + +That publication keeps the login form in the middle of unrelated behavior. `CommentInput.svelte` and `SelfDisplay.svelte` still depend on the legacy stores, but the source of those stores should be the shared auth service, not the presence and lifecycle of `Login.svelte`. + +For this slice, success means: + +- `Login.svelte` stops publishing auth/session state to `currentUserStore` and `loginStateStore`, +- the current legacy consumers keep working because a temporary compatibility bridge publishes those stores from `auth-service`, +- `Login.svelte` may still publish selected-tab UI state because that is local UI state, not shared auth/session state, +- no relay behavior is removed yet from `CommentInput.svelte` or `SelfDisplay.svelte`. + +In plain terms: auth state should flow from the auth service, while the login form should remain just a form. + +## In Scope + +- Remove `Login.svelte` publication of auth/session state: + - `currentUserStore.set(self)` + - `loginStateStore.set({ state, nextEvents })` +- Preserve `Login.svelte` publication of selected-tab UI state for now: + - `loginStateStore.set({ select: selectedIndex })` +- Add a temporary compatibility bridge from the widget-scoped `AuthService` to the existing shared stores, expected as a small helper such as `src/lib/auth-store-bridge.ts`. +- Bridge `authService.currentUser` into the existing current-user path so currently unreworked descendants continue to receive `currentUser`. +- Bridge `authService.authRuntimeSnapshot` into `loginStateStore` so `CommentInput.svelte` and `SelfDisplay.svelte` keep receiving the auth state shape they currently consume. +- Keep the bridge widget-scoped and lifecycle-cleaned; do not introduce a singleton auth-service or a broad new event bus. +- Add fail-first tests that prove store publication no longer depends on rendering `Login.svelte`. + +## Out of Scope + +- Removing `dispatchableStore`. +- Removing `loginStateStore`. +- Removing login relay behavior from `CommentInput.svelte`. +- Removing logout relay behavior from `SelfDisplay.svelte`. +- Changing `CommentInput.svelte` submit/login state-machine behavior. +- Changing `SelfDisplay.svelte` logout button behavior. +- Moving selected-tab UI state out of `Login.svelte`. +- Choosing the final direct-props versus thin auth-service-backed store architecture for the whole widget. +- Splitting `Login.svelte` into smaller components. +- Changing backend/API contracts. + +## Constraints + +- `auth-service` remains widget-scoped; no singleton service import. +- The compatibility bridge is transitional and should be easy to delete in later slices. +- Do not make `auth-service` import the legacy global stores directly unless a later checklist explicitly approves that coupling. +- Do not fold Slice 9 or Slice 10 relay removal into this slice. +- Test and implementation passes remain separate per current team convention. + +## Current State + +At the start of this slice: + +- `auth-service` owns auth commands, auth persistence, and the live auth runtime. +- `Login.svelte` observes `authService.currentUser` and `authService.authRuntimeSnapshot`. +- `Login.svelte` still writes observed auth state into `currentUserStore` and `loginStateStore`. +- `SimpleComment.svelte` reads `currentUserStore` to update its local `currentUser` prop flow. +- `CommentInput.svelte` reads `loginStateStore` to react to login results and selected-tab state. +- `SelfDisplay.svelte` reads `loginStateStore` to show login/logout processing state. + +## Detailed File Impact + +### `src/lib/auth-service.ts` + +Expected role: source of auth state, not legacy-store publisher. + +This file already exposes the state needed by the bridge: + +- `currentUser` +- `authRuntimeSnapshot` + +The preferred Slice 8 plan does not require `auth-service.ts` to import `currentUserStore` or `loginStateStore`. If implementation discovers the service needs a small helper to expose bridge-ready state more clearly, that helper must remain service-local and widget-scoped. + +Expected changes: + +- likely none, unless tests reveal the current readable stores are insufficient for a clean bridge. + +Non-goal: + +- do not make `auth-service.ts` write to global legacy stores directly. + +### `src/lib/auth-store-bridge.ts` + +Expected role: temporary compatibility bridge from widget-scoped auth-service state to legacy shared stores. + +This helper should subscribe to: + +- `authService.currentUser` +- `authService.authRuntimeSnapshot` + +It should publish to: + +- `currentUserStore` +- `loginStateStore` + +Expected changes: + +- add a small helper that accepts an `AuthService` instance and optional store dependencies for tests, +- publish `{ state, nextEvents }` from `authRuntimeSnapshot` to `loginStateStore`, +- publish service-owned current user values to `currentUserStore`, +- return a cleanup function that unsubscribes from service stores. + +This helper keeps bridge behavior testable without making `auth-service.ts` import global legacy stores directly. + +### `src/components/Login.svelte` + +Expected role: form-local UI and auth command delegation only. + +`Login.svelte` should stop publishing shared auth/session state: + +- remove `currentUserStore.set(self)` on destroy, +- remove reactive `currentUserStore.set(self)`, +- remove `loginStateStore.set({ state, nextEvents })` from the auth runtime snapshot handler. + +`Login.svelte` should continue to own selected-tab UI behavior for this slice: + +- keep `simple_comment_login_tab` persistence, +- keep `loginStateStore.set({ select: selectedIndex })` unless a later slice replaces selected-tab coupling. + +This preserves the current guest/login/signup UX while removing auth-state publication from the form component. + +### `src/components/SimpleComment.svelte` + +Expected role: current composition root and temporary legacy-store bridge installer. + +Because `SimpleComment.svelte` creates the widget-scoped `authService`, it is the safest current place to install and clean up transitional subscriptions from `authService` to the existing shared stores. + +Expected changes: + +- call the temporary bridge helper with the widget-scoped `authService`, +- keep updating local `currentUser` from the legacy `currentUserStore` while the prop flow is still unreworked, +- clean up bridge subscriptions in `onDestroy`. + +This keeps the bridge widget-scoped and avoids a singleton auth service. + +### `src/components/CommentInput.svelte` + +Expected role: unchanged consumer during Slice 8. + +`CommentInput.svelte` currently consumes `loginStateStore` and dispatches login intents. This slice should not rewrite that relay behavior. + +Expected changes: + +- no production changes expected. + +Validation relevance: + +- tests should protect that `CommentInput.svelte` can still observe login outcomes through the existing `loginStateStore` shape after publication moves away from `Login.svelte`. + +### `src/components/SelfDisplay.svelte` + +Expected role: unchanged consumer during Slice 8. + +`SelfDisplay.svelte` currently consumes `loginStateStore` for processing state and dispatches logout intents. This slice should not rewrite that relay behavior. + +Expected changes: + +- no production changes expected. + +Validation relevance: + +- tests should protect that the existing processing-state shape still reaches `SelfDisplay.svelte` through `loginStateStore`. + +### `src/lib/svelte-stores.ts` + +Expected role: unchanged legacy compatibility surface. + +This slice should not remove or redesign the existing stores. It only changes who publishes auth/session state into them. + +Expected changes: + +- no production changes expected. + +## Approach + +1. Add fail-first tests proving auth-state store publication comes from the widget-scoped `authService` bridge and does not require `Login.svelte` to publish auth/session state. +2. Add the temporary compatibility bridge as a small helper and install it at the current composition root, where the widget-scoped `authService` already exists. +3. Remove auth/session store publication from `Login.svelte`. +4. Keep `Login.svelte` selected-tab publication intact for now. +5. Stop there. Do not remove relay stores or rewrite `CommentInput.svelte` / `SelfDisplay.svelte`. + +## Risks and Mitigations + +- Risk: Slice 8 quietly becomes Slice 9 or Slice 10 by rewriting relay consumers. + - Mitigation: keep `CommentInput.svelte` and `SelfDisplay.svelte` production behavior unchanged. + +- Risk: the bridge becomes a new permanent architecture by accident. + - Mitigation: keep it explicit, widget-scoped, and documented as transitional compatibility. + +- Risk: selected-tab state gets confused with auth/session state. + - Mitigation: leave selected-tab publication in `Login.svelte` for this slice and defer that decision. + +- Risk: auth-service becomes coupled to global legacy stores. + - Mitigation: put store publication in the composition root rather than inside `auth-service.ts`. + +## Acceptance Criteria + +1. `Login.svelte` no longer publishes `currentUserStore`. +2. `Login.svelte` no longer publishes `{ state, nextEvents }` auth/session updates to `loginStateStore`. +3. `Login.svelte` still publishes selected-tab UI state for current unreworked consumers. +4. The existing `loginStateStore` auth/session shape remains available to unreworked consumers from a widget-scoped `authService` subscription. +5. Current user flow remains available to the existing component tree from `authService.currentUser`. +6. `CommentInput.svelte` and `SelfDisplay.svelte` production relay behavior is unchanged. +7. No singleton auth-service, new event bus, or broad auth-state redesign is introduced. + +## Validation Strategy + +Required evidence types for Slice 8: + +- **Unit/component evidence** + - Pass: tests prove auth/session store updates can be produced from `authService` without relying on `Login.svelte` as publisher. + - Fail: tests require `Login.svelte` to publish auth/session store updates. + +- **Compatibility evidence** + - Pass: existing `CommentInput.svelte` and `SelfDisplay.svelte` consumers still receive the current `loginStateStore` auth/session shape. + - Fail: relay consumers lose the `state` / `nextEvents` data they currently use. + +- **Type/build evidence** + - Pass: frontend typecheck succeeds after bridge installation and Login publication removal. + - Fail: component composition or subscription cleanup introduces type errors. + +## Open Questions / Assumptions + +- Assumption: `SimpleComment.svelte` is the correct temporary bridge location because it creates the widget-scoped `AuthService`. +- Assumption: selected-tab publication remains in `Login.svelte` until a later slice decides whether that UI state still belongs in `loginStateStore`. +- Assumption: Slice 9 and Slice 10 will remove the login/logout relay behavior later, so Slice 8 should preserve the current store contract rather than eliminate it. +- Assumption: the temporary bridge should be a tiny helper rather than inline `SimpleComment.svelte` code, because that gives the slice a clean fail-first unit-test seam while keeping service coupling out of `auth-service.ts`. + +## Scope Guard + +The following work is explicitly deferred and must not be folded into Slice 8 without a separate approved plan/checklist update: + +- Slice 9 login relay removal from `CommentInput.svelte` +- Slice 10 logout relay removal from `SelfDisplay.svelte` +- Final direct-props versus thin-store architecture decision +- Deleting `loginStateStore`, `currentUserStore`, or `dispatchableStore` +- Moving selected-tab UI state out of `Login.svelte` + +## Conformance QC (Plan) + +- Intent clarity issues: none observed; intent distinguishes auth/session store publication from selected-tab UI publication. +- Missing required sections: none (`Goal`, `Intent`, `In Scope`, `Out of Scope`, `Acceptance Criteria`, and `Validation Strategy` are present). +- Ambiguities/assumptions to resolve: none blocking implementation; the bridge helper seam is explicit and intentionally temporary. +- Validation strategy gaps: none for the transitional bridge scope. +- Traceability readiness: ready; headings and acceptance criteria are stable and quoteable for checklist authoring. +- Pass/Fail: structurally ready for checklist execution — **Pass**. diff --git a/docs/archive/Priority5AuthServiceSlice9Checklist.md b/docs/archive/Priority5AuthServiceSlice9Checklist.md new file mode 100644 index 00000000..21f8749f --- /dev/null +++ b/docs/archive/Priority5AuthServiceSlice9Checklist.md @@ -0,0 +1,132 @@ +# Priority 5 Auth Service Slice 9 Checklist + +Status: archived, completed + +Classification: approved implementation checklist + +Source plan: `docs/plans/Priority5AuthServiceSlice9Plan.md` + +Parent plan: `docs/plans/Priority5Completion.md` (Item 9: Draft a slice for removing `dispatchableStore` / `loginStateStore` login relay behavior from `CommentInput.svelte`) + +## Scope Lock + +In scope: + +- remove `CommentInput.svelte` use of `dispatchableStore` for `loginIntent` +- remove `CommentInput.svelte` subscription to `loginStateStore` +- use the injected widget-scoped `authService` to request authentication before comment posting +- use request-scoped auth outcomes to continue or stop the comment-post flow +- preserve selected-tab-dependent button copy and guest-comment validation without `CommentInput.svelte` reading `loginStateStore` +- teach `Login.svelte` to consume pending auth requests from `authService` +- keep test-writing passes separate from production implementation passes + +Out of scope: + +- removing logout relay behavior from `SelfDisplay.svelte` +- removing `dispatchableStore` from `Login.svelte` for logout handling +- removing `loginStateStore` selected-tab publication from `Login.svelte` +- removing legacy store definitions +- removing the temporary Slice 8 auth bridge +- changing backend/API contracts +- choosing the final direct-props versus thin auth-service-backed store architecture + +## Slice Intent + +This slice removes the login-before-comment relay from `CommentInput.svelte`. After the slice, unauthenticated comment submission should request auth through the injected `authService`, `Login.svelte` should consume that pending request through the same service, and `CommentInput.svelte` should continue posting only after the matching auth request succeeds. The old `dispatchableStore` / `loginStateStore` login relay should no longer be part of `CommentInput.svelte`. + +## Atomic Checklist Items + +- [x] T01 `[tests]` Add fail-first frontend unit tests proving pending auth requests produce request-scoped success and remote-error outcomes from `auth-service` auth commands. + - Depends on: none. + - Required coverage: + - a pending `requestAuth(...)` followed by successful `login(...)` publishes an `authOutcome` success with the matching request id and authenticated user. + - a pending `requestAuth(...)` followed by failed `login(...)` publishes an `authOutcome` remote error with the matching request id and returns `authRequest` to idle. + - pending request success and remote-error outcome behavior is covered for `signup(...)`. + - pending request success and remote-error outcome behavior is covered for `loginGuest(...)`. + - command behavior without a pending auth request preserves existing session/current-user behavior. + - Trace: + - "Ensure successful `login()`, `signup()`, and `loginGuest()` commands publish a matching `authOutcome` success when there is a pending auth request." (`docs/plans/Priority5AuthServiceSlice9Plan.md`, Detailed File Impact) + - "Ensure failed remote auth commands publish a matching `authOutcome` remote error when there is a pending auth request." (`docs/plans/Priority5AuthServiceSlice9Plan.md`, Detailed File Impact) + - "Pass: `auth-service` tests prove pending auth requests produce success and remote-error outcomes from auth commands." (`docs/plans/Priority5AuthServiceSlice9Plan.md`, Validation Strategy) + +- [x] C01 `[frontend]` Implement request-scoped auth outcome publication in `src/lib/auth-service.ts` while preserving existing auth command behavior. + - Depends on: T01. + - Validated by: T01. + - Trace: + - "Preserve the existing public `AuthService` surface." (`docs/plans/Priority5AuthServiceSlice9Plan.md`, Detailed File Impact) + - "Ensure completed or failed pending auth requests return `authRequest` to idle." (`docs/plans/Priority5AuthServiceSlice9Plan.md`, Detailed File Impact) + - "Preserve existing session state, current-user publication, and persistence behavior." (`docs/plans/Priority5AuthServiceSlice9Plan.md`, Detailed File Impact) + +- [x] T02 `[tests]` Add fail-first `Login.svelte` component tests for pending `authService.authRequest` consumption and local validation failure reporting. + - Depends on: C01. + - Required coverage: + - a pending auth request triggers the currently selected auth form path without requiring `dispatchableStore.dispatch("loginIntent")`. + - a pending auth request with invalid local form data calls `authService.reportLocalValidationError(...)` with the pending request id. + - existing direct form-submit behavior remains intact. + - Trace: + - "Subscribe to `authService.authRequest`." (`docs/plans/Priority5AuthServiceSlice9Plan.md`, Detailed File Impact) + - "When a pending request is observed, submit the currently selected auth form through the existing local submit functions." (`docs/plans/Priority5AuthServiceSlice9Plan.md`, Detailed File Impact) + - "Pass: `Login.svelte` component tests prove pending auth requests trigger the selected auth form path and local validation failures are reported through `authService`." (`docs/plans/Priority5AuthServiceSlice9Plan.md`, Validation Strategy) + +- [x] C02 `[frontend]` Implement `Login.svelte` consumption of pending `authService.authRequest` values without removing existing logout relay behavior. + - Depends on: T02. + - Validated by: T02. + - Trace: + - "Keep existing direct form-submit behavior intact for user-triggered form submission." (`docs/plans/Priority5AuthServiceSlice9Plan.md`, Detailed File Impact) + - "Keep `dispatchableStore` logout handling intact for Slice 10." (`docs/plans/Priority5AuthServiceSlice9Plan.md`, Detailed File Impact) + - "Do not move field state or validation out of `Login.svelte`." (`docs/plans/Priority5AuthServiceSlice9Plan.md`, Detailed File Impact) + +- [x] T03 `[tests]` Add fail-first `CommentInput.svelte` component tests for auth-service request/outcome flow and selected-tab behavior without legacy login stores. + - Depends on: C02. + - Required coverage: + - unauthenticated submit calls `authService.requestAuth(...)` instead of dispatching `loginIntent`. + - a matching success `authOutcome` continues the existing comment-post flow and calls `postComment(...)`. + - a matching failed or cancelled `authOutcome` leaves the comment form out of the processing login state. + - selected-tab-dependent button copy and guest-comment validation continue without `CommentInput.svelte` subscribing to `loginStateStore`. + - source guard proves `CommentInput.svelte` does not import `dispatchableStore` or `loginStateStore`. + - Trace: + - "Remove `dispatchableStore` import and `loginIntent` dispatch." (`docs/plans/Priority5AuthServiceSlice9Plan.md`, Detailed File Impact) + - "Remove `loginStateStore` import and subscription." (`docs/plans/Priority5AuthServiceSlice9Plan.md`, Detailed File Impact) + - "Pass: `CommentInput.svelte` component tests prove unauthenticated submission calls `authService.requestAuth(...)`, matching success continues posting, selected-tab behavior remains available, and legacy login stores are not used." (`docs/plans/Priority5AuthServiceSlice9Plan.md`, Validation Strategy) + +- [x] C03 `[frontend]` Refactor `src/components/CommentInput.svelte` to use injected `authService` request/outcome flow and direct `Login.svelte` selected-tab binding instead of legacy login relay stores. + - Depends on: T03. + - Validated by: T03 and `yarn typecheck`. + - Trace: + - "Track the pending auth request id returned by `authService.requestAuth(...)`." (`docs/plans/Priority5AuthServiceSlice9Plan.md`, Detailed File Impact) + - "Observe `authService.authOutcome`." (`docs/plans/Priority5AuthServiceSlice9Plan.md`, Detailed File Impact) + - "Bind selected-tab UI state directly with `Login.svelte` so button copy and guest-comment validation keep working without `loginStateStore`." (`docs/plans/Priority5AuthServiceSlice9Plan.md`, Detailed File Impact) + +## Behavior Slices + +### Slice 9A + +Goal: make auth-service request outcomes reliable enough for comment-submit auth flow. + +Items: T01, C01 + +Type: behavior + +### Slice 9B + +Goal: let `Login.svelte` consume widget-scoped pending auth requests without the login relay event. + +Items: T02, C02 + +Type: behavior + +### Slice 9C + +Goal: remove `CommentInput.svelte` dependency on legacy login relay stores while preserving comment-submit UX. + +Items: T03, C03 + +Type: behavior + +## Conformance QC (Checklist) + +- Missing from plan: none. +- Extra beyond plan: none; each item maps to the plan's file impacts, approach, and validation strategy. +- Atomicity fixes needed: none; each test item and implementation item can be checked and committed independently. +- Validation mapping gaps: none; each implementation item is validated by a preceding fail-first test item, plus typecheck where component typing changes. +- Pass/Fail: checklist achieves plan goals — **Pass**. diff --git a/docs/archive/Priority5AuthServiceSlice9Plan.md b/docs/archive/Priority5AuthServiceSlice9Plan.md new file mode 100644 index 00000000..69901dc7 --- /dev/null +++ b/docs/archive/Priority5AuthServiceSlice9Plan.md @@ -0,0 +1,229 @@ +# Priority 5 Auth Service Slice 9 Plan + +Status: archived, completed + +Source backlog: `docs/RepoHealthImprovementBacklog.md` (`Priority 5`) + +Parent plan: `docs/plans/Priority5Completion.md` (Item 9) + +Related artifacts: + +- `docs/plans/Priority5AuthServiceSlice8Plan.md` +- `docs/plans/Priority5AuthServiceSlice8Checklist.md` + +## Goal + +Remove the login relay dependency from `CommentInput.svelte` so comment submission requests authentication through the widget-scoped `auth-service` instead of through `dispatchableStore` and `loginStateStore`. + +## Intent + +This slice is about letting `CommentInput.svelte` ask for authentication directly through the shared auth service. + +Today, when an unauthenticated user tries to submit a comment, `CommentInput.svelte` enters a login state, dispatches a `loginIntent` event through `dispatchableStore`, and waits for auth/session updates through `loginStateStore`. That keeps comment submission coupled to legacy global stores and to `Login.svelte` as a relay participant. + +After this slice, `CommentInput.svelte` should no longer import or use those legacy stores for login flow. Instead, it should: + +- request authentication through its injected `authService`, +- observe request-scoped auth outcomes from that same service, +- continue posting the comment after the matching auth request succeeds, +- keep selected-tab UI behavior working through a direct component binding with `Login.svelte`. + +In plain terms: when a comment needs login first, the comment form should talk to the auth service, not shout across the room through old shared stores. + +## In Scope + +- Remove `CommentInput.svelte` use of `dispatchableStore` for `loginIntent`. +- Remove `CommentInput.svelte` subscription to `loginStateStore`. +- Use the existing injected widget-scoped `authService` in `CommentInput.svelte` to create an auth request when comment submission requires login. +- Have `CommentInput.svelte` observe auth request outcomes from `authService` and continue its existing comment-post state machine after the matching request succeeds. +- Preserve the current behavior where the active `Login.svelte` tab determines comment-submit validation and button copy. +- Replace `CommentInput.svelte` selected-tab reads from `loginStateStore` with a narrow direct binding to `Login.svelte` selected-tab UI state. +- Teach `Login.svelte` to respond to pending `authService.authRequest` values by submitting the currently selected auth form, preserving the current outer comment-submit UX without using `dispatchableStore`. +- Use existing `auth-service` request/outcome primitives rather than introducing a new event bus or singleton store. +- Add fail-first tests before production changes. + +## Out of Scope + +- Removing logout relay behavior from `SelfDisplay.svelte`. +- Removing `dispatchableStore` from `Login.svelte` for logout handling. +- Removing `loginStateStore` selected-tab publication from `Login.svelte`. +- Removing `loginStateStore`, `dispatchableStore`, or `currentUserStore` definitions. +- Removing the temporary auth store bridge introduced in Slice 8. +- Redesigning `CommentInput.svelte` or `Login.svelte` layout. +- Splitting `Login.svelte` into smaller form components. +- Changing backend/API contracts. +- Choosing the final direct-props versus thin auth-service-backed store architecture for the whole widget. + +## Constraints + +- Keep `auth-service` widget-scoped; do not introduce a singleton auth-service import. +- Keep the implementation narrow to login-before-comment behavior. +- Keep test-writing passes separate from production implementation passes. +- Do not edit tests during implementation unless the implementation stops and explains why a test is wrong. +- Preserve existing comment-post behavior for already-authenticated users. +- Preserve existing form-local validation ownership in `Login.svelte`. + +## Current State + +At the start of this slice: + +- `CommentInput.svelte` receives `authService` as a prop. +- `CommentInput.svelte` imports `dispatchableStore` and dispatches `loginIntent` from its `loggingIn` state. +- `CommentInput.svelte` imports `loginStateStore` to observe auth state changes and selected-tab changes. +- `Login.svelte` still subscribes to `dispatchableStore` for `loginIntent` and `logoutIntent`. +- `Login.svelte` still publishes selected-tab UI state to `loginStateStore`. +- `auth-service.ts` exposes `authRequest`, `authOutcome`, `requestAuth`, `clearAuthOutcome`, `cancelAuthRequest`, and `reportLocalValidationError`, but request outcomes are not yet sufficient for `CommentInput.svelte` to complete the login-before-comment flow without legacy stores. + +## Detailed File Impact + +### `src/lib/auth-service.ts` + +Expected role: request/outcome coordinator for auth that was requested by another component. + +Expected changes: + +- Preserve the existing public `AuthService` surface. +- Ensure successful `login()`, `signup()`, and `loginGuest()` commands publish a matching `authOutcome` success when there is a pending auth request. +- Ensure failed remote auth commands publish a matching `authOutcome` remote error when there is a pending auth request. +- Ensure completed or failed pending auth requests return `authRequest` to idle. +- Preserve existing session state, current-user publication, and persistence behavior. + +Non-goals: + +- Do not add a new store or event bus. +- Do not make `auth-service.ts` import component stores. + +### `src/components/Login.svelte` + +Expected role: form-local UI and auth command delegate. + +Expected changes: + +- Subscribe to `authService.authRequest`. +- When a pending request is observed, submit the currently selected auth form through the existing local submit functions. +- When form-local validation fails during a pending request, report the validation failure through `authService.reportLocalValidationError(...)`. +- Keep existing direct form-submit behavior intact for user-triggered form submission. +- Keep `dispatchableStore` logout handling intact for Slice 10. +- Keep selected-tab publication to `loginStateStore` intact for now. + +Non-goals: + +- Do not remove logout relay handling. +- Do not move field state or validation out of `Login.svelte`. + +### `src/components/CommentInput.svelte` + +Expected role: comment form that requests auth through `authService` when needed. + +Expected changes: + +- Remove `dispatchableStore` import and `loginIntent` dispatch. +- Remove `loginStateStore` import and subscription. +- Track the pending auth request id returned by `authService.requestAuth(...)`. +- Observe `authService.authOutcome`. +- When the matching auth outcome succeeds, continue the existing state machine with `SUCCESS`. +- When the matching auth outcome fails or is cancelled, return the comment form to a non-processing state with the existing error path. +- Bind selected-tab UI state directly with `Login.svelte` so button copy and guest-comment validation keep working without `loginStateStore`. + +Non-goals: + +- Do not change `postComment(...)` ownership in this slice. +- Do not change the `commentPostMachine` unless implementation proves the current machine cannot represent the required request outcomes cleanly. + +### `src/lib/svelte-stores.ts` + +Expected role: unchanged legacy compatibility surface. + +Expected changes: + +- none. + +### `src/components/SelfDisplay.svelte` + +Expected role: unchanged logout relay consumer during Slice 9. + +Expected changes: + +- none. + +## Approach + +1. Add fail-first tests for `auth-service` request outcomes so request-scoped success and remote-error behavior is explicit before `CommentInput.svelte` depends on it. +2. Implement request-outcome publication in `auth-service.ts` without changing command ownership or persistence behavior. +3. Add fail-first component tests for `Login.svelte` consuming pending `authService.authRequest` values and reporting local validation failures through the service. +4. Implement the narrow `Login.svelte` request-consumption behavior. +5. Add fail-first component tests for `CommentInput.svelte` using `authService.requestAuth` / `authOutcome` instead of legacy stores. +6. Implement the `CommentInput.svelte` relay removal. +7. Stop there. Do not remove logout relay behavior, legacy store definitions, or the temporary auth bridge. + +## Risks and Mitigations + +- Risk: replacing the relay with `authService.authRequest` becomes just another event bus. + - Mitigation: keep the request channel request-scoped, widget-scoped, and tied to existing `authOutcome` semantics rather than adding a new generic dispatcher. + +- Risk: `CommentInput.svelte` loses selected-tab awareness when it stops reading `loginStateStore`. + - Mitigation: bind selected-tab UI state directly from the rendered `Login.svelte` instance. + +- Risk: local login validation failure leaves `CommentInput.svelte` stuck in a processing state. + - Mitigation: require `Login.svelte` to report pending-request validation failures through `authService.reportLocalValidationError(...)`. + +- Risk: this slice accidentally removes logout relay behavior. + - Mitigation: keep `SelfDisplay.svelte` and `Login.svelte` logout relay behavior out of scope. + +## Acceptance Criteria + +1. `CommentInput.svelte` no longer imports or uses `dispatchableStore`. +2. `CommentInput.svelte` no longer imports or subscribes to `loginStateStore`. +3. Unauthenticated comment submission creates an auth request through the injected `authService`. +4. A matching successful auth outcome causes the existing comment-post flow to continue. +5. A matching failed or cancelled auth outcome returns the comment form out of the processing login state through an existing error/reset path. +6. Comment submission for an already-authenticated user still posts without requesting auth. +7. Selected-tab-dependent button copy and guest-comment validation still work without `CommentInput.svelte` reading `loginStateStore`. +8. `Login.svelte` can consume pending auth requests from `authService` without relying on `dispatchableStore` login events. +9. Logout relay behavior remains unchanged for Slice 10. + +## Validation Strategy + +Required evidence types for Slice 9: + +- **Unit evidence** + - Pass: `auth-service` tests prove pending auth requests produce success and remote-error outcomes from auth commands. + - Fail: `CommentInput.svelte` must infer request completion from global legacy stores or non-request-scoped state. + +- **Component evidence** + - Pass: `Login.svelte` component tests prove pending auth requests trigger the selected auth form path and local validation failures are reported through `authService`. + - Fail: `Login.svelte` still requires `dispatchableStore` login events for comment-submit auth requests. + +- **Component evidence** + - Pass: `CommentInput.svelte` component tests prove unauthenticated submission calls `authService.requestAuth(...)`, matching success continues posting, selected-tab behavior remains available, and legacy login stores are not used. + - Fail: `CommentInput.svelte` still imports `dispatchableStore` or `loginStateStore` for login flow. + +- **Type/build evidence** + - Pass: frontend typecheck succeeds after removing legacy-store relay dependencies from `CommentInput.svelte`. + - Fail: component composition or auth-service request typing introduces type errors. + +## Open Questions / Assumptions + +- Assumption: using `authService.authRequest` / `authOutcome` is the intended narrow replacement for the legacy `dispatchableStore` login relay because those primitives already exist on the service. +- Assumption: direct selected-tab binding between `Login.svelte` and `CommentInput.svelte` is acceptable for this slice because selected-tab state is local UI state, not shared auth/session state. +- Assumption: `SelfDisplay.svelte` logout relay removal remains Slice 10 and should not be folded into this slice. +- Assumption: the temporary Slice 8 auth bridge remains until the cleanup slice after relay consumers are removed. + +## Scope Guard + +The following work is explicitly deferred and must not be folded into Slice 9 without a separate approved plan/checklist update: + +- Slice 10 logout relay removal from `SelfDisplay.svelte` +- Slice 11 temporary auth bridge cleanup +- Deleting `loginStateStore`, `currentUserStore`, or `dispatchableStore` +- Removing selected-tab publication from `Login.svelte` +- Redesigning the frontend auth architecture beyond the existing widget-scoped `authService` + +## Conformance QC (Plan) + +- Intent clarity issues: none; the plan states the user-facing goal and the narrow replacement path. +- Missing required sections: none. +- Ambiguities/assumptions to resolve: none blocking; assumptions are explicit and defer broader architecture decisions. +- Validation strategy gaps: none; unit, component, and type/build evidence are defined. +- Traceability readiness: ready; scope, approach, acceptance criteria, and validation statements are quoteable under stable headings. +- Pass/Fail: ready for checklist authoring — **Pass**. diff --git a/docs/archive/Priority5Completion.md b/docs/archive/Priority5Completion.md new file mode 100644 index 00000000..c6b214c4 --- /dev/null +++ b/docs/archive/Priority5Completion.md @@ -0,0 +1,169 @@ +# Priority 5 Completion Items + +Status: archived, completed + +- 1. [x] Confirm completed baseline: `auth-service` owns login-machine runtime, initial verification, `login()`, and `logout()` behavior. + + Findings: + + - Confirmed for the `auth-service` service boundary: `src/lib/auth-service.ts` now creates and owns the live interpreted `loginMachine` runtime via `interpret(loginMachine)`. + - Confirmed initial verification ownership: `authService.init()` calls `verifySelf()`, maps success / `401` / non-`401` outcomes onto the live login machine, and publishes `currentUser` from the service. + - Confirmed `login()` command ownership: `authService.login()` calls `postAuth()`, verifies the resulting session with `verifySelf()`, maps success/error onto the live login machine, and publishes authenticated `currentUser` from the service. + - Confirmed `logout()` command ownership: `authService.logout()` calls `deleteAuth()`, maps success/error onto the live login machine, clears `currentUser` on successful logout, and preserves `currentUser` on logout failure. + - Validation evidence: `yarn test:frontend --runInBand src/tests/frontend/auth-service.test.ts src/tests/frontend/auth-service.init.test.ts src/tests/frontend/auth-service.login.test.ts src/tests/frontend/auth-service.logout.test.ts` passed with 4 suites and 12 tests passing. + - Scope caveat: this confirms the service baseline only. `Login.svelte` still contains legacy direct auth side-effect handlers and has not yet been rewired to use `auth-service`; that remains future completion work. + +- 2. [x] Archive or mark completed any finished auth-service slice docs so active planning only shows unfinished work. + + Findings: + + - Confirmed no `Priority5AuthServiceSlice*Checklist.md` files remain under `docs/plans/`. + - Confirmed completed auth-service slice checklists are archived under `docs/archive/`: `Priority5AuthServiceSlice1Checklist.md`, `Priority5AuthServiceSlice2Checklist.md`, and `Priority5AuthServiceSlice3Checklist.md`. + - Confirmed all checklist items in slices 1, 2, and 3 are checked. + - Updated slice 1 and slice 2 archive headers from `Status: archived` to `Status: archived, completed` so they match the completed state already shown by their checked items. + - Slice 3 was already marked `Status: archived, completed`. + - Active planning surface is now clear for auth-service slices: future Priority 5 planning should start from this completion inventory rather than any stale active slice checklist. + +- 3. [x] Identify remaining direct auth side effects in `Login.svelte`, limited to signup, guest login, guest profile update, session/localStorage handling, and shared-store publication. + + Findings: + + - Important caveat: the item wording is too narrow if read literally. `Login.svelte` still contains legacy direct calls for behaviors now owned by `auth-service`: `verifySelf()`, `postAuth()`, and `deleteAuth()`. These should not be treated as new service-surface work, but they must be removed when `Login.svelte` is rewired to call `auth-service`. + - Already service-owned but still duplicated in `Login.svelte`: initial verification uses `verifySelf()` and writes `simple_comment_user`; user login uses `postAuth(userId, userPassword)`; logout uses `deleteAuth()`. + - Still not owned by `auth-service`: signup uses `createUser(userInfo)` from the `signingUp` state handler. This is the cleanest next command-ownership candidate because it is simpler than guest login and already maps onto the existing `SIGNUP -> signedUp -> loggingIn` machine path. + - Still not owned by `auth-service`: guest login flow reads stored guest credentials, attempts `postAuth(storedId, storedChallenge)`, calls `verifyUser()`, falls back to `getGuestToken()`, calls `verifyUser()` again, creates a guest with `createGuestUser(...)`, and sends success/error machine events. + - Still not owned by `auth-service`: guest profile update uses `updateUser({ id: storedId, name: displayName, email: userEmail })` when stored guest identity differs from the submitted guest form values. + - Session/localStorage handling remains in `Login.svelte`: login tab selection reads/writes `simple_comment_login_tab`; verified/current user state reads/writes `simple_comment_user`; guest login reads stored `simple_comment_user` to reuse guest credentials; mount logic hydrates form fields from stored user data. + - Shared-store publication remains in `Login.svelte`: `currentUserStore.set(self)` runs on destroy and reactively; `loginStateStore.set({ state, nextEvents })` publishes machine state; `loginStateStore.set({ select: selectedIndex })` publishes selected tab state. + - Relay coupling remains in `Login.svelte`: it subscribes to `dispatchableStore` and reacts to `loginIntent` / `logoutIntent` by driving its local machine. This is the component-side half of the later `CommentInput.svelte` / `SelfDisplay.svelte` decoupling work. + - Recommendation: do not plan all of these as one implementation phase. Keep the next slice to one command surface, preferably `signup()` first, then guest login/profile update, then rewiring/removal of legacy verify/login/logout handlers from `Login.svelte`. + +- 4. [x] Draft a slice for `signup()` command ownership in `auth-service`, with fail-first tests first and implementation in a separate pass. + + Findings: + + - Created `docs/plans/Priority5AuthServiceSlice4Checklist.md` as the proposed slice-4 checklist draft. + - Kept the slice narrow: `signup()` command ownership in `auth-service` only. + - Preserved the test/code separation convention: T01 adds fail-first tests, C01 implements production code later, and the checklist explicitly says implementation must stop if tests cannot be made green without changing tests. + - Kept out of scope: `Login.svelte` UI rewiring, form-local signup validation, guest-login behavior, localStorage handling, shared-store publication, `CommentInput.svelte`, and `SelfDisplay.svelte`. + - The proposed slice preserves current behavior intent: successful signup should create the user and continue through the existing post-signup login/session verification path to publish authenticated `currentUser`. + +- 5. [x] Draft a slice for `loginGuest()` command ownership in `auth-service`, including existing guest-token, verify, create guest, and update-if-changed behavior. + + Findings: + + - Created `docs/plans/Priority5AuthServiceSlice5Checklist.md` as the proposed slice-5 checklist draft. + - Kept the slice narrow: `loginGuest()` command ownership in `auth-service` only. + - Captured the storage seam explicitly: preserving stored guest reuse requires stored guest `id`/`challenge`/profile data, but this slice should not move `localStorage` ownership into `auth-service`; the service contract should accept reusable stored guest identity as explicit command input/dependency. + - Required reuse of existing `src/apiClient.ts` primitives: `postAuth`, `verifyUser`, `getGuestToken`, `createGuestUser`, `updateUser`, and `verifySelf`. + - Preserved the test/code separation convention: C01 tightens the command contract, T01 adds fail-first tests, C02 implements production code later, and implementation must stop if tests cannot be made green without changing tests. + - Kept out of scope: `Login.svelte` UI rewiring, guest form-local validation, `localStorage` extraction, shared-store publication, `CommentInput.svelte`, and `SelfDisplay.svelte`. + +- 6. [x] Draft a slice for moving session/localStorage persistence out of `Login.svelte` only if it blocks service ownership or component decoupling. + + Findings: + + - Resolved the conditional in favor of moving session/guest persistence out of `Login.svelte`: auth/session continuity and guest reuse should not depend on the `Login.svelte` component being mounted. + - Created `docs/plans/Priority5AuthServiceSlice6Checklist.md` as the proposed slice-6 checklist draft. + - Kept the slice narrow: move only `simple_comment_user` session/guest persistence behind a small auth persistence boundary, then let `auth-service` consume that boundary through an injectable dependency. + - Explicitly kept `simple_comment_login_tab` persistence out of scope because it is UI preference state owned by `Login.svelte`, not auth/session persistence. + - Preserved server verification as the source of truth: persisted user data is cache/form/guest-reuse support, not authoritative authentication state. + - Avoided broad architecture churn: no auth controller, no runtime component, no workflow module, no new event bus, and no `CommentInput.svelte` / `SelfDisplay.svelte` rewiring in this slice. + +- 7. [x] Draft a slice for wiring `Login.svelte` to call `auth-service` commands while keeping form-local state and field validation in `Login.svelte`. + + Findings: + + - Created `docs/plans/Priority5AuthServiceSlice7Checklist.md` as the proposed slice-7 checklist draft. + - Backed up and created `docs/plans/Priority5AuthServiceSlice7Plan.md` per `docs/norms/plan.md`, with explicit `Intent`, `Motivation`, scope boundaries, acceptance criteria, and validation strategy. + - Kept the slice narrow: `Login.svelte` should stop calling auth APIs directly and instead delegate auth commands to `auth-service` while retaining form-local state, field validation, selected-tab UI, selected-tab persistence, and status-message rendering. + - Chose the narrowest wiring seam: create a widget-scoped `AuthService` instance and thread it explicitly through the current component path rather than introducing a singleton import, new event bus, or broad store redesign. + - Called out a key constraint explicitly: `Login.svelte` should not keep a second authoritative auth runtime once it is delegating to `auth-service`; it should observe service-owned auth state and preserve the existing `loginStateStore` publication shape only as transitional compatibility for unreworked consumers. + - Kept item 8 and the relay-removal work out of scope: `CommentInput.svelte` and `SelfDisplay.svelte` still rely on `loginStateStore` / `dispatchableStore`, so slice 7 preserves those contracts rather than removing them prematurely. + - The existing slice-7 checklist should now be treated as pre-plan draft input and reconciled to the approved slice-7 plan before it is used for implementation. + +- 8. [x] Draft a slice for moving `Login.svelte` auth/session shared-store publication to a temporary widget-scoped `auth-service` bridge. + + Findings: + + - Created `docs/plans/Priority5AuthServiceSlice8Plan.md` and `docs/plans/Priority5AuthServiceSlice8Checklist.md` for the temporary bridge slice. + - Confirmed the slice keeps the bridge widget-scoped at the current composition root and avoids making `auth-service.ts` import legacy global stores directly. + - Confirmed all slice-8 checklist items are complete: fail-first bridge tests, bridge implementation, composition-root installation, `Login.svelte` tests, and removal of `Login.svelte` auth/session store publication. + - Confirmed `Login.svelte` still owns selected-tab UI publication only, preserving the current unreworked relay consumers for later slices. + - Validation evidence: `yarn run ci:local` passed after slice-8 implementation. + +- 9. [x] Draft a slice for removing `dispatchableStore` / `loginStateStore` login relay behavior from `CommentInput.svelte`. + + Findings: + + - Created and approved `docs/plans/Priority5AuthServiceSlice9Plan.md` and `docs/plans/Priority5AuthServiceSlice9Checklist.md`. + - Confirmed the slice used the existing widget-scoped `authService.authRequest` / `authOutcome` seam rather than introducing a new event bus or singleton service. + - Implemented request-scoped auth outcomes in `auth-service` so pending comment-submit auth requests can complete with success or remote/local failure. + - Updated `Login.svelte` to consume pending auth requests from `authService` while preserving form-local validation, direct form submissions, selected-tab publication, and the existing logout relay for Slice 10. + - Removed `CommentInput.svelte` imports and use of `dispatchableStore` / `loginStateStore` for login-before-comment behavior. + - Preserved selected-tab-dependent button copy and guest-comment validation through direct `Login.svelte` selected-tab binding. + - Validation evidence: `yarn run ci:local` passed after Slice 9 implementation. + +- 10. [x] Draft a slice for removing `dispatchableStore` / `loginStateStore` logout relay behavior from `SelfDisplay.svelte`. + + Findings: + + - Created and approved `docs/plans/Priority5AuthServiceSlice10Plan.md` and `docs/plans/Priority5AuthServiceSlice10Checklist.md`. + - Confirmed the slice kept the logout-relay removal narrow: `SelfDisplay.svelte` now receives the widget-scoped `authService`, reads logout availability and processing state from `authService.authRuntimeSnapshot`, and calls `authService.logout()` directly. + - Confirmed `src/components/SimpleComment.svelte` passes the existing widget-scoped `authService` to `SelfDisplay.svelte`. + - Added `src/tests/frontend/components/SelfDisplay.auth-service.test.ts` component coverage for logout button visibility, direct `authService.logout()` delegation, processing skeleton visibility, and a source guard against legacy relay store imports. + - Kept out-of-scope cleanup deferred: `Login.svelte` logout relay handling, legacy store definitions, and the temporary auth-service bridge remain for the planned cleanup slice. + - Validation evidence: `yarn run ci:local` passed after Slice 10 implementation. + +- 11. [x] Draft a cleanup slice for removing the temporary auth-service bridge and any legacy auth/session store paths made obsolete by slices 8-10. + + Findings: + + - Created and approved `docs/plans/Priority5AuthServiceSlice11Plan.md` and `docs/plans/Priority5AuthServiceSlice11Checklist.md`. + - Removed `Login.svelte` legacy `dispatchableStore` / `loginStateStore` relay handling, including `loginIntent`, `logoutIntent`, and selected-tab store publication, while preserving selected-tab binding and `simple_comment_login_tab` persistence. + - Replaced `SimpleComment.svelte` temporary bridge/global-store plumbing with a direct widget-scoped `authService.currentUser` subscription. + - Removed obsolete bridge/store unit tests and legacy store resets from component test setup after migrated component tests covered the auth-service path directly. + - Deleted `src/lib/auth-store-bridge.ts` and `src/lib/svelte-stores.ts` after repository import search confirmed no runtime or test imports remained. + - Validation evidence: repository import search found no `auth-store-bridge` or `svelte-stores` references under `src`, focused auth-service component tests passed, and `yarn run ci:local` passed. + - Cypress note: Cypress bootstrap was fixed separately in commit `53a1ec9`, but known behavioral Cypress failures remain intentionally out of scope for this cleanup slice. + +- 12. [x] Decide whether auth state should be passed directly through component props or exposed through a thin auth-service-backed store. + + Findings: + + - Decision: prefer the current direct widget-scoped `authService` plus explicit props architecture. + - `SimpleComment.svelte` remains the composition root: it creates one widget-scoped `authService`, subscribes to `authService.currentUser`, and passes `authService` / `currentUser` to children that need auth behavior or identity display. + - Components that need auth behavior should receive `authService`; components that only need identity display should receive `currentUser`. + - Do not add a new global auth store, singleton auth service, replacement event bus, or speculative `AuthRuntime.svelte` as part of Priority 5. + - A thin auth-service-backed store remains a possible later slice only if repeated prop threading or shared derived auth views become a concrete maintenance problem. + - Architectural impact: this keeps auth ownership in `auth-service.ts`, composition ownership in `SimpleComment.svelte`, and component dependencies explicit and testable. + +- 13. [x] Prefer direct `auth-service` API or a thin service-backed store; avoid introducing another ad-hoc event bus. + + Findings: + + - Resolved by item 12's architecture decision: prefer direct widget-scoped `authService` plus explicit props for current Priority 5 work. + - Do not introduce another ad-hoc event bus. The former `dispatchableStore` / `loginStateStore` relay path has been removed, and future auth interactions should not recreate that pattern under a new name. + - Use `authService` directly for auth commands and auth runtime observation at component boundaries that need behavior. + - Use `currentUser` props for identity display where no auth command/runtime behavior is needed. + - Keep a thin service-backed store as a deferred option only if a future slice demonstrates concrete prop-threading or shared-derived-state pressure. + +- 14. [x] Add validation expectations per slice: fail-first tests in one pass, production implementation in a later pass, no test edits during implementation unless the implementation session stops and explains why the test is wrong. + + Findings: + + - Resolved as a Priority 5 governance expectation rather than a new behavior slice. + - `docs/norms/implementation.md` already requires atomic checklist-item commits, item checkoff in the same commit, baseline validation, production-code-first fixes, broad regression checks, and stop conditions when scope or tests are wrong. + - Priority 5 additionally adopts the explicit test/code separation guard used during the auth-service slices: fail-first tests are written in their own pass, production implementation happens in a later pass, and tests must move from failing to passing through production code changes only. + - If a test cannot be made green without editing the test during an implementation pass, implementation must stop and explain why the test is bad before any test change is made. + - Documentation-only slices may satisfy validation by checking references, commands, and cited evidence consistency; they must not be used as approval for downstream behavior changes. + +- 15. [x] Keep explicitly out of scope: splitting `Login.svelte` into form components, adding `auth-controller.ts`, adding `AuthRuntime.svelte`, creating broad auth workflow modules, or redesigning frontend state architecture. + + Findings: + + - Resolved as a Priority 5 scope boundary rather than an implementation slice. + - Priority 5 intentionally ends with the narrow auth-service ownership model, widget-scoped `authService`, and explicit component props recorded in items 12 and 13. + - Splitting `Login.svelte` into form components remains out of scope; it may be worthwhile later, but it is not required to complete the auth-service extraction. + - Adding `auth-controller.ts`, `AuthRuntime.svelte`, broad auth workflow modules, a singleton auth service, or a replacement auth event bus remains out of scope because those options would reintroduce the architecture churn Priority 5 was trying to escape. + - Any future frontend state architecture redesign should be opened as a separate priority with its own plan, checklist, validation strategy, and explicit approval. diff --git a/docs/plans/Priority5FrontendArchitectureDecouplingChecklist.md b/docs/archive/Priority5FrontendArchitectureDecouplingChecklist.md similarity index 82% rename from docs/plans/Priority5FrontendArchitectureDecouplingChecklist.md rename to docs/archive/Priority5FrontendArchitectureDecouplingChecklist.md index d348299c..9307b9ec 100644 --- a/docs/plans/Priority5FrontendArchitectureDecouplingChecklist.md +++ b/docs/archive/Priority5FrontendArchitectureDecouplingChecklist.md @@ -1,6 +1,54 @@ # Priority 5 Frontend Architecture Decoupling Checklist -Status: planning +Status: archived, superseded + +## Supersession Note + +This checklist is archived and superseded. Do not use it as an implementation +guide for Priority 5. + +This document was useful as an early exploration of the frontend auth/login +coupling problem, but it is now considered unsafe for execution because it +over-specifies a large architecture for a comparatively straightforward +extraction task: + +- extract auth/login side effects out of `Login.svelte` into shared + TypeScript; +- keep auth transport calls delegated to `src/apiClient.ts`; +- lift login/auth coordination out of `CommentInput.svelte` and related relay + store coupling. + +The checklist violates the repo's current planning and checklist standards in +practice because it turns a small, reversible extraction into a broad +architecture program. It prescribes multiple new modules and abstractions +(`auth-storage.ts`, `auth-workflows.ts`, `auth-controller.ts`, +`auth-stores.ts`, `AuthRuntime.svelte`, and split form components) before the +current `auth-service` seam is complete or proven insufficient. That creates +too much harness for the task, weakens atomicity, and increases the chance of +behavioral drift. + +The checklist also conflicts with the backlog constraint to prefer small +planning slices over broad modernization efforts. It mixes mechanical +extraction, runtime ownership, storage policy, component decomposition, shared +store migration, consumer rewiring, and validation into one dependency chain. +That shape contributed to previous Priority 5 churn and should not be +reintroduced. + +The safer superseding direction is the incremental `auth-service` path already +started by the slice checklists: + +- finish `auth-service.login()` and `auth-service.logout()` using + `src/apiClient.ts`; +- keep `Login.svelte` responsible for form-local UI state for now; +- remove auth side effects from `Login.svelte` only after service behavior is + covered by focused tests; +- then decouple `CommentInput.svelte` and `SelfDisplay.svelte` from + `dispatchableStore` / `loginStateStore` relay behavior in small, reviewable + slices. + +Future Priority 5 plans should cite this document only as superseded context +and should not revive its module graph, controller/runtime split, or form +component split without a fresh approved plan that justifies those choices. Classification: proposed implementation checklist draft (not approved) diff --git a/docs/archive/Priority5SvelteComponentTestingChecklist.md b/docs/archive/Priority5SvelteComponentTestingChecklist.md new file mode 100644 index 00000000..afb69f5c --- /dev/null +++ b/docs/archive/Priority5SvelteComponentTestingChecklist.md @@ -0,0 +1,86 @@ +# Priority 5 Svelte Component Testing Checklist + +Status: archived + +Classification: archived implementation checklist + +Source plan: `docs/archive/Priority5SvelteComponentTestingPlan.md` + +## Scope Lock + +In scope: + +- add a dedicated frontend component-test lane for `.svelte` components using Vitest +- use `@testing-library/svelte` for DOM-oriented component tests +- keep the current Jest frontend suite in place for existing non-component tests +- add the minimal setup needed for component tests to run reliably in this repo: Vite-aligned Svelte compilation, DOM test environment, shared setup/cleanup, and frontend scripts that compose Jest plus component tests into one required validation path +- prove the harness with a narrow smoke-level `Login.svelte` component test that renders the component and checks stable, user-visible basics such as the login/signup/guest tabs +- preserve CI/local parity if the required frontend validation path changes + +Out of scope: + +- rewriting the existing frontend Jest suite to Vitest +- teaching the current Jest frontend config to become the primary Svelte component runner +- introducing `svelte-jester` as the main direction for repo component testing +- adding Vitest Browser Mode, Playwright component testing, Storybook, or Cypress into the required PR gate for this slice +- rewiring `Login.svelte` auth behavior to `auth-service` +- redesigning frontend state architecture, relay stores, or auth runtime ownership +- backfilling component tests across the whole component tree in the same pass + +## Atomic Checklist Items + +- [x] C01 `[frontend]` Add a dedicated Svelte component-test harness in `vitest.components.config.ts` and `src/tests/frontend/components/vitest.setup.ts` with Vite-aligned Svelte compilation, `jsdom`, DOM matchers, and per-test cleanup/reset for `localStorage` and any shared store state touched by `Login.svelte`. + - Depends on: none. + - Validated by: T01. + - Trace: + - "Add a dedicated frontend component-test lane for `.svelte` components using Vitest." (`In Scope`) + - "Add the minimal setup needed for component tests to run reliably in this repo: Svelte compilation through Vite-aligned config, DOM test environment, test setup/cleanup for DOM matchers and browser-local state" (`In Scope`) + - "Add a separate Vitest component-test configuration instead of retrofitting the current Jest frontend config to compile `.svelte`." (`Approach`) + - "Add one shared component-test setup file for DOM matchers and per-test cleanup of browser-local state that can leak across `Login.svelte` tests" (`Approach`) + +- [x] C02 `[frontend]` Update `package.json` and `yarn.lock` to add `vitest`, `@testing-library/svelte`, and `@testing-library/jest-dom`, then wire dedicated frontend component-test scripts and compose them into the required `yarn test:frontend` entry point while keeping the current Jest frontend suite intact. + - Depends on: C01. + - Validated by: `yarn test:frontend`. + - Trace: + - "Add a dedicated frontend component-test lane for `.svelte` components using Vitest." (`In Scope`) + - "Use `@testing-library/svelte` for DOM-oriented component tests." (`In Scope`) + - "Add the minimal setup needed for component tests to run reliably in this repo: Svelte compilation through Vite-aligned config, DOM test environment, test setup/cleanup for DOM matchers and browser-local state" (`In Scope`) + - "Keep the current Jest frontend suite in place for existing non-component tests." (`In Scope`) + - "frontend scripts that compose Jest plus component tests into one required validation path." (`In Scope`) + - "Required frontend validation includes the Svelte component-test lane through the normal repo entry point rather than an optional side command." (`Acceptance Criteria`) + - "Pass: the existing Jest frontend suite still passes after the component-test lane is introduced." (`Validation Strategy`) + +- [x] T01 `[tests]` Add a smoke-level `Login.svelte` component test in `src/tests/frontend/components/Login.smoke.test.ts` that renders the component in the Vitest lane and asserts stable, user-visible basics such as the login/signup/guest tabs, without broader auth-behavior assertions. + - Depends on: C01, C02. + - Validated by: `yarn test:frontend`. + - Trace: + - "Prove the harness with a narrow `Login.svelte` component test so the repo has a real working example rather than infrastructure-only churn." (`In Scope`) + - "Prove the harness with a narrow smoke-level `Login.svelte` test that renders the component and checks stable, user-visible basics such as the login/signup/guest tabs" (`Approach`) + - "At least one smoke-level `Login.svelte` component test runs successfully in the new lane as proof that the harness works in this repo." (`Acceptance Criteria`) + - "Pass: a real `Login.svelte` component test executes through the new Vitest lane using the repo's Svelte 5/Vite-compatible setup." (`Validation Strategy`) + +## Behavior Slices + +### Slice 1A + +Goal: establish a dedicated Svelte component-test lane that fits the current Vite/Svelte toolchain and required frontend test entry point. + +Items: C01, C02 + +Type: mechanical + +### Slice 1B + +Goal: prove the new harness works in this repo with one smoke-level `Login.svelte` component test. + +Items: T01 + +Type: behavior + +## Conformance QC (Checklist) + +- Missing from plan: none. +- Extra beyond plan: none. +- Atomicity fixes needed: none observed; each item is independently checkable and committable. +- Validation mapping gaps: none observed; harness, regression, and smoke-proof evidence are mapped to checklist items. +- Pass/Fail: checklist achieves plan goals — **Pass**. diff --git a/docs/archive/Priority5SvelteComponentTestingPlan.md b/docs/archive/Priority5SvelteComponentTestingPlan.md new file mode 100644 index 00000000..847afd71 --- /dev/null +++ b/docs/archive/Priority5SvelteComponentTestingPlan.md @@ -0,0 +1,167 @@ +# Priority 5 Svelte Component Testing Plan + +Status: archived + +Related artifacts: + +- `package.json` +- `jest.frontend.config.ts` +- `vite.config.ts` +- `tsconfig.frontend.json` +- `src/components/Login.svelte` +- `.github/workflows/netlify-api-test.yml` +- `scripts/ci-local.sh` + +## Goal + +Introduce a first-class, maintainable Svelte component testing path for this repository so `Login.svelte` component tests can run in required frontend validation without forcing a broad frontend test-runner migration. + +## Intent + +Success means contributors can write focused Svelte 5 component tests against real `.svelte` files in this repo, starting with `Login.svelte`, and run them through the normal frontend validation path. + +In plain language: + +- the existing Jest frontend suite keeps doing what it already does well, +- Svelte component tests get a dedicated runner that actually understands the repo's Vite/Svelte setup, +- required CI and `ci:local` treat those component tests as first-class validation, +- and this side quest stops before it turns into a full Jest-to-Vitest migration or a broader Priority 5 architecture rewrite. + +## Motivation + +Priority 5 work now needs first-class component tests for `Login.svelte`, but the current frontend Jest setup does not compile `.svelte` files. That is the real tooling gap. + +The current repo state makes a Jest-first Svelte component path possible but awkward: + +- frontend Jest currently handles `.ts` and `.js`, not `.svelte`, +- the repo does not have a root `svelte.config.js`; Svelte preprocessing currently lives inside `vite.config.ts`, +- `Login.svelte` depends on browser-facing behavior such as DOM rendering and `localStorage`, +- and a Jest Svelte path would require `svelte-jester`, Jest ESM mode, extra preprocess wiring, and additional `node_modules` transform exceptions. + +By contrast, the repo already builds frontend Svelte through Vite. A narrow Vitest component lane aligns with the existing Svelte 5 toolchain and is the smallest good way to unlock first-class component tests without disturbing the current Jest unit/integration coverage. + +## In Scope + +- Add a dedicated frontend component-test lane for `.svelte` components using Vitest. +- Use `@testing-library/svelte` for DOM-oriented component tests. +- Keep the current Jest frontend suite in place for existing non-component tests. +- Scope the new component lane to a dedicated test location so Jest and Vitest do not compete for the same test files. +- Add the minimal setup needed for component tests to run reliably in this repo: + - Svelte compilation through Vite-aligned config, + - DOM test environment, + - test setup/cleanup for DOM matchers and browser-local state, + - frontend scripts that compose Jest plus component tests into one required validation path. +- Prove the harness with a narrow `Login.svelte` component test so the repo has a real working example rather than infrastructure-only churn. +- Update CI/local parity if the required frontend validation path changes. + +## Out of Scope + +- Rewriting the existing frontend Jest suite to Vitest. +- Teaching the current Jest frontend config to become the primary Svelte component runner. +- Introducing `svelte-jester` as the main direction for repo component testing. +- Adding Vitest Browser Mode, Playwright component testing, Storybook, or Cypress into the required PR gate for this slice. +- Rewiring `Login.svelte` auth behavior to `auth-service`; that remains separate auth implementation work. +- Redesigning frontend state architecture, relay stores, or auth runtime ownership. +- Backfilling component tests across the whole component tree in the same pass. + +## Constraints + +- Keep Priority 5 churn low; this is enabling infrastructure, not a modernization campaign. +- Preserve the current contributor mental model where `yarn test:frontend` is the required frontend validation entry point. +- Preserve CI/local parity under `docs/norms/ci-parity.md` if required validation commands change. +- Keep fail-first testing and production implementation as separate passes. +- Prefer the smallest runner split that is easy to explain and maintain: Jest for current frontend unit/service/state tests, Vitest for Svelte component tests. +- Avoid introducing a second test approach for the same class of tests unless there is a clear boundary. + +## Current State + +At the start of this side quest: + +- `package.json` contains Jest, `ts-jest`, `babel-jest`, `jsdom`, Vite, and Svelte 5. +- `jest.frontend.config.ts` transforms `.ts` and `.js`, but not `.svelte`. +- the current frontend Jest suite passes and already covers stores, XState logic, utilities, and `auth-service`. +- `Login.svelte` has no first-class component tests. +- Cypress indirectly covers login/signup/logout/guest flows, but Cypress is intentionally outside required CI and `ci:local`. +- `vite.config.ts` already holds the repo's active Svelte preprocess/plugin configuration. + +## Approach + +1. Add a separate Vitest component-test configuration instead of retrofitting the current Jest frontend config to compile `.svelte`. +2. Use `@testing-library/svelte` with a DOM environment for component-boundary tests that focus on user-visible behavior and submission/validation outcomes. +3. Keep existing Jest frontend tests on their current path and give Vitest ownership only of Svelte component tests under a dedicated directory such as `src/tests/frontend/components/`. +4. Add one shared component-test setup file for DOM matchers and per-test cleanup of browser-local state that can leak across `Login.svelte` tests, such as `localStorage` and shared stores. +5. Make `yarn test:frontend` the composed entry point for both lanes so required CI and `ci:local` remain simple and first-class component tests are not optional. +6. Prove the harness with a narrow smoke-level `Login.svelte` test that renders the component and checks stable, user-visible basics such as the login/signup/guest tabs, without folding auth-service delegation work into the same change. +7. Stop there. Leave broader component coverage growth and later auth-behavior component tests to explicitly scoped follow-up checklist work. + +## Risks and Mitigations + +- Risk: adding Vitest quietly turns into a repo-wide migration away from Jest. + - Mitigation: keep the boundary explicit and stable: Vitest owns `.svelte` component tests only; existing Jest tests stay where they are. + +- Risk: this side quest quietly absorbs separate auth rewiring work by changing `Login.svelte` production behavior just to make tests possible. + - Mitigation: keep the proof test narrow and treat auth-service delegation assertions as later follow-up work. + +- Risk: using the existing `vite.config.ts` directly for tests introduces unintended build-root assumptions because that file is build-oriented and sets `root` to `src/entry`. + - Mitigation: use a dedicated Vitest config that reuses only the Svelte plugin/preprocess parts needed for tests. + +- Risk: component tests become flaky because shared stores or `localStorage` state leak between tests. + - Mitigation: add explicit per-test cleanup/reset in the shared component-test setup file. + +- Risk: required CI grows confusing if scripts or parity paths split in an ad hoc way. + - Mitigation: keep `yarn test:frontend` as the single required frontend test entry point and update `.github/workflows/netlify-api-test.yml` and `scripts/ci-local.sh` together only if necessary. + +## Acceptance Criteria + +1. The repo has a dedicated, documented test path that can execute Svelte 5 component tests against real `.svelte` files. +2. `@testing-library/svelte` is the standard DOM-level API for this component test lane. +3. The existing frontend Jest suite remains intact and continues to own the current non-component frontend tests. +4. Required frontend validation includes the Svelte component-test lane through the normal repo entry point rather than an optional side command. +5. At least one smoke-level `Login.svelte` component test runs successfully in the new lane as proof that the harness works in this repo. +6. The plan does not require `svelte-jester`, Jest ESM mode, or a broader Jest frontend reconfiguration as the primary solution. +7. The change remains narrow enough that later `Login.svelte` behavior-focused component tests can build on the same runner selection without reopening framework choice. + +## Validation Strategy + +This plan changes required test infrastructure and the frontend validation path, so explicit evidence is required. + +- **Component-harness evidence** + - Pass: a real `Login.svelte` component test executes through the new Vitest lane using the repo's Svelte 5/Vite-compatible setup. + - Fail: `.svelte` tests still cannot compile or run, or the harness only exists on paper. + +- **Frontend-regression evidence** + - Pass: the existing Jest frontend suite still passes after the component-test lane is introduced. + - Fail: enabling component tests breaks the current frontend Jest suite or forces unrelated test rewrites. + +- **Parity evidence** + - Pass: if `yarn test:frontend` or other required frontend validation commands change, the mirrored CI/local parity surfaces remain aligned in `.github/workflows/netlify-api-test.yml` and `scripts/ci-local.sh`. + - Fail: required PR-gate behavior and `ci:local` drift apart. + +- **Scope evidence** + - Pass: the side quest introduces component-test infrastructure and a smoke-level proof test without also implementing `Login.svelte` auth rewiring. + - Fail: the same slice changes `Login.svelte` auth behavior, relay architecture, or broader frontend state design. + +## Open Questions / Assumptions + +- Assumption: `jsdom` is sufficient for the first `Login.svelte` component tests because the immediate need is form rendering, submission, validation, and `localStorage` interaction rather than layout-accurate browser rendering. +- Assumption: a dedicated component-test directory is the cleanest way to keep Jest and Vitest ownership boundaries obvious. +- Assumption: the proof `Login.svelte` test should stay smoke-level only: render the component and assert stable, user-visible basics rather than broader auth behavior. + +## Scope Guard + +The following work is explicitly deferred and must not be folded into this plan without a separate approved plan/checklist update: + +- converting existing frontend Jest tests to Vitest, +- expanding required CI to Browser Mode, Playwright, Cypress, or Storybook, +- rewriting `Login.svelte` to use `auth-service`, +- broad component-test backfill across unrelated components, +- frontend state or auth architecture redesign. + +## Conformance QC (Plan) + +- Intent clarity issues: none observed; the plan distinguishes the test-harness side quest from separate `Login.svelte` auth rewiring work in plain language. +- Missing required sections: none (`Goal`, `Intent`, `In Scope`, `Out of Scope`, `Acceptance Criteria`, and `Validation Strategy` are present). +- Ambiguities/assumptions to resolve: none blocking checklist authoring. +- Validation strategy gaps: none for the runner-selection, parity, and proof-of-harness scope. +- Traceability readiness: ready; stable headings and explicit acceptance criteria are present for checklist citation. +- Pass/Fail: structurally ready for collaborative review and checklist authoring once the plain-language intent is approved — **Pass**. diff --git a/jest.frontend.config.ts b/jest.frontend.config.ts index 8ecbb3e3..051ea81d 100644 --- a/jest.frontend.config.ts +++ b/jest.frontend.config.ts @@ -11,7 +11,12 @@ export default { coverageProvider: "v8", resetMocks: true, roots: ["/src/tests/frontend/"], - testPathIgnorePatterns: ["\\\\node_modules\\\\", "RAW", ".js$"], + testPathIgnorePatterns: [ + "\\\\node_modules\\\\", + "RAW", + ".js$", + "/src/tests/frontend/components/", + ], transform: { "^.+\\.js$": "babel-jest", "^.+\\.ts$": [ diff --git a/package.json b/package.json index eac4477c..113e719c 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "@eslint/js": "^10.0.1", "@shelf/jest-mongodb": "^6.0.2", "@sveltejs/vite-plugin-svelte": "^4.0.0", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/svelte": "^5.3.1", "@types/bcryptjs": "^2.4.2", "@types/jest": "^29.5.3", "@types/jsonwebtoken": "^8.5.0", @@ -15,10 +17,10 @@ "cross-env": "^7.0.3", "globals": "^17.4.0", "knip": "^6.0.3", - "vite": "^6.4.2" + "vite": "^6.4.2", + "vitest": "^3.2.4" }, "optionalDependencies": { - "cypress": "^12.17.4", "netlify-cli": "^24.0.1" }, "scripts": { @@ -39,13 +41,17 @@ "dev": "concurrently \"yarn run watch:backend\" \"NODE_ENV=development NODE_OPTIONS=\\\"--max-old-space-size=4096\\\" netlify dev\" --kill-others", "test": "yarn test:backend && yarn test:frontend", "test:backend": "jest --config jest.backend.config.ts", - "test:cypress": "cypress run", - "test:frontend": "cross-env TZ=UTC jest --config jest.frontend.config.ts", + "test:cypress": "cross-env ELECTRON_RUN_AS_NODE= cypress run", + "test:frontend": "yarn test:frontend:unit && yarn test:frontend:components", + "test:frontend:components": "cross-env TZ=UTC vitest --config vitest.components.config.ts run", + "test:frontend:unit": "cross-env TZ=UTC jest --config jest.frontend.config.ts", "typecheck": "tsc --noEmit -p tsconfig.frontend.json && tsc --noEmit -p tsconfig.netlify.functions.json", "watch:backend": "webpack --config ./webpack.netlify.functions.cjs --watch", "watch:frontend": "vite build --config ./vite.config.ts --watch", "watch:lint": "esw -w ./src/**/*.ts --color --clear --changed", - "watch:test": "jest --watch --noStackTrace", + "watch:test": "concurrently \"yarn watch:test:unit\" \"yarn watch:test:components\"", + "watch:test:components": "cross-env TZ=UTC vitest --config vitest.components.config.ts", + "watch:test:unit": "jest --watch --noStackTrace", "knip": "knip" }, "dependencies": { @@ -62,6 +68,7 @@ "bcryptjs": "^2.4.3", "carbon-icons-svelte": "^13.10.0", "concurrently": "^7.1.0", + "cypress": "^15.14.1", "dotenv": "^16.0.0", "eslint": "^10.1.0", "eslint-plugin-svelte": "^3.16.0", @@ -95,7 +102,8 @@ "resolutions": { "@cypress/request": "^3.0.0", "async": ">2.6.4 || ^2.6.4", - "glob-parent": ">6.0.2 || ^6.0.2" + "glob-parent": ">6.0.2 || ^6.0.2", + "vite": "6.4.2" }, "documentation": { "resolutions": { diff --git a/src/components/CommentDisplay.svelte b/src/components/CommentDisplay.svelte index 25994388..88694583 100644 --- a/src/components/CommentDisplay.svelte +++ b/src/components/CommentDisplay.svelte @@ -18,10 +18,12 @@ import OverflowMenuHorizontal from "carbon-icons-svelte/lib/OverflowMenuHorizontal.svelte" import ChevronLeft from "carbon-icons-svelte/lib/ChevronLeft.svelte" import { linear } from "svelte/easing" + import type { AuthService } from "../lib/auth-service" export let comment: (Comment & { isNew?: true }) | undefined = undefined export let showReply: string export let currentUser: User | undefined + export let authService: AuthService export let onDeleteSuccess export let onDeleteCommentClick export let onOpenCommentInput @@ -189,6 +191,7 @@ {/if} {#if showReply === comment.id && !isEditing} 0} { - dispatchableStore.dispatch("loginIntent") + if (pendingAuthRequestId) return + + const { requestId } = authService.requestAuth("comment-submit") + + pendingAuthRequestId = requestId } - const unsubscribeLoginState = loginStateStore.subscribe(loginState => { - const { state: stateValue, select } = loginState - - if (stateValue) { - loginStateValue = stateValue - const commentInputStateValue = $state.value - - //TODO: This state handling should be done via XState, probably by combining these state machines - switch (commentInputStateValue) { - case "loggingIn": - switch (loginStateValue) { - case "loggedIn": - setTimeout(() => send("SUCCESS"), 1) - break - case "error": - setTimeout(() => send({ type: "ERROR", error: "Login error" })) - break - case "loggedOut": - dispatchableStore.dispatch("loginIntent") - break - - default: - console.warn( - `Unhandled loginState '${loginStateValue}' in CommentInput` - ) - break - } - break + const handleAuthOutcome = (authOutcome: AuthOutcomeState) => { + if (!pendingAuthRequestId || authOutcome.status === "none") return + if (authOutcome.requestId !== pendingAuthRequestId) return - default: - break - } - } else if (select !== undefined) { - loginTabSelect = select + pendingAuthRequestId = undefined + + switch (authOutcome.status) { + case "success": + authOutcomeUser = authOutcome.user + authService.clearAuthOutcome(authOutcome.requestId) + send("SUCCESS") + break + + case "localValidationError": + authService.clearAuthOutcome(authOutcome.requestId) + send({ type: "ERROR", error: authOutcome.message }) + break + + case "remoteError": + authService.clearAuthOutcome(authOutcome.requestId) + send({ type: "ERROR", error: authOutcome.error }) + break + + case "cancelled": + authService.clearAuthOutcome(authOutcome.requestId) + send({ type: "ERROR", error: "Authentication cancelled" }) + break + + default: + break } - }) + } + + const unsubscribeAuthOutcome = authService.authOutcome.subscribe( + handleAuthOutcome + ) const postingStateHandler = async () => { try { @@ -170,7 +177,7 @@ }) onDestroy(() => { - unsubscribeLoginState() + unsubscribeAuthOutcome() }) $: { @@ -192,7 +199,11 @@ ["validating", "loggingIn", "posting", "deleting"] as StateValue[] ).includes($state.value) - $: buttonCopy = getButtonCopy(loginTabSelect, commentText, loginStateValue) + $: buttonCopy = getButtonCopy( + loginTabSelect, + commentText, + currentUser ?? authOutcomeUser ? "loggedIn" : undefined + ) - + {#if !currentUser || (commentText && commentText.length)}
{#if onCancel !== null} diff --git a/src/components/CommentList.svelte b/src/components/CommentList.svelte index 962e31a2..4ef863ac 100644 --- a/src/components/CommentList.svelte +++ b/src/components/CommentList.svelte @@ -10,8 +10,10 @@ import { commentDeleteMachine } from "../lib/commentDelete.xstate" import { deleteComment } from "../apiClient" import CommentDisplay from "./CommentDisplay.svelte" + import type { AuthService } from "../lib/auth-service" export let currentUser: User | undefined + export let authService: AuthService export let replies: (Comment & { isNew?: true; isDelete?: true })[] = [] export let depth: number = 0 export let showReply = "" @@ -105,6 +107,7 @@
    4}> {#each replies as comment} import { fly } from "svelte/transition" import type { - AdminSafeUser, ServerResponse, - ServerResponseSuccess, - TokenClaim, User, UserId, ValidationResult, } from "../lib/simple-comment-types" import { LoginTab } from "../lib/simple-comment-types" - import { useMachine } from "@xstate/svelte" - import { loginMachine } from "../lib/login.xstate" - import { - createGuestUser, - createUser, - deleteAuth, - getGuestToken, - getOneUser, - postAuth, - updateUser, - verifySelf, - verifyUser, - } from "../apiClient" + import { getOneUser } from "../apiClient" import { debounceFunc, isValidationTrue, - isResponseOk, validatePassword, validateUserId, formatUserId, } from "../frontend-utilities" import InputField from "./low-level/InputField.svelte" - import { - currentUserStore, - dispatchableStore, - loginStateStore, - } from "../lib/svelte-stores" import { isGuestId, isValidResult, @@ -48,7 +27,12 @@ import PasswordInput from "./low-level/PasswordInput.svelte" import PasswordTwinInput from "./low-level/PasswordTwinInput.svelte" import Avatar from "./low-level/Avatar.svelte" - import type { StateValue } from "xstate" + import type { + AuthRequestState, + AuthRuntimeSnapshot, + AuthService, + } from "../lib/auth-service" + import { loadStoredUser } from "../lib/auth-persistence" const DISPLAY_NAME_HELPER_TEXT = "This is the name that others will see" const USER_EMAIL_HELPER_TEXT = @@ -56,12 +40,15 @@ const USER_ID_HELPER_TEXT = "This is the user id that uniquely identifies you" export let currentUser: User | undefined + export let authService: AuthService + export let selectedTab: LoginTab = LoginTab.guest + + type PendingAuthRequest = Extract let self: User = currentUser let isError = false let isLoaded = false // Hide the component until isLoaded is true - let nextEvents = [] let statusMessage = "" let displayName = "" @@ -87,54 +74,63 @@ let userPasswordMessage = undefined let userPasswordStatus = undefined - let selectedIndex = isNaN( + let selectedIndex: LoginTab = isNaN( parseInt(localStorage.getItem("simple_comment_login_tab")) ) ? LoginTab.guest - : parseInt(localStorage.getItem("simple_comment_login_tab")) + : (parseInt(localStorage.getItem("simple_comment_login_tab")) as LoginTab) let lastIdChecked + let pendingAuthRequestId: string | undefined = undefined + let handledAuthRequestId: string | undefined = undefined - const { state, send } = useMachine(loginMachine) const updateStatusDisplay = (message = "", error = false) => { statusMessage = message isError = error } - //TODO: Move the log in *functionality* away from the Login.svelte *component*. Currently the Login component must be on the page for login functionality to occur. + const reportLocalError = (message: string) => + updateStatusDisplay(message, true) - /** Note that usually these onClick events will not be used. Rather, "loginIntent" will be sent.*/ - const onGuestClick = async (e: Event) => { - e.preventDefault() - updateStatusDisplay() + const reportPendingAuthValidationError = (message: string) => { + if (!pendingAuthRequestId) return - const validations = [ - () => validateDisplayName(displayName), - () => validateEmail(userEmail), - ].map(validation => validation()) - const result = joinValidations(validations) + authService.reportLocalValidationError({ + message, + requestId: pendingAuthRequestId, + }) + } - if (isValidResult(result)) - send({ type: "GUEST", guest: { name: displayName, email: userEmail } }) - else send({ type: "ERROR", error: result.reason }) + const onGuestClick = async (e: Event) => { + e.preventDefault() + await submitGuestLogin() } const onLoginClick = async (e: Event) => { e.preventDefault() + await submitLogin() + } + + const onSignupClick = async (e: Event) => { + e.preventDefault() + await submitSignup() + } + + const submitLogin = async () => { updateStatusDisplay() const result = checkLoginValid() - if (isValidResult(result)) send({ type: "LOGIN" }) - else - send({ - type: "ERROR", - error: result.reason, - }) + if (!isValidResult(result)) { + reportLocalError(result.reason) + reportPendingAuthValidationError(result.reason) + return + } + + await authService.login({ userId, password: userPassword }) } - const onSignupClick = async (e: Event) => { - e.preventDefault() + const submitSignup = async () => { updateStatusDisplay() const result = joinValidations([ @@ -145,98 +141,134 @@ checkPasswordsMatch(), ]) - if (isValidResult(result)) send({ type: "SIGNUP" }) - else - send({ - type: "ERROR", - error: result.reason, - }) - } - - /** Handler for XState "verifying" state */ - const verifyingStateHandler = () => { - updateStatusDisplay() - - if (self || currentUser) send({ type: "SUCCESS" }) - else - verifySelf() - .then((user: AdminSafeUser) => { - self = user - localStorage.setItem("simple_comment_user", JSON.stringify(user)) - send({ type: "SUCCESS" }) - }) - .catch(error => { - const { status } = error - if (status === 401) send({ type: "FIRST_VISIT" }) - else send({ type: "ERROR", error }) - }) - } - - const loggingInStateHandler = () => { - updateStatusDisplay() - const result = checkLoginValid() if (!isValidResult(result)) { - send({ type: "ERROR", error: result.reason }) + reportLocalError(result.reason) + reportPendingAuthValidationError(result.reason) return } - postAuth(userId, userPassword) - .then(response => { - if (isResponseOk(response)) { - send("SUCCESS") - } else { - send({ type: "ERROR", error: response }) - } - }) - .catch(error => { - send({ type: "ERROR", error }) - }) + + await authService.signup({ + userId, + password: userPassword, + displayName, + email: userEmail, + }) } - const signingUpStateHandler = () => { + const submitGuestLogin = async () => { updateStatusDisplay() - const result = joinValidations([ + + const guestValidationResult = joinValidations([ checkDisplayNameValid(), - checkUserIdValid(), checkUserEmailValid(), - checkPasswordValid(), - checkPasswordsMatch(), ]) - if (!isValidResult(result)) { - send({ type: "ERROR", error: result.reason }) + + if (!isValidResult(guestValidationResult)) { + reportLocalError(guestValidationResult.reason) + reportPendingAuthValidationError(guestValidationResult.reason) return } - const userInfo = { - id: userId, - name: displayName, + await authService.loginGuest({ + displayName, email: userEmail, - password: userPassword, + }) + } + + const handleAuthRuntimeSnapshot = ({ + state, + error, + }: AuthRuntimeSnapshot) => { + isLoaded = + isLoaded || + (["loggedIn", "loggedOut", "error"] as string[]).includes(state) + + switch (state) { + case "loggedIn": + updateStatusDisplay() + break + + case "loggedOut": + updateStatusDisplay() + self = undefined + break + + case "error": + errorStateHandler(error) + break + + default: + updateStatusDisplay() + break } - createUser(userInfo) - .then(() => send("SUCCESS")) - .catch(error => send({ type: "ERROR", error })) } - const loggedInStateHandler = () => { - updateStatusDisplay() + const handleAuthCurrentUser = (user: User | undefined) => { + self = user } - const loggingOutStateHandler = () => { - updateStatusDisplay() - deleteAuth() - .then(() => send("SUCCESS")) - .catch(error => send({ type: "ERROR", error })) + const submitSelectedAuthRequest = async ({ + requestId, + }: PendingAuthRequest) => { + if (handledAuthRequestId === requestId) return + + handledAuthRequestId = requestId + pendingAuthRequestId = requestId + + try { + switch (selectedIndex) { + case LoginTab.guest: + await submitGuestLogin() + break + + case LoginTab.signup: + await submitSignup() + break + + case LoginTab.login: + await submitLogin() + break + + default: + reportLocalError(`Unknown selectedTabIndex ${selectedIndex}`) + reportPendingAuthValidationError( + `Unknown selectedTabIndex ${selectedIndex}` + ) + break + } + } finally { + pendingAuthRequestId = undefined + } } - const loggedOutStateHandler = () => { - updateStatusDisplay() - self = undefined + const handleAuthRequest = (authRequest: AuthRequestState) => { + if (authRequest.status === "idle") { + handledAuthRequestId = undefined + return + } + + submitSelectedAuthRequest(authRequest) + } + + let unsubscribeAuthRuntimeSnapshot = () => undefined + let unsubscribeAuthCurrentUser = () => undefined + let unsubscribeAuthRequest = () => undefined + + const hydrateStoredUserFields = () => { + const storedUser = loadStoredUser() + + if (!storedUser) return + + const { id, name, email } = storedUser + + if (id && !isGuestId(id)) userId = id + if (name) displayName = name + if (email) userEmail = email } - const errorStateHandler = () => { + const errorStateHandler = (error?: ServerResponse | string) => { updateStatusDisplay() - const error = $state.context.error if (!error) { updateStatusDisplay( "Apologies. An unknown error occurred. Please reload the page and try again. If the error persists, contact the site administrator", @@ -456,120 +488,6 @@ checkUserIdExists_debounced(userId) } - const guestLoggingInStateHandler = () => { - const guestValidationResult = joinValidations([ - checkDisplayNameValid(), - checkUserEmailValid(), - ]) - - if (!isValidResult(guestValidationResult)) { - send({ type: "ERROR", error: guestValidationResult.reason }) - return - } - - const storedItem: string | null = localStorage.getItem( - "simple_comment_user" - ) - - const storedUser = storedItem - ? (JSON.parse(storedItem) as { - id?: string - name?: string - email?: string - challenge?: string - }) - : { id: undefined, challenge: undefined } - - const { - id: storedId, - challenge: storedChallenge, - name: storedName, - email: storedEmail, - } = storedUser - - const postAuthFlow = () => - postAuth(storedId, storedChallenge) - .then(() => verifyUser()) - .then((response: ServerResponseSuccess) => response) - .then(response => { - if (isResponseOk(response)) { - send("SUCCESS") - } else getTokenFlow() - }) - .catch(getTokenFlow) - - const getTokenFlow = () => - getGuestToken() - .then(() => verifyUser()) - .then( - (response: ServerResponseSuccess) => response.body.user - ) - .then(id => - createGuestUser({ id, name: displayName, email: userEmail }) - ) - .then(response => { - if (isResponseOk(response)) send("SUCCESS") - else send({ type: "ERROR", error: response }) - }) - .catch(error => { - console.error(error) - send({ type: "ERROR", error }) - }) - - const updateIfChanged = () => { - if (displayName !== storedName || userEmail !== storedEmail) { - updateUser({ id: storedId, name: displayName, email: userEmail }).catch( - error => send({ type: "ERROR", error }) - ) - } - } - - if (storedId && storedChallenge) postAuthFlow().then(updateIfChanged) - else getTokenFlow() - } - - const unsubscribeDispatchableStore = dispatchableStore.subscribe(event => { - switch (event.name) { - case "logoutIntent": { - const canLogout = nextEvents?.includes("LOGOUT") - if (canLogout) send("LOGOUT") - else console.warn("Received logoutIntent at state", $state.value) - break - } - - case "loginIntent": { - const canLogin = nextEvents?.some(event => - ["LOGIN", "GUEST", "SIGNUP"].includes(event) - ) - if (canLogin) { - switch (selectedIndex) { - case LoginTab.guest: - send("GUEST") - break - case LoginTab.signup: - send("SIGNUP") - break - case LoginTab.login: - send("LOGIN") - break - default: - send({ - type: "ERROR", - error: `Unknown selectedTabIndex ${selectedIndex}`, - }) - break - } - } else if ($state.value === "error") send("RESET") - else console.warn("Received loginIntent at state", $state.value) - break - } - - default: - // Intentionally left blank. Do not respond to other events. - break - } - }) - const checkPasswordValid = () => { const result = validatePassword(userPassword) @@ -624,57 +542,23 @@ onMount(() => { self = currentUser - const storedItem: string | null = localStorage.getItem( - "simple_comment_user" + hydrateStoredUserFields() + unsubscribeAuthRuntimeSnapshot = authService.authRuntimeSnapshot.subscribe( + handleAuthRuntimeSnapshot ) - if (storedItem) { - const storedUser = JSON.parse(storedItem) as { - id?: string - name?: string - email?: string - } - - const { id, name, email } = storedUser - - if (id && !isGuestId(id)) userId = id - if (name) displayName = name - if (email) userEmail = email - } + unsubscribeAuthCurrentUser = + authService.currentUser.subscribe(handleAuthCurrentUser) + unsubscribeAuthRequest = authService.authRequest.subscribe(handleAuthRequest) + authService.init() }) onDestroy(() => { - currentUserStore.set(self) - unsubscribeDispatchableStore() + unsubscribeAuthRuntimeSnapshot() + unsubscribeAuthCurrentUser() + unsubscribeAuthRequest() }) - $: { - const stateHandlers: [string, () => void][] = [ - ["verifying", verifyingStateHandler], - ["guestLoggingIn", guestLoggingInStateHandler], - ["loggingIn", loggingInStateHandler], - ["signingUp", signingUpStateHandler], - ["loggedIn", loggedInStateHandler], - ["loggingOut", loggingOutStateHandler], - ["loggedOut", loggedOutStateHandler], - ["error", errorStateHandler], - ] - - isLoaded = - isLoaded || - (["loggedIn", "loggedOut", "error"] as StateValue[]).includes( - $state.value - ) - - nextEvents = $state.nextEvents ?? [] - loginStateStore.set({ state: $state.value, nextEvents }) - stateHandlers.forEach(([stateValue, stateHandler]) => { - if ($state.value === stateValue) setTimeout(stateHandler, 1) - }) - } - - $: currentUserStore.set(self) - - $: loginStateStore.set({ select: selectedIndex }) + $: selectedTab = selectedIndex $: { if (userId.length < 3 && !userIdStatus) diff --git a/src/components/SelfDisplay.svelte b/src/components/SelfDisplay.svelte index 8ec8fd26..24066a5f 100644 --- a/src/components/SelfDisplay.svelte +++ b/src/components/SelfDisplay.svelte @@ -1,33 +1,42 @@ @@ -60,7 +69,7 @@

    {currentUser.email}

- {#if loginStateNextEvents?.includes("LOGOUT")} + {#if authNextEvents.includes("LOGOUT")} {/if} diff --git a/src/components/SimpleComment.svelte b/src/components/SimpleComment.svelte index 62308f05..ae9ab5e2 100644 --- a/src/components/SimpleComment.svelte +++ b/src/components/SimpleComment.svelte @@ -1,16 +1,28 @@
- - + +
diff --git a/src/lib/auth-persistence.ts b/src/lib/auth-persistence.ts new file mode 100644 index 00000000..a7234650 --- /dev/null +++ b/src/lib/auth-persistence.ts @@ -0,0 +1,102 @@ +import type { Email, User, UserId } from "./simple-comment-types" + +const STORED_USER_KEY = "simple_comment_user" + +type StorageLike = Pick + +export type StoredGuestIdentity = { + id?: UserId + challenge?: string + name?: string + email?: Email +} + +export type AuthPersistence = { + loadStoredUser: () => User | undefined + saveStoredUser: (user: User) => void + clearStoredUser: () => void + loadStoredGuestIdentity: () => StoredGuestIdentity | undefined +} + +const getStorage = (): StorageLike | undefined => { + if (typeof globalThis.localStorage === "undefined") return undefined + return globalThis.localStorage +} + +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null + +const asString = (value: unknown): string | undefined => + typeof value === "string" && value.length > 0 ? value : undefined + +const parseStoredUser = (): Record | undefined => { + const storage = getStorage() + const storedValue = storage?.getItem(STORED_USER_KEY) + + if (!storedValue) return undefined + + try { + const parsedValue = JSON.parse(storedValue) as unknown + + return isRecord(parsedValue) ? parsedValue : undefined + } catch { + return undefined + } +} + +export const loadStoredUser = (): User | undefined => { + const storedUser = parseStoredUser() + + if (!storedUser) return undefined + + const id = asString(storedUser.id) + const name = asString(storedUser.name) + const email = asString(storedUser.email) + + if (!id || !name || !email) return undefined + + return { + ...storedUser, + id, + name, + email, + } as User +} + +export const saveStoredUser = (user: User): void => { + const storage = getStorage() + + storage?.setItem(STORED_USER_KEY, JSON.stringify(user)) +} + +export const clearStoredUser = (): void => { + const storage = getStorage() + + storage?.removeItem(STORED_USER_KEY) +} + +export const loadStoredGuestIdentity = (): StoredGuestIdentity | undefined => { + const storedUser = parseStoredUser() + + if (!storedUser) return undefined + + const storedGuestIdentity: StoredGuestIdentity = { + id: asString(storedUser.id), + challenge: asString(storedUser.challenge), + name: asString(storedUser.name), + email: asString(storedUser.email), + } + + const hasReusableGuestIdentity = Object.values(storedGuestIdentity).some( + value => value !== undefined + ) + + return hasReusableGuestIdentity ? storedGuestIdentity : undefined +} + +export const authPersistence: AuthPersistence = { + loadStoredUser, + saveStoredUser, + clearStoredUser, + loadStoredGuestIdentity, +} diff --git a/src/lib/auth-service.ts b/src/lib/auth-service.ts new file mode 100644 index 00000000..c0dfcc2e --- /dev/null +++ b/src/lib/auth-service.ts @@ -0,0 +1,509 @@ +/** + * Widget-scoped auth service for Simple Comment. + * + * This module owns the live auth machine/runtime instance and the auth API + * side effects for a single mounted widget. It exposes the auth session state, + * current user, auth request state, and auth request outcome as a single + * shared boundary for auth-aware components. + * + * `Login.svelte` uses this service to submit login, signup, guest-login, and + * logout commands while retaining ownership of form-local field state and + * field-level validation UI. `CommentInput.svelte` and reply flows use this + * service to request authentication and observe request-scoped outcomes + * without depending on `Login.svelte` mount timing or component-mediated side + * effects. `SelfDisplay.svelte` uses this service to read the authenticated + * user and trigger logout. + * + * The auth session lifecycle vocabulary comes from `login.xstate.ts`, which + * remains the source of truth for auth machine states and transitions. This + * service interprets that machine, publishes readable session state derived + * from it, and keeps request bookkeeping separate from machine lifecycle + * state so callers can distinguish auth session status from the outcome of a + * specific auth request. + */ +import type { Readable } from "svelte/store" +import { get, writable } from "svelte/store" +import { interpret } from "xstate" +import type { StateValueFrom } from "xstate" +import { + createGuestUser, + createUser, + deleteAuth, + getGuestToken, + postAuth, + updateUser, + verifySelf, + verifyUser, +} from "../apiClient" +import { + authPersistence, + type AuthPersistence, + type StoredGuestIdentity, +} from "./auth-persistence" +import { loginMachine } from "./login.xstate" +import type { + Email, + ServerResponse, + User, + UserId, +} from "./simple-comment-types" + +type LoginMachineState = StateValueFrom + +export type AuthSessionState = LoginMachineState + +export type AuthRuntimeSnapshot = { + state: AuthSessionState + nextEvents: string[] + error?: ServerResponse | string +} + +export type AuthRequestReason = + | "comment-submit" + | "reply-submit" + | "manual-login" + +export type AuthRequestState = + | { status: "idle" } + | { + status: "pending" + reason: AuthRequestReason + requestId: string + } + +export type AuthOutcomeState = + | { status: "none" } + | { + status: "localValidationError" + message: string + requestId: string + } + | { + status: "remoteError" + error: ServerResponse | string + requestId: string + } + | { + status: "success" + user: User + requestId: string + } + | { + status: "cancelled" + requestId: string + } + +type PendingAuthRequest = Extract +type ActiveAuthOutcome = Exclude + +export type LoginPayload = { + userId: UserId + password: string +} + +export type SignupPayload = { + userId: UserId + password: string + displayName: string + email: Email +} + +export type { StoredGuestIdentity } from "./auth-persistence" + +export type GuestLoginPayload = { + displayName: string + email: Email + storedGuest?: StoredGuestIdentity +} + +export type ReportLocalValidationErrorInput = { + message: string + requestId?: string +} + +export type CreateAuthServiceOptions = { + initialUser?: User + persistence?: AuthPersistence +} + +export type AuthService = { + sessionState: Readable + currentUser: Readable + authRequest: Readable + authOutcome: Readable + authRuntimeSnapshot: Readable + init: () => Promise + requestAuth: (reason: AuthRequestReason) => { requestId: string } + clearAuthOutcome: (requestId?: string) => void + cancelAuthRequest: (requestId?: string) => void + reportLocalValidationError: (input: ReportLocalValidationErrorInput) => void + login: (payload: LoginPayload) => Promise + signup: (payload: SignupPayload) => Promise + loginGuest: (payload: GuestLoginPayload) => Promise + logout: () => Promise + destroy: () => void +} + +const matchesRequestId = ( + value: { requestId: string }, + requestId?: string +): boolean => requestId === undefined || value.requestId === requestId + +export const createAuthService = ( + options: CreateAuthServiceOptions = {} +): AuthService => { + const { initialUser, persistence = authPersistence } = options + + let requestSequence = 0 + const authRuntime = interpret(loginMachine) + const initialLoginState = loginMachine.initialState.value + + if (typeof initialLoginState !== "string") + throw new Error("Expected a flat login machine state") + + const sessionStateStore = writable( + initialUser ? "loggedIn" : (initialLoginState as AuthSessionState) + ) + const currentUserStore = writable(initialUser) + const authRequestStore = writable({ status: "idle" }) + const authOutcomeStore = writable({ status: "none" }) + const authRuntimeSnapshotStore = writable({ + state: initialLoginState as AuthSessionState, + nextEvents: loginMachine.initialState.nextEvents ?? [], + error: loginMachine.initialState.context?.error, + }) + + authRuntime.onTransition(state => { + if (typeof state.value !== "string") + throw new Error("Expected a flat login machine state") + + const sessionState = state.value as AuthSessionState + + sessionStateStore.set(sessionState) + authRuntimeSnapshotStore.set({ + state: sessionState, + nextEvents: state.nextEvents ?? [], + error: state.context?.error, + }) + }) + + authRuntime.start() + + const getPendingRequest = (): PendingAuthRequest | undefined => { + const activeRequest = get(authRequestStore) + + if (activeRequest.status !== "pending") return undefined + return activeRequest + } + + const getActiveOutcome = (): ActiveAuthOutcome | undefined => { + const activeOutcome = get(authOutcomeStore) + + if (activeOutcome.status === "none") return undefined + return activeOutcome + } + + const requestAuth = (reason: AuthRequestReason): { requestId: string } => { + requestSequence += 1 + + const requestId = `auth-request-${requestSequence}` + + authOutcomeStore.set({ status: "none" }) + authRequestStore.set({ + status: "pending", + reason, + requestId, + }) + + return { requestId } + } + + const clearAuthOutcome = (requestId?: string): void => { + const activeOutcome = getActiveOutcome() + + if (activeOutcome && !matchesRequestId(activeOutcome, requestId)) return + authOutcomeStore.set({ status: "none" }) + } + + const cancelAuthRequest = (requestId?: string): void => { + const activeRequest = getPendingRequest() + + if (!activeRequest || !matchesRequestId(activeRequest, requestId)) return + + authRequestStore.set({ status: "idle" }) + authOutcomeStore.set({ + status: "cancelled", + requestId: activeRequest.requestId, + }) + } + + const reportLocalValidationError = ({ + message, + requestId, + }: ReportLocalValidationErrorInput): void => { + const activeRequest = getPendingRequest() + + if (!activeRequest || !matchesRequestId(activeRequest, requestId)) return + + authRequestStore.set({ status: "idle" }) + authOutcomeStore.set({ + status: "localValidationError", + message, + requestId: activeRequest.requestId, + }) + } + + const completePendingAuthSuccess = (user: User): void => { + const activeRequest = getPendingRequest() + + if (!activeRequest) return + + authRequestStore.set({ status: "idle" }) + authOutcomeStore.set({ + status: "success", + user, + requestId: activeRequest.requestId, + }) + } + + const completePendingAuthRemoteError = ( + error: ServerResponse | string + ): void => { + const activeRequest = getPendingRequest() + + if (!activeRequest) return + + authRequestStore.set({ status: "idle" }) + authOutcomeStore.set({ + status: "remoteError", + error, + requestId: activeRequest.requestId, + }) + } + + return { + sessionState: { + subscribe: sessionStateStore.subscribe, + }, + currentUser: { + subscribe: currentUserStore.subscribe, + }, + authRequest: { + subscribe: authRequestStore.subscribe, + }, + authOutcome: { + subscribe: authOutcomeStore.subscribe, + }, + authRuntimeSnapshot: { + subscribe: authRuntimeSnapshotStore.subscribe, + }, + init: async () => { + if (initialUser !== undefined) { + currentUserStore.set(initialUser) + persistence.saveStoredUser(initialUser) + authRuntime.send("SUCCESS") + return + } + + currentUserStore.set(undefined) + + try { + const verifiedUser = await verifySelf() + + currentUserStore.set(verifiedUser) + persistence.saveStoredUser(verifiedUser) + authRuntime.send("SUCCESS") + } catch (error) { + currentUserStore.set(undefined) + + const { status } = (error ?? {}) as { status?: number } + + if (status === 401) { + persistence.clearStoredUser() + authRuntime.send("FIRST_VISIT") + } else + authRuntime.send({ + type: "ERROR", + error: error as ServerResponse | string, + }) + } + }, + requestAuth, + clearAuthOutcome, + cancelAuthRequest, + reportLocalValidationError, + login: async ({ userId, password }) => { + currentUserStore.set(undefined) + authRuntime.send("LOGIN") + + try { + const authResponse = await postAuth(userId, password) + + if (!authResponse.ok) { + authRuntime.send({ type: "ERROR", error: authResponse }) + completePendingAuthRemoteError(authResponse) + return + } + + authRuntime.send("SUCCESS") + + const verifiedUser = await verifySelf() + + currentUserStore.set(verifiedUser) + persistence.saveStoredUser(verifiedUser) + authRuntime.send("SUCCESS") + completePendingAuthSuccess(verifiedUser) + } catch (error) { + currentUserStore.set(undefined) + authRuntime.send({ + type: "ERROR", + error: error as ServerResponse | string, + }) + completePendingAuthRemoteError(error as ServerResponse | string) + } + }, + signup: async ({ userId, password, displayName, email }) => { + currentUserStore.set(undefined) + authRuntime.send("SIGNUP") + + try { + const signupResponse = await createUser({ + id: userId, + name: displayName, + email, + password, + }) + + if (!signupResponse.ok) { + authRuntime.send({ type: "ERROR", error: signupResponse }) + completePendingAuthRemoteError(signupResponse) + return + } + + authRuntime.send("SUCCESS") + await postAuth(userId, password) + authRuntime.send("SUCCESS") + + const verifiedUser = await verifySelf() + + currentUserStore.set(verifiedUser) + persistence.saveStoredUser(verifiedUser) + authRuntime.send("SUCCESS") + completePendingAuthSuccess(verifiedUser) + } catch (error) { + currentUserStore.set(undefined) + authRuntime.send({ + type: "ERROR", + error: error as ServerResponse | string, + }) + completePendingAuthRemoteError(error as ServerResponse | string) + } + }, + loginGuest: async ({ displayName, email, storedGuest }) => { + currentUserStore.set(undefined) + authRuntime.send({ type: "GUEST", guest: { name: displayName, email } }) + const guestIdentity = storedGuest ?? persistence.loadStoredGuestIdentity() + + const createNewGuest = async (): Promise => { + const guestTokenResponse = await getGuestToken() + + if (!guestTokenResponse.ok) throw guestTokenResponse + + const tokenClaimResponse = await verifyUser() + + if (!tokenClaimResponse.ok) throw tokenClaimResponse + + const createGuestResponse = await createGuestUser({ + id: tokenClaimResponse.body.user, + name: displayName, + email, + }) + + if (!createGuestResponse.ok) throw createGuestResponse + } + + const reuseStoredGuest = async (): Promise => { + const { id, challenge } = guestIdentity ?? {} + + if (!id || !challenge) return false + + try { + const authResponse = await postAuth(id, challenge) + + if (!authResponse.ok) return false + + const verifyResponse = await verifyUser() + + if (!verifyResponse.ok) return false + + return true + } catch { + return false + } + } + + try { + const reusedStoredGuest = await reuseStoredGuest() + + if (!reusedStoredGuest) await createNewGuest() + + const { id, name: storedName, email: storedEmail } = guestIdentity ?? {} + + const shouldUpdateStoredGuest = + reusedStoredGuest && + id && + (displayName !== storedName || email !== storedEmail) + + if (shouldUpdateStoredGuest) { + const updateResponse = await updateUser({ + id, + name: displayName, + email, + }) + + if (!updateResponse.ok) throw updateResponse + } + + authRuntime.send("SUCCESS") + + const verifiedUser = await verifySelf() + + currentUserStore.set(verifiedUser) + persistence.saveStoredUser(verifiedUser) + authRuntime.send("SUCCESS") + completePendingAuthSuccess(verifiedUser) + } catch (error) { + currentUserStore.set(undefined) + authRuntime.send({ + type: "ERROR", + error: error as ServerResponse | string, + }) + completePendingAuthRemoteError(error as ServerResponse | string) + } + }, + logout: async () => { + authRuntime.send("LOGOUT") + + try { + const logoutResponse = await deleteAuth() + + if (!logoutResponse.ok) { + authRuntime.send({ type: "ERROR", error: logoutResponse }) + return + } + + currentUserStore.set(undefined) + persistence.clearStoredUser() + authRuntime.send("SUCCESS") + } catch (error) { + authRuntime.send({ + type: "ERROR", + error: error as ServerResponse | string, + }) + } + }, + destroy: () => { + authRuntime.stop() + }, + } +} diff --git a/src/lib/svelte-stores.ts b/src/lib/svelte-stores.ts deleted file mode 100644 index bcdf8848..00000000 --- a/src/lib/svelte-stores.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { StateValue } from "xstate" -import type { User } from "../lib/simple-comment-types" -import { writable } from "svelte/store" -import { LoginTab } from "../lib/simple-comment-types" - -type EventDispatch = { name: string } - -const createDispatchableStore = () => { - const { subscribe, set, update } = writable({ name: "init" }) - - return { - subscribe, - set, - update, - dispatch: (eventName: string) => set({ name: eventName }), - } -} - -export const dispatchableStore = createDispatchableStore() - -export const loginStateStore = writable<{ - state?: StateValue - nextEvents?: string[] - select?: LoginTab -}>({ - state: undefined, - nextEvents: undefined, - select: undefined, -}) - -export const currentUserStore = writable(undefined) diff --git a/src/tests/frontend/auth-persistence.test.ts b/src/tests/frontend/auth-persistence.test.ts new file mode 100644 index 00000000..7fb30467 --- /dev/null +++ b/src/tests/frontend/auth-persistence.test.ts @@ -0,0 +1,84 @@ +import { beforeEach, describe, expect, test } from "@jest/globals" +import { + clearStoredUser, + loadStoredGuestIdentity, + loadStoredUser, + saveStoredUser, +} from "../../lib/auth-persistence" +import type { AdminSafeUser } from "../../lib/simple-comment-types" + +const STORAGE_KEY = "simple_comment_user" + +const storedUser: AdminSafeUser = { + id: "alice", + name: "Alice Example", + email: "alice@example.com", + isAdmin: false, + isVerified: true, + challenge: "challenge-token", +} + +const installLocalStorage = () => { + const values = new Map() + + Object.defineProperty(globalThis, "localStorage", { + configurable: true, + value: { + getItem: jest.fn((key: string) => values.get(key) ?? null), + setItem: jest.fn((key: string, value: string) => { + values.set(key, value) + }), + removeItem: jest.fn((key: string) => { + values.delete(key) + }), + }, + }) +} + +describe("auth persistence", () => { + beforeEach(() => { + installLocalStorage() + }) + + test("returns undefined when simple_comment_user is missing", () => { + expect(loadStoredUser()).toBeUndefined() + }) + + test("returns undefined for malformed simple_comment_user without throwing", () => { + localStorage.setItem(STORAGE_KEY, "{malformed-json") + + expect(() => loadStoredUser()).not.toThrow() + expect(loadStoredUser()).toBeUndefined() + }) + + test("round-trips saved users through storage", () => { + saveStoredUser(storedUser) + + expect(loadStoredUser()).toEqual(storedUser) + }) + + test("clears the stored user", () => { + saveStoredUser(storedUser) + clearStoredUser() + + expect(loadStoredUser()).toBeUndefined() + expect(localStorage.removeItem).toHaveBeenCalledWith(STORAGE_KEY) + }) + + test("loads only reusable stored guest identity fields", () => { + localStorage.setItem( + STORAGE_KEY, + JSON.stringify({ + ...storedUser, + extraRuntimeOnlyField: "ignored", + }) + ) + + expect(loadStoredGuestIdentity()).toEqual({ + id: "alice", + challenge: "challenge-token", + name: "Alice Example", + email: "alice@example.com", + }) + }) +}) diff --git a/src/tests/frontend/auth-service.auth-request.test.ts b/src/tests/frontend/auth-service.auth-request.test.ts new file mode 100644 index 00000000..fe224f85 --- /dev/null +++ b/src/tests/frontend/auth-service.auth-request.test.ts @@ -0,0 +1,244 @@ +import { beforeEach, describe, expect, jest, test } from "@jest/globals" +import { get } from "svelte/store" +import { + createGuestUser, + createUser, + getGuestToken, + postAuth, + verifySelf, + verifyUser, +} from "../../apiClient" +import { createAuthService } from "../../lib/auth-service" +import type { + AdminSafeUser, + AuthToken, + ServerResponse, + TokenClaim, +} from "../../lib/simple-comment-types" + +jest.mock("../../apiClient", () => ({ + createGuestUser: jest.fn(), + createUser: jest.fn(), + getGuestToken: jest.fn(), + postAuth: jest.fn(), + verifySelf: jest.fn(), + verifyUser: jest.fn(), +})) + +const mockCreateGuestUser = jest.mocked(createGuestUser) +const mockCreateUser = jest.mocked(createUser) +const mockGetGuestToken = jest.mocked(getGuestToken) +const mockPostAuth = jest.mocked(postAuth) +const mockVerifySelf = jest.mocked(verifySelf) +const mockVerifyUser = jest.mocked(verifyUser) + +const verifiedUser: AdminSafeUser = { + id: "alice", + name: "Alice Example", + email: "alice@example.com", + isAdmin: false, + isVerified: true, + challenge: "verified-user-challenge", +} + +const guestUser: AdminSafeUser = { + id: "00000000-0000-4000-8000-000000000001", + name: "Guest Example", + email: "guest@example.com", + isAdmin: false, + isVerified: true, + challenge: "guest-challenge", +} + +const validAuthResponse: ServerResponse = { + status: 200, + ok: true, + statusText: "OK", + body: "auth-token", +} + +const validSignupResponse: ServerResponse = { + status: 201, + ok: true, + statusText: "Created", + body: verifiedUser, +} + +const validGuestTokenResponse: ServerResponse = { + status: 200, + ok: true, + statusText: "OK", + body: "guest-token", +} + +const validTokenClaimResponse: ServerResponse = { + status: 200, + ok: true, + statusText: "OK", + body: { user: guestUser.id, exp: 9999999999 }, +} + +const validGuestCreateResponse: ServerResponse = { + status: 201, + ok: true, + statusText: "Created", + body: guestUser, +} + +const remoteErrorResponse: ServerResponse = { + status: 401, + ok: false, + statusText: "Unauthorized", + body: "Bad credentials", +} + +const signupPayload = { + userId: "alice", + password: "password123", + displayName: "Alice Example", + email: "alice@example.com", +} + +const guestLoginPayload = { + displayName: "Guest Example", + email: "guest@example.com", +} + +const bootstrapLoggedOut = async () => { + const authService = createAuthService() + + mockVerifySelf.mockRejectedValue({ status: 401 }) + await authService.init() + + expect(get(authService.sessionState)).toBe("loggedOut") + expect(get(authService.currentUser)).toBeUndefined() + + jest.clearAllMocks() + + return authService +} + +const mockSuccessfulLogin = () => { + mockPostAuth.mockResolvedValue(validAuthResponse) + mockVerifySelf.mockResolvedValue(verifiedUser) +} + +const mockSuccessfulSignup = () => { + mockCreateUser.mockResolvedValue(validSignupResponse) + mockPostAuth.mockResolvedValue(validAuthResponse) + mockVerifySelf.mockResolvedValue(verifiedUser) +} + +const mockSuccessfulGuestLogin = () => { + mockGetGuestToken.mockResolvedValue(validGuestTokenResponse) + mockVerifyUser.mockResolvedValue(validTokenClaimResponse) + mockCreateGuestUser.mockResolvedValue(validGuestCreateResponse) + mockVerifySelf.mockResolvedValue(guestUser) +} + +describe("auth-service auth requests", () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + test("publishes matching success outcome for pending login request", async () => { + const authService = await bootstrapLoggedOut() + const { requestId } = authService.requestAuth("comment-submit") + + mockSuccessfulLogin() + + await authService.login({ userId: "alice", password: "password123" }) + + expect(get(authService.authRequest)).toEqual({ status: "idle" }) + expect(get(authService.authOutcome)).toEqual({ + status: "success", + user: verifiedUser, + requestId, + }) + }) + + test("publishes matching remote error outcome for failed pending login request", async () => { + const authService = await bootstrapLoggedOut() + const { requestId } = authService.requestAuth("comment-submit") + + mockPostAuth.mockRejectedValue(remoteErrorResponse) + + await authService.login({ userId: "alice", password: "wrong-password" }) + + expect(get(authService.authRequest)).toEqual({ status: "idle" }) + expect(get(authService.authOutcome)).toEqual({ + status: "remoteError", + error: remoteErrorResponse, + requestId, + }) + }) + + test("publishes matching request outcome for pending signup request", async () => { + const authService = await bootstrapLoggedOut() + const { requestId } = authService.requestAuth("comment-submit") + + mockSuccessfulSignup() + + await authService.signup(signupPayload) + + expect(get(authService.authRequest)).toEqual({ status: "idle" }) + expect(get(authService.authOutcome)).toEqual({ + status: "success", + user: verifiedUser, + requestId, + }) + + authService.requestAuth("comment-submit") + mockCreateUser.mockRejectedValue(remoteErrorResponse) + + await authService.signup(signupPayload) + + expect(get(authService.authRequest)).toEqual({ status: "idle" }) + expect(get(authService.authOutcome)).toEqual({ + status: "remoteError", + error: remoteErrorResponse, + requestId: "auth-request-2", + }) + }) + + test("publishes matching request outcome for pending guest login request", async () => { + const authService = await bootstrapLoggedOut() + const { requestId } = authService.requestAuth("comment-submit") + + mockSuccessfulGuestLogin() + + await authService.loginGuest(guestLoginPayload) + + expect(get(authService.authRequest)).toEqual({ status: "idle" }) + expect(get(authService.authOutcome)).toEqual({ + status: "success", + user: guestUser, + requestId, + }) + + authService.requestAuth("comment-submit") + mockGetGuestToken.mockRejectedValue(remoteErrorResponse) + + await authService.loginGuest(guestLoginPayload) + + expect(get(authService.authRequest)).toEqual({ status: "idle" }) + expect(get(authService.authOutcome)).toEqual({ + status: "remoteError", + error: remoteErrorResponse, + requestId: "auth-request-2", + }) + }) + + test("preserves command behavior when no auth request is pending", async () => { + const authService = await bootstrapLoggedOut() + + mockSuccessfulLogin() + + await authService.login({ userId: "alice", password: "password123" }) + + expect(get(authService.authRequest)).toEqual({ status: "idle" }) + expect(get(authService.authOutcome)).toEqual({ status: "none" }) + expect(get(authService.sessionState)).toBe("loggedIn") + expect(get(authService.currentUser)).toEqual(verifiedUser) + }) +}) diff --git a/src/tests/frontend/auth-service.init.test.ts b/src/tests/frontend/auth-service.init.test.ts new file mode 100644 index 00000000..a04db4e1 --- /dev/null +++ b/src/tests/frontend/auth-service.init.test.ts @@ -0,0 +1,77 @@ +import { beforeEach, describe, expect, jest, test } from "@jest/globals" +import { get } from "svelte/store" +import { createAuthService } from "../../lib/auth-service" +import type { AdminSafeUser } from "../../lib/simple-comment-types" +import { verifySelf } from "../../apiClient" + +jest.mock("../../apiClient", () => ({ + verifySelf: jest.fn(), +})) + +const mockVerifySelf = jest.mocked(verifySelf) + +const verifiedUser: AdminSafeUser = { + id: "alice", + name: "Alice Example", + email: "alice@example.com", + isAdmin: false, + isVerified: true, + challenge: "challenge-token", +} + +describe("auth-service init", () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + test("calls verifySelf when no initialUser is provided", async () => { + mockVerifySelf.mockResolvedValue(verifiedUser) + const authService = createAuthService() + + await authService.init() + + expect(mockVerifySelf).toHaveBeenCalledTimes(1) + }) + + test("transitions to loggedIn and publishes currentUser on successful verification", async () => { + mockVerifySelf.mockResolvedValue(verifiedUser) + const authService = createAuthService() + + await authService.init() + + expect(get(authService.sessionState)).toBe("loggedIn") + expect(get(authService.currentUser)).toEqual(verifiedUser) + }) + + test("leaves currentUser undefined and transitions to loggedOut on 401 verification failure", async () => { + mockVerifySelf.mockRejectedValue({ status: 401 }) + const authService = createAuthService() + + await authService.init() + + expect(mockVerifySelf).toHaveBeenCalledTimes(1) + expect(get(authService.sessionState)).toBe("loggedOut") + expect(get(authService.currentUser)).toBeUndefined() + }) + + test("leaves currentUser undefined and transitions to error on non-401 verification failure", async () => { + mockVerifySelf.mockRejectedValue({ status: 500 }) + const authService = createAuthService() + + await authService.init() + + expect(mockVerifySelf).toHaveBeenCalledTimes(1) + expect(get(authService.sessionState)).toBe("error") + expect(get(authService.currentUser)).toBeUndefined() + }) + + test("skips verifySelf and preserves initialUser when initialUser is supplied", async () => { + const authService = createAuthService({ initialUser: verifiedUser }) + + await authService.init() + + expect(mockVerifySelf).not.toHaveBeenCalled() + expect(get(authService.sessionState)).toBe("loggedIn") + expect(get(authService.currentUser)).toEqual(verifiedUser) + }) +}) diff --git a/src/tests/frontend/auth-service.login-guest.test.ts b/src/tests/frontend/auth-service.login-guest.test.ts new file mode 100644 index 00000000..01249ad1 --- /dev/null +++ b/src/tests/frontend/auth-service.login-guest.test.ts @@ -0,0 +1,241 @@ +import { beforeEach, describe, expect, jest, test } from "@jest/globals" +import { get } from "svelte/store" +import { createAuthService } from "../../lib/auth-service" +import type { + AdminSafeUser, + AuthToken, + ServerResponse, + TokenClaim, +} from "../../lib/simple-comment-types" +import { + createGuestUser, + getGuestToken, + postAuth, + updateUser, + verifySelf, + verifyUser, +} from "../../apiClient" + +jest.mock("../../apiClient", () => ({ + createGuestUser: jest.fn(), + getGuestToken: jest.fn(), + postAuth: jest.fn(), + updateUser: jest.fn(), + verifySelf: jest.fn(), + verifyUser: jest.fn(), +})) + +const mockCreateGuestUser = jest.mocked(createGuestUser) +const mockGetGuestToken = jest.mocked(getGuestToken) +const mockPostAuth = jest.mocked(postAuth) +const mockUpdateUser = jest.mocked(updateUser) +const mockVerifySelf = jest.mocked(verifySelf) +const mockVerifyUser = jest.mocked(verifyUser) + +const guestUser: AdminSafeUser = { + id: "00000000-0000-4000-8000-000000000001", + name: "Guest Example", + email: "guest@example.com", + isAdmin: false, + isVerified: true, + challenge: "stored-guest-challenge", +} + +const updatedGuestUser: AdminSafeUser = { + ...guestUser, + name: "Updated Guest", + email: "updated@example.com", +} + +const validAuthResponse: ServerResponse = { + status: 200, + ok: true, + statusText: "OK", + body: "auth-token", +} + +const validGuestTokenResponse: ServerResponse = { + status: 200, + ok: true, + statusText: "OK", + body: "guest-token", +} + +const validTokenClaimResponse: ServerResponse = { + status: 200, + ok: true, + statusText: "OK", + body: { user: guestUser.id, exp: 9999999999 }, +} + +const validGuestCreateResponse: ServerResponse = { + status: 201, + ok: true, + statusText: "Created", + body: guestUser, +} + +const validGuestUpdateResponse: ServerResponse = { + status: 200, + ok: true, + statusText: "OK", + body: updatedGuestUser, +} + +const bootstrapLoggedOut = async () => { + const authService = createAuthService() + + mockVerifySelf.mockRejectedValue({ status: 401 }) + await authService.init() + + expect(get(authService.sessionState)).toBe("loggedOut") + expect(get(authService.currentUser)).toBeUndefined() + + jest.clearAllMocks() + + return authService +} + +describe("auth-service guest login", () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + test("reuses stored guest credentials and publishes authenticated user", async () => { + const authService = await bootstrapLoggedOut() + + mockPostAuth.mockResolvedValue(validAuthResponse) + mockVerifyUser.mockResolvedValue(validTokenClaimResponse) + mockVerifySelf.mockResolvedValue(guestUser) + + await authService.loginGuest({ + displayName: "Guest Example", + email: "guest@example.com", + storedGuest: { + id: guestUser.id, + challenge: "stored-guest-challenge", + name: "Guest Example", + email: "guest@example.com", + }, + }) + + expect(mockPostAuth).toHaveBeenCalledTimes(1) + expect(mockPostAuth).toHaveBeenCalledWith( + guestUser.id, + "stored-guest-challenge" + ) + expect(mockVerifyUser).toHaveBeenCalledTimes(1) + expect(mockGetGuestToken).not.toHaveBeenCalled() + expect(mockCreateGuestUser).not.toHaveBeenCalled() + expect(mockUpdateUser).not.toHaveBeenCalled() + expect(mockVerifySelf).toHaveBeenCalledTimes(1) + expect(get(authService.sessionState)).toBe("loggedIn") + expect(get(authService.currentUser)).toEqual(guestUser) + }) + + test("updates reused guest profile when submitted identity changes", async () => { + const authService = await bootstrapLoggedOut() + + mockPostAuth.mockResolvedValue(validAuthResponse) + mockVerifyUser.mockResolvedValue(validTokenClaimResponse) + mockUpdateUser.mockResolvedValue(validGuestUpdateResponse) + mockVerifySelf.mockResolvedValue(updatedGuestUser) + + await authService.loginGuest({ + displayName: "Updated Guest", + email: "updated@example.com", + storedGuest: { + id: guestUser.id, + challenge: "stored-guest-challenge", + name: "Guest Example", + email: "guest@example.com", + }, + }) + + expect(mockUpdateUser).toHaveBeenCalledTimes(1) + expect(mockUpdateUser).toHaveBeenCalledWith({ + id: guestUser.id, + name: "Updated Guest", + email: "updated@example.com", + }) + expect(get(authService.sessionState)).toBe("loggedIn") + expect(get(authService.currentUser)).toEqual(updatedGuestUser) + }) + + test("creates a new guest when stored guest credentials are missing", async () => { + const authService = await bootstrapLoggedOut() + + mockGetGuestToken.mockResolvedValue(validGuestTokenResponse) + mockVerifyUser.mockResolvedValue(validTokenClaimResponse) + mockCreateGuestUser.mockResolvedValue(validGuestCreateResponse) + mockVerifySelf.mockResolvedValue(guestUser) + + await authService.loginGuest({ + displayName: "Guest Example", + email: "guest@example.com", + }) + + expect(mockPostAuth).not.toHaveBeenCalled() + expect(mockGetGuestToken).toHaveBeenCalledTimes(1) + expect(mockVerifyUser).toHaveBeenCalledTimes(1) + expect(mockCreateGuestUser).toHaveBeenCalledTimes(1) + expect(mockCreateGuestUser).toHaveBeenCalledWith({ + id: guestUser.id, + name: "Guest Example", + email: "guest@example.com", + }) + expect(get(authService.sessionState)).toBe("loggedIn") + expect(get(authService.currentUser)).toEqual(guestUser) + }) + + test("falls back to new guest creation when stored guest auth fails", async () => { + const authService = await bootstrapLoggedOut() + + mockPostAuth.mockRejectedValue({ + status: 401, + ok: false, + statusText: "Unauthorized", + body: "Bad credentials", + }) + mockGetGuestToken.mockResolvedValue(validGuestTokenResponse) + mockVerifyUser.mockResolvedValue(validTokenClaimResponse) + mockCreateGuestUser.mockResolvedValue(validGuestCreateResponse) + mockVerifySelf.mockResolvedValue(guestUser) + + await authService.loginGuest({ + displayName: "Guest Example", + email: "guest@example.com", + storedGuest: { + id: guestUser.id, + challenge: "stale-challenge", + }, + }) + + expect(mockPostAuth).toHaveBeenCalledTimes(1) + expect(mockGetGuestToken).toHaveBeenCalledTimes(1) + expect(mockCreateGuestUser).toHaveBeenCalledTimes(1) + expect(get(authService.sessionState)).toBe("loggedIn") + expect(get(authService.currentUser)).toEqual(guestUser) + }) + + test("leaves currentUser undefined and transitions to error on guest login failure", async () => { + const authService = await bootstrapLoggedOut() + + mockGetGuestToken.mockRejectedValue({ + status: 403, + ok: false, + statusText: "Forbidden", + body: "Guest login disabled", + }) + + await authService.loginGuest({ + displayName: "Guest Example", + email: "guest@example.com", + }) + + expect(mockGetGuestToken).toHaveBeenCalledTimes(1) + expect(mockCreateGuestUser).not.toHaveBeenCalled() + expect(get(authService.sessionState)).toBe("error") + expect(get(authService.currentUser)).toBeUndefined() + }) +}) diff --git a/src/tests/frontend/auth-service.login.test.ts b/src/tests/frontend/auth-service.login.test.ts new file mode 100644 index 00000000..aa51236e --- /dev/null +++ b/src/tests/frontend/auth-service.login.test.ts @@ -0,0 +1,85 @@ +import { beforeEach, describe, expect, jest, test } from "@jest/globals" +import { get } from "svelte/store" +import { createAuthService } from "../../lib/auth-service" +import type { + AdminSafeUser, + ServerResponse, +} from "../../lib/simple-comment-types" +import { postAuth, verifySelf } from "../../apiClient" + +jest.mock("../../apiClient", () => ({ + postAuth: jest.fn(), + verifySelf: jest.fn(), +})) + +const mockPostAuth = jest.mocked(postAuth) +const mockVerifySelf = jest.mocked(verifySelf) + +const verifiedUser: AdminSafeUser = { + id: "alice", + name: "Alice Example", + email: "alice@example.com", + isAdmin: false, + isVerified: true, + challenge: "challenge-token", +} + +const validLoginResponse: ServerResponse = { + status: 200, + ok: true, + statusText: "OK", + body: "auth-token", +} + +const bootstrapLoggedOut = async () => { + const authService = createAuthService() + + mockVerifySelf.mockRejectedValue({ status: 401 }) + await authService.init() + + expect(get(authService.sessionState)).toBe("loggedOut") + expect(get(authService.currentUser)).toBeUndefined() + + jest.clearAllMocks() + + return authService +} + +describe("auth-service login", () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + test("owns the postAuth side effect with exact credentials and publishes authenticated user on success", async () => { + const authService = await bootstrapLoggedOut() + + mockPostAuth.mockResolvedValue(validLoginResponse) + mockVerifySelf.mockResolvedValue(verifiedUser) + + await authService.login({ userId: "alice", password: "password123" }) + + expect(mockPostAuth).toHaveBeenCalledTimes(1) + expect(mockPostAuth).toHaveBeenCalledWith("alice", "password123") + expect(mockVerifySelf).toHaveBeenCalledTimes(1) + expect(get(authService.sessionState)).toBe("loggedIn") + expect(get(authService.currentUser)).toEqual(verifiedUser) + }) + + test("leaves currentUser undefined and transitions to error on failed login", async () => { + const authService = await bootstrapLoggedOut() + + mockPostAuth.mockRejectedValue({ + status: 401, + ok: false, + statusText: "Unauthorized", + body: "Bad credentials", + }) + + await authService.login({ userId: "alice", password: "wrong-password" }) + + expect(mockPostAuth).toHaveBeenCalledTimes(1) + expect(mockPostAuth).toHaveBeenCalledWith("alice", "wrong-password") + expect(get(authService.sessionState)).toBe("error") + expect(get(authService.currentUser)).toBeUndefined() + }) +}) diff --git a/src/tests/frontend/auth-service.logout.test.ts b/src/tests/frontend/auth-service.logout.test.ts new file mode 100644 index 00000000..b784f176 --- /dev/null +++ b/src/tests/frontend/auth-service.logout.test.ts @@ -0,0 +1,80 @@ +import { beforeEach, describe, expect, jest, test } from "@jest/globals" +import { get } from "svelte/store" +import { createAuthService } from "../../lib/auth-service" +import type { + AdminSafeUser, + ServerResponse, +} from "../../lib/simple-comment-types" +import { deleteAuth } from "../../apiClient" + +jest.mock("../../apiClient", () => ({ + deleteAuth: jest.fn(), + postAuth: jest.fn(), + verifySelf: jest.fn(), +})) + +const mockDeleteAuth = jest.mocked(deleteAuth) + +const verifiedUser: AdminSafeUser = { + id: "alice", + name: "Alice Example", + email: "alice@example.com", + isAdmin: false, + isVerified: true, + challenge: "challenge-token", +} + +const validLogoutResponse: ServerResponse = { + status: 200, + ok: true, + statusText: "OK", + body: "logged out", +} + +const bootstrapLoggedIn = async () => { + const authService = createAuthService({ initialUser: verifiedUser }) + + await authService.init() + + expect(get(authService.sessionState)).toBe("loggedIn") + expect(get(authService.currentUser)).toEqual(verifiedUser) + + jest.clearAllMocks() + + return authService +} + +describe("auth-service logout", () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + test("owns the deleteAuth side effect and clears authenticated user on success", async () => { + const authService = await bootstrapLoggedIn() + + mockDeleteAuth.mockResolvedValue(validLogoutResponse) + + await authService.logout() + + expect(mockDeleteAuth).toHaveBeenCalledTimes(1) + expect(get(authService.sessionState)).toBe("loggedOut") + expect(get(authService.currentUser)).toBeUndefined() + }) + + test("transitions to error and preserves currentUser on failed logout", async () => { + const authService = await bootstrapLoggedIn() + + mockDeleteAuth.mockRejectedValue({ + status: 500, + ok: false, + statusText: "Internal Server Error", + body: "Logout failed", + }) + + await authService.logout() + + expect(mockDeleteAuth).toHaveBeenCalledTimes(1) + expect(get(authService.sessionState)).toBe("error") + expect(get(authService.currentUser)).toEqual(verifiedUser) + }) +}) diff --git a/src/tests/frontend/auth-service.persistence.test.ts b/src/tests/frontend/auth-service.persistence.test.ts new file mode 100644 index 00000000..30e1dfe1 --- /dev/null +++ b/src/tests/frontend/auth-service.persistence.test.ts @@ -0,0 +1,275 @@ +import { beforeEach, describe, expect, jest, test } from "@jest/globals" +import { createAuthService } from "../../lib/auth-service" +import type { StoredGuestIdentity } from "../../lib/auth-service" +import type { AuthPersistence } from "../../lib/auth-persistence" +import type { + AdminSafeUser, + AuthToken, + ServerResponse, + TokenClaim, +} from "../../lib/simple-comment-types" +import { + createGuestUser, + createUser, + deleteAuth, + getGuestToken, + postAuth, + verifySelf, + verifyUser, +} from "../../apiClient" + +jest.mock("../../apiClient", () => ({ + createGuestUser: jest.fn(), + createUser: jest.fn(), + deleteAuth: jest.fn(), + getGuestToken: jest.fn(), + postAuth: jest.fn(), + verifySelf: jest.fn(), + verifyUser: jest.fn(), +})) + +const mockCreateGuestUser = jest.mocked(createGuestUser) +const mockCreateUser = jest.mocked(createUser) +const mockDeleteAuth = jest.mocked(deleteAuth) +const mockGetGuestToken = jest.mocked(getGuestToken) +const mockPostAuth = jest.mocked(postAuth) +const mockVerifySelf = jest.mocked(verifySelf) +const mockVerifyUser = jest.mocked(verifyUser) + +const verifiedUser: AdminSafeUser = { + id: "alice", + name: "Alice Example", + email: "alice@example.com", + isAdmin: false, + isVerified: true, + challenge: "challenge-token", +} + +const guestUser: AdminSafeUser = { + id: "00000000-0000-4000-8000-000000000001", + name: "Guest Example", + email: "guest@example.com", + isAdmin: false, + isVerified: true, + challenge: "stored-guest-challenge", +} + +const persistedGuest: StoredGuestIdentity = { + id: guestUser.id, + challenge: "stored-guest-challenge", + name: "Guest Example", + email: "guest@example.com", +} + +const explicitGuest: StoredGuestIdentity = { + id: guestUser.id, + challenge: "explicit-guest-challenge", + name: "Explicit Guest", + email: "explicit@example.com", +} + +const validAuthResponse: ServerResponse = { + status: 200, + ok: true, + statusText: "OK", + body: "auth-token", +} + +const validSignupResponse: ServerResponse = { + status: 201, + ok: true, + statusText: "Created", + body: verifiedUser, +} + +const validLogoutResponse: ServerResponse = { + status: 200, + ok: true, + statusText: "OK", + body: "logged out", +} + +const validTokenClaimResponse: ServerResponse = { + status: 200, + ok: true, + statusText: "OK", + body: { user: guestUser.id, exp: 9999999999 }, +} + +const validGuestCreateResponse: ServerResponse = { + status: 201, + ok: true, + statusText: "Created", + body: guestUser, +} + +const createPersistence = ({ + storedGuest, +}: { + storedGuest?: StoredGuestIdentity +} = {}): jest.Mocked => ({ + loadStoredUser: jest.fn(), + saveStoredUser: jest.fn(), + clearStoredUser: jest.fn(), + loadStoredGuestIdentity: jest.fn(() => storedGuest), +}) + +const bootstrapLoggedOut = async ( + persistence: jest.Mocked +) => { + const authService = createAuthService({ persistence }) + + mockVerifySelf.mockRejectedValue({ status: 401 }) + await authService.init() + + jest.clearAllMocks() + + return authService +} + +describe("auth-service persistence integration", () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + test("saves verified users after successful init", async () => { + const persistence = createPersistence() + const authService = createAuthService({ persistence }) + + mockVerifySelf.mockResolvedValue(verifiedUser) + + await authService.init() + + expect(persistence.saveStoredUser).toHaveBeenCalledWith(verifiedUser) + }) + + test("clears stored user data after unauthenticated initial verification", async () => { + const persistence = createPersistence() + const authService = createAuthService({ persistence }) + + mockVerifySelf.mockRejectedValue({ status: 401 }) + + await authService.init() + + expect(persistence.clearStoredUser).toHaveBeenCalledTimes(1) + expect(persistence.saveStoredUser).not.toHaveBeenCalled() + }) + + test("saves verified users after successful login", async () => { + const persistence = createPersistence() + const authService = await bootstrapLoggedOut(persistence) + + mockPostAuth.mockResolvedValue(validAuthResponse) + mockVerifySelf.mockResolvedValue(verifiedUser) + + await authService.login({ userId: "alice", password: "password123" }) + + expect(persistence.saveStoredUser).toHaveBeenCalledWith(verifiedUser) + }) + + test("saves verified users after successful signup", async () => { + const persistence = createPersistence() + const authService = await bootstrapLoggedOut(persistence) + + mockCreateUser.mockResolvedValue(validSignupResponse) + mockPostAuth.mockResolvedValue(validAuthResponse) + mockVerifySelf.mockResolvedValue(verifiedUser) + + await authService.signup({ + userId: "alice", + password: "password123", + displayName: "Alice Example", + email: "alice@example.com", + }) + + expect(persistence.saveStoredUser).toHaveBeenCalledWith(verifiedUser) + }) + + test("saves verified users after successful guest login", async () => { + const persistence = createPersistence() + const authService = await bootstrapLoggedOut(persistence) + + mockGetGuestToken.mockResolvedValue(validAuthResponse) + mockVerifyUser.mockResolvedValue(validTokenClaimResponse) + mockCreateGuestUser.mockResolvedValue(validGuestCreateResponse) + mockVerifySelf.mockResolvedValue(guestUser) + + await authService.loginGuest({ + displayName: "Guest Example", + email: "guest@example.com", + }) + + expect(persistence.saveStoredUser).toHaveBeenCalledWith(guestUser) + }) + + test("clears stored user data after successful logout", async () => { + const persistence = createPersistence() + const authService = createAuthService({ + initialUser: verifiedUser, + persistence, + }) + + mockDeleteAuth.mockResolvedValue(validLogoutResponse) + + await authService.logout() + + expect(persistence.clearStoredUser).toHaveBeenCalledTimes(1) + }) + + test("does not save a new stored user after failed login", async () => { + const persistence = createPersistence() + const authService = await bootstrapLoggedOut(persistence) + + mockPostAuth.mockRejectedValue({ + status: 401, + ok: false, + statusText: "Unauthorized", + body: "Bad credentials", + }) + + await authService.login({ userId: "alice", password: "wrong-password" }) + + expect(persistence.saveStoredUser).not.toHaveBeenCalled() + }) + + test("uses persisted guest identity when guest command input omits storedGuest", async () => { + const persistence = createPersistence({ storedGuest: persistedGuest }) + const authService = await bootstrapLoggedOut(persistence) + + mockPostAuth.mockResolvedValue(validAuthResponse) + mockVerifyUser.mockResolvedValue(validTokenClaimResponse) + mockVerifySelf.mockResolvedValue(guestUser) + + await authService.loginGuest({ + displayName: "Guest Example", + email: "guest@example.com", + }) + + expect(persistence.loadStoredGuestIdentity).toHaveBeenCalledTimes(1) + expect(mockPostAuth).toHaveBeenCalledWith( + guestUser.id, + "stored-guest-challenge" + ) + }) + + test("prefers explicit storedGuest input over persisted guest identity", async () => { + const persistence = createPersistence({ storedGuest: persistedGuest }) + const authService = await bootstrapLoggedOut(persistence) + + mockPostAuth.mockResolvedValue(validAuthResponse) + mockVerifyUser.mockResolvedValue(validTokenClaimResponse) + mockVerifySelf.mockResolvedValue(guestUser) + + await authService.loginGuest({ + displayName: "Explicit Guest", + email: "explicit@example.com", + storedGuest: explicitGuest, + }) + + expect(persistence.loadStoredGuestIdentity).not.toHaveBeenCalled() + expect(mockPostAuth).toHaveBeenCalledWith( + guestUser.id, + "explicit-guest-challenge" + ) + }) +}) diff --git a/src/tests/frontend/auth-service.signup.test.ts b/src/tests/frontend/auth-service.signup.test.ts new file mode 100644 index 00000000..db5a6259 --- /dev/null +++ b/src/tests/frontend/auth-service.signup.test.ts @@ -0,0 +1,125 @@ +import { beforeEach, describe, expect, jest, test } from "@jest/globals" +import { get } from "svelte/store" +import { createAuthService } from "../../lib/auth-service" +import type { + AdminSafeUser, + ServerResponse, +} from "../../lib/simple-comment-types" +import { createUser, postAuth, verifySelf } from "../../apiClient" + +jest.mock("../../apiClient", () => ({ + createUser: jest.fn(), + postAuth: jest.fn(), + verifySelf: jest.fn(), +})) + +const mockCreateUser = jest.mocked(createUser) +const mockPostAuth = jest.mocked(postAuth) +const mockVerifySelf = jest.mocked(verifySelf) + +const signupPayload = { + userId: "alice", + password: "password123", + displayName: "Alice Example", + email: "alice@example.com", +} + +const createdUser: AdminSafeUser = { + id: "alice", + name: "Alice Example", + email: "alice@example.com", + isAdmin: false, + isVerified: true, + challenge: "created-user-challenge", +} + +const verifiedUser: AdminSafeUser = { + id: "alice", + name: "Alice Example", + email: "alice@example.com", + isAdmin: false, + isVerified: true, + challenge: "verified-user-challenge", +} + +const validSignupResponse: ServerResponse = { + status: 201, + ok: true, + statusText: "Created", + body: createdUser, +} + +const validLoginResponse: ServerResponse = { + status: 200, + ok: true, + statusText: "OK", + body: "auth-token", +} + +const bootstrapLoggedOut = async () => { + const authService = createAuthService() + + mockVerifySelf.mockRejectedValue({ status: 401 }) + await authService.init() + + expect(get(authService.sessionState)).toBe("loggedOut") + expect(get(authService.currentUser)).toBeUndefined() + + jest.clearAllMocks() + + return authService +} + +describe("auth-service signup", () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + test("owns createUser and publishes authenticated user after successful signup", async () => { + const authService = await bootstrapLoggedOut() + + mockCreateUser.mockResolvedValue(validSignupResponse) + mockPostAuth.mockResolvedValue(validLoginResponse) + mockVerifySelf.mockResolvedValue(verifiedUser) + + await authService.signup(signupPayload) + + expect(mockCreateUser).toHaveBeenCalledTimes(1) + expect(mockCreateUser).toHaveBeenCalledWith({ + id: "alice", + name: "Alice Example", + email: "alice@example.com", + password: "password123", + }) + expect(mockPostAuth).toHaveBeenCalledTimes(1) + expect(mockPostAuth).toHaveBeenCalledWith("alice", "password123") + expect(mockVerifySelf).toHaveBeenCalledTimes(1) + expect(get(authService.sessionState)).toBe("loggedIn") + expect(get(authService.currentUser)).toEqual(verifiedUser) + }) + + test("leaves currentUser undefined and transitions to error on failed signup", async () => { + const authService = await bootstrapLoggedOut() + + mockCreateUser.mockRejectedValue({ + status: 409, + ok: false, + statusText: "Conflict", + body: "User already exists", + }) + + await authService.signup(signupPayload) + + expect(mockCreateUser).toHaveBeenCalledTimes(1) + expect(mockCreateUser).toHaveBeenCalledWith({ + id: "alice", + name: "Alice Example", + email: "alice@example.com", + password: "password123", + }) + expect(mockPostAuth).not.toHaveBeenCalled() + expect(mockVerifySelf).not.toHaveBeenCalled() + expect(get(authService.sessionState)).toBe("error") + expect(get(authService.currentUser)).toBeUndefined() + }) +}) diff --git a/src/tests/frontend/auth-service.test.ts b/src/tests/frontend/auth-service.test.ts new file mode 100644 index 00000000..4e76e280 --- /dev/null +++ b/src/tests/frontend/auth-service.test.ts @@ -0,0 +1,72 @@ +import { beforeEach, describe, expect, jest, test } from "@jest/globals" +import { get } from "svelte/store" +import { loginMachine } from "../../lib/login.xstate" +import { createAuthService } from "../../lib/auth-service" + +type TransitionState = { value: string } + +const mockInterpret = jest.fn() + +jest.mock("xstate", () => { + const actual = jest.requireActual("xstate") + + return { + ...actual, + interpret: (...args: unknown[]) => mockInterpret(...args), + } +}) + +describe("auth-service", () => { + let transitionListener: ((state: TransitionState) => void) | undefined + let mockInterpreter: { + onTransition: jest.Mock + start: jest.Mock + stop: jest.Mock + } + + beforeEach(() => { + jest.clearAllMocks() + transitionListener = undefined + + mockInterpreter = { + onTransition: jest.fn(listener => { + transitionListener = listener as (state: TransitionState) => void + return mockInterpreter + }), + start: jest.fn(() => { + transitionListener?.({ value: "verifying" }) + return mockInterpreter + }), + stop: jest.fn(), + } + + mockInterpret.mockReturnValue(mockInterpreter) + }) + + test("creates and starts a live interpreted login runtime", () => { + createAuthService() + + expect(mockInterpret).toHaveBeenCalledWith(loginMachine) + expect(mockInterpreter.onTransition).toHaveBeenCalledTimes(1) + expect(mockInterpreter.start).toHaveBeenCalledTimes(1) + }) + + test("publishes sessionState from runtime transitions", () => { + const authService = createAuthService() + + expect(get(authService.sessionState)).toBe("verifying") + + expect(transitionListener).toBeDefined() + transitionListener?.({ value: "loggedIn" }) + + expect(get(authService.sessionState)).toBe("loggedIn") + }) + + test("disposes the runtime when destroy is called", () => { + const authService = createAuthService() + + authService.destroy() + + expect(mockInterpreter.stop).toHaveBeenCalledTimes(1) + }) +}) diff --git a/src/tests/frontend/components/CommentInput.auth-service.test.ts b/src/tests/frontend/components/CommentInput.auth-service.test.ts new file mode 100644 index 00000000..21a1efc3 --- /dev/null +++ b/src/tests/frontend/components/CommentInput.auth-service.test.ts @@ -0,0 +1,223 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/svelte" +import { readFileSync } from "node:fs" +import { resolve } from "node:path" +import type { Writable } from "svelte/store" +import { readable, writable } from "svelte/store" +import { beforeEach, describe, expect, test, vi } from "vitest" +import { getOneUser, postComment } from "../../../apiClient" +import CommentInput from "../../../components/CommentInput.svelte" +import type { + AuthOutcomeState, + AuthRequestState, + AuthRuntimeSnapshot, + AuthService, + AuthSessionState, +} from "../../../lib/auth-service" +import type { + Comment, + ServerResponse, + User, +} from "../../../lib/simple-comment-types" + +vi.mock("../../../apiClient", () => ({ + getOneUser: vi.fn(), + postComment: vi.fn(), +})) + +vi.mock("../../../frontend-utilities", async importOriginal => { + const actual = + await importOriginal() + + return { + ...actual, + idIconDataUrl: vi.fn( + () => + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" + ), + } +}) + +type AuthServiceStub = AuthService & { + authOutcomeStore: Writable + authRequestStore: Writable +} + +const mockPostComment = vi.mocked(postComment) +const mockGetOneUser = vi.mocked(getOneUser) + +const defaultUser: User = { + id: "alice-user", + name: "Alice Example", + email: "alice@example.com", +} + +const postedComment: Comment = { + id: "comment-1", + parentId: "topic-1", + text: "Hello from the tests", + userId: "alice-user", + user: defaultUser, + dateCreated: new Date("2026-01-01T00:00:00.000Z"), +} + +const validPostCommentResponse: ServerResponse = { + status: 201, + ok: true, + statusText: "Created", + body: postedComment, +} + +const createAuthServiceStub = (): AuthServiceStub => { + const authOutcomeStore = writable({ status: "none" }) + const authRequestStore = writable({ status: "idle" }) + + return { + sessionState: readable("loggedOut"), + currentUser: readable(undefined), + authRequest: { subscribe: authRequestStore.subscribe }, + authOutcome: { subscribe: authOutcomeStore.subscribe }, + authRuntimeSnapshot: readable({ + state: "loggedOut", + nextEvents: ["LOGIN", "SIGNUP", "GUEST"], + }), + init: vi.fn().mockResolvedValue(undefined), + requestAuth: vi.fn(() => ({ requestId: "request-1" })), + clearAuthOutcome: vi.fn(), + cancelAuthRequest: vi.fn(), + reportLocalValidationError: vi.fn(), + login: vi.fn().mockResolvedValue(undefined), + signup: vi.fn().mockResolvedValue(undefined), + loginGuest: vi.fn().mockResolvedValue(undefined), + logout: vi.fn().mockResolvedValue(undefined), + destroy: vi.fn(), + authOutcomeStore, + authRequestStore, + } +} + +const renderCommentInput = ({ + authService = createAuthServiceStub(), + currentUser, +}: { + authService?: AuthServiceStub + currentUser?: User +} = {}) => { + render( + CommentInput as never, + { + authService, + commentId: "topic-1", + currentUser, + } as never + ) + + return { authService } +} + +const submitComment = async (text = "Hello from the tests") => { + await fireEvent.input(screen.getByPlaceholderText("Your comment"), { + target: { value: text }, + }) + await fireEvent.click(screen.getByRole("button", { name: "Add comment" })) +} + +describe("CommentInput auth-service delegation", () => { + beforeEach(() => { + globalThis.ResizeObserver = class ResizeObserver { + observe = vi.fn() + unobserve = vi.fn() + disconnect = vi.fn() + } as never + + Element.prototype.animate = + Element.prototype.animate ?? + vi.fn(() => ({ cancel: vi.fn(), finished: Promise.resolve() }) as never) + mockGetOneUser.mockResolvedValue({ ok: true, body: defaultUser } as never) + mockPostComment.mockResolvedValue(validPostCommentResponse) + }) + + test("requests auth through authService for unauthenticated comment submit", async () => { + const { authService } = renderCommentInput() + + await submitComment() + + await waitFor(() => { + expect(authService.requestAuth).toHaveBeenCalledWith("comment-submit") + }) + }) + + test("continues posting after matching auth success outcome", async () => { + const { authService } = renderCommentInput() + + await submitComment() + await waitFor(() => { + expect(authService.requestAuth).toHaveBeenCalledWith("comment-submit") + }) + + authService.authOutcomeStore.set({ + status: "success", + user: defaultUser, + requestId: "request-1", + }) + + await waitFor(() => { + expect(mockPostComment).toHaveBeenCalledWith( + "topic-1", + "Hello from the tests" + ) + }) + }) + + test("leaves processing state after matching auth failure outcome", async () => { + const { authService } = renderCommentInput() + + await submitComment() + await waitFor(() => { + expect(authService.requestAuth).toHaveBeenCalledWith("comment-submit") + }) + + authService.authOutcomeStore.set({ + status: "remoteError", + error: "Login error", + requestId: "request-1", + }) + + await waitFor(() => { + expect(document.querySelector("form.comment-form")).not.toHaveClass( + "is-hidden" + ) + }) + }) + + test("preserves selected-tab dependent button copy", async () => { + renderCommentInput() + + await fireEvent.click(await screen.findByRole("button", { name: "Login" })) + + expect(screen.getByRole("button", { name: "Log in" })).toBeInTheDocument() + }) + + test("posts immediately for already authenticated users", async () => { + const { authService } = renderCommentInput({ currentUser: defaultUser }) + + await submitComment() + + await waitFor(() => { + expect(mockPostComment).toHaveBeenCalledWith( + "topic-1", + "Hello from the tests" + ) + }) + expect(authService.requestAuth).not.toHaveBeenCalled() + }) + + test("does not import legacy login relay stores", () => { + const commentInputSource = readFileSync( + resolve(process.cwd(), "src/components/CommentInput.svelte"), + "utf8" + ) + + expect(commentInputSource).not.toMatch(/\bdispatchableStore\b/) + expect(commentInputSource).not.toMatch(/\bloginStateStore\b/) + }) +}) diff --git a/src/tests/frontend/components/Login.auth-service.test.ts b/src/tests/frontend/components/Login.auth-service.test.ts new file mode 100644 index 00000000..d5f3661d --- /dev/null +++ b/src/tests/frontend/components/Login.auth-service.test.ts @@ -0,0 +1,356 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/svelte" +import { readFileSync } from "node:fs" +import { resolve } from "node:path" +import type { Writable } from "svelte/store" +import { readable, writable } from "svelte/store" +import { beforeEach, describe, expect, test, vi } from "vitest" +import { + createGuestUser, + createUser, + getGuestToken, + postAuth, + updateUser, + verifySelf, + verifyUser, +} from "../../../apiClient" +import Login from "../../../components/Login.svelte" +import type { + AuthRequestState, + AuthService, + AuthSessionState, +} from "../../../lib/auth-service" +import type { User } from "../../../lib/simple-comment-types" + +vi.mock("../../../apiClient", () => ({ + createGuestUser: vi.fn(), + createUser: vi.fn(), + getGuestToken: vi.fn(), + getOneUser: vi.fn(), + postAuth: vi.fn(), + updateUser: vi.fn(), + verifySelf: vi.fn(), + verifyUser: vi.fn(), +})) + +vi.mock("../../../frontend-utilities", async importOriginal => { + const actual = + await importOriginal() + + return { + ...actual, + idIconDataUrl: vi.fn( + () => + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" + ), + } +}) + +type AuthRuntimeSnapshot = { + state: AuthSessionState + nextEvents: string[] + error?: unknown +} + +type AuthServiceUnderTest = AuthService & { + authRequest: Writable + authRuntimeSnapshot: Writable +} + +const mockVerifySelf = vi.mocked(verifySelf) +const mockPostAuth = vi.mocked(postAuth) +const mockCreateUser = vi.mocked(createUser) +const mockGetGuestToken = vi.mocked(getGuestToken) +const mockCreateGuestUser = vi.mocked(createGuestUser) +const mockUpdateUser = vi.mocked(updateUser) +const mockVerifyUser = vi.mocked(verifyUser) +const directAuthCommandNames = [ + "verifySelf", + "verifyUser", + "postAuth", + "createUser", + "getGuestToken", + "createGuestUser", + "updateUser", + "deleteAuth", +] as const + +const createAuthServiceStub = ({ + currentUser, + snapshot = { state: "loggedOut", nextEvents: ["LOGIN", "SIGNUP", "GUEST"] }, +}: { + currentUser?: User + snapshot?: AuthRuntimeSnapshot +} = {}): AuthServiceUnderTest => ({ + sessionState: readable(snapshot.state), + currentUser: readable(currentUser), + authRequest: writable({ status: "idle" }), + authOutcome: readable({ status: "none" }), + authRuntimeSnapshot: writable(snapshot), + init: vi.fn().mockResolvedValue(undefined), + requestAuth: vi.fn(() => ({ requestId: "request-1" })), + clearAuthOutcome: vi.fn(), + cancelAuthRequest: vi.fn(), + reportLocalValidationError: vi.fn(), + login: vi.fn().mockResolvedValue(undefined), + signup: vi.fn().mockResolvedValue(undefined), + loginGuest: vi.fn().mockResolvedValue(undefined), + logout: vi.fn().mockResolvedValue(undefined), + destroy: vi.fn(), +}) + +const renderLogin = ({ + authService = createAuthServiceStub(), + currentUser, +}: { + authService?: AuthServiceUnderTest + currentUser?: User +} = {}) => { + render( + Login as never, + { + authService, + currentUser, + } as never + ) + + return { authService } +} + +const submitForm = async (selector: string): Promise => { + const form = document.querySelector(selector) + + expect(form).toBeInTheDocument() + await fireEvent.submit(form as HTMLFormElement) +} + +describe("Login auth-service delegation", () => { + beforeEach(() => { + Element.prototype.animate = + Element.prototype.animate ?? + vi.fn(() => ({ cancel: vi.fn(), finished: Promise.resolve() }) as never) + mockVerifySelf.mockRejectedValue({ status: 401 }) + mockPostAuth.mockResolvedValue({ ok: true } as never) + mockCreateUser.mockResolvedValue({ ok: true } as never) + mockGetGuestToken.mockResolvedValue({ ok: true } as never) + mockCreateGuestUser.mockResolvedValue({ ok: true } as never) + mockUpdateUser.mockResolvedValue({ ok: true } as never) + mockVerifyUser.mockResolvedValue({ ok: true } as never) + }) + + test("delegates mount initialization through authService.init", async () => { + const { authService } = renderLogin() + + await waitFor(() => { + expect(authService.init).toHaveBeenCalledTimes(1) + }) + expect(mockVerifySelf).not.toHaveBeenCalled() + }) + + test("delegates valid login submissions to authService.login", async () => { + const { authService } = renderLogin() + + await fireEvent.click(await screen.findByRole("button", { name: "Login" })) + await fireEvent.input(screen.getByLabelText("User handle"), { + target: { value: "alice-user" }, + }) + await fireEvent.input(screen.getByLabelText("Password"), { + target: { value: "secret" }, + }) + await submitForm("#user-login-form") + + await waitFor(() => { + expect(authService.login).toHaveBeenCalledWith({ + userId: "alice-user", + password: "secret", + }) + }) + expect(mockPostAuth).not.toHaveBeenCalled() + }) + + test("delegates valid signup submissions to authService.signup", async () => { + const { authService } = renderLogin() + + await fireEvent.click(await screen.findByRole("button", { name: "Signup" })) + await fireEvent.input(screen.getByLabelText("Display name"), { + target: { value: "Alice Example" }, + }) + await fireEvent.input(screen.getByLabelText("User handle"), { + target: { value: "alice-user" }, + }) + await fireEvent.input(screen.getByLabelText("Email"), { + target: { value: "alice@example.com" }, + }) + await fireEvent.input(screen.getByLabelText("Password"), { + target: { value: "secret" }, + }) + await fireEvent.input(screen.getByLabelText("Confirm password"), { + target: { value: "secret" }, + }) + await submitForm("#signup-form") + + await waitFor(() => { + expect(authService.signup).toHaveBeenCalledWith({ + userId: "alice-user", + password: "secret", + displayName: "Alice Example", + email: "alice@example.com", + }) + }) + expect(mockCreateUser).not.toHaveBeenCalled() + }) + + test("delegates new guest submissions to authService.loginGuest", async () => { + const { authService } = renderLogin() + + await fireEvent.input(await screen.findByLabelText("Display Name"), { + target: { value: "Guest Example" }, + }) + await fireEvent.input(screen.getByLabelText("Email"), { + target: { value: "guest@example.com" }, + }) + await submitForm("#guest-login-form") + + await waitFor(() => { + expect(authService.loginGuest).toHaveBeenCalledWith({ + displayName: "Guest Example", + email: "guest@example.com", + }) + }) + expect(mockGetGuestToken).not.toHaveBeenCalled() + expect(mockCreateGuestUser).not.toHaveBeenCalled() + }) + + test("submits selected auth form when authService has a pending auth request", async () => { + const { authService } = renderLogin() + + await fireEvent.input(await screen.findByLabelText("Display Name"), { + target: { value: "Guest Example" }, + }) + await fireEvent.input(screen.getByLabelText("Email"), { + target: { value: "guest@example.com" }, + }) + + authService.authRequest.set({ + status: "pending", + reason: "comment-submit", + requestId: "request-1", + }) + + await waitFor(() => { + expect(authService.loginGuest).toHaveBeenCalledWith({ + displayName: "Guest Example", + email: "guest@example.com", + }) + }) + expect(authService.reportLocalValidationError).not.toHaveBeenCalled() + }) + + test("reports local validation failures for pending auth requests", async () => { + const { authService } = renderLogin() + + authService.authRequest.set({ + status: "pending", + reason: "comment-submit", + requestId: "request-1", + }) + + await waitFor(() => { + expect(authService.reportLocalValidationError).toHaveBeenCalledWith( + expect.objectContaining({ + message: expect.stringContaining("Display name is required."), + requestId: "request-1", + }) + ) + }) + expect(authService.loginGuest).not.toHaveBeenCalled() + }) + + test("omits stored guest identity from guest submissions", async () => { + const storedGuest = { + id: "guest-ab123-abc12", + challenge: "stored-challenge", + name: "Stored Guest", + email: "stored@example.com", + } + localStorage.setItem("simple_comment_user", JSON.stringify(storedGuest)) + const { authService } = renderLogin() + + await fireEvent.input(await screen.findByLabelText("Display Name"), { + target: { value: "Updated Guest" }, + }) + await fireEvent.input(screen.getByLabelText("Email"), { + target: { value: "updated@example.com" }, + }) + await submitForm("#guest-login-form") + + await waitFor(() => { + expect(authService.loginGuest).toHaveBeenCalledWith({ + displayName: "Updated Guest", + email: "updated@example.com", + }) + }) + expect(mockGetGuestToken).not.toHaveBeenCalled() + expect(mockCreateGuestUser).not.toHaveBeenCalled() + }) + + test("keeps login validation local before authService.login", async () => { + const { authService } = renderLogin() + + await fireEvent.click(await screen.findByRole("button", { name: "Login" })) + await submitForm("#user-login-form") + + expect(await screen.findByText("User handle is required.")).toBeVisible() + expect(screen.getByText("Password is required.")).toBeVisible() + expect(authService.login).not.toHaveBeenCalled() + }) + + test("keeps signup validation local before authService.signup", async () => { + const { authService } = renderLogin() + + await fireEvent.click(await screen.findByRole("button", { name: "Signup" })) + await submitForm("#signup-form") + + const displayNameErrors = await screen.findAllByText( + /Display name is required/ + ) + + expect(displayNameErrors[0]).toBeVisible() + expect(authService.signup).not.toHaveBeenCalled() + }) + + test("keeps guest validation local before authService.loginGuest", async () => { + const { authService } = renderLogin() + + await submitForm("#guest-login-form") + + const displayNameErrors = await screen.findAllByText( + /Display name is required/ + ) + + expect(displayNameErrors[0]).toBeVisible() + expect(authService.loginGuest).not.toHaveBeenCalled() + }) + + test("does not call direct auth API commands from Login.svelte", () => { + const loginSource = readFileSync( + resolve(process.cwd(), "src/components/Login.svelte"), + "utf8" + ) + + directAuthCommandNames.forEach(commandName => { + expect(loginSource).not.toMatch(new RegExp(`\\b${commandName}\\s*\\(`)) + }) + }) + + test("does not import or handle legacy auth relay stores", () => { + const loginSource = readFileSync( + resolve(process.cwd(), "src/components/Login.svelte"), + "utf8" + ) + + expect(loginSource).not.toMatch(/\bdispatchableStore\b/) + expect(loginSource).not.toMatch(/\bloginStateStore\b/) + expect(loginSource).not.toMatch(/\bloginIntent\b/) + expect(loginSource).not.toMatch(/\blogoutIntent\b/) + }) +}) diff --git a/src/tests/frontend/components/Login.smoke.test.ts b/src/tests/frontend/components/Login.smoke.test.ts new file mode 100644 index 00000000..2e637f8d --- /dev/null +++ b/src/tests/frontend/components/Login.smoke.test.ts @@ -0,0 +1,53 @@ +import { render, screen, waitFor } from "@testing-library/svelte" +import { readable, writable } from "svelte/store" +import { describe, expect, test, vi } from "vitest" +import Login from "../../../components/Login.svelte" +import type { + AuthRuntimeSnapshot, + AuthService, + AuthSessionState, +} from "../../../lib/auth-service" + +const createAuthServiceStub = (): AuthService => ({ + sessionState: readable("loggedOut"), + currentUser: readable(undefined), + authRequest: readable({ status: "idle" }), + authOutcome: readable({ status: "none" }), + authRuntimeSnapshot: writable({ + state: "loggedOut", + nextEvents: ["LOGIN", "SIGNUP", "GUEST"], + }), + init: vi.fn().mockResolvedValue(undefined), + requestAuth: vi.fn(() => ({ requestId: "auth-request-1" })), + clearAuthOutcome: vi.fn(), + cancelAuthRequest: vi.fn(), + reportLocalValidationError: vi.fn(), + login: vi.fn().mockResolvedValue(undefined), + signup: vi.fn().mockResolvedValue(undefined), + loginGuest: vi.fn().mockResolvedValue(undefined), + logout: vi.fn().mockResolvedValue(undefined), + destroy: vi.fn(), +}) + +describe("Login smoke", () => { + test("renders the login, signup, and guest tabs", async () => { + const authService = createAuthServiceStub() + + render(Login as never, { authService } as never) + + expect( + await screen.findByRole("button", { + name: "Login", + }) + ).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Signup" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Guest" })).toBeInTheDocument() + + await waitFor(() => { + expect(authService.init).toHaveBeenCalledTimes(1) + expect( + document.querySelector("section.simple-comment-login") + ).not.toHaveClass("is-loading") + }) + }) +}) diff --git a/src/tests/frontend/components/SelfDisplay.auth-service.test.ts b/src/tests/frontend/components/SelfDisplay.auth-service.test.ts new file mode 100644 index 00000000..44c33d34 --- /dev/null +++ b/src/tests/frontend/components/SelfDisplay.auth-service.test.ts @@ -0,0 +1,115 @@ +import { fireEvent, render, screen } from "@testing-library/svelte" +import { readFileSync } from "node:fs" +import { resolve } from "node:path" +import { readable, writable } from "svelte/store" +import { describe, expect, test, vi } from "vitest" +import SelfDisplay from "../../../components/SelfDisplay.svelte" +import type { + AuthRuntimeSnapshot, + AuthService, + AuthSessionState, +} from "../../../lib/auth-service" +import type { User } from "../../../lib/simple-comment-types" + +vi.mock("../../../frontend-utilities", async importOriginal => { + const actual = + await importOriginal() + + return { + ...actual, + idIconDataUrl: vi.fn( + () => + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" + ), + } +}) + +const defaultUser: User = { + id: "alice-user", + name: "Alice Example", + email: "alice@example.com", +} + +const createAuthServiceStub = ( + snapshot: AuthRuntimeSnapshot = { + state: "loggedIn", + nextEvents: ["LOGOUT"], + } +): AuthService => ({ + sessionState: readable(snapshot.state), + currentUser: readable(defaultUser), + authRequest: readable({ status: "idle" }), + authOutcome: readable({ status: "none" }), + authRuntimeSnapshot: writable(snapshot), + init: vi.fn().mockResolvedValue(undefined), + requestAuth: vi.fn(() => ({ requestId: "request-1" })), + clearAuthOutcome: vi.fn(), + cancelAuthRequest: vi.fn(), + reportLocalValidationError: vi.fn(), + login: vi.fn().mockResolvedValue(undefined), + signup: vi.fn().mockResolvedValue(undefined), + loginGuest: vi.fn().mockResolvedValue(undefined), + logout: vi.fn().mockResolvedValue(undefined), + destroy: vi.fn(), +}) + +const renderSelfDisplay = ({ + authService = createAuthServiceStub(), + currentUser = defaultUser, +}: { + authService?: AuthService + currentUser?: User +} = {}) => { + render( + SelfDisplay as never, + { + authService, + currentUser, + } as never + ) + + return { authService } +} + +describe("SelfDisplay auth-service delegation", () => { + test("shows logout when authService runtime allows logout", async () => { + renderSelfDisplay() + + expect( + await screen.findByRole("button", { name: "Log out" }) + ).toBeInTheDocument() + }) + + test("calls authService.logout directly from the logout button", async () => { + const { authService } = renderSelfDisplay() + + await fireEvent.click( + await screen.findByRole("button", { name: "Log out" }) + ) + + expect(authService.logout).toHaveBeenCalledTimes(1) + }) + + test("keeps the skeleton visible while auth runtime is processing", () => { + renderSelfDisplay({ + authService: createAuthServiceStub({ + state: "loggingOut", + nextEvents: [], + }), + }) + + expect( + document.querySelector("section.skeleton.self-display") + ).toBeVisible() + }) + + test("does not import legacy logout relay stores", () => { + const selfDisplaySource = readFileSync( + resolve(process.cwd(), "src/components/SelfDisplay.svelte"), + "utf8" + ) + + expect(selfDisplaySource).not.toMatch(/\bdispatchableStore\b/) + expect(selfDisplaySource).not.toMatch(/\bloginStateStore\b/) + }) +}) diff --git a/src/tests/frontend/components/SimpleComment.auth-cleanup.test.ts b/src/tests/frontend/components/SimpleComment.auth-cleanup.test.ts new file mode 100644 index 00000000..e1f8071d --- /dev/null +++ b/src/tests/frontend/components/SimpleComment.auth-cleanup.test.ts @@ -0,0 +1,17 @@ +import { readFileSync } from "node:fs" +import { resolve } from "node:path" +import { describe, expect, test } from "vitest" + +const readComponentSource = (componentPath: string) => + readFileSync(resolve(process.cwd(), componentPath), "utf8") + +describe("SimpleComment auth cleanup source guards", () => { + test("SimpleComment no longer installs the temporary auth store bridge", () => { + const simpleCommentSource = readComponentSource( + "src/components/SimpleComment.svelte" + ) + + expect(simpleCommentSource).not.toMatch(/\bcreateAuthStoreBridge\b/) + expect(simpleCommentSource).not.toMatch(/\bcurrentUserStore\b/) + }) +}) diff --git a/src/tests/frontend/components/vitest.setup.ts b/src/tests/frontend/components/vitest.setup.ts new file mode 100644 index 00000000..3174a3bb --- /dev/null +++ b/src/tests/frontend/components/vitest.setup.ts @@ -0,0 +1,12 @@ +import { cleanup } from "@testing-library/svelte" +import "@testing-library/jest-dom/vitest" +import { afterEach, beforeEach } from "vitest" + +beforeEach(() => { + localStorage.clear() +}) + +afterEach(() => { + cleanup() + localStorage.clear() +}) diff --git a/src/tests/frontend/svelte-stores.test.ts b/src/tests/frontend/svelte-stores.test.ts deleted file mode 100644 index 7ac692d9..00000000 --- a/src/tests/frontend/svelte-stores.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { get } from "svelte/store" -import { dispatchableStore } from "../../lib/svelte-stores" - -describe("dispatchableStore", () => { - it("should dispatch and update the state correctly", () => { - const testEventName = "testEvent" - dispatchableStore.dispatch(testEventName) - - expect(get(dispatchableStore)).toEqual({ name: testEventName }) - }) -}) diff --git a/vite.config.ts b/vite.config.ts index f7fbd77e..f5d1895f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -48,6 +48,9 @@ export default defineConfig(async ({ mode }) => { }, }, ], + resolve: { + conditions: ["svelte", "browser", "module", "development|production"], + }, build: { outDir: distDir, emptyOutDir: true, diff --git a/vitest.components.config.ts b/vitest.components.config.ts new file mode 100644 index 00000000..c6e615d9 --- /dev/null +++ b/vitest.components.config.ts @@ -0,0 +1,35 @@ +import { svelte } from "@sveltejs/vite-plugin-svelte" +import sveltePreprocess from "svelte-preprocess" +import { defineConfig } from "vitest/config" + +export default defineConfig({ + plugins: [ + svelte({ + emitCss: false, + preprocess: [ + sveltePreprocess({ + typescript: { tsconfigFile: "tsconfig.frontend.json" }, + }), + ], + compilerOptions: { dev: true }, + }), + ], + resolve: process.env.VITEST + ? { + conditions: ["browser"], + } + : undefined, + test: { + clearMocks: true, + css: true, + environment: "jsdom", + environmentOptions: { + jsdom: { + url: "http://localhost/", + }, + }, + include: ["src/tests/frontend/components/**/*.test.ts"], + restoreMocks: true, + setupFiles: ["src/tests/frontend/components/vitest.setup.ts"], + }, +}) diff --git a/yarn.lock b/yarn.lock index 800ed47e..2d214b9c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,6 +7,11 @@ resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== +"@adobe/css-tools@^4.4.0": + version "4.4.4" + resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.4.tgz#2856c55443d3d461693f32d2b96fb6ea92e1ffa9" + integrity sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg== + "@ampproject/remapping@^2.2.0": version "2.2.1" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" @@ -23,7 +28,7 @@ "@babel/highlight" "^7.22.10" chalk "^2.4.2" -"@babel/code-frame@^7.26.2", "@babel/code-frame@^7.28.6", "@babel/code-frame@^7.29.0": +"@babel/code-frame@^7.10.4", "@babel/code-frame@^7.26.2", "@babel/code-frame@^7.28.6", "@babel/code-frame@^7.29.0": version "7.29.0" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.0.tgz#7cd7a59f15b3cc0dcd803038f7792712a7d0b15c" integrity sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw== @@ -1036,6 +1041,11 @@ resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== +"@babel/runtime@^7.12.5": + version "7.29.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.29.2.tgz#9a6e2d05f4b6692e1801cd4fb176ad823930ed5e" + integrity sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g== + "@babel/runtime@^7.21.0": version "7.22.10" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.10.tgz#ae3e9631fd947cb7e3610d3e9d8fef5f76696682" @@ -1172,7 +1182,7 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@cypress/request@2.88.12", "@cypress/request@^3.0.0": +"@cypress/request@^3.0.0", "@cypress/request@^3.0.10": version "3.0.0" resolved "https://registry.yarnpkg.com/@cypress/request/-/request-3.0.0.tgz#7f58dfda087615ed4e6aab1b25fffe7630d6dd85" integrity sha512-GKFCqwZwMYmL3IBoNeR2MM1SnxRIGERsQOTWeQKoYBt2JLqcqiy7JXqO894FLrpjZYqGxW92MNwRH2BN56obdQ== @@ -3745,6 +3755,45 @@ dependencies: defer-to-connect "^2.0.1" +"@testing-library/dom@9.x.x || 10.x.x": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.4.1.tgz#d444f8a889e9a46e9a3b4f3b88e0fcb3efb6cf95" + integrity sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^5.0.1" + aria-query "5.3.0" + dom-accessibility-api "^0.5.9" + lz-string "^1.5.0" + picocolors "1.1.1" + pretty-format "^27.0.2" + +"@testing-library/jest-dom@^6.9.1": + version "6.9.1" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz#7613a04e146dd2976d24ddf019730d57a89d56c2" + integrity sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA== + dependencies: + "@adobe/css-tools" "^4.4.0" + aria-query "^5.0.0" + css.escape "^1.5.1" + dom-accessibility-api "^0.6.3" + picocolors "^1.1.1" + redent "^3.0.0" + +"@testing-library/svelte-core@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@testing-library/svelte-core/-/svelte-core-1.0.0.tgz#09ad79f5491600afa1cd064203223c9cdcd5799f" + integrity sha512-VkUePoLV6oOYwSUvX6ShA8KLnJqZiYMIbP2JW2t0GLWLkJxKGvuH5qrrZBV/X7cXFnLGuFQEC7RheYiZOW68KQ== + +"@testing-library/svelte@^5.3.1": + version "5.3.1" + resolved "https://registry.yarnpkg.com/@testing-library/svelte/-/svelte-5.3.1.tgz#8142c1894be5e173f1fea9afcedbb2df537e37e3" + integrity sha512-8Ez7ZOqW5geRf9PF5rkuopODe5RGy3I9XR+kc7zHh26gBiktLaxTfKmhlGaSHYUOTQE7wFsLMN9xCJVCszw47w== + dependencies: + "@testing-library/dom" "9.x.x || 10.x.x" + "@testing-library/svelte-core" "1.0.0" + "@tokenizer/token@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276" @@ -3787,6 +3836,11 @@ dependencies: tslib "^2.4.0" +"@types/aria-query@^5.0.1": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708" + integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== + "@types/aws-lambda@^8.10.95": version "8.10.119" resolved "https://registry.yarnpkg.com/@types/aws-lambda/-/aws-lambda-8.10.119.tgz#aaf010a9c892b3e29a290e5c49bfe8bcec82c455" @@ -3830,6 +3884,19 @@ resolved "https://registry.yarnpkg.com/@types/bcryptjs/-/bcryptjs-2.4.2.tgz#e3530eac9dd136bfdfb0e43df2c4c5ce1f77dfae" integrity sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ== +"@types/chai@^5.2.2": + version "5.2.3" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-5.2.3.tgz#8e9cd9e1c3581fa6b341a5aed5588eb285be0b4a" + integrity sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA== + dependencies: + "@types/deep-eql" "*" + assertion-error "^2.0.1" + +"@types/deep-eql@*": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/deep-eql/-/deep-eql-4.0.2.tgz#334311971d3a07121e7eb91b684a605e7eea9cbd" + integrity sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw== + "@types/eslint-scope@^3.7.3": version "3.7.4" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" @@ -3950,11 +4017,6 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.1.tgz#178d58ee7e4834152b0e8b4d30cbfab578b9bb30" integrity sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg== -"@types/node@^16.18.39": - version "16.18.41" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.41.tgz#61b14360fd3f7444b326ac3207c83005371e3f8a" - integrity sha512-YZJjn+Aaw0xihnpdImxI22jqGbp0DCgTFKRycygjGx/Y27NnWFJa5FJ7P+MRT3u07dogEeMVh70pWpbIQollTA== - "@types/node@^25.5.0": version "25.5.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-25.5.0.tgz#5c99f37c443d9ccc4985866913f1ed364217da31" @@ -3983,15 +4045,20 @@ integrity sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g== "@types/sizzle@^2.3.2": - version "2.3.3" - resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.3.tgz#ff5e2f1902969d305225a047c8a0fd5c915cebef" - integrity sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ== + version "2.3.10" + resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.10.tgz#277a542aff6776d8a9b15f2ac682a663e3e94bbd" + integrity sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww== "@types/stack-utils@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/tmp@^0.2.3": + version "0.2.6" + resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.2.6.tgz#d785ee90c52d7cc020e249c948c36f7b32d1e217" + integrity sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA== + "@types/tough-cookie@*": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397" @@ -4209,6 +4276,67 @@ picomatch "^4.0.2" resolve-from "^5.0.0" +"@vitest/expect@3.2.4": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-3.2.4.tgz#8362124cd811a5ee11c5768207b9df53d34f2433" + integrity sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig== + dependencies: + "@types/chai" "^5.2.2" + "@vitest/spy" "3.2.4" + "@vitest/utils" "3.2.4" + chai "^5.2.0" + tinyrainbow "^2.0.0" + +"@vitest/mocker@3.2.4": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-3.2.4.tgz#4471c4efbd62db0d4fa203e65cc6b058a85cabd3" + integrity sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ== + dependencies: + "@vitest/spy" "3.2.4" + estree-walker "^3.0.3" + magic-string "^0.30.17" + +"@vitest/pretty-format@3.2.4", "@vitest/pretty-format@^3.2.4": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-3.2.4.tgz#3c102f79e82b204a26c7a5921bf47d534919d3b4" + integrity sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA== + dependencies: + tinyrainbow "^2.0.0" + +"@vitest/runner@3.2.4": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-3.2.4.tgz#5ce0274f24a971f6500f6fc166d53d8382430766" + integrity sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ== + dependencies: + "@vitest/utils" "3.2.4" + pathe "^2.0.3" + strip-literal "^3.0.0" + +"@vitest/snapshot@3.2.4": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-3.2.4.tgz#40a8bc0346ac0aee923c0eefc2dc005d90bc987c" + integrity sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ== + dependencies: + "@vitest/pretty-format" "3.2.4" + magic-string "^0.30.17" + pathe "^2.0.3" + +"@vitest/spy@3.2.4": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-3.2.4.tgz#cc18f26f40f3f028da6620046881f4e4518c2599" + integrity sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw== + dependencies: + tinyspy "^4.0.3" + +"@vitest/utils@3.2.4": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-3.2.4.tgz#c0813bc42d99527fb8c5b138c7a88516bca46fea" + integrity sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA== + dependencies: + "@vitest/pretty-format" "3.2.4" + loupe "^3.1.4" + tinyrainbow "^2.0.0" + "@vue/compiler-core@3.5.29": version "3.5.29" resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.29.tgz#3fb70630c62a2e715eeddc3c2a48f46aa4507adc" @@ -4809,11 +4937,23 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +aria-query@5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" + integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== + dependencies: + dequal "^2.0.3" + aria-query@5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.1.tgz#ebcb2c0d7fc43e68e4cb22f774d1209cb627ab42" integrity sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g== +aria-query@^5.0.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59" + integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw== + array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz#384d12a37295aec3769ab022ad323a18a51ccf8b" @@ -4867,6 +5007,11 @@ assert-plus@1.0.0, assert-plus@^1.0.0: resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== +assertion-error@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" + integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== + ast-module-types@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/ast-module-types/-/ast-module-types-6.0.1.tgz#4b4ca0251c57b815bab62604dcb22f8c903e2523" @@ -4894,7 +5039,7 @@ async-sema@^3.1.1: resolved "https://registry.yarnpkg.com/async-sema/-/async-sema-3.1.1.tgz#e527c08758a0f8f6f9f15f799a173ff3c40ea808" integrity sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg== -"async@>2.6.4 || ^2.6.4", async@^2.6.4, async@^3.2.0, async@^3.2.3, async@^3.2.4: +"async@>2.6.4 || ^2.6.4", async@^2.6.4, async@^3.2.3, async@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== @@ -5328,7 +5473,7 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -buffer@^5.2.1, buffer@^5.5.0, buffer@^5.6.0: +buffer@^5.2.1, buffer@^5.5.0, buffer@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== @@ -5361,6 +5506,11 @@ bytes@3.1.2, bytes@^3.1.2, bytes@~3.1.2: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== +cac@^6.7.14: + version "6.7.14" + resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" + integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== + cacheable-lookup@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz#3476a8215d046e5a3202a9209dd13fec1f933a27" @@ -5463,6 +5613,17 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== +chai@^5.2.0: + version "5.3.3" + resolved "https://registry.yarnpkg.com/chai/-/chai-5.3.3.tgz#dd3da955e270916a4bd3f625f4b919996ada7e06" + integrity sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw== + dependencies: + assertion-error "^2.0.1" + check-error "^2.1.1" + deep-eql "^5.0.1" + loupe "^3.1.0" + pathval "^2.0.0" + chalk@5.6.2, chalk@^5.3.0: version "5.6.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.6.2.tgz#b1238b6e23ea337af71c7f8a295db5af0c158aea" @@ -5500,10 +5661,10 @@ chardet@^2.1.1: resolved "https://registry.yarnpkg.com/chardet/-/chardet-2.1.1.tgz#5c75593704a642f71ee53717df234031e65373c8" integrity sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ== -check-more-types@^2.24.0: - version "2.24.0" - resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" - integrity sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA== +check-error@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.3.tgz#2427361117b70cca8dc89680ead32b157019caf5" + integrity sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA== chokidar@4.0.3, chokidar@^4.0.1: version "4.0.3" @@ -5544,7 +5705,7 @@ chrome-trace-event@^1.0.2: resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== -ci-info@4.4.0: +ci-info@4.4.0, ci-info@^4.1.0: version "4.4.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.4.0.tgz#7d54eff9f54b45b62401c26032696eb59c8bd18c" integrity sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg== @@ -5616,14 +5777,14 @@ cli-spinners@^2.5.0: resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41" integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== -cli-table3@~0.6.1: - version "0.6.3" - resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.3.tgz#61ab765aac156b52f222954ffc607a6f01dbeeb2" - integrity sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg== +cli-table3@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.1.tgz#36ce9b7af4847f288d3cdd081fbd09bf7bd237b8" + integrity sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA== dependencies: string-width "^4.2.0" optionalDependencies: - "@colors/colors" "1.5.0" + colors "1.4.0" cli-truncate@^2.1.0: version "2.1.0" @@ -5989,19 +6150,19 @@ cross-env@^7.0.3: dependencies: cross-spawn "^7.0.1" -cross-spawn@^7.0.0, cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== +cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.6: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" which "^2.0.1" -cross-spawn@^7.0.1, cross-spawn@^7.0.6: - version "7.0.6" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" - integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== +cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" @@ -6046,6 +6207,11 @@ css-what@^6.1.0: resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.2.2.tgz#cdcc8f9b6977719fdfbd1de7aec24abf756b9dea" integrity sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA== +css.escape@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" + integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg== + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" @@ -6085,25 +6251,25 @@ cyclist@^1.0.1: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.2.tgz#673b5f233bf34d8e602b949429f8171d9121bea3" integrity sha512-0sVXIohTfLqVIW3kb/0n6IiWF3Ifj5nm2XaSrLq2DI6fKIGa2fYAZdk917rUneaeLVpYfFcyXE2ft0fe3remsA== -cypress@^12.17.4: - version "12.17.4" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.17.4.tgz#b4dadf41673058493fa0d2362faa3da1f6ae2e6c" - integrity sha512-gAN8Pmns9MA5eCDFSDJXWKUpaL3IDd89N9TtIupjYnzLSmlpVr+ZR+vb4U/qaMp+lB6tBvAmt7504c3Z4RU5KQ== +cypress@^15.14.1: + version "15.14.1" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-15.14.1.tgz#396a6f0aa4f0e7fa98e714c1d435e9affd60cde0" + integrity sha512-AkuiHNSnmm0a+h/horcvbjmY6dWpCe1Ebp1R0LjMP5I6pjMaNA50Mw1YP/d07pLHJ/sV8FZoGecUWFCJ/Nifpw== dependencies: - "@cypress/request" "2.88.12" + "@cypress/request" "^3.0.10" "@cypress/xvfb" "^1.2.4" - "@types/node" "^16.18.39" "@types/sinonjs__fake-timers" "8.1.1" "@types/sizzle" "^2.3.2" + "@types/tmp" "^0.2.3" arch "^2.2.0" blob-util "^2.0.2" bluebird "^3.7.2" - buffer "^5.6.0" + buffer "^5.7.1" cachedir "^2.3.0" chalk "^4.1.0" - check-more-types "^2.24.0" + ci-info "^4.1.0" cli-cursor "^3.1.0" - cli-table3 "~0.6.1" + cli-table3 "0.6.1" commander "^6.2.1" common-tags "^1.8.0" dayjs "^1.10.4" @@ -6115,12 +6281,10 @@ cypress@^12.17.4: extract-zip "2.0.1" figures "^3.2.0" fs-extra "^9.1.0" - getos "^3.2.1" - is-ci "^3.0.0" + hasha "5.2.2" is-installed-globally "~0.4.0" - lazy-ass "^1.6.0" listr2 "^3.8.3" - lodash "^4.17.21" + lodash "^4.17.23" log-symbols "^4.0.0" minimist "^1.2.8" ospath "^1.2.2" @@ -6128,9 +6292,11 @@ cypress@^12.17.4: process "^0.11.10" proxy-from-env "1.0.0" request-progress "^3.0.0" - semver "^7.5.3" supports-color "^8.1.1" - tmp "~0.2.1" + systeminformation "^5.31.1" + tmp "~0.2.4" + tree-kill "1.2.2" + tslib "1.14.1" untildify "^4.0.0" yauzl "^2.10.0" @@ -6190,9 +6356,9 @@ date-fns@^2.29.1: "@babel/runtime" "^7.21.0" dayjs@^1.10.4: - version "1.11.9" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.9.tgz#9ca491933fadd0a60a2c19f6c237c03517d71d1a" - integrity sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA== + version "1.11.20" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.20.tgz#88d919fd639dc991415da5f4cb6f1b6650811938" + integrity sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ== debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" @@ -6208,7 +6374,7 @@ debug@4.4.1: dependencies: ms "^2.1.3" -debug@4.4.3, debug@^4.3.5, debug@^4.3.6, debug@^4.3.7, debug@^4.4.0, debug@^4.4.3: +debug@4.4.3, debug@^4.3.5, debug@^4.3.6, debug@^4.3.7, debug@^4.4.0, debug@^4.4.1, debug@^4.4.3: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== @@ -6246,6 +6412,11 @@ dedent@^1.0.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.1.tgz#4f3fc94c8b711e9bb2800d185cd6ad20f2a90aff" integrity sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg== +deep-eql@^5.0.1: + version "5.0.2" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-5.0.2.tgz#4b756d8d770a9257300825d52a2c2cff99c3a341" + integrity sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q== + deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" @@ -6450,6 +6621,16 @@ diff@^4.0.1: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== +dom-accessibility-api@^0.5.9: + version "0.5.16" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" + integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== + +dom-accessibility-api@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8" + integrity sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w== + dom-serializer@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" @@ -6744,7 +6925,7 @@ es-module-lexer@^1.0.0, es-module-lexer@^1.2.1: resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.0.tgz#6be9c9e0b4543a60cd166ff6f8b4e9dae0b0c16f" integrity sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA== -es-module-lexer@^1.5.3: +es-module-lexer@^1.5.3, es-module-lexer@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a" integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA== @@ -7071,6 +7252,13 @@ estree-walker@2.0.2, estree-walker@^2.0.2: resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== +estree-walker@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" + integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== + dependencies: + "@types/estree" "^1.0.0" + esutils@^2.0.2, esutils@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" @@ -7165,6 +7353,11 @@ exit@^0.1.2: resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== +expect-type@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.3.0.tgz#0d58ed361877a31bbc4dd6cf71bbfef7faf6bd68" + integrity sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA== + expect@^29.0.0, expect@^29.6.2: version "29.6.2" resolved "https://registry.yarnpkg.com/expect/-/expect-29.6.2.tgz#7b08e83eba18ddc4a2cf62b5f2d1918f5cd84521" @@ -7867,13 +8060,6 @@ get-tsconfig@4.13.7: dependencies: resolve-pkg-maps "^1.0.0" -getos@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/getos/-/getos-3.2.1.tgz#0134d1f4e00eb46144c5a9c0ac4dc087cbb27dc5" - integrity sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q== - dependencies: - async "^3.2.0" - getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" @@ -8139,6 +8325,14 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hasha@5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/hasha/-/hasha-5.2.2.tgz#a48477989b3b327aea3c04f53096d816d97522a1" + integrity sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ== + dependencies: + is-stream "^2.0.0" + type-fest "^0.8.0" + hasown@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" @@ -8600,13 +8794,6 @@ is-callable@^1.1.3, is-callable@^1.2.7: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-ci@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.1.tgz#db6ecbed1bd659c43dac0f45661e7674103d1867" - integrity sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ== - dependencies: - ci-info "^3.2.0" - is-core-module@^2.13.0, is-core-module@^2.9.0: version "2.13.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db" @@ -9494,6 +9681,11 @@ js-tokens@^4.0.0: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== +js-tokens@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-9.0.1.tgz#2ec43964658435296f6761b34e10671c2d9527f4" + integrity sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ== + js-yaml@^3.13.1: version "3.14.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" @@ -9612,9 +9804,9 @@ jsonc-parser@^3.2.0: integrity sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ== jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + version "6.2.1" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.2.1.tgz#b6e31717f22cc37330b081ce0051ed5de53af2f6" + integrity sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q== dependencies: universalify "^2.0.0" optionalDependencies: @@ -9771,11 +9963,6 @@ latest-version@^9.0.0: dependencies: package-json "^10.0.0" -lazy-ass@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" - integrity sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw== - lazystream@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638" @@ -9971,6 +10158,11 @@ lodash@^4.17.15, lodash@^4.17.21: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +lodash@^4.17.23: + version "4.18.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c" + integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q== + log-process-errors@^11.0.0: version "11.0.1" resolved "https://registry.yarnpkg.com/log-process-errors/-/log-process-errors-11.0.1.tgz#7f660758e0d1a717a81e1f005b0648edc78286ed" @@ -10022,6 +10214,11 @@ logform@^2.3.2, logform@^2.4.0: safe-stable-stringify "^2.3.1" triple-beam "^1.3.0" +loupe@^3.1.0, loupe@^3.1.4: + version "3.2.1" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.2.1.tgz#0095cf56dc5b7a9a7c08ff5b1a8796ec8ad17e76" + integrity sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ== + lowercase-keys@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-3.0.0.tgz#c5e7d442e37ead247ae9db117a9d0a467c89d4f2" @@ -10056,12 +10253,17 @@ luxon@^3.2.1: resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.4.0.tgz#17cb754efecbf76994f05b2a3f1f91fad7ddfde7" integrity sha512-7eDo4Pt7aGhoCheGFIuq4Xa2fJm4ZpmldpGhjTYBNUYNCN6TIEP6v7chwwwt3KRp7YR+rghbfvjyo3V5y9hgBw== +lz-string@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" + integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== + macos-release@^3.3.0: version "3.4.0" resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-3.4.0.tgz#1b223706b13106c158e2b40cb81ba35dd74d7856" integrity sha512-wpGPwyg/xrSp4H4Db4xYSeAr6+cFQGHfspHzDUdYxswDnUW0L5Ov63UuJiSr8NMSpyaChO4u1n0MXUvVPtrN6A== -magic-string@^0.30.11, magic-string@^0.30.12, magic-string@^0.30.21: +magic-string@^0.30.11, magic-string@^0.30.12, magic-string@^0.30.17, magic-string@^0.30.21: version "0.30.21" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91" integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ== @@ -10254,6 +10456,11 @@ mimic-response@^4.0.0: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-4.0.0.tgz#35468b19e7c75d10f5165ea25e75a5ceea7cf70f" integrity sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg== +min-indent@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" + integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + minimatch@^10.2.2, minimatch@^10.2.4: version "10.2.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.4.tgz#465b3accbd0218b8281f5301e27cedc697f96fde" @@ -11253,6 +11460,11 @@ pathe@^2.0.1, pathe@^2.0.3: resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== +pathval@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.1.tgz#8855c5a2899af072d6ac05d11e46045ad0dc605d" + integrity sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ== + peek-readable@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-5.0.0.tgz#7ead2aff25dc40458c60347ea76cfdfd63efdfec" @@ -11268,16 +11480,16 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== +picocolors@1.1.1, picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picocolors@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" - integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== - picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1, picomatch@^2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.2.tgz#5a942915e26b372dc0f0e6753149a16e6b1c5601" @@ -11462,6 +11674,15 @@ pretty-bytes@^5.6.0: resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== +pretty-format@^27.0.2: + version "27.5.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" + integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== + dependencies: + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^17.0.1" + pretty-format@^29.0.0, pretty-format@^29.6.2: version "29.6.2" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.6.2.tgz#3d5829261a8a4d89d8b9769064b29c50ed486a47" @@ -11697,6 +11918,11 @@ rc@1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-is@^17.0.1: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + react-is@^18.0.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" @@ -11818,6 +12044,14 @@ rechoir@^0.7.0: dependencies: resolve "^1.9.0" +redent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" + integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== + dependencies: + indent-string "^4.0.0" + strip-indent "^3.0.0" + reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9: version "1.0.10" resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz#c629219e78a3316d8b604c765ef68996964e7bf9" @@ -12156,14 +12390,14 @@ rxjs@^6.6.2: dependencies: tslib "^1.9.0" -rxjs@^7.0.0, rxjs@^7.5.1: +rxjs@^7.0.0: version "7.8.1" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== dependencies: tslib "^2.1.0" -rxjs@^7.5.5: +rxjs@^7.5.1, rxjs@^7.5.5: version "7.8.2" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.2.tgz#955bc473ed8af11a002a2be52071bf475638607b" integrity sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA== @@ -12488,6 +12722,11 @@ side-channel@^1.1.0: side-channel-map "^1.0.1" side-channel-weakmap "^1.0.2" +siginfo@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30" + integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g== + signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" @@ -12710,6 +12949,11 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" +stackback@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" + integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== + stackframe@^1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310" @@ -12730,7 +12974,7 @@ statuses@^2.0.1, statuses@^2.0.2, statuses@~2.0.2: resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== -std-env@^3.7.0: +std-env@^3.7.0, std-env@^3.9.0: version "3.10.0" resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.10.0.tgz#d810b27e3a073047b2b5e40034881f5ea6f9c83b" integrity sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg== @@ -12901,6 +13145,13 @@ strip-final-newline@^3.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" + strip-json-comments@5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-5.0.3.tgz#b7304249dd402ee67fd518ada993ab3593458bcf" @@ -12916,6 +13167,13 @@ strip-json-comments@~2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== +strip-literal@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/strip-literal/-/strip-literal-3.1.0.tgz#222b243dd2d49c0bcd0de8906adbd84177196032" + integrity sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg== + dependencies: + js-tokens "^9.0.1" + strip-outer@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-outer/-/strip-outer-2.0.0.tgz#c45c724ed9b1ff6be5f660503791404f4714084b" @@ -13051,6 +13309,11 @@ system-architecture@^0.1.0: resolved "https://registry.yarnpkg.com/system-architecture/-/system-architecture-0.1.0.tgz#71012b3ac141427d97c67c56bc7921af6bff122d" integrity sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA== +systeminformation@^5.31.1: + version "5.31.5" + resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.31.5.tgz#e839fa6b40620a8bee010eb9d9d55c2d5f7042c8" + integrity sha512-5SyLdip4/3alxD4Kh+63bUQTJmu7YMfYQTC+koZy7X73HgNqZSD2P4wOZQWtUncvPvcEmnfIjCoygN4MRoEejQ== + tagged-tag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/tagged-tag/-/tagged-tag-1.0.0.tgz#a0b5917c2864cba54841495abfa3f6b13edcf4d6" @@ -13164,9 +13427,9 @@ thread-stream@^4.0.0: real-require "^0.2.0" throttleit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" - integrity sha512-rkTVqu6IjfQ/6+uNuuc3sZek4CEYxTJom3IktzgdSxcZqdARuebbA/f4QmAxMQIxqq9ZLEUkSYqvuk1I6VKq4g== + version "1.0.1" + resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.1.tgz#304ec51631c3b770c65c6c6f76938b384000f4d5" + integrity sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ== through2@^2.0.0, through2@~2.0.0: version "2.0.5" @@ -13181,7 +13444,17 @@ through@^2.3.6, through@^2.3.8: resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== -tinyglobby@^0.2.13, tinyglobby@^0.2.15: +tinybench@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b" + integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg== + +tinyexec@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.2.tgz#941794e657a85e496577995c6eef66f53f42b3d2" + integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA== + +tinyglobby@^0.2.13, tinyglobby@^0.2.14, tinyglobby@^0.2.15: version "0.2.16" resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.16.tgz#1c3b7eb953fce42b226bc5a1ee06428281aff3d6" integrity sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg== @@ -13189,6 +13462,21 @@ tinyglobby@^0.2.13, tinyglobby@^0.2.15: fdir "^6.5.0" picomatch "^4.0.4" +tinypool@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.1.1.tgz#059f2d042bd37567fbc017d3d426bdd2a2612591" + integrity sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg== + +tinyrainbow@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-2.0.0.tgz#9509b2162436315e80e3eee0fcce4474d2444294" + integrity sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw== + +tinyspy@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-4.0.4.tgz#d77a002fb53a88aa1429b419c1c92492e0c81f78" + integrity sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q== + tmp-promise@^3.0.2, tmp-promise@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/tmp-promise/-/tmp-promise-3.0.3.tgz#60a1a1cc98c988674fcbfd23b6e3367bdeac4ce7" @@ -13196,13 +13484,18 @@ tmp-promise@^3.0.2, tmp-promise@^3.0.3: dependencies: tmp "^0.2.0" -tmp@^0.2.0, tmp@~0.2.1: +tmp@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== dependencies: rimraf "^3.0.0" +tmp@~0.2.4: + version "0.2.5" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.5.tgz#b06bcd23f0f3c8357b426891726d16015abfd8f8" + integrity sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow== + tmpl@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" @@ -13277,7 +13570,7 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== -tree-kill@^1.2.2: +tree-kill@1.2.2, tree-kill@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== @@ -13362,7 +13655,7 @@ ts-node@^10.9.2: v8-compile-cache-lib "^3.0.1" yn "3.1.1" -tslib@^1.9.0: +tslib@1.14.1, tslib@^1.9.0: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== @@ -13406,6 +13699,11 @@ type-fest@^0.21.3: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== +type-fest@^0.8.0: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + type-fest@^4.18.2, type-fest@^4.21.0, type-fest@^4.39.1, type-fest@^4.41.0, type-fest@^4.6.0: version "4.41.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.41.0.tgz#6ae1c8e5731273c2bf1f58ad39cbae2c91a46c58" @@ -13603,9 +13901,9 @@ universalify@^0.2.0: integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== unix-dgram@2.x: version "2.0.6" @@ -13783,7 +14081,18 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -vite@^6.4.2: +vite-node@3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-3.2.4.tgz#f3676d94c4af1e76898c162c92728bca65f7bb07" + integrity sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg== + dependencies: + cac "^6.7.14" + debug "^4.4.1" + es-module-lexer "^1.7.0" + pathe "^2.0.3" + vite "^5.0.0 || ^6.0.0 || ^7.0.0-0" + +vite@6.4.2, "vite@^5.0.0 || ^6.0.0 || ^7.0.0-0", vite@^6.4.2: version "6.4.2" resolved "https://registry.yarnpkg.com/vite/-/vite-6.4.2.tgz#a4e548ca3a90ca9f3724582cab35e1ba15efc6f2" integrity sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ== @@ -13802,6 +14111,35 @@ vitefu@^1.0.3: resolved "https://registry.yarnpkg.com/vitefu/-/vitefu-1.1.2.tgz#b63fcf3b606170702318b8c432663a3b9030f51d" integrity sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw== +vitest@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-3.2.4.tgz#0637b903ad79d1539a25bc34c0ed54b5c67702ea" + integrity sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A== + dependencies: + "@types/chai" "^5.2.2" + "@vitest/expect" "3.2.4" + "@vitest/mocker" "3.2.4" + "@vitest/pretty-format" "^3.2.4" + "@vitest/runner" "3.2.4" + "@vitest/snapshot" "3.2.4" + "@vitest/spy" "3.2.4" + "@vitest/utils" "3.2.4" + chai "^5.2.0" + debug "^4.4.1" + expect-type "^1.2.1" + magic-string "^0.30.17" + pathe "^2.0.3" + picomatch "^4.0.2" + std-env "^3.9.0" + tinybench "^2.9.0" + tinyexec "^0.3.2" + tinyglobby "^0.2.14" + tinypool "^1.1.1" + tinyrainbow "^2.0.0" + vite "^5.0.0 || ^6.0.0 || ^7.0.0-0" + vite-node "3.2.4" + why-is-node-running "^2.3.0" + w3c-xmlserializer@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz#aebdc84920d806222936e3cdce408e32488a3073" @@ -14013,6 +14351,14 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +why-is-node-running@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz#a3f69a97107f494b3cdc3bdddd883a7d65cebf04" + integrity sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w== + dependencies: + siginfo "^2.0.0" + stackback "0.0.2" + widest-line@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-5.0.0.tgz#b74826a1e480783345f0cd9061b49753c9da70d0"