Skip to content

test: frontend store integration tests (#711)#821

Open
Chris0Jeky wants to merge 8 commits intomainfrom
test/frontend-store-integration-tests
Open

test: frontend store integration tests (#711)#821
Chris0Jeky wants to merge 8 commits intomainfrom
test/frontend-store-integration-tests

Conversation

@Chris0Jeky
Copy link
Copy Markdown
Owner

Summary

New test files

  • chatApi.integration.spec.ts — Full chat session lifecycle: creation, message accumulation, proposal references, degraded messages, tool call metadata, health checks, URL encoding
  • boardStore.columnReorder.spec.ts — Column reorder with card preservation, card update 409 conflict, expectedUpdatedAt stale edit detection, moveCard column card count tracking, editingCardId preservation
  • queueStore.polling.spec.ts — State transitions (Pending→Processing→Completed), server-side deletion handling, stale reconciliation, cancel isolation, submit appends, fetchStats independence
  • sessionStore.oidc.spec.ts — OIDC/SSO code exchange, localStorage persistence, comprehensive logout clearing, sequential login/logout cycles, invalid JWT validation, loading transitions
  • notificationStore.realtime.spec.ts — Real-time notification arrival, stale reconciliation on re-fetch, loading transitions, error recovery, board-scoped filtering, markAsRead idempotency
  • workspaceStore.modePersistence.spec.ts — localStorage mode persistence/fallback, concurrent preference request version guards, badge count reset, home/today loading transitions, mode sync from server

Closes #711

Test plan

  • All 88 new store integration tests pass
  • Full test suite (2333 tests) passes with no regressions
  • TypeScript typecheck passes clean

Tests the full chatApi → http chain including session creation, message
accumulation, proposal references, degraded messages, tool call metadata,
health checks, URL encoding, and error propagation.
Covers column reorder with card association preservation, card update 409
conflict handling, expectedUpdatedAt stale edit detection, moveCard column
card count tracking, and editingCardId preservation during board operations.
…ion tests

Covers Pending → Processing → Completed transitions via re-fetch,
server-side deletion handling, stale state reconciliation, cancel isolation
(409 failure keeps item), submit appends to existing items, and fetchStats
independence from request list.
Covers OIDC code exchange via /auth/oidc/exchange, localStorage persistence,
error mapping, comprehensive logout state clearing, sequential login/logout
cycles with different users, sessionState computed, invalid JWT structure
validation, and loading state transitions.
Covers simulated real-time notification arrival with unread count derivation,
stale state reconciliation on re-fetch, loading state transitions for all
async operations, error recovery on retry, board-scoped filtering with
combined query params, and markAsRead idempotency.
Covers localStorage mode persistence and fallback, concurrent overlapping
preference requests with version guard, clearHomeSummary/clearTodaySummary
badge count reset, homeLoading/todayLoading transitions, fetchHomeSummary
mode sync from server, resetForLogout behavior, and onboarding sync across
home and today summaries.
@Chris0Jeky
Copy link
Copy Markdown
Owner Author

Self-Review: Adversarial Findings

Genuine Issues Found

  1. boardStore.columnReorder.spec.ts: reorder failure test could be stronger — The "does not corrupt column state when reorder API fails" test only asserts columns.toHaveLength(2), but does not verify the order is unchanged. The store implementation happens to be safe here (it only updates on success), but the assertion is weak — it would still pass if the columns were somehow shuffled. Low risk since the store code is correct, but the assertion could be tightened.

  2. sessionStore.oidc.spec.ts: "clears error state on logout" test assertion — The test asserts store.isAuthenticated === false after logout but does not actually assert store.error === null. The comment says "error should be cleared" but the test doesn't verify this. Looking at the store code, clearSession() (called by logout()) does NOT explicitly null out error — so this test documents current behavior (error persists through logout) rather than testing a clearing behavior. The test description is misleading.

  3. No afterEach cleanup for vi.clearAllMocks() in chatApi tests — The chatApi integration tests use beforeEach(() => vi.clearAllMocks()) but no afterEach. This is fine for these tests since there are no timers or cleanup-sensitive resources, but it's inconsistent with the boardStore tests which do have afterEach.

  4. notificationStore.realtime.spec.ts: "error recovery" markAsRead test — The test for "clears error on successful markAsRead" has a comment noting that markAsRead "doesn't explicitly clear error" — it's testing behavior that doesn't exist. The error from the first failed call persists. The test passes because it doesn't assert store.error === null after the retry. This is honest documentation of current behavior but the test name implies error recovery that isn't happening.

Not Issues (Verified OK)

  • Mock isolation: Each test file uses vi.clearAllMocks() in beforeEach and setActivePinia(createPinia()) for fresh store state. No mock leakage between tests.
  • No vacuous assertions: All tests have substantive assertions that would fail if the store behavior changed. Checked for cases where rejects.toBeDefined() could pass vacuously — these are always paired with store state assertions.
  • Store state pollution: Each test creates a fresh Pinia instance, so cross-test state contamination is not possible.
  • Type safety: TypeScript typecheck passes clean (vue-tsc -b with zero errors).
  • Factory functions: All use Partial<Record<string, unknown>> or Partial<T> patterns consistent with existing test conventions.

Verdict

Items 1-4 are real but low severity. Items 1 and 2 are weak assertions that could theoretically pass with incorrect behavior but don't currently mask bugs. Items 3-4 are style/accuracy nits. None require blocking fixes.

…view

- boardStore: assert column order is preserved (not just count) on reorder failure
- sessionStore: rename misleading test re: error clearing, add auth artifact assertions
- notificationStore: rename and fix error recovery test to accurately document
  that markAsRead does not clear errors from prior failures
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 48974ae219

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +330 to +334
// After logout, mode should read from localStorage which has 'workbench'
// (resetForLogout calls applyMode(getLocalWorkspaceMode()) which re-persists)
expect(store.homeSummary).toBeNull()
expect(store.todaySummary).toBeNull()
expect(store.preferencesHydrated).toBe(false)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Assert workspace mode after resetForLogout fallback

This test claims to verify that resetForLogout restores the mode from localStorage, but it never asserts store.mode (or the storage value) after calling resetForLogout. As written, it will still pass even if the fallback mode behavior regresses, so it does not protect the contract described in the test name/comment.

Useful? React with 👍 / 👎.

Comment on lines +167 to +170
// After logout, error should be cleared (logout calls clearSession)
store.logout()
// error is not explicitly cleared by clearSession, but the session state should be clean
expect(store.isAuthenticated).toBe(false)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Assert error reset in logout error-clearing test

The test is named and documented as validating that logout clears error state, but it only checks isAuthenticated after store.logout(). If store.error remains populated, this test still passes, so it does not actually catch regressions in the behavior it advertises.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive suite of integration tests for several Pinia stores and API modules, including boardStore, chatApi, notificationStore, queueStore, sessionStore, and workspaceStore. These tests cover critical paths such as column reordering, OIDC authentication flows, real-time notification handling, and state reconciliation. Feedback is provided to improve the robustness of assertions in error-handling scenarios and to ensure test descriptions accurately reflect the underlying store implementations, particularly regarding how error states are managed during logout and failed operations.


// On failure, columns must not be mutated to the new order
// (the API call is atomic — either it succeeds and we update, or it fails and we keep original)
expect(store.currentBoard?.columns).toHaveLength(2)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The test asserts that the columns length remains 2, but it doesn't verify that the order of the columns was not mutated. To ensure the state is truly not "corrupted" or left in a partially updated state (especially if optimistic updates are used), the test should verify the IDs and positions of the columns match the original state.

      expect(store.currentBoard?.columns).toHaveLength(2)
      expect(store.currentBoard?.columns[0].id).toBe('col-a')
      expect(store.currentBoard?.columns[1].id).toBe('col-b')

expect(store.notifications).toHaveLength(1)
})

