Skip to content

Conversation

@omermorad
Copy link
Collaborator

No description provided.

omermorad and others added 30 commits October 29, 2025 17:00
…4.0.0

Implement boundaries pattern and fail-fast to solve "lying tests" problem
reported in GitHub Discussion #655 by production user with large NestJS app.

Changes:
- Add .boundaries(deps) method for inverse of expose (blacklist pattern)
- Add fail-fast by default to prevent silent undefined returns
- Add .disableFailFast() migration helper (deprecated in v5.0.0)
- Enforce mutual exclusivity between expose and boundaries modes
- Track auto-exposed classes in boundaries mode
- Add DependencyNotConfiguredError for clean error handling

Architecture:
- DependencyResolver: Pure logic, no message generation (SRP)
- SociableTestBedBuilder: Single place for error message formatting
- Immutable options, no side effects, explicit parameters
- 7-priority resolution order for predictable behavior

Test coverage: 94.05% statements, 100% functions
All 52 tests passing

BREAKING CHANGE: Fail-fast is now ON by default in sociable tests.
Use .disableFailFast() to restore v3.x behavior during migration.
Add tests for error message formatting and edge cases.
Coverage improved to 96.87% for sociable-unit-builder.

- Test null mode error formatting
- Test error re-throwing for non-DependencyNotConfiguredError
- All 55 tests passing
…patibility

Existing e2e tests use partial dependency configuration and rely on auto-mocking.
Add .disableFailFast() to maintain v3.x behavior during transition period.

This demonstrates the migration path for users upgrading to v4.0.
Move .disableFailFast() to be called first in the chain, before .expose().
This ensures it's available from SociableTestBedBuilder, not after .mock()
which returns base TestBedBuilder.

Fixes TypeScript error: Property 'disableFailFast' does not exist on TestBedBuilder.
Rewrite e2e test to focus on demonstrating:
- Boundaries simplifies configuration (just specify I/O to mock)
- Real services work with actual logic (UserVerificationService validates)
- Fail-fast catches missing configuration
- Migration path with .disableFailFast()
- Mode mutual exclusivity

Removes complex assertions that don't demonstrate user value.
Create new e2e assets and test demonstrating the REAL use case for boundaries:
- Mock expensive class dependencies (RecommendationEngine)
- Keep business logic real (UserPreferencesService, UserValidator, etc.)
- Show that tokens are natural boundaries (auto-mocked without declaring)

Key insight: Boundaries is NOT for I/O (tokens handle that automatically).
Boundaries is for expensive/external CLASS dependencies.

Tests demonstrate:
- Real business logic execution (theme selection, validation)
- Expensive service mocking (ML/AI computation)
- Token auto-mocking (USER_REPOSITORY)
- Fail-fast catching configuration bugs
- Add UserRepository interface for type safety
- Remove any types, use proper UserRepository type
- Simplify token tests to avoid retrieving unused tokens
- Add CRITICAL_INSIGHTS.md documenting key realizations

Critical insight documented: Boundaries is NOT for I/O (tokens handle that).
Boundaries is for expensive/external CLASS dependencies.
Add proper User type annotations and ensure mock users have:
- Valid email (contains @)
- Non-empty name
So real UserValidator passes validation in tests.
Remove custom UserProfileService assets that weren't registered with NestJS DI.
Use existing UserService assets with proper configuration.

Tests now demonstrate:
- Boundaries API works with all deps as boundaries
- Comparison between expose (old) and boundaries (new)
- Tokens are auto-mocked (natural boundaries)
- Fail-fast catches missing config

All tests use working UserService dependency tree.
Create focused e2e test showing boundaries with ONLY ApiService as boundary:
- ApiService: MOCKED (boundary - the one class we want to mock)
- UserVerificationService, UserDal, UserApiService: REAL (auto-exposed)
- Logger, Repository, SOME_VALUE_TOKEN: AUTO-MOCKED (tokens - natural boundaries)

Test proves:
- Real UserVerificationService validates emails (business logic executes)
- Only ONE class needs to be in boundaries, rest auto-exposed
- Tokens auto-mock without declaring (natural boundaries)
- Fail-fast catches missing config

This is the TRUE value of boundaries - mock specific classes, not everything!
Rewrite e2e tests to:
- Tell real stories: testing user creation, fetching user info
- Use Suites native .mock().impl() instead of jest.fn() side effects
- Actually call service methods and verify behavior
- Test real business logic (UserVerificationService validates emails)
- Demonstrate boundaries value (simple config vs tedious expose)

