|
/** |
|
* Real Smoke Test: Custom Class Serialization with Workflow Framework |
|
* |
|
* This test uses the actual Workflow serialization system to demonstrate |
|
* custom class serialization in v4.0.1-beta.49. |
|
* |
|
* Key differences from a mock test: |
|
* - Uses real WORKFLOW_SERIALIZE/DESERIALIZE from @workflow/serde |
|
* - Uses actual dehydrateWorkflowArguments/hydrateWorkflowArguments |
|
* - Tests the real serialization pipeline, not a simulation |
|
*/ |
|
|
|
import { describe, it, expect } from 'bun:test'; |
|
import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from './workflow/packages/serde/src/index'; |
|
|
|
// Import the real serialization functions from Workflow core |
|
// These are what actually serialize/deserialize in real workflows |
|
import { |
|
dehydrateWorkflowArguments, |
|
hydrateWorkflowArguments, |
|
} from './workflow/packages/core/src/serialization'; |
|
|
|
import { registerSerializationClass } from './workflow/packages/core/src/class-serialization'; |
|
|
|
const mockRunId = 'wrun_test_order_123'; |
|
|
|
/** |
|
* Order class with real Workflow serialization |
|
*/ |
|
class Order { |
|
// NOTE: In real usage, the SWC compiler plugin automatically adds this |
|
static classId = 'Order'; |
|
|
|
constructor( |
|
public orderId: string, |
|
public customerId: string, |
|
public items: Array<{ sku: string; price: number; quantity: number }> = [], |
|
public discountPercent: number = 0, |
|
public shippingZone: 'domestic' | 'international' = 'domestic' |
|
) {} |
|
|
|
calculateSubtotal(): number { |
|
return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0); |
|
} |
|
|
|
applyDiscount(percent: number): void { |
|
this.discountPercent = percent; |
|
} |
|
|
|
getDiscountAmount(): number { |
|
return this.calculateSubtotal() * (this.discountPercent / 100); |
|
} |
|
|
|
calculateShipping(): number { |
|
const subtotal = this.calculateSubtotal(); |
|
if (this.shippingZone === 'domestic') { |
|
return subtotal > 5000 ? 0 : 500; |
|
} |
|
return 1500; |
|
} |
|
|
|
calculateTotal(): number { |
|
const subtotal = this.calculateSubtotal(); |
|
const discount = this.getDiscountAmount(); |
|
const shipping = this.calculateShipping(); |
|
return subtotal - discount + shipping; |
|
} |
|
|
|
getSummary(): string { |
|
return `Order ${this.orderId}: ${this.items.length} items, Total: $${(this.calculateTotal() / 100).toFixed(2)}`; |
|
} |
|
|
|
hasFreeShipping(): boolean { |
|
return this.shippingZone === 'domestic' && this.calculateSubtotal() > 5000; |
|
} |
|
|
|
// Real Workflow serialization method |
|
static [WORKFLOW_SERIALIZE](instance: Order) { |
|
return { |
|
orderId: instance.orderId, |
|
customerId: instance.customerId, |
|
items: instance.items, |
|
discountPercent: instance.discountPercent, |
|
shippingZone: instance.shippingZone, |
|
}; |
|
} |
|
|
|
// Real Workflow deserialization method |
|
static [WORKFLOW_DESERIALIZE](data: { |
|
orderId: string; |
|
customerId: string; |
|
items: Array<{ sku: string; price: number; quantity: number }>; |
|
discountPercent: number; |
|
shippingZone: 'domestic' | 'international'; |
|
}) { |
|
return new Order( |
|
data.orderId, |
|
data.customerId, |
|
data.items, |
|
data.discountPercent, |
|
data.shippingZone |
|
); |
|
} |
|
} |
|
|
|
// Register the Order class for deserialization |
|
registerSerializationClass('Order', Order); |
|
|
|
describe('Real Custom Class Serialization', () => { |
|
it('should serialize and deserialize Order with real Workflow functions', () => { |
|
// Create an order instance |
|
const order = new Order('ord_123', 'cus_456', [ |
|
{ sku: 'LAPTOP', price: 120000, quantity: 1 }, |
|
{ sku: 'MOUSE', price: 3000, quantity: 2 }, |
|
]); |
|
|
|
order.applyDiscount(15); |
|
|
|
// Calculate values before serialization |
|
const totalBefore = order.calculateTotal(); |
|
const subtotalBefore = order.calculateSubtotal(); |
|
const summaryBefore = order.getSummary(); |
|
|
|
// Use REAL Workflow serialization |
|
const serialized = dehydrateWorkflowArguments(order, [], mockRunId); |
|
|
|
// Verify serialization format |
|
expect(serialized).toBeDefined(); |
|
expect(Array.isArray(serialized)).toBe(true); |
|
|
|
// Deserialize using REAL Workflow deserialization |
|
const hydrated = hydrateWorkflowArguments(serialized, globalThis) as Order; |
|
|
|
// Verify it's an Order instance |
|
expect(hydrated).toBeInstanceOf(Order); |
|
|
|
// Verify all data is preserved |
|
expect(hydrated.orderId).toBe('ord_123'); |
|
expect(hydrated.customerId).toBe('cus_456'); |
|
expect(hydrated.items).toHaveLength(2); |
|
expect(hydrated.discountPercent).toBe(15); |
|
expect(hydrated.shippingZone).toBe('domestic'); |
|
|
|
// MOST IMPORTANT: Verify methods work after deserialization! |
|
expect(hydrated.calculateTotal()).toBe(totalBefore); |
|
expect(hydrated.calculateSubtotal()).toBe(subtotalBefore); |
|
expect(hydrated.getSummary()).toBe(summaryBefore); |
|
expect(hydrated.getDiscountAmount()).toBe(18900); |
|
expect(hydrated.hasFreeShipping()).toBe(true); |
|
}); |
|
|
|
it('should handle Order with no items', () => { |
|
const emptyOrder = new Order('ord_empty', 'cus_123'); |
|
|
|
const serialized = dehydrateWorkflowArguments(emptyOrder, [], mockRunId); |
|
const hydrated = hydrateWorkflowArguments(serialized, globalThis) as Order; |
|
|
|
expect(hydrated).toBeInstanceOf(Order); |
|
expect(hydrated.items).toHaveLength(0); |
|
expect(hydrated.calculateSubtotal()).toBe(0); |
|
expect(hydrated.calculateTotal()).toBe(500); // Just shipping |
|
}); |
|
|
|
it('should preserve complex state across serialization', () => { |
|
const order = new Order('ord_456', 'cus_789', [ |
|
{ sku: 'WIDGET-PRO', price: 8000, quantity: 3 }, |
|
]); |
|
|
|
order.applyDiscount(10); |
|
|
|
// Serialize/deserialize |
|
const serialized1 = dehydrateWorkflowArguments(order, [], mockRunId); |
|
let hydrated1 = hydrateWorkflowArguments(serialized1, globalThis) as Order; |
|
|
|
// Modify and serialize again |
|
hydrated1.items.push({ sku: 'ADDON', price: 1000, quantity: 2 }); |
|
|
|
const serialized2 = dehydrateWorkflowArguments(hydrated1, [], mockRunId); |
|
const hydrated2 = hydrateWorkflowArguments(serialized2, globalThis) as Order; |
|
|
|
// Verify everything survived two serialization cycles |
|
expect(hydrated2).toBeInstanceOf(Order); |
|
expect(hydrated2.items).toHaveLength(2); |
|
expect(hydrated2.calculateSubtotal()).toBe(26000); |
|
expect(hydrated2.calculateTotal()).toBe(23400); |
|
expect(hydrated2.discountPercent).toBe(10); |
|
}); |
|
|
|
it('should work with international shipping', () => { |
|
const order = new Order( |
|
'ord_intl', |
|
'cus_intl', |
|
[{ sku: 'ITEM', price: 10000, quantity: 1 }], |
|
0, |
|
'international' |
|
); |
|
|
|
const serialized = dehydrateWorkflowArguments(order, [], mockRunId); |
|
const hydrated = hydrateWorkflowArguments(serialized, globalThis) as Order; |
|
|
|
expect(hydrated.shippingZone).toBe('international'); |
|
expect(hydrated.calculateShipping()).toBe(1500); |
|
expect(hydrated.hasFreeShipping()).toBe(false); |
|
expect(hydrated.calculateTotal()).toBe(11500); |
|
}); |
|
}); |
|
|
|
describe('Before vs After Demonstration', () => { |
|
it('demonstrates what we had to do BEFORE custom serialization', () => { |
|
const order = new Order('ord_old', 'cus_old', [ |
|
{ sku: 'ITEM', price: 5000, quantity: 2 }, |
|
]); |
|
|
|
// BEFORE: Manual serialization to plain object |
|
const manualSerialization = { |
|
orderId: order.orderId, |
|
customerId: order.customerId, |
|
items: order.items, |
|
discountPercent: order.discountPercent, |
|
shippingZone: order.shippingZone, |
|
// Lost all methods! |
|
}; |
|
|
|
// BEFORE: Manual reconstruction |
|
const manuallyReconstructed = new Order( |
|
manualSerialization.orderId, |
|
manualSerialization.customerId, |
|
manualSerialization.items, |
|
manualSerialization.discountPercent, |
|
manualSerialization.shippingZone |
|
); |
|
|
|
// This worked but required boilerplate everywhere |
|
expect(manuallyReconstructed.calculateTotal()).toBe(10000); |
|
}); |
|
|
|
it('demonstrates what we can do AFTER custom serialization', () => { |
|
const order = new Order('ord_new', 'cus_new', [ |
|
{ sku: 'ITEM', price: 5000, quantity: 2 }, |
|
]); |
|
|
|
// AFTER: Automatic serialization |
|
const serialized = dehydrateWorkflowArguments(order, [], mockRunId); |
|
|
|
// AFTER: Automatic deserialization with methods intact |
|
const hydrated = hydrateWorkflowArguments(serialized, globalThis) as Order; |
|
|
|
// Methods just work! |
|
expect(hydrated.calculateTotal()).toBe(10000); |
|
expect(hydrated).toBeInstanceOf(Order); |
|
}); |
|
}); |
|
|
|
describe('Real-World Workflow Scenarios', () => { |
|
it('simulates abandoned cart workflow with serialization', () => { |
|
// Day 1: User creates cart |
|
const cart = new Order('ord_cart', 'cus_123', [ |
|
{ sku: 'SHIRT', price: 3000, quantity: 2 }, |
|
{ sku: 'PANTS', price: 5000, quantity: 1 }, |
|
]); |
|
|
|
// Workflow sleeps for 24 hours (serialization happens here) |
|
const serialized = dehydrateWorkflowArguments(cart, [], mockRunId); |
|
|
|
// Day 2: Workflow resumes (deserialization happens here) |
|
const resumedCart = hydrateWorkflowArguments(serialized, globalThis) as Order; |
|
|
|
// Apply discount code from email |
|
resumedCart.applyDiscount(20); |
|
|
|
// Calculate final price - methods work! |
|
expect(resumedCart.calculateTotal()).toBe(8800); |
|
expect(resumedCart.getSummary()).toContain('$88.00'); |
|
}); |
|
|
|
it('simulates multi-step order processing', () => { |
|
// Step 1: Create order |
|
let order = new Order('ord_process', 'cus_vip', [ |
|
{ sku: 'LAPTOP', price: 100000, quantity: 1 }, |
|
]); |
|
|
|
// Serialize after step 1 |
|
let serialized = dehydrateWorkflowArguments(order, [], mockRunId); |
|
order = hydrateWorkflowArguments(serialized, globalThis) as Order; |
|
|
|
// Step 2: Apply VIP discount |
|
order.applyDiscount(25); |
|
|
|
// Serialize after step 2 |
|
serialized = dehydrateWorkflowArguments(order, [], mockRunId); |
|
order = hydrateWorkflowArguments(serialized, globalThis) as Order; |
|
|
|
// Step 3: Add gift item |
|
order.items.push({ sku: 'GIFT', price: 0, quantity: 1 }); |
|
|
|
// Serialize after step 3 |
|
serialized = dehydrateWorkflowArguments(order, [], mockRunId); |
|
order = hydrateWorkflowArguments(serialized, globalThis) as Order; |
|
|
|
// Final step: Calculate total - all state preserved! |
|
expect(order.items).toHaveLength(2); |
|
expect(order.discountPercent).toBe(25); |
|
expect(order.calculateTotal()).toBe(75000); // $750 after 25% off |
|
}); |
|
}); |