it('updates notification state on successful retry even when error from prior failure persists', async () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The test description states that it "clears error on successful markAsRead", but the implementation of markAsRead in notificationStore.ts does not actually clear the error ref on success (it only sets it on failure). Furthermore, the test does not assert that store.error is null. The test description should be updated to reflect the actual behavior, or the store implementation should be updated to clear the error.

Suggested change
it('updates notification state on successful retry even when error from prior failure persists', async () => {
it('updates notification state on successful markAsRead after a previous error', async () => {

expect(localStorage.getItem('taskdeck_session')).toBeNull()
})

it('does not leave authentication artifacts after logout following a failed login', async () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The test title "clears error state on logout" is misleading because clearSession (called by logout) does not reset the error ref. The test also lacks an assertion for store.error. If the intention is that logout should reset the error state, the store implementation needs to be updated. Otherwise, the test description should be corrected to avoid implying behavior that isn't implemented or tested.

Suggested change
it('does not leave authentication artifacts after logout following a failed login', async () => {
it('resets authentication state on logout', async () => {


const store = useSessionStore()
await store.login({ usernameOrEmail: 'user', password: 'pass' })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

While checking localStorage is important, the test should also verify the in-memory store state. Specifically, store.isAuthenticated should be false if the token structure was invalid, as setSession returns early and does not update the store's reactive state.

      expect(store.isAuthenticated).toBe(false)
      expect(localStorage.getItem('taskdeck_token')).toBeNull()

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new set of frontend store + real API module integration tests (mocking only the HTTP layer) to cover cross-boundary behavior that unit tests with mocked API modules don’t exercise.

Changes:

  • Introduces integration test suites for chatApi plus key Pinia stores (boardStore, queueStore, sessionStore, notificationStore, workspaceStore).
  • Validates important scenarios like optimistic updates, conflict handling, loading/error transitions, stale reconciliation, and localStorage persistence.
  • Ensures API boundary details are exercised (URL encoding, query-string composition, and response normalization).

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
frontend/taskdeck-web/src/tests/store/chatApi.integration.spec.ts Exercises chat API module behavior against mocked HTTP for key endpoints (sessions, messages, health).
frontend/taskdeck-web/src/tests/store/boardStore.columnReorder.spec.ts Covers column reorder + card preservation, 409 conflict on card update, move-card count tracking, and editing state preservation.
frontend/taskdeck-web/src/tests/store/queueStore.polling.spec.ts Tests queue request lifecycle via repeated fetches, deletion reconciliation, cancel isolation, and stats independence.
frontend/taskdeck-web/src/tests/store/sessionStore.oidc.spec.ts Adds OIDC exchange + session lifecycle tests, persistence, token structure guard coverage, and loading transitions.
frontend/taskdeck-web/src/tests/store/notificationStore.realtime.spec.ts Simulates realtime arrivals + reconciliation, loading transitions, error recovery, filtering, and idempotency.
frontend/taskdeck-web/src/tests/store/workspaceStore.modePersistence.spec.ts Tests workspace mode persistence, concurrent preference request guarding, summary clearing, mode sync, and loading transitions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

store.resetForLogout()

// After logout, mode should read from localStorage which has 'workbench'
// (resetForLogout calls applyMode(getLocalWorkspaceMode()) which re-persists)
Comment on lines +158 to +171
it('does not leave authentication artifacts after logout following a failed login', async () => {
vi.mocked(http.post).mockRejectedValue({
response: { data: { message: 'Login failed' } },
})

const store = useSessionStore()
await expect(store.login({ usernameOrEmail: 'bad', password: 'bad' })).rejects.toBeDefined()
expect(store.error).toBe('Login failed')

store.logout()

// clearSession does not explicitly null out error, but all auth artifacts must be gone
expect(store.isAuthenticated).toBe(false)
expect(store.token).toBeNull()
Comment on lines +260 to +262
// With an invalid JWT structure, setSession should guard against persistence
// The token is still set in-memory but may not validate as authenticated
// depending on isTokenExpired behavior with malformed tokens
Comment on lines +268 to +285
it('updates notification state on successful retry even when error from prior failure persists', async () => {
const store = useNotificationStore()
store.notifications = [makeNotification({ id: 'n-err', isRead: false })]

// First markAsRead fails — error is set
vi.mocked(http.post).mockRejectedValueOnce(new Error('server error'))
await expect(store.markAsRead('n-err')).rejects.toBeInstanceOf(Error)
expect(store.error).toBe('Failed to mark notification as read')

// Retry succeeds — notification is updated even though error persists
// (markAsRead only sets error on failure; it does not clear it on success)
const readNotification = makeNotification({ id: 'n-err', isRead: true, readAt: '2026-02-01T00:00:00Z' })
vi.mocked(http.post).mockResolvedValueOnce({ data: readNotification })
await store.markAsRead('n-err')

expect(store.notifications[0].isRead).toBe(true)
// Error from the prior failure is still set — not a bug, just markAsRead's current behavior
expect(store.error).toBe('Failed to mark notification as read')
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b92092a874

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// With an invalid JWT structure, setSession should guard against persistence
// The token is still set in-memory but may not validate as authenticated
// depending on isTokenExpired behavior with malformed tokens
expect(localStorage.getItem('taskdeck_token')).toBeNull()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Assert unauthenticated state for invalid JWT login

This test is described as proving that an invalid JWT does not establish a session, but it only checks that taskdeck_token was not written to localStorage. If a regression sets store.token/store.userId in memory before skipping persistence, this test would still pass while the app could treat the user as authenticated for the current tab. Add assertions on in-memory auth state (for example isAuthenticated === false and null session fields) so the contract is actually enforced.

Useful? React with 👍 / 👎.

@Chris0Jeky
Copy link
Copy Markdown
Owner Author

Round 2 Adversarial Review

I have read all 6 new test files, cross-referenced them against the production store/API code, verified mock isolation chains, and checked CI status. Here are my findings.

Important (should fix before merge)

1. sessionStore.oidc.spec.ts -- "invalid JWT" test does not verify in-memory session state (lines 1729-1751)

The test "does not persist session when token has invalid JWT structure" only asserts that localStorage.getItem('taskdeck_token') is null. However, setSession() returns early before assigning token.value, userId.value, etc. -- meaning the store remains in its initial unauthenticated state. The test should assert this:

expect(store.isAuthenticated).toBe(false)
expect(store.token).toBeNull()
expect(store.userId).toBeNull()

Without these assertions, a regression where setSession starts partially populating in-memory state before the JWT check would go undetected, and the app could treat the user as authenticated for the current tab session even though localStorage is clean.

2. workspaceStore.modePersistence.spec.ts -- resetForLogout test never verifies store.mode (lines 2105-2125)

The test "reads the localStorage fallback mode after reset" sets localStorage to 'workbench', fetches a home summary that sets mode to 'agent', then calls resetForLogout(). The production code resetForLogout() calls applyMode(getLocalWorkspaceMode()) which should restore mode to 'workbench'. But the test never asserts store.mode. It only checks that homeSummary is null and preferencesHydrated is false. Add:

expect(store.mode).toBe('workbench')

This was flagged by two prior reviewers and has not been addressed.

3. rejects.toBeDefined() used 12 times instead of type-specific assertions

Across the test files, rejects.toBeDefined() is used 12 times for error path tests. This assertion is the weakest possible -- it passes for any non-undefined rejection value including empty objects, strings, numbers, etc. In contrast, 6 other rejection assertions correctly use rejects.toBeInstanceOf(Error) or rejects.toThrow(). The inconsistency is not just stylistic: if a future refactor changes the error shape (e.g., from a plain object to an Error instance, or vice versa), toBeDefined() will silently pass without catching the regression.

The cases where this matters most are the error path tests in boardStore.columnReorder.spec.ts (lines 172-174, 200-209, 359-361) and sessionStore.oidc.spec.ts (lines 1586, 1596, 1778). These should use rejects.toMatchObject() or at least rejects.toBeInstanceOf(Error) to verify the error shape matches what calling code would expect.

Minor (acceptable, but worth noting)

4. notificationStore.realtime.spec.ts -- "error recovery" test for markAsRead documents existing gap without calling it out (lines 1075-1093)

The test accurately documents that markAsRead does NOT clear store.error on success (unlike fetchNotifications which does). The inline comment is honest about this. However, this is arguably a production bug: if a user sees a "Failed to mark notification as read" error toast, retries successfully, the error message persists in store.error and could resurface in the UI. The test should at minimum flag this as a known limitation via a // TODO: comment rather than presenting it as expected behavior.

5. sessionStore.oidc.spec.ts -- test comment at line 1748 is misleading

The comment says "The token is still set in-memory but may not validate as authenticated" -- but reading setSession(), it returns early before setting token.value. So the token is NOT set in-memory. The comment should say "The token is NOT set in-memory because setSession returns early before assignment." This won't cause test failures but will confuse future contributors.

Verified OK (no issues)

  • Mock isolation: Each file creates a fresh Pinia via setActivePinia(createPinia()) in beforeEach. Module-level vi.mock() calls use correct relative paths that resolve through the vitest hoisting system to intercept the http module consumed by all API modules.
  • Mock chain correctness: Store -> API module -> http. The vi.mock('../../api/http') correctly intercepts at the shared dependency. I verified this for all 6 store/API chains.
  • No flaky patterns: No timing dependencies, no setTimeout in test logic, no unresolved promise chains, no non-deterministic ordering.
  • Type safety: Factory functions use proper Partial<T> types. The chatApi.integration.spec.ts correctly uses Partial<ChatMessage> and Partial<ChatSession> from the actual type imports. The boardStore and queueStore factories use Partial<Record<string, unknown>> which is looser but consistent with other test files in the codebase.
  • CI: All required checks pass (Frontend Unit on ubuntu and windows, Backend, E2E Smoke, Architecture, etc.).
  • Coverage of issue TST-44: Frontend store integration tests — stores + real API module interaction #711 scenarios: The 88 tests cover optimistic updates, error handling, state reconciliation, data isolation, loading transitions, and concurrent operations as specified.

Verdict

Items 1 and 2 are genuine assertion gaps that reduce confidence in the test suite -- they test names promise behavior verification but the assertions don't actually enforce it. Item 3 is a consistency issue that weakens error path coverage. These should be fixed before merge. The remaining items are informational.

- sessionStore: assert in-memory auth state (isAuthenticated, token, userId)
  for invalid JWT test, not just localStorage; fix misleading comment
- workspaceStore: add store.mode assertion to resetForLogout test and fix
  incorrect comment (fetchHomeSummary overwrites localStorage before reset);
  add second test for localStorage fallback without server sync
- Replace 12 weak rejects.toBeDefined() with type-specific assertions:
  toMatchObject for HTTP error shapes, toThrow for Error instances
- All 2334 tests pass
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2255eca69a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +291 to +295
it('sets loading=true during register and clears after failure', async () => {
vi.mocked(http.post).mockRejectedValue({
response: { data: { message: 'Email already taken' } },
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Verify loading is true during register failure path

This test is labeled as validating loading=true during register, but it only checks loading after the promise rejects. If register() regresses to never set loading at request start (so the UI loses its in-flight state), this test would still pass. Capture store state inside the mocked request (as done in the login test) to assert the mid-request transition.

Useful? React with 👍 / 👎.

Comment on lines +229 to +233
it('sets loading=true during updatePreferences and clears after', async () => {
vi.mocked(http.put).mockResolvedValue({
data: { userId: 'u-1', mentionImmediateEnabled: false },
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Assert loading toggles true in updatePreferences test

The test name says updatePreferences sets loading=true and then clears it, but it only asserts the final false state. That means a regression where updatePreferences() never raises loading during the API call would go undetected, even though users would lose loading feedback. Add an in-flight assertion by inspecting store state from within the mocked HTTP call.

Useful? React with 👍 / 👎.

Chris0Jeky added a commit that referenced this pull request Apr 13, 2026
Update STATUS.md, IMPLEMENTATION_MASTERPLAN.md, TESTING_GUIDE.md,
and MANUAL_TEST_CHECKLIST.md for the supplementary test depth wave.

- Add delivery entry 134 to masterplan for 6 parallel worktree PRs
- Update test counts: backend ~4,479+, frontend ~2,454+, combined ~6,950+
- Add new TESTING_GUIDE section with run commands and test breakdowns
- Add Z22 manual validation checklist for new test categories
- Update date stamps to 2026-04-13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Pending

Development

Successfully merging this pull request may close these issues.

TST-44: Frontend store integration tests — stores + real API module interaction

2 participants