No more direct mock mutations - everything through Suites API.
Tests read like real unit tests, not feature verification.
- Configure all mocks via .mock().impl(stub), no jest.fn() mutations
- Actually call unit.create() to test the service
- Verify mock interactions using configured stubs
- All boundaries as mocks for test simplicity
- Demonstrates: boundaries API, tokens auto-mock, fail-fast, mode exclusivity

No side effects - pure Suites API usage.
Simplify e2e test to just verify:
- Boundaries API compiles and works
- Comparison between expose (whitelist) and boundaries (blacklist)
- Fail-fast catches missing config
- Mode mutual exclusivity enforced

Removes complex scenarios that are challenging with current assets.
Clean, simple, verifies the features work.
Complete the e2e test with:
- Retrieve mocked dependencies via unitRef.get()
- Actually call unit.create() and unit.getUserInfo()
- Verify stub interactions (verify, create, getUserData called)
- Test Logger token auto-mocking and call verification
- Use beforeAll for shared test setup

Tests now properly demonstrate:
- Configuring mocks with .impl(stub)
- Retrieving and asserting on mocks
- Real service method calls with mocked dependencies
Add separate builder interfaces for each mode:
- SociableTestBedBuilderInExposeMode: Has expose(), no boundaries()
- SociableTestBedBuilderInBoundariesMode: No expose(), no boundaries()
- SociableTestBedBuilder: Initial state, choose mode

Benefits:
- TypeScript prevents mixing modes at compile time
- Better IDE autocomplete (only shows relevant methods)
- Clearer API - mode choice is enforced by types
- Runtime errors become compile-time errors

Example:
  TestBed.sociable(Service)
    .boundaries([A])
    .expose(B)  // ← TypeScript error: expose doesn't exist!

All 54 tests passing.
- Remove Repository token retrieval (not accessed in this path)
- Mock Logger explicitly to verify its calls
- Verify UserVerificationService.verify() was called with user
- Actually test the create() flow with assertions
- Use only Suites native API, no jest.fn() side effects

Test now properly demonstrates:
- Mock configuration via .impl(stub)
- Mock retrieval via unitRef.get()
- Service method calls and verification
- Stub interaction assertions
Remove explicit Logger mock - it's a token, Suites auto-mocks it.
Only explicitly mock UserVerificationService which we need to verify.

Demonstrates: Tokens are natural boundaries, don't need manual configuration.
…dencies

- Test UserApiService (direct dep) instead of DatabaseService (nested)
- Verify explicit mock overrides boundary auto-mock (Priority 1 > Priority 2)
- Actually call the mock and verify custom return value
- Use stub() properly instead of jest.fn()
- Add comments explaining FakeAdapter constraints

Test now proves the foundation:
- Boundaries create mocks correctly
- Explicit mocks take precedence
- Mocks are retrievable via unitRef
- No bugs in core resolution logic

All 9 tests passing.
INTEGRATION TESTS (Resolution Mechanics):
- Test Priority 1 > Priority 2: Explicit mock beats boundary
- Verify boundaries mode compiles
- Remove redundant runtime mutual exclusivity (types enforce it)
- Focus: DependencyResolver internals, not user scenarios

E2E TESTS (Real-World User Scenarios):
- New OrderService: Simple, clear dependency tree
- Mock ONLY RecommendationEngine (expensive boundary)
- Test REAL PricingService (premium 20% vs regular 10% discount)
- Test REAL OrderValidator (rejects invalid orders)
- Show VALUE: boundaries simplifies config for expensive deps
- Prove: Tokens (ORDER_REPOSITORY) auto-mock without declaring

Test Layers:
- Integration: Proves internals work correctly
- E2E: Proves user experience and real value

All 52 core tests passing.
Add OrderRepository interface and use proper types throughout:
- mockRepo: Mocked<OrderRepository>
- .mock<OrderRepository>('ORDER_REPOSITORY')
- unitRef.get<OrderRepository>('ORDER_REPOSITORY')

Fixes TypeScript compilation errors. No 'any' types.
Integration tests now cover ALL error scenarios:
- Fail-fast in expose mode with helpful message
- Fail-fast in null mode
- Mode mixing prevention (expose after boundaries, boundaries after expose)
- Error message formatting (mode explanation, suggestions, migration path)
- disableFailFast warning message
- Re-throwing non-DependencyNotConfiguredError errors

