191 lines
7.7 KiB
JavaScript
191 lines
7.7 KiB
JavaScript
const { test, expect } = require('@playwright/test');
|
|
const { getCurrentWorkspace, login, uniqueName } = require('./helpers');
|
|
|
|
test('Operation lifecycle keeps published versions immutable and exposes safe correlation', async ({ page }) => {
|
|
await login(page);
|
|
const workspace = await getCurrentWorkspace(page);
|
|
const name = uniqueName('immutable_lifecycle');
|
|
|
|
const evidence = await page.evaluate(async ({ workspaceId, operationName }) => {
|
|
const schema = (field, description) => ({
|
|
type: 'object',
|
|
description,
|
|
required: true,
|
|
nullable: false,
|
|
fields: {
|
|
[field]: {
|
|
type: 'string',
|
|
description: `${field} value used by the lifecycle acceptance test`,
|
|
required: true,
|
|
nullable: false,
|
|
},
|
|
},
|
|
});
|
|
const payload = {
|
|
name: operationName,
|
|
display_name: 'Immutable lifecycle acceptance Operation',
|
|
category: 'quality',
|
|
protocol: 'rest',
|
|
security_level: 'standard',
|
|
target: {
|
|
kind: 'rest',
|
|
base_url: 'https://httpbin.org',
|
|
method: 'GET',
|
|
path_template: '/anything',
|
|
static_headers: {},
|
|
},
|
|
input_schema: schema('input', 'Lifecycle input contract'),
|
|
output_schema: schema('output', 'Lifecycle output contract'),
|
|
input_mapping: {
|
|
rules: [{
|
|
source: '$.mcp.input',
|
|
target: '$.request.query.input',
|
|
required: true,
|
|
}],
|
|
},
|
|
output_mapping: {
|
|
rules: [{
|
|
source: '$.response.body.output',
|
|
target: '$.output.output',
|
|
required: true,
|
|
}],
|
|
},
|
|
execution_config: { timeout_ms: 1000, headers: {} },
|
|
tool_description: {
|
|
title: 'Immutable lifecycle acceptance Operation',
|
|
description: 'Exercises immutable Draft, Published Version, YAML, archive and correlation behavior.',
|
|
tags: ['quality', 'lifecycle'],
|
|
examples: [],
|
|
},
|
|
wizard_state: null,
|
|
};
|
|
|
|
const created = await window.CrankApi.createOperation(workspaceId, payload);
|
|
const operationId = created.operation_id;
|
|
await window.CrankApi.getOperation(workspaceId, operationId);
|
|
let testResult;
|
|
try {
|
|
testResult = await window.CrankApi.runOperationTest(workspaceId, operationId, {
|
|
version: 1,
|
|
input: { input: 'acceptance' },
|
|
});
|
|
} catch (error) {
|
|
testResult = {
|
|
tested_version: 1,
|
|
request_id: error.requestId,
|
|
trace_id: error.traceId,
|
|
rejected_safely: true,
|
|
};
|
|
}
|
|
const published = await window.CrankApi.publishOperation(workspaceId, operationId, 1);
|
|
const publishedV1 = await window.CrankApi.getOperationVersion(workspaceId, operationId, 1);
|
|
|
|
await window.CrankApi.getOperation(workspaceId, operationId);
|
|
const changedPayload = JSON.parse(JSON.stringify(payload));
|
|
changedPayload.display_name = 'Changed Draft after publication';
|
|
changedPayload.tool_description.title = 'Changed Draft after publication';
|
|
const changed = await window.CrankApi.updateOperation(workspaceId, operationId, changedPayload);
|
|
const publishedV1AfterEdit = await window.CrankApi.getOperationVersion(workspaceId, operationId, 1);
|
|
|
|
const yamlV1 = await window.CrankApi.exportOperation(workspaceId, operationId, { version: 1 });
|
|
await window.CrankApi.getOperation(workspaceId, operationId);
|
|
const imported = await window.CrankApi.importOperation(
|
|
workspaceId,
|
|
yamlV1,
|
|
'upsert',
|
|
operationId,
|
|
);
|
|
await window.CrankApi.getOperation(workspaceId, operationId);
|
|
const archived = await window.CrankApi.archiveOperation(workspaceId, operationId);
|
|
const publishedV1AfterArchive = await window.CrankApi.getOperationVersion(workspaceId, operationId, 1);
|
|
|
|
return {
|
|
operationId,
|
|
testResult,
|
|
published,
|
|
changed,
|
|
imported,
|
|
archived,
|
|
originalSnapshot: publishedV1.snapshot,
|
|
afterEditSnapshot: publishedV1AfterEdit.snapshot,
|
|
afterArchiveSnapshot: publishedV1AfterArchive.snapshot,
|
|
yamlV1,
|
|
};
|
|
}, { workspaceId: workspace.id, operationName: name });
|
|
|
|
expect(evidence.testResult.tested_version).toBe(1);
|
|
expect(evidence.testResult.request_id).toMatch(/^[!-~]{1,128}$/);
|
|
expect(evidence.testResult.trace_id).toMatch(/^[0-9a-f]{32}$/);
|
|
expect(evidence.published.published_version).toBe(1);
|
|
expect(evidence.changed.version).toBe(2);
|
|
expect(evidence.imported.version).toBe(3);
|
|
expect(evidence.archived.status).toBe('archived');
|
|
expect(evidence.afterEditSnapshot).toEqual(evidence.originalSnapshot);
|
|
expect(evidence.afterArchiveSnapshot).toEqual(evidence.originalSnapshot);
|
|
expect(evidence.yamlV1).toContain("format_version: '2'");
|
|
for (const forbidden of ['wizard_state:', 'created_at:', 'published_at:', 'operation_id:']) {
|
|
expect(evidence.yamlV1).not.toContain(forbidden);
|
|
}
|
|
});
|
|
|
|
for (const locale of ['ru', 'en']) {
|
|
test(`catalog lifecycle actions are state-aware in ${locale}`, async ({ page }) => {
|
|
await login(page);
|
|
await page.evaluate((language) => localStorage.setItem('crank_lang', language), locale);
|
|
const workspace = await getCurrentWorkspace(page);
|
|
const name = uniqueName(`catalog_lifecycle_${locale}`);
|
|
const operation = await page.evaluate(async ({ workspaceId, operationName, language }) => {
|
|
const schema = {
|
|
type: 'object',
|
|
description: 'Catalog lifecycle UI contract',
|
|
required: true,
|
|
nullable: false,
|
|
fields: {},
|
|
};
|
|
const created = await window.CrankApi.createOperation(workspaceId, {
|
|
name: operationName,
|
|
display_name: `Catalog lifecycle ${language}`,
|
|
category: 'quality',
|
|
protocol: 'rest',
|
|
security_level: 'standard',
|
|
target: { kind: 'rest', base_url: 'https://example.test', method: 'GET', path_template: '/', static_headers: {} },
|
|
input_schema: schema,
|
|
output_schema: schema,
|
|
input_mapping: { rules: [] },
|
|
output_mapping: { rules: [] },
|
|
execution_config: { timeout_ms: 1000, headers: {} },
|
|
tool_description: {
|
|
title: `Catalog lifecycle ${language}`,
|
|
description: 'A sufficiently detailed description for lifecycle UI acceptance.',
|
|
tags: ['quality'],
|
|
examples: [],
|
|
},
|
|
wizard_state: null,
|
|
});
|
|
await window.CrankApi.getOperation(workspaceId, created.operation_id);
|
|
await window.CrankApi.publishOperation(workspaceId, created.operation_id, 1);
|
|
return created;
|
|
}, { workspaceId: workspace.id, operationName: name, language: locale });
|
|
|
|
await page.goto('/');
|
|
await page.getByPlaceholder(locale === 'ru' ? 'Поиск операций' : 'Search operations').fill(name);
|
|
const row = page.locator('tbody tr').filter({ hasText: name });
|
|
await expect(row).toHaveCount(1);
|
|
await expect(row.locator('.row-btn-delete')).toBeHidden();
|
|
await expect(row.locator('.row-btn-edit')).toBeVisible();
|
|
await row.locator('.row-btn-edit').click();
|
|
await expect(page).toHaveURL(new RegExp(`/wizard/\\?mode=edit&operationId=${operation.operation_id}`));
|
|
await expect(page.locator('.wizard-shell')).toBeVisible();
|
|
await expect(page.locator('#back-to-catalog')).toBeVisible();
|
|
|
|
await page.locator('#back-to-catalog').click();
|
|
await expect(page).toHaveURL(/\/$/);
|
|
await page.getByPlaceholder(locale === 'ru' ? 'Поиск операций' : 'Search operations').fill(name);
|
|
const refreshedRow = page.locator('tbody tr').filter({ hasText: name });
|
|
page.once('dialog', (dialog) => dialog.accept());
|
|
await refreshedRow.locator('[data-testid="operation-archive"]').click();
|
|
await expect(refreshedRow).toContainText(locale === 'ru' ? 'Неактивные' : 'Inactive');
|
|
await expect(refreshedRow.locator('.row-btn-delete')).toBeHidden();
|
|
});
|
|
}
|