Skip to content
Open
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
63 changes: 57 additions & 6 deletions workflow-canvas/src/api/http-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,35 +105,86 @@ describe('createHttpApi', () => {
expect(await api.getWorkflow('t')).toEqual({
name: 'hello',
text: 'name: hello\n',
hash: 'a'.repeat(64),
readOnly: false,
view: DOCUMENT_BODY.view,
diagnostics: [],
});
});

it('reads a read-only workflow', async () => {
const { api } = apiFor(reply({ ...DOCUMENT_BODY, read_only: true }));

expect(await api.getWorkflow('t')).toMatchObject({ readOnly: true });
});

it('refuses a workflow that does not say whether it is read-only', async () => {
const without: Record<string, unknown> = { ...DOCUMENT_BODY };
delete without.read_only;
const { api } = apiFor(reply(without));

await expect(api.getWorkflow('t')).rejects.toBeInstanceOf(ApiError);
});

it('reads a workflow with no file as no text and no view', async () => {
const none = { ...DOCUMENT_BODY, text: null, hash: null, view: null };
const { api } = apiFor(reply(none));

expect(await api.getWorkflow('t')).toMatchObject({
text: null,
hash: null,
view: null,
});
});

it('reads the message and line of each diagnostic', async () => {
it('reads the message, line, path and severity of each diagnostic', async () => {
const body = {
...DOCUMENT_BODY,
view: null,
diagnostics: [
{ code: 'MISSING_KEY', message: 'Missing key', line: 3 },
{ code: 'WRONG_TYPE', message: 'Wrong type' },
{
code: 'MISSING_KEY',
severity: 'error',
path: 'stages.write',
message: 'Missing key',
line: 3,
},
{
code: 'WRONG_TYPE',
severity: 'warning',
path: 'intent',
message: 'Wrong type',
},
],
};
const { api } = apiFor(reply(body));

expect((await api.getWorkflow('t')).diagnostics).toEqual([
{ message: 'Missing key', line: 3 },
{ message: 'Wrong type', line: undefined },
{
message: 'Missing key',
line: 3,
path: 'stages.write',
severity: 'error',
},
{
message: 'Wrong type',
line: undefined,
path: 'intent',
severity: 'warning',
},
]);
});

it('takes a severity it does not know as an error', async () => {
const body = {
...DOCUMENT_BODY,
view: null,
diagnostics: [{ message: 'Odd', severity: 'fatal', path: '' }],
};
const { api } = apiFor(reply(body));

expect((await api.getWorkflow('t')).diagnostics).toMatchObject([
{ severity: 'error' },
]);
});

Expand All @@ -147,7 +198,7 @@ describe('createHttpApi', () => {
};
const { api } = apiFor(reply(body));

expect((await api.getWorkflow('t')).diagnostics).toEqual([
expect((await api.getWorkflow('t')).diagnostics).toMatchObject([
{ message: 'Odd line', line: undefined },
]);
},
Expand Down
24 changes: 23 additions & 1 deletion workflow-canvas/src/api/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
RunStatus,
RunView,
Session,
Severity,
StageRunStatus,
StageView,
WorkflowDocument,
Expand All @@ -32,6 +33,10 @@ function stringOf(value: unknown): string {
return typeof value === 'string' ? value : fail();
}

function booleanOf(value: unknown): boolean {
return typeof value === 'boolean' ? value : fail();
}

function arrayOf(value: unknown): unknown[] {
return Array.isArray(value) ? value : fail();
}
Expand Down Expand Up @@ -73,14 +78,28 @@ function lineOf(value: unknown): number | undefined {
: undefined;
}

const SEVERITIES: readonly Severity[] = ['error', 'warning', 'info'];

// A severity that a later version adds is taken as an error, so a finding of
// a kind this page cannot judge never reads as harmless.
function severityOf(value: unknown): Severity {
return SEVERITIES.find((candidate) => candidate === value) ?? 'error';
}

function parseDiagnostic(value: unknown): DiagnosticView {
const diagnostic = objectOf(value);
return {
message: stringOf(diagnostic.message),
line: lineOf(diagnostic.line),
path: typeof diagnostic.path === 'string' ? diagnostic.path : '',
severity: severityOf(diagnostic.severity),
};
}

function parseDiagnostics(value: unknown): DiagnosticView[] {
return arrayOf(value).map(parseDiagnostic);
}

const RUN_STATUSES: readonly RunStatus[] = ['running', 'passed', 'halted'];
const STAGE_STATUSES: readonly StageRunStatus[] = [
'pending',
Expand Down Expand Up @@ -175,11 +194,14 @@ export function parseSession(body: unknown): Session {
export function parseWorkflowDocument(body: unknown): WorkflowDocument {
const document = objectOf(body);
const text = document.text;
const hash = document.hash;
return {
name: stringOf(document.name),
text: text === null ? null : stringOf(text),
hash: hash === null ? null : stringOf(hash),
readOnly: booleanOf(document.read_only),
view: parseView(document.view),
diagnostics: arrayOf(document.diagnostics).map(parseDiagnostic),
diagnostics: parseDiagnostics(document.diagnostics),
};
}

Expand Down
21 changes: 21 additions & 0 deletions workflow-canvas/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,44 @@ export interface WorkflowView {
stages: StageView[];
}

/** How serious a finding of the check is. */
export type Severity = 'error' | 'warning' | 'info';

/** A problem the server found in the workflow text. */
export interface DiagnosticView {
message: string;
/** The line in the text, counted from 1; `undefined` when the reply has none. */
line: number | undefined;
/**
* The key path of the finding, such as `stages.write.agent` or
* `stages[1].name`; the empty string for the whole text.
*/
path: string;
/** A severity this page does not know counts as `error`. */
severity: Severity;
}

/** What `GET /api/workflow` returns. */
export interface WorkflowDocument {
name: string;
/** `null` when the file does not exist yet. */
text: string | null;
/** The hash to send back as `base_hash`; `null` when there is no file. */
hash: string | null;
/** True when the file cannot be saved. */
readOnly: boolean;
/** `null` when there is no text or the text has errors. */
view: WorkflowView | null;
diagnostics: DiagnosticView[];
}

/** What `PUT /api/workflow` returns: the file as it was written. */
export interface SaveResult {
hash: string;
view: WorkflowView;
diagnostics: DiagnosticView[];
}

/** How far a stage of a run has got. `blocked` failed and stopped the run. */
export type StageRunStatus = 'pending' | 'running' | 'passed' | 'blocked';

Expand Down
32 changes: 28 additions & 4 deletions workflow-canvas/src/designer/Designer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,14 @@ describe('Designer', () => {
const broken = {
...HELLO_DOCUMENT,
view: null,
diagnostics: [{ message: 'Missing key: intent', line: 2 }],
diagnostics: [
{
message: 'Missing key: intent',
line: 2,
path: '',
severity: 'error' as const,
},
],
};

render(<Designer result={loaded(broken)} />);
Expand All @@ -83,7 +90,12 @@ describe('Designer', () => {
...HELLO_DOCUMENT,
view: null,
diagnostics: [
{ message: 'The mapping needs the key `intent`.', line: 2 },
{
message: 'The mapping needs the key `intent`.',
line: 2,
path: '',
severity: 'error' as const,
},
],
};

Expand All @@ -108,7 +120,14 @@ describe('Designer', () => {
const broken = {
...HELLO_DOCUMENT,
view: null,
diagnostics: [{ message: 'The file is empty', line: undefined }],
diagnostics: [
{
message: 'The file is empty',
line: undefined,
path: '',
severity: 'error' as const,
},
],
};

render(<Designer result={loaded(broken)} />);
Expand All @@ -121,7 +140,12 @@ describe('Designer', () => {
});

it('lists every problem when the server repeats an identical one', () => {
const repeated = { message: 'Unknown key: owner', line: 3 };
const repeated = {
message: 'Unknown key: owner',
line: 3,
path: 'owner',
severity: 'error' as const,
};
const broken = {
...HELLO_DOCUMENT,
view: null,
Expand Down
Loading
Loading