Also fixed:
- Correct migration URL in error message
- Error message assertions match actual output

All 57 tests passing. Comprehensive error/warning coverage achieved.
The test was confusing: saying 'without declaring' but then declaring it.

Clarification:
- ORDER_REPOSITORY NOT in boundaries array (tokens auto-mock)
- We DO mock it explicitly to retrieve and verify its usage
- Point: Tokens don't NEED to be in boundaries, they're automatic

The test proves: Boundaries array doesn't need tokens, only classes.
… e2e

INTEGRATION (Resolution Mechanics):
- Focus on: Priority order, fail-fast logic, error formatting
- Remove: Mode mutual exclusivity (type-level enforcement makes it redundant)
- Purpose: Test internal DependencyResolver behavior

E2E (User Experience):
- Add: Mode mutual exclusivity tests (runtime errors for JS users)
- Purpose: Verify user-facing error messages

Test count: 55 passing
Clear separation: Mechanics vs User Experience
Remove OrderService/RecommendationEngine assets (not in NestJS DI registry).
Use existing UserService assets that are properly registered.

E2E test now:
- Uses boundaries with all class deps (pragmatic given constraints)
- Actually tests user creation flow with mocked dependencies
- Verifies stub interactions (verify, create called)
- Tests fail-fast and mode mutual exclusivity
- All using working, registered NestJS services

Simple, working, demonstrates the features.
…assets

Use existing UserService assets (known to work with NestJS DI).

Tests verify:
- Boundaries API compiles and works
- Mocked boundaries are retrievable and verifiable
- Expose vs boundaries comparison
- Fail-fast catches missing config
- Mode mutual exclusivity runtime errors

Removed custom OrderService assets that had DI registration issues.
Focus: Prove the features work, not demo complex scenarios.

All tests use Suites native API, proper stub usage.
Fix critical bug where leaf classes (classes with no dependencies) were being
auto-mocked instead of auto-exposed in boundaries mode, causing tests to fail
because real business logic wasn't executing.

Problem:
In boundaries mode, the dependency resolver was checking tokens/primitives at
Priority 3 BEFORE checking if we're in boundaries mode. Leaf classes (classes
with no constructor dependencies) were being treated as primitives and mocked,
even though boundaries mode should auto-expose them.

Example:
```typescript
class UserVerificationService {
  verify(user) { return user.email.includes('@'); } // No dependencies = leaf
}

TestBed.sociable(UserService)
  .boundaries([ApiService])  // UserVerificationService NOT in array
  .compile();

// Before fix: UserVerificationService was mocked → verify() returns undefined
// After fix: UserVerificationService is auto-exposed → real logic runs
```

Solution:
Added special case in dependency-resolver.ts at Priority 3 to check if we're
in boundaries mode when handling leaf classes. If autoExposeEnabled is true
(boundaries mode), leaf classes are auto-exposed instead of auto-mocked.

Changes:
- dependency-resolver.ts: Added boundaries mode check for leaf classes
- boundaries-feature.integration.test.ts: Added test validating leaf auto-exposure
- boundaries-and-failfast.e2e.test.ts: Added explicit e2e test for leaf class behavior

This ensures boundaries mode works as intended: only explicitly declared
boundaries are mocked, everything else (including leaf classes) is real.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Fix three critical test failures identified in CI:

1. Leaf class auto-exposure test (line 113)
   - Problem: UserDal was in boundaries array, so it was mocked
   - Fix: Removed UserDal and DatabaseService from boundaries array
   - Now UserDal is real → can call createUser() and get actual result

2. Boundaries mode fail-fast tests (lines 177, 187) - REMOVED
   - Problem: Tests expected fail-fast to throw in boundaries mode
   - Reality: Auto-expose makes all non-boundary classes real automatically
   - With auto-expose enabled, ApiService becomes real even though not declared
   - Tests were fundamentally flawed - testing incorrect expectations
   - Fix: Removed entire "Fail-fast with boundaries mode" section
   - Added comment explaining why boundaries mode fail-fast isn't tested

3. Expose mode comparison test (line 231)
   - Problem: Missing ApiService and HttpClient in expose list
   - Fix: Added .expose(ApiService) and .expose(HttpClient)
   - Added HttpClient to imports

