Files
core2026/packages/logic/test/actions/turn/executionHelper.test.ts
T

125 lines
4.1 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest';
import { processGeneralActionWithFallback, type CommandLoader } from '../../../src/actions/turn/executionHelper.js';
import {
type GeneralActionResolver,
type GeneralActionResolveInputContext,
type TurnScheduleContext,
} from '../../../src/actions/engine.js';
describe('processGeneralActionWithFallback', () => {
const mockContext = {
general: { id: 100, name: 'TestGeneral' } as any,
} as GeneralActionResolveInputContext;
const mockSchedule = {
now: new Date('2025-01-01T00:00:00Z'),
schedule: {
entries: [{ startMinute: 0, tickMinutes: 1 }],
},
} as TurnScheduleContext;
// Mock Resolvers
const successResolver: GeneralActionResolver = {
key: 'successCmd',
resolve: () => ({
effects: [],
}),
};
const fallbackResolver: GeneralActionResolver = {
key: 'fallbackCmd',
resolve: () => ({
effects: [
{
type: 'log',
entry: { text: 'Primary failed', scope: 'general', category: 'action', format: 'month' },
} as any,
],
alternative: {
commandKey: 'alternativeCmd',
args: { foo: 'bar' },
},
}),
};
const alternativeResolver: GeneralActionResolver = {
key: 'alternativeCmd',
resolve: (_, args) => ({
effects: [
{
type: 'log',
entry: {
text: `Alternative executed with ${JSON.stringify(args)}`,
scope: 'general',
category: 'action',
format: 'month',
},
} as any,
],
}),
};
const infiniteResolver: GeneralActionResolver = {
key: 'infiniteCmd',
resolve: () => ({
effects: [],
alternative: {
commandKey: 'infiniteCmd',
args: {},
},
}),
};
// Mock Loader
const mockLoader: CommandLoader = {
load: vi.fn(async (key: string) => {
if (key === 'alternativeCmd') return alternativeResolver;
if (key === 'infiniteCmd') return infiniteResolver;
return null;
}),
};
it('should execute a simple command successfully', async () => {
const resolution = await processGeneralActionWithFallback(
successResolver,
mockContext,
mockSchedule,
{},
mockLoader
);
expect(resolution.effects).toHaveLength(0);
expect(resolution.alternative).toBeUndefined();
});
it('should handle alternative command fallback', async () => {
const resolution = await processGeneralActionWithFallback(
fallbackResolver,
mockContext,
mockSchedule,
{},
mockLoader
);
// It should eventually execute alternativeResolver
// BUT resolveGeneralAction creates a FRESH resolution from the FINAL resolver.
// It does NOT merge logs currently. (As per my implementation comment)
// Wait, did I implement log merging? No.
// I implemented a simple loop that re-runs `resolveGeneralAction`.
// So the final resolution comes from `alternativeResolver`.
// Let's verify what we expect.
// If we want legacy parity, we might expect logs from the first attempt too.
// But for now, let's verify the loop works.
expect(resolution.logs).toHaveLength(2); // 'Primary failed' + 'Alternative executed...'
expect(resolution.alternative).toBeUndefined(); // The final one succeeded
expect(mockLoader.load).toHaveBeenCalledWith('alternativeCmd');
});
it('should prevent infinite loops', async () => {
await expect(
processGeneralActionWithFallback(infiniteResolver, mockContext, mockSchedule, {}, mockLoader)
).rejects.toThrow('Command fallback loop limit exceeded');
});
});