Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { processApplicationRun } from './process-application-run.js';
import type { RunReadResponse } from '../../generated/index.js';

function buildRun(
overrides: Partial<Pick<RunReadResponse, 'state' | 'termination_reason'>> & {
overrides: Partial<Pick<RunReadResponse, 'state' | 'termination_reason' | 'output'>> & {
statistics?: Partial<RunReadResponse['statistics']>;
} = {}
): RunReadResponse {
Expand All @@ -12,7 +12,7 @@ function buildRun(
application_id: 'app-1',
version_number: '1.0.0',
state: overrides.state ?? 'PENDING',
output: 'NONE',
output: overrides.output ?? 'NONE',
termination_reason: overrides.termination_reason ?? null,
error_code: null,
error_message: null,
Expand Down Expand Up @@ -62,6 +62,7 @@ describe('processApplicationRun', () => {
const raw = buildRun({
state: 'TERMINATED',
termination_reason: 'ALL_ITEMS_PROCESSED',
output: 'FULL',
statistics: { item_count: 10, item_succeeded_count: 10 },
});
const result = processApplicationRun(raw);
Expand All @@ -75,6 +76,7 @@ describe('processApplicationRun', () => {
const raw = buildRun({
state: 'TERMINATED',
termination_reason: 'ALL_ITEMS_PROCESSED',
output: 'PARTIAL',
statistics: {
item_count: 10,
item_succeeded_count: 7,
Expand Down
3 changes: 2 additions & 1 deletion packages/sdk/src/entities/application-run/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ export type RunStatus =
| 'COMPLETED'
| 'COMPLETED_WITH_ERRORS'
| 'CANCELED'
| 'FAILED';
| 'FAILED'
| 'UNKNOWN';
72 changes: 56 additions & 16 deletions packages/sdk/src/entities/application-run/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { RunReadResponse } from '../../generated/index.js';
* Only the fields used by the utility functions need to be realistic.
*/
function buildRun(
overrides: Partial<Pick<RunReadResponse, 'state' | 'termination_reason'>> & {
overrides: Partial<Pick<RunReadResponse, 'state' | 'termination_reason' | 'output'>> & {
statistics?: Partial<RunReadResponse['statistics']>;
} = {}
): RunReadResponse {
Expand All @@ -16,7 +16,7 @@ function buildRun(
application_id: 'app-1',
version_number: '1.0.0',
state: overrides.state ?? 'PENDING',
output: 'NONE',
output: overrides.output ?? 'NONE',
termination_reason: overrides.termination_reason ?? null,
error_code: null,
error_message: null,
Expand Down Expand Up @@ -118,44 +118,75 @@ describe('getRunStatus', () => {
expect(getRunStatus(run)).toBe('CANCELED');
});

it('should return FAILED when terminated by system', () => {
it('should return FAILED when all items processed but output is NONE', () => {
const run = buildRun({
state: 'TERMINATED',
termination_reason: 'ALL_ITEMS_PROCESSED',
output: 'NONE',
});
expect(getRunStatus(run)).toBe('FAILED');
});

it('should return FAILED when canceled by system with no output', () => {
const run = buildRun({
state: 'TERMINATED',
termination_reason: 'CANCELED_BY_SYSTEM',
output: 'NONE',
});
expect(getRunStatus(run)).toBe('FAILED');
});

it('should return COMPLETED when all items succeeded', () => {
it('should return COMPLETED when all items processed and output is FULL', () => {
const run = buildRun({
state: 'TERMINATED',
termination_reason: 'ALL_ITEMS_PROCESSED',
statistics: { item_count: 10, item_succeeded_count: 10 },
output: 'FULL',
});
expect(getRunStatus(run)).toBe('COMPLETED');
});

it('should return COMPLETED when canceled by system but output is FULL', () => {
const run = buildRun({
state: 'TERMINATED',
termination_reason: 'CANCELED_BY_SYSTEM',
output: 'FULL',
});
expect(getRunStatus(run)).toBe('COMPLETED');
});

it('should return COMPLETED_WITH_ERRORS when not all items succeeded', () => {
it('should return COMPLETED_WITH_ERRORS when all items processed and output is PARTIAL', () => {
const run = buildRun({
state: 'TERMINATED',
termination_reason: 'ALL_ITEMS_PROCESSED',
statistics: { item_count: 10, item_succeeded_count: 7 },
output: 'PARTIAL',
});
expect(getRunStatus(run)).toBe('COMPLETED_WITH_ERRORS');
});

it('should return COMPLETED_WITH_ERRORS when some items had errors', () => {
it('should return COMPLETED_WITH_ERRORS when canceled by system with output PARTIAL', () => {
const run = buildRun({
state: 'TERMINATED',
termination_reason: 'ALL_ITEMS_PROCESSED',
statistics: {
item_count: 10,
item_succeeded_count: 8,
item_user_error_count: 2,
},
termination_reason: 'CANCELED_BY_SYSTEM',
output: 'PARTIAL',
});
expect(getRunStatus(run)).toBe('COMPLETED_WITH_ERRORS');
});

it('should return UNKNOWN when terminated with no termination reason', () => {
const run = buildRun({
state: 'TERMINATED',
termination_reason: null,
});
expect(getRunStatus(run)).toBe('UNKNOWN');
});

it('should return UNKNOWN for an unrecognized termination reason', () => {
const run = buildRun({
state: 'TERMINATED',
termination_reason: 'SOMETHING_ELSE' as RunReadResponse['termination_reason'],
});
expect(getRunStatus(run)).toBe('UNKNOWN');
});
});

describe('canDownloadRunItems', () => {
Expand All @@ -181,6 +212,7 @@ describe('canDownloadRunItems', () => {
const run = buildRun({
state: 'TERMINATED',
termination_reason: 'CANCELED_BY_SYSTEM',
output: 'NONE',
});
expect(canDownloadRunItems(run)).toBe(false);
});
Expand All @@ -189,7 +221,7 @@ describe('canDownloadRunItems', () => {
const run = buildRun({
state: 'TERMINATED',
termination_reason: 'ALL_ITEMS_PROCESSED',
statistics: { item_count: 5, item_succeeded_count: 5 },
output: 'FULL',
});
expect(canDownloadRunItems(run)).toBe(true);
});
Expand All @@ -198,7 +230,15 @@ describe('canDownloadRunItems', () => {
const run = buildRun({
state: 'TERMINATED',
termination_reason: 'ALL_ITEMS_PROCESSED',
statistics: { item_count: 5, item_succeeded_count: 3 },
output: 'PARTIAL',
});
expect(canDownloadRunItems(run)).toBe(true);
});

it('should return true for UNKNOWN runs', () => {
const run = buildRun({
state: 'TERMINATED',
termination_reason: null,
});
expect(canDownloadRunItems(run)).toBe(true);
});
Expand Down
43 changes: 28 additions & 15 deletions packages/sdk/src/entities/application-run/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,36 +28,49 @@ export function getRunProgress(run: RunReadResponse): number {
}

/**
* Derive a human-readable {@link RunStatus} from the raw API `state` and `termination_reason`.
* Derive a human-readable {@link RunStatus} from the raw API `state`,
* `termination_reason`, and `output`.
*
* Mapping rules:
* - PENDING / PROCESSING → passed through as-is
* - TERMINATED + CANCELED_BY_USER → CANCELED
* - TERMINATED + CANCELED_BY_SYSTEM → FAILED
* - TERMINATED + not all items succeeded → COMPLETED_WITH_ERRORS
* - TERMINATED + all items succeeded → COMPLETED
* - TERMINATED + CANCELED_BY_SYSTEM or ALL_ITEMS_PROCESSED → derived from `output`:
* - NONE → FAILED (no items succeeded)
* - PARTIAL → COMPLETED_WITH_ERRORS (some items succeeded)
* - FULL → COMPLETED (all items succeeded)
* - otherwise → UNKNOWN
* - any other termination reason, or non-terminal state, → UNKNOWN
*
* @param run - Raw run response from the API
*/
export function getRunStatus(run: RunReadResponse): RunStatus {
const { state, statistics, termination_reason } = run;
const { state, termination_reason, output } = run;
switch (state) {
case 'PENDING':
return 'PENDING';
case 'PROCESSING':
return 'PROCESSING';
case 'TERMINATED':
if (termination_reason === 'CANCELED_BY_USER') {
return 'CANCELED';
switch (termination_reason) {
case 'CANCELED_BY_USER':
return 'CANCELED';
case 'CANCELED_BY_SYSTEM':
case 'ALL_ITEMS_PROCESSED':
switch (output) {
case 'NONE':
return 'FAILED';
case 'PARTIAL':
return 'COMPLETED_WITH_ERRORS';
case 'FULL':
return 'COMPLETED';
default:
return 'UNKNOWN';
}
default:
return 'UNKNOWN';
}
if (termination_reason === 'CANCELED_BY_SYSTEM') {
return 'FAILED';
}
// If any items did not succeed, the run completed with errors
if (statistics.item_count !== statistics.item_succeeded_count) {
return 'COMPLETED_WITH_ERRORS';
}
return 'COMPLETED';
default:
return 'UNKNOWN';
}
}

Expand Down
7 changes: 7 additions & 0 deletions packages/sdk/src/entities/run-item/process-run-item.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,11 @@ describe('processRunItem', () => {
expect(result.status).toBe('SKIPPED');
expect(result.can_download).toBe(false);
});

it('should mark a terminated item with no termination reason as UNKNOWN and not downloadable', () => {
const result = processRunItem(buildItem({ state: 'TERMINATED' }));

expect(result.status).toBe('UNKNOWN');
expect(result.can_download).toBe(false);
});
});
2 changes: 1 addition & 1 deletion packages/sdk/src/entities/run-item/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ export interface ApplicationRunItem extends ItemResultReadResponse {
}

/** Derived item status that simplifies the raw `state` + `termination_reason` combination. */
export type ItemStatus = 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED' | 'SKIPPED';
export type ItemStatus = 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED' | 'SKIPPED' | 'UNKNOWN';
41 changes: 41 additions & 0 deletions packages/sdk/src/entities/run-item/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,48 @@ describe('getItemStatus', () => {
).toBe('FAILED');
});

it('should return FAILED when terminated with CANCELED_BY_SYSTEM', () => {
expect(
getItemStatus(
buildItem({
state: 'TERMINATED',
termination_reason: 'CANCELED_BY_SYSTEM' as ItemResultReadResponse['termination_reason'],
})
)
).toBe('FAILED');
});

it('should return FAILED when terminated with CANCELED_BY_USER', () => {
expect(
getItemStatus(
buildItem({
state: 'TERMINATED',
termination_reason: 'CANCELED_BY_USER' as ItemResultReadResponse['termination_reason'],
})
)
).toBe('FAILED');
});

it('should return SKIPPED when terminated with SKIPPED', () => {
expect(getItemStatus(buildItem({ state: 'TERMINATED', termination_reason: 'SKIPPED' }))).toBe(
'SKIPPED'
);
});

it('should return UNKNOWN when terminated with no termination reason', () => {
expect(getItemStatus(buildItem({ state: 'TERMINATED' }))).toBe('UNKNOWN');
});

it('should return UNKNOWN for an unrecognized termination reason', () => {
expect(
getItemStatus(
buildItem({
state: 'TERMINATED',
termination_reason: 'SOMETHING_ELSE' as ItemResultReadResponse['termination_reason'],
})
)
).toBe('UNKNOWN');
});
});

describe('canDownloadItem', () => {
Expand Down Expand Up @@ -88,4 +125,8 @@ describe('canDownloadItem', () => {
false
);
});

it('should return false for UNKNOWN items', () => {
expect(canDownloadItem(buildItem({ state: 'TERMINATED' }))).toBe(false);
});
});
31 changes: 25 additions & 6 deletions packages/sdk/src/entities/run-item/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,24 @@ export const canDownloadItem = (item: ItemResultReadResponse): boolean => {
return getItemStatus(item) === 'COMPLETED';
};

/** Termination reasons that mark a terminated item as FAILED. */
const ERROR_TERMINATION_REASONS = new Set([
'CANCELED_BY_SYSTEM',
'CANCELED_BY_USER',
'SYSTEM_ERROR',
'USER_ERROR',
]);

/**
* Derive a human-readable {@link ItemStatus} from the raw API `state` and `termination_reason`.
*
* Mapping rules:
* - PENDING / PROCESSING → passed through as-is
* - TERMINATED + SYSTEM_ERROR or USER_ERROR → FAILED
* - TERMINATED + no termination_reason → UNKNOWN
* - TERMINATED + an error reason (SYSTEM_ERROR, USER_ERROR, CANCELED_BY_SYSTEM, CANCELED_BY_USER) → FAILED
* - TERMINATED + SKIPPED → SKIPPED
* - TERMINATED (otherwise) → COMPLETED
* - TERMINATED + SUCCEEDED → COMPLETED
* - TERMINATED + any unrecognized reason → UNKNOWN
*
* @param item - Raw item response from the API
*/
Expand All @@ -31,14 +41,23 @@ export function getItemStatus(item: ItemResultReadResponse): ItemStatus {
case 'PROCESSING':
return 'PROCESSING';
case 'TERMINATED':
// Items terminated due to errors are marked as FAILED
if (termination_reason === 'SYSTEM_ERROR' || termination_reason === 'USER_ERROR') {
if (!termination_reason) {
// tip: report to the sentry in the consumer
return 'UNKNOWN';
Comment thread
mk0x9 marked this conversation as resolved.
}
if (ERROR_TERMINATION_REASONS.has(termination_reason)) {
return 'FAILED';
}
// Explicitly skipped items get their own status

if (termination_reason === 'SKIPPED') {
return 'SKIPPED';
}
return 'COMPLETED';

if (termination_reason === 'SUCCEEDED') {
return 'COMPLETED';
}

// tip: report to the sentry in the consumer
return 'UNKNOWN';
Comment thread
mk0x9 marked this conversation as resolved.
}
}
Loading