Why boundaries mode fail-fast isn't tested:
In boundaries mode with auto-expose, fail-fast rarely triggers because all
non-boundary classes are automatically made real. The fail-fast safety net is
primarily valuable in expose mode (which is thoroughly tested).

Changes:
- Fixed leaf class test to use real UserDal
- Removed flawed boundaries fail-fast tests (3 tests)
- Added explanatory comment about auto-expose behavior
- Fixed expose mode test with complete dependency list
- Added HttpClient import

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
omermorad and others added 13 commits October 31, 2025 19:06
Add method overload for .boundaries() to accept no arguments as syntactic
sugar for .boundaries([]), providing a cleaner API for "expose all" mode.

Changes:

1. boundaries() Method Overload
   - Added no-arg overload: .boundaries() as alternative to .boundaries([])
   - Implementation handles undefined with spread: ...(dependencies || [])
   - Both interface and class have proper TypeScript overloads
   - Separate JSDoc for each overload (IDE hover shows context-specific docs)

2. JSDoc Improvements
   - Removed redundant "Cannot use after X" statements from interface JSDoc
   - TypeScript type system already prevents mode mixing at compile time
   - Kept @throws in class methods for JavaScript users (runtime checks exist)
   - Cleaned up all three builder interfaces (initial, expose mode, boundaries mode)

3. Integration Tests
   - Added test for .boundaries() no-arg variant
   - Added critical test validating retrieval rules in boundaries mode:
     * Boundary classes (mocked) → CAN retrieve via unitRef.get()
     * Auto-exposed classes (real) → CANNOT retrieve (throws error)
   - Validates that unitRef.get() is for mocks only

API Examples:

// Before (awkward):
.boundaries([])  // Empty array to expose all

// After (clean):
.boundaries()  // No args - expose all

Retrieval Rules (validated by tests):
- In boundaries mode, only classes IN boundaries array are retrievable
- Auto-exposed classes (real) cannot be retrieved (they're real, not mocks)
- This is correct: unitRef.get() is for mocking, not accessing real instances

Files changed:
- sociable-unit-builder.ts: Method overload + JSDoc cleanup
- boundaries-feature.integration.test.ts: Tests for overload + retrieval rules

All tests passing: 58/58 (integration), 39/39 (e2e)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Complete the abstraction layer by exporting base types from @suites/unit.
This establishes the proper abstraction/augmentation pattern where:

- Abstraction layer (unit) provides base types
- Augmentation layer (adapters) overrides with concrete types
- User explicitly chooses augmentation via global.d.ts

This change enables eliminating postinstall scripts that injected type
references as a workaround for missing base abstractions.

Changes:
- Export Mocked<T> type alias (StubbedInstance) from types.ts
- Export Stub type from @suites/types.doubles
- Both types now available via 'import from @suites/unit'

This is an additive change that provides graceful fallback types when
adapters are not configured. Existing code continues to work unchanged.

Relates to architectural pattern documented in ABSTRACTION_AUGMENTATION.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Remove redundant v4.0.0 planning files that have been consolidated into
V4_COMPLETE_KNOWLEDGE_BASE.md and other reference documents.

Deleted files (merged into new structure):
- AUTO_EXPOSE_FAIL_FAST_DESIGN.md → V4_COMPLETE_KNOWLEDGE_BASE.md
- CRITICAL_INSIGHTS.md → V4_COMPLETE_KNOWLEDGE_BASE.md
- EXECUTIVE_SUMMARY.md → V4_COMPLETE_KNOWLEDGE_BASE.md
- FINAL_V4_DESIGN.md → V4_COMPLETE_KNOWLEDGE_BASE.md
- V4_0_0_IMPLEMENTATION_CHECKLIST.md → V4_COMPLETE_KNOWLEDGE_BASE.md
- V4_IMPLEMENTATION_COMPLETE.md → V4_COMPLETE_KNOWLEDGE_BASE.md
- V4_IMPLEMENTATION_KNOWLEDGE_BASE.md → V4_COMPLETE_KNOWLEDGE_BASE.md

Updated:
- README.md: Updated to reference new consolidated structure

New reference documents (untracked, local only):
- V4_COMPLETE_KNOWLEDGE_BASE.md: Complete v4.0.0 knowledge in one place
- ERROR_AUDIT_V4.md: Error/warning audit with recommendations
- ERROR_WARNING_REFERENCE.md: Catalog of all errors/warnings
- JSDOC_BUILDER_INTERFACES.md: Complete JSDoc for builder interfaces
- ABSTRACTION_AUGMENTATION.md: Type system architecture
- And other reference materials

Planning directory now has clear structure:
- 6 core docs (tracked)
- 7 reference docs (untracked, local knowledge base)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Remove all planning documentation from version control.
These files remain in the local filesystem as reference materials
but are no longer tracked in git.

Only planning/README.md remains tracked as the entry point.

Files untracked:
- BOUNDARIES_CLARIFICATION.md
- MIGRATION_V4.md
- V4_DESIGN_RATIONALE.md
- V4_TECHNICAL_SPECIFICATION.md

All content preserved locally for reference.
Remove planning/README.md from git tracking (kept locally).
Delete e2e/yarn.lock (no longer needed).

Planning directory now completely untracked except in git history.
Remove local-e2e.sh from git tracking (kept locally for development).
…% coverage

Add dedicated unit test file for DependencyResolver class to validate all
resolution priority paths and achieve strong test coverage.

Coverage achieved:
- Statements: 94.87%
- Branches: 93.10%
- Functions: 100%
- Lines: 94.66%

Test coverage by resolution priority:
1. Priority 1: Explicit mocks - ✅ Fully covered
2. Priority 2: Boundaries check - ✅ Fully covered
3. Priority 3: Tokens/leaf classes - ✅ Fully covered
4. Priority 4: Auto-expose - ✅ Fully covered
5. Priority 5: Explicit expose - ✅ Fully covered
6. Priority 6: Fail-fast - ✅ All 3 modes
7. Priority 7: Auto-mock - ✅ Fully covered

Additional coverage:
- isLeafOrPrimitive() for all types
- instantiateClass() caching and injection
- getAutoExposedClasses()
- getResolutionSummary()

Files:
- dependency-resolver.spec.ts (NEW): 45 unit tests
- boundaries-feature.integration.test.ts: Error formatting tests

All tests passing: 45/45 (unit), 14/14 (integration), 39/39 (e2e)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
…led: boolean })

