128 lines
4.3 KiB
TypeScript
128 lines
4.3 KiB
TypeScript
import { TestBed } from '@angular/core/testing';
|
|
import { BiasImpactJob } from '@models/bias-impact';
|
|
import { TaskExecution } from '@models/task-execution';
|
|
import { lastValueFrom, of, throwError } from 'rxjs';
|
|
import { vi } from 'vitest';
|
|
|
|
import { TaskExecutionsService } from './task-executions';
|
|
|
|
const job = (status: BiasImpactJob['status'], terminal: boolean): BiasImpactJob => ({
|
|
id: 'job-1',
|
|
status,
|
|
executionId: 'execution-1',
|
|
stepId: 'step-1',
|
|
createdAt: '2026-07-21T10:00:00',
|
|
startedAt: null,
|
|
completedAt: terminal ? '2026-07-21T10:00:10' : null,
|
|
reportId: null,
|
|
report: null,
|
|
errorCode: null,
|
|
errorMessage: null,
|
|
terminal
|
|
});
|
|
|
|
describe('TaskExecutionsService bias operations', () => {
|
|
let service: TaskExecutionsService;
|
|
|
|
beforeEach(() => {
|
|
TestBed.configureTestingModule({});
|
|
service = TestBed.inject(TaskExecutionsService);
|
|
});
|
|
|
|
it('maps side-effect policy conflicts using the backend error code', async () => {
|
|
service.taskExecutionsCallService = {
|
|
runBiasImpactExperiment: () => throwError(() => ({
|
|
status: 409,
|
|
error: {
|
|
detail: 'External side effects are blocked',
|
|
errors: [{ code: 'BIAS_SIDE_EFFECT_BLOCKED', message: 'External side effects are blocked' }]
|
|
}
|
|
}))
|
|
} as unknown as typeof service.taskExecutionsCallService;
|
|
|
|
await expect(lastValueFrom(service.runBiasImpactExperiment('execution-1', 'step-1', {
|
|
annotationIds: ['annotation-1'],
|
|
repetitions: 3,
|
|
includeRawOutputs: true,
|
|
externalSideEffectPolicy: 'BLOCK',
|
|
confirmExternalSideEffects: false
|
|
}))).rejects.toEqual({
|
|
reason: 'SIDE_EFFECT_BLOCKED',
|
|
code: 'BIAS_SIDE_EFFECT_BLOCKED',
|
|
message: 'External side effects are blocked'
|
|
});
|
|
expect(service.biasExperimentInProgress()).toBe(false);
|
|
});
|
|
|
|
it('polls until a terminal job and stops after completion', async () => {
|
|
vi.useFakeTimers();
|
|
const getBiasImpactJob = vi.fn()
|
|
.mockReturnValueOnce(of(job('QUEUED', false)))
|
|
.mockReturnValueOnce(of(job('RUNNING', false)))
|
|
.mockReturnValueOnce(of(job('COMPLETED', true)));
|
|
service.taskExecutionsCallService = { getBiasImpactJob } as unknown as typeof service.taskExecutionsCallService;
|
|
|
|
const terminalJob = lastValueFrom(service.pollBiasImpactJob('job-1'));
|
|
await vi.advanceTimersByTimeAsync(3_000);
|
|
|
|
await expect(terminalJob).resolves.toEqual(expect.objectContaining({ status: 'COMPLETED', terminal: true }));
|
|
expect(getBiasImpactJob).toHaveBeenCalledTimes(3);
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('retries a transient polling failure without abandoning the job', async () => {
|
|
vi.useFakeTimers();
|
|
const getBiasImpactJob = vi.fn()
|
|
.mockReturnValueOnce(throwError(() => ({ status: 0 })))
|
|
.mockReturnValueOnce(of(job('RUNNING', false)))
|
|
.mockReturnValueOnce(of(job('COMPLETED', true)));
|
|
service.taskExecutionsCallService = { getBiasImpactJob } as unknown as typeof service.taskExecutionsCallService;
|
|
|
|
const terminalJob = lastValueFrom(service.pollBiasImpactJob('job-1'));
|
|
await vi.advanceTimersByTimeAsync(5_000);
|
|
|
|
await expect(terminalJob).resolves.toEqual(expect.objectContaining({ status: 'COMPLETED' }));
|
|
expect(getBiasImpactJob).toHaveBeenCalledTimes(3);
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('replaces the local execution immediately with the interaction response', async () => {
|
|
const baseline: TaskExecution = {
|
|
id: 'execution-1',
|
|
name: 'Human flow',
|
|
creationTime: 1,
|
|
context: {
|
|
inputs: {},
|
|
result: {},
|
|
errors: {},
|
|
warnings: [],
|
|
steps: {},
|
|
status: 'WAITING',
|
|
waitingSteps: ['decision-1']
|
|
}
|
|
};
|
|
const updated: TaskExecution = {
|
|
...baseline,
|
|
context: {
|
|
...baseline.context,
|
|
status: 'SUCCESS',
|
|
waitingSteps: []
|
|
}
|
|
};
|
|
(service as any)._taskExecutions.set([baseline]);
|
|
vi.spyOn(service, 'refresh').mockImplementation(() => undefined);
|
|
service.taskExecutionsCallService = {
|
|
submitInteractionText: vi.fn().mockReturnValue(of(updated))
|
|
} as unknown as typeof service.taskExecutionsCallService;
|
|
|
|
await lastValueFrom(service.submitInteractionText(
|
|
'execution-1',
|
|
'decision-1',
|
|
'choice',
|
|
'approve'
|
|
));
|
|
|
|
expect(service.taskExecutions()[0].context.status).toBe('SUCCESS');
|
|
});
|
|
});
|