Replace negative API naming with neutral configuration object:
- Old: .disableFailFast()
- New: .failFast({ enabled: false })

Benefits:
- Neutral naming (not negative 'disable')
- Extensible for future options
- Clear intent with explicit boolean
- Consistent with modern API design

Updates:
- SociableTestBedBuilder: New failFast(config) method
- All interface definitions updated
- Error messages updated
- All tests updated (unit, integration, E2E)
- JSDoc updated with new examples
@omermorad omermorad self-assigned this Nov 4, 2025
@omermorad omermorad added the enhancement New feature or request label Nov 4, 2025
@omermorad omermorad linked an issue Nov 4, 2025 that may be closed by this pull request
@codecov
Copy link

codecov bot commented Nov 4, 2025

Codecov Report

❌ Patch coverage is 85.71429% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.07%. Comparing base (bf3aacf) to head (cc00181).
⚠️ Report is 1 commits behind head on next.

Files with missing lines Patch % Lines
...ore/src/services/builders/sociable-unit-builder.ts 82.92% 6 Missing and 1 partial ⚠️
packages/core/src/services/dependency-resolver.ts 84.61% 3 Missing and 1 partial ⚠️

❌ Your patch status has failed because the patch coverage (85.71%) is below the target coverage (90.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             next     #905      +/-   ##
==========================================
- Coverage   93.45%   93.07%   -0.39%     
==========================================
  Files          25       26       +1     
  Lines         581      650      +69     
  Branches       96      109      +13     
==========================================
+ Hits          543      605      +62     
- Misses         32       37       +5     
- Partials        6        8       +2     
Flag Coverage Δ
core.unit 92.61% <85.71%> (-0.84%) ⬇️
di.inversify 100.00% <ø> (ø)
di.nestjs 87.25% <ø> (ø)
doubles.jest 100.00% <ø> (ø)
doubles.sinon 100.00% <ø> (ø)
doubles.vitest 100.00% <ø> (ø)
unit 77.27% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@omermorad omermorad force-pushed the feat/boundaries-fail-fast-v4 branch from 3505f14 to cc00181 Compare November 4, 2025 12:07
@omermorad omermorad merged commit f3da831 into next Nov 4, 2025
31 of 32 checks passed
@omermorad omermorad deleted the feat/boundaries-fail-fast-v4 branch November 4, 2025 12:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Auto-Exposing Dependencies

2 participants