feat: implement SMB folder creation and publication linking flow
This commit is contained in:
parent
0a5fb28263
commit
a72d6a2ec8
@ -506,6 +506,20 @@ se añade una primitiva genérica de wizard sin otros consumidores. Véase
|
||||
[el comportamiento del alta](recursos-campos-y-plantillas.md#alta-de-recursos-en-tres-pasos).
|
||||
|
||||
|
||||
### Vinculación del publicador tras el alta SMB
|
||||
|
||||
`ResourceForm` entrega al callback `onSaved` el UUID y el recurso confirmado por el
|
||||
servidor. `ResourcesPage.onCreated` permite completar una operación posterior al alta
|
||||
antes de abrir la ficha; el consumidor llama a `onComplete` al finalizarla o cancelarla.
|
||||
`CompanyResources` utiliza este contrato para abrir el `PublicationEditor` de Assets
|
||||
cuando la carpeta recién creada está activa y tiene capacidad `network:smb-share`.
|
||||
|
||||
El editor conserva la selección explícita del activo, búsqueda, paginación, errores e
|
||||
invalidación de la relación existente. Informa de que la carpeta ya está guardada y ofrece
|
||||
«Vincular más tarde». Reintentar la publicación no repite el alta del recurso. Las
|
||||
ediciones, los recursos ordinarios y las altas inactivas siguen abriendo directamente
|
||||
su ficha. Resources no importa Assets; Companies compone ambos módulos.
|
||||
|
||||
### Presentación compacta del alta por pasos
|
||||
|
||||
El alta de recursos y el alta de activos desde el inventario de compañía usan
|
||||
|
||||
@ -129,3 +129,65 @@ Incidencias previas indicadas por el encargo: dos expectativas de texto de Compa
|
||||
| `git diff --check` | Correcto. |
|
||||
|
||||
Los comandos PNPM se ejecutaron con `--config.verify-deps-before-run=false` para utilizar las dependencias instaladas. No se instalaron librerías ni se cambiaron lockfiles. La última adaptación de los editores de Access fija su altura y coloca `EntityHelp` mediante `FormHelp` junto al pie; conserva los mismos textos y la variante original de ayuda para los demás consumidores.
|
||||
|
||||
## Continuación del alta hacia el activo publicador
|
||||
|
||||
Al guardar una carpeta SMB nueva y activa desde Recursos de la compañía, se cierra
|
||||
el alta y se abre el editor de publicación existente, con el UUID del recurso ya
|
||||
confirmado. El operador selecciona expresamente un activo de esa compañía; no se
|
||||
infiere a partir del host UNC ni del campo documental Servidor.
|
||||
|
||||
La carpeta y su publicación son dos operaciones explícitas. El editor explica que la
|
||||
carpeta ya está guardada, conserva la selección ante errores y permite reintentar solo
|
||||
la publicación. «Vincular más tarde», Escape o el cierre conservan la carpeta sin
|
||||
publicador; la ficha mantiene la acción de vincularlo después. Las carpetas inactivas,
|
||||
los demás recursos y las ediciones no abren esta continuación.
|
||||
|
||||
La composición pertenece a `CompanyResources`. Resources entrega el resultado del alta
|
||||
mediante `onCreated` y abre la ficha cuando el consumidor completa el flujo; Assets
|
||||
conserva el editor, la selección remota, los comandos y la invalidación de publicaciones.
|
||||
No se añaden tablas, endpoints, migraciones ni conversiones de carpetas históricas.
|
||||
|
||||
Regresión reproducible con datos sintéticos y el MCP Docker fijado:
|
||||
|
||||
```bash
|
||||
python3 scripts/visual-audit.py --network-share-publisher --check
|
||||
```
|
||||
|
||||
El preparador utiliza exclusivamente una instalación desechable de `metis_desk_test`,
|
||||
con 15 compañías y una principal con 50 personas, 51 activos, 50 perfiles, 50 puestos y
|
||||
66 recursos antes del recorrido. La prueba verifica alta, selección, reintento sin
|
||||
duplicados, aplazamiento, edición, recursos inactivos y ordinarios y aislamiento de
|
||||
publicaciones; genera capturas a 1440, 390 y 320 px. Los resultados de cada ejecución
|
||||
se guardan en `publisher-report.json` dentro del directorio local de evidencias.
|
||||
|
||||
### Validación de la continuación del alta (26 de octubre de 2026)
|
||||
|
||||
- `python3 scripts/visual-audit.py --network-share-publisher --check`: 18 comprobaciones
|
||||
correctas; 15 compañías, 50 personas, 51 activos, 50 perfiles, 50 puestos y 70 recursos
|
||||
finales en la principal. Teclado, retorno de foco, búsqueda, paginación, búsqueda vacía,
|
||||
error simulado de publicación, reintento con persistencia real, aplazamiento, edición,
|
||||
altas inactivas/ordinarias y aislamiento. Capturas inspeccionadas a 1440 y 320 px;
|
||||
también se generó y comprobó ausencia de desbordamiento a 390 px. Entorno retirado.
|
||||
- `pnpm --filter @metis/resources --filter @metis/assets --filter @metis/companies
|
||||
--filter @metis/web typecheck`: correcto. `pnpm build`: correcto.
|
||||
- Suites completas con `--test-isolation=none`: Resources 124/124; Assets 150/150;
|
||||
Companies 310/312. Persisten las expectativas de «Personas (1)» y del texto de acceso
|
||||
de personas en `companies-page.test.tsx`, ajenas a este cambio.
|
||||
- `pnpm test`, repetido con acceso a loopback: termina con esos dos fallos de Companies.
|
||||
La primera ejecución de Resources dentro del sandbox falló al abrir un puerto local;
|
||||
su suite completa pasó al repetir con acceso a loopback.
|
||||
- `pnpm typecheck`: falla en `packages/shadcn-ui/src/components/calendar.tsx:165`
|
||||
(TS2375), fuera del cambio y sin modificar vendor. `pnpm lint` y `pnpm check` globales
|
||||
fallan en archivos ajenos, incluidos ejemplos de skills y código de fotos de personas.
|
||||
- Biome y ESLint dirigidos a todos los archivos TS/TSX/JSON del cambio: correctos;
|
||||
sintaxis de los dos scripts Python y `git diff --check`: correctos.
|
||||
`python3 scripts/visual-audit.test.py`: 10/10.
|
||||
|
||||
Skills consultadas: `rdx-solid` para reutilizar ownership y contratos; `ux-inputs-and-forms`
|
||||
para explicar el guardado y el aplazamiento; `shadcn` para componer el editor existente;
|
||||
`vercel-react-best-practices` para coordinar el avance desde el callback sin efectos ni
|
||||
copias de estado remoto. Los auditores especializados referidos por la skill de formularios
|
||||
no están instalados; las comprobaciones citadas son directas mediante Playwright MCP,
|
||||
no una certificación de accesibilidad. No se han ejecutado suites MariaDB adicionales:
|
||||
los comandos existentes se han ejercitado a través de HTTP en el entorno desechable.
|
||||
|
||||
@ -628,6 +628,8 @@
|
||||
"publisher": "Publishing asset"
|
||||
},
|
||||
"shareUi": {
|
||||
"linkLater": "Link later",
|
||||
"resourceCreated": "The share “{{name}}” has been saved. Select its publishing asset. If you leave this for later, you can link it from the share details.",
|
||||
"published": "Published network shares",
|
||||
"publisher": "Published by",
|
||||
"publish": "Assign publication",
|
||||
|
||||
@ -628,6 +628,8 @@
|
||||
"publisher": "Activo publicador"
|
||||
},
|
||||
"shareUi": {
|
||||
"linkLater": "Vincular más tarde",
|
||||
"resourceCreated": "La carpeta «{{name}}» está guardada. Selecciona el activo que la publica. Si lo dejas para más tarde, podrás vincularlo desde la ficha de la carpeta.",
|
||||
"published": "Recursos compartidos publicados",
|
||||
"publisher": "Publicado por",
|
||||
"publish": "Asignar publicación",
|
||||
|
||||
@ -16,6 +16,7 @@ export {
|
||||
} from "./infrastructure/shared/api/infrastructure-api";
|
||||
export { getInfrastructureItems } from "./infrastructure/shared/helpers/infrastructure-items";
|
||||
export { NetworkSharePublications } from "./network-share-publications/detail/ui/network-share-publications";
|
||||
export { PublicationEditor } from "./network-share-publications/manage/ui/publication-editor";
|
||||
export { PublicationPicker } from "./network-share-publications/select/ui/publication-picker";
|
||||
export { assetsEnglish, assetsSpanish } from "./shared";
|
||||
export { inventoryApi } from "./shared/api";
|
||||
|
||||
@ -22,8 +22,10 @@ export function PublicationEditor({
|
||||
companyId,
|
||||
operation,
|
||||
onClose,
|
||||
createdResourceName,
|
||||
}: {
|
||||
companyId: string;
|
||||
createdResourceName?: string;
|
||||
operation: PublicationOperation;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
@ -95,7 +97,7 @@ export function PublicationEditor({
|
||||
return (
|
||||
<>
|
||||
<FormDialog
|
||||
cancelLabel={t("cancel")}
|
||||
cancelLabel={t(createdResourceName ? "shareUi.linkLater" : "cancel")}
|
||||
formActions
|
||||
layout="stable"
|
||||
onOpenChange={(open) => {
|
||||
@ -107,6 +109,11 @@ export function PublicationEditor({
|
||||
title={t(`shareUi.${operation.action}`)}
|
||||
>
|
||||
<FormBody>
|
||||
{createdResourceName && (
|
||||
<p className="text-sm" role="status">
|
||||
{t("shareUi.resourceCreated", { name: createdResourceName })}
|
||||
</p>
|
||||
)}
|
||||
{operation.action !== "end" && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<FormLabel htmlFor={id} required>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { AssetCatalogPage, InfrastructurePage } from "@metis/assets/client";
|
||||
import { useBrowserSearchParams, useCreateNavigation } from "@metis/core/client";
|
||||
import { ResourceCatalogPage, ResourcesPage } from "@metis/resources/client";
|
||||
import { ResourceCatalogPage } from "@metis/resources/client";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import type { CompanyDTO } from "../../../../common";
|
||||
@ -14,14 +14,13 @@ import { PeopleSection } from "../../../people";
|
||||
import { PrinterConfigurationsPage } from "../../../printer-configurations";
|
||||
import { ProcessesPage } from "../../../processes";
|
||||
import { RemoteConnectionsPage } from "../../../remote-connections";
|
||||
import { CompanyResourceRelatedContent } from "../../../resources/detail/ui/company-resource-related-content";
|
||||
import { CompanyResources } from "../../../resources/list/ui/company-resources";
|
||||
import { SiteSection } from "../../../sites";
|
||||
import { TeleworkAuthorizationsPage } from "../../../telework-authorizations";
|
||||
import { WorkProfilesPage } from "../../../work-profiles";
|
||||
import { CatalogOverview } from "../../../workspace/catalogs/ui/catalog-overview";
|
||||
import type { CompanyTab } from "../../shared/types";
|
||||
|
||||
import { CompanyEntityContext } from "./company-entity-context";
|
||||
import { CompanyInventory } from "./company-inventory";
|
||||
import { CompanyOverview } from "./company-overview";
|
||||
|
||||
@ -156,20 +155,12 @@ export function CompanySection({
|
||||
);
|
||||
case "resources":
|
||||
return (
|
||||
<ResourcesPage
|
||||
companyId={company.id}
|
||||
<CompanyResources
|
||||
company={company}
|
||||
initialCreate={create}
|
||||
initialId={initialEntityId}
|
||||
relatedContent={(resource) => (
|
||||
<CompanyEntityContext
|
||||
companyId={company.id}
|
||||
initialFocus={false}
|
||||
key={resource.id}
|
||||
layoutReady={layoutReady}
|
||||
>
|
||||
<CompanyResourceRelatedContent company={company} resource={resource} />
|
||||
</CompanyEntityContext>
|
||||
)}
|
||||
key={company.id}
|
||||
layoutReady={layoutReady}
|
||||
/>
|
||||
);
|
||||
case "resource-catalog":
|
||||
|
||||
@ -0,0 +1,70 @@
|
||||
import { PublicationEditor } from "@metis/assets/client";
|
||||
import { ResourcesPage } from "@metis/resources/client";
|
||||
import { useState } from "react";
|
||||
|
||||
import type { CompanyDTO } from "../../../../common";
|
||||
import { CompanyEntityContext } from "../../../companies/detail/ui/company-entity-context";
|
||||
import { CompanyResourceRelatedContent } from "../../detail/ui/company-resource-related-content";
|
||||
|
||||
export function CompanyResources({
|
||||
company,
|
||||
initialCreate,
|
||||
initialId,
|
||||
layoutReady,
|
||||
}: {
|
||||
company: CompanyDTO;
|
||||
initialCreate?: boolean | undefined;
|
||||
initialId?: string | undefined;
|
||||
layoutReady: boolean;
|
||||
}) {
|
||||
const [publication, setPublication] = useState<{
|
||||
resourceId: string;
|
||||
resourceName: string;
|
||||
onComplete: () => void;
|
||||
}>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResourcesPage
|
||||
companyId={company.id}
|
||||
initialCreate={initialCreate}
|
||||
initialId={initialId}
|
||||
onCreated={(resource, onComplete) => {
|
||||
if (resource.networkShare?.capability === "network:smb-share" && resource.values.active) {
|
||||
setPublication({
|
||||
resourceId: resource.id,
|
||||
resourceName: resource.values.name,
|
||||
onComplete,
|
||||
});
|
||||
} else {
|
||||
onComplete();
|
||||
}
|
||||
}}
|
||||
relatedContent={(resource) => (
|
||||
<CompanyEntityContext
|
||||
companyId={company.id}
|
||||
initialFocus={false}
|
||||
key={resource.id}
|
||||
layoutReady={layoutReady}
|
||||
>
|
||||
<CompanyResourceRelatedContent company={company} resource={resource} />
|
||||
</CompanyEntityContext>
|
||||
)}
|
||||
/>
|
||||
{publication && (
|
||||
<PublicationEditor
|
||||
companyId={company.id}
|
||||
createdResourceName={publication.resourceName}
|
||||
onClose={() => {
|
||||
setPublication(undefined);
|
||||
publication.onComplete();
|
||||
}}
|
||||
operation={{
|
||||
action: "publish",
|
||||
anchor: { type: "resources:resource", id: publication.resourceId },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -4,6 +4,8 @@ import type { ResourceDTO } from "../../../../common";
|
||||
|
||||
export type ResourcesPageProps = {
|
||||
companyId: string;
|
||||
/** Completa el flujo posterior al alta antes de abrir la ficha del recurso guardado. */
|
||||
onCreated?: (resource: ResourceDTO, onComplete: () => void) => void;
|
||||
relatedContent?: (resource: ResourceDTO) => ReactNode;
|
||||
initialId?: string | undefined;
|
||||
initialCreate?: boolean | undefined;
|
||||
|
||||
@ -27,7 +27,7 @@ import { ResourceInventoryTable } from "./resource-inventory-table";
|
||||
|
||||
export function ResourcesPage(props: ResourcesPageProps) {
|
||||
const controller = useResourcesPageController(props);
|
||||
const { companyId, relatedContent } = props;
|
||||
const { companyId, relatedContent, onCreated } = props;
|
||||
const {
|
||||
editor,
|
||||
setEditor,
|
||||
@ -213,9 +213,13 @@ export function ResourcesPage(props: ResourcesPageProps) {
|
||||
initialKind={creationKind}
|
||||
items={catalog.data.items}
|
||||
onClose={() => setEditor(undefined)}
|
||||
onSaved={(id) => {
|
||||
onSaved={(id, resource) => {
|
||||
setEditor(undefined);
|
||||
selectResource(id);
|
||||
if (editor === "new" && onCreated) {
|
||||
onCreated(resource, () => selectResource(id));
|
||||
} else {
|
||||
selectResource(id);
|
||||
}
|
||||
}}
|
||||
resource={edited}
|
||||
/>
|
||||
|
||||
@ -89,7 +89,7 @@ export function useResourceFormController({
|
||||
)
|
||||
);
|
||||
if (mounted.current) {
|
||||
onSaved(response.resource.id);
|
||||
onSaved(response.resource.id, response.resource);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -5,7 +5,7 @@ export type ResourceFormProps = {
|
||||
companyId: string;
|
||||
resource?: ResourceDTO | undefined;
|
||||
items: readonly CatalogItemDTO[];
|
||||
onSaved: (id: string) => void;
|
||||
onSaved: (id: string, resource: ResourceDTO) => void;
|
||||
allowQuickCreate?: boolean;
|
||||
creationLayout?: "single-page" | "steps";
|
||||
initialKind?: "mailbox" | "access" | "application" | "certificate" | "support" | "os" | undefined;
|
||||
|
||||
@ -19,7 +19,7 @@ export function ResourceEditorDialog({
|
||||
items: ComponentProps<typeof ResourceForm>["items"];
|
||||
onClose: () => void;
|
||||
finalFocus?: ComponentProps<typeof FormDialog>["finalFocus"];
|
||||
onSaved: (id: string) => void;
|
||||
onSaved: ComponentProps<typeof ResourceForm>["onSaved"];
|
||||
initialKind?: ComponentProps<typeof ResourceForm>["initialKind"];
|
||||
}) {
|
||||
const { t } = useTranslation("resources");
|
||||
|
||||
78
scripts/network-share-publisher-ui-audit.py
Normal file
78
scripts/network-share-publisher-ui-audit.py
Normal file
@ -0,0 +1,78 @@
|
||||
"""Alta SMB y publicación: regresión con HTTP real en el entorno visual desechable."""
|
||||
import json
|
||||
|
||||
|
||||
def verify(context, fixture, admin_password):
|
||||
tool, browser, capture = context['tool'], context['browser'], context['capture']
|
||||
origin, directory = context['origin'], context['directory']
|
||||
company = fixture['company']
|
||||
base = origin + '/companies/' + company + '/resources'
|
||||
screenshots, checks = [], []
|
||||
|
||||
def code(client, source):
|
||||
result = context['result_json'](tool(client, 'browser_run_code_unsafe', {
|
||||
'code': 'async (page) => { page.setDefaultTimeout(15000); try { ' + source + ' return true; } catch(error) { return {failure: String(error.message)}; } }'}))
|
||||
if isinstance(result, dict) and 'failure' in result:
|
||||
raise RuntimeError(result['failure'])
|
||||
return result
|
||||
|
||||
def create(client, name, active=True, smb=True):
|
||||
print('Publicador SMB: ' + name, flush=True)
|
||||
tool(client, 'browser_navigate', {'url': base})
|
||||
code(client, "await page.getByRole('button',{name:'Nuevo recurso',exact:true}).click(); await page.getByRole('combobox',{name:'Plantilla de recurso',exact:true}).click(); await page.getByRole('option',{name:" + json.dumps('Carpeta compartida' if smb else 'Licencia de software') + ",exact:true}).click(); await page.getByRole('button',{name:'Siguiente',exact:true}).click();")
|
||||
if smb:
|
||||
unc = '\\\\alias-documental\\' + name.replace(' ', '-')
|
||||
code(client, "await page.getByRole('textbox',{name:'Ruta UNC',exact:true}).fill(" + json.dumps(unc) + ");")
|
||||
code(client, "await page.getByRole('button',{name:'Siguiente',exact:true}).click(); await page.getByRole('textbox',{name:'Nombre',exact:true}).fill(" + json.dumps(name) + ");")
|
||||
if not active:
|
||||
code(client, "await page.getByRole('combobox',{name:'Estado',exact:true}).click(); await page.getByRole('option',{name:'Inactivo',exact:true}).click();")
|
||||
code(client, "await page.getByRole('button',{name:'Guardar',exact:true}).click(); await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Nuevo recurso',exact:true})}).waitFor({state:'hidden'});")
|
||||
|
||||
with browser(context['config']) as client:
|
||||
try:
|
||||
tool(client, 'browser_resize', {'width': 1440, 'height': 1000})
|
||||
tool(client, 'browser_navigate', {'url': origin + '/login'})
|
||||
code(client, "await page.getByRole('button',{name:'Entrar',exact:true}).waitFor();")
|
||||
tool(client, 'browser_fill_form', {'fields': [
|
||||
{'name': 'Correo electrónico', 'type': 'textbox', 'target': 'input[type=email]', 'value': fixture['email']},
|
||||
{'name': 'Contraseña', 'type': 'textbox', 'target': 'input[type=password]', 'value': fixture['password']}]})
|
||||
tool(client, 'browser_click', {'target': 'button[type=submit]'})
|
||||
code(client, "await page.locator('[data-slot=workspace]').waitFor({timeout:30000});")
|
||||
# Las comprobaciones y escrituras siguientes apuntan solo al contenedor sintético.
|
||||
create(client, 'AUDIT-SMB Álvarez vinculado')
|
||||
code(client, "await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Asignar publicación',exact:true})}).waitFor(); await page.getByRole('status').filter({hasText:'está guardada'}).waitFor(); if(!await page.getByRole('button',{name:'Confirmar',exact:true}).isDisabled())throw Error('Confirmación sin activo'); await page.getByRole('button',{name:'Seleccionar activo publicador',exact:true}).focus(); await page.keyboard.press('Enter'); await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Seleccionar activo publicador',exact:true})}).waitFor();")
|
||||
checks.extend(['ordinary-user', 'create-smb-opens-publication', 'saved-resource-explained', 'no-default-publisher', 'keyboard-open-picker'])
|
||||
code(client, "await page.getByRole('button',{name:'Siguiente',exact:true}).click(); await page.getByRole('button',{name:'Anterior',exact:true}).click(); await page.getByRole('textbox',{name:'Buscar',exact:true}).fill('NO-EXISTE-SMB'); await page.getByText('No hay elementos disponibles para esta búsqueda.',{exact:true}).waitFor(); await page.getByRole('textbox',{name:'Buscar',exact:true}).fill('AUDIT-R23-PORT-50'); await page.getByRole('radio',{name:'Seleccionar: AUDIT-R23-PORT-50',exact:true}).check(); await page.getByRole('button',{name:'Seleccionar',exact:true}).click(); await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Seleccionar activo publicador',exact:true})}).waitFor({state:'hidden'}); if(!await page.getByRole('button',{name:'Seleccionar activo publicador',exact:true}).evaluate(e=>e===document.activeElement))throw Error('Foco no restaurado');")
|
||||
checks.extend(['asset-pagination', 'empty-search-recovery', 'asset-search', 'picker-focus-return'])
|
||||
for width in [1440, 390, 320]:
|
||||
tool(client, 'browser_resize', {'width': width, 'height': 900})
|
||||
code(client, "if(await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Asignar publicación',exact:true})}).evaluate(e=>e.scrollWidth>e.clientWidth+1))throw Error('Desbordamiento horizontal');")
|
||||
screenshots.append(capture(client, directory, 'publisher-create-' + str(width)))
|
||||
tool(client, 'browser_resize', {'width': 1440, 'height': 1000})
|
||||
pattern = '**/api/asset-network-share-publications/' + company
|
||||
code(client, "await page.route(" + json.dumps(pattern) + ",route=>route.request().method()==='POST'?route.fulfill({status:409,contentType:'application/json',json:{code:'ASSET_INACTIVE'}}):route.continue()); await page.getByRole('button',{name:'Confirmar',exact:true}).click(); await page.getByRole('alert').waitFor(); if(await page.getByRole('button',{name:'Seleccionar activo publicador',exact:true}).textContent()!=='AUDIT-R23-PORT-50')throw Error('Selección perdida'); await page.unroute(" + json.dumps(pattern) + "); await page.getByRole('button',{name:'Confirmar',exact:true}).click(); await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Asignar publicación',exact:true})}).waitFor({state:'hidden'});")
|
||||
outcome = code(client, "const resources=await (await page.request.get('" + origin + "/api/resources/" + company + "')).json(); const found=resources.resources.filter(r=>r.values.name==='AUDIT-SMB Álvarez vinculado'); if(found.length!==1)throw Error('Recurso duplicado'); const publications=await (await page.request.get('" + origin + "/api/asset-network-share-publications/" + company + "?resourceId='+found[0].id)).json(); if(publications.publications.length!==1||publications.publications[0].assetId!==" + json.dumps(fixture['softwareAssets'][-1]) + ")throw Error('Publicador incorrecto'); return {id:found[0].id};")
|
||||
checks.extend(['publication-error-retains-selection', 'retry-no-duplicate-resource', 'real-publication-persisted', 'alias-does-not-infer-asset'])
|
||||
tool(client, 'browser_navigate', {'url': base + '/' + outcome['id']})
|
||||
code(client, "await page.getByRole('complementary').getByRole('button',{name:'Editar',exact:true}).click(); await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Editar recurso',exact:true})}).waitFor(); await page.getByRole('button',{name:'Guardar',exact:true}).click(); await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Editar recurso',exact:true})}).waitFor({state:'hidden'}); if(await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Asignar publicación',exact:true})}).count())throw Error('Edición repite alta');")
|
||||
checks.append('editing-does-not-reopen-publication')
|
||||
create(client, 'AUDIT-SMB pendiente')
|
||||
code(client, "await page.getByRole('button',{name:'Vincular más tarde',exact:true}).click(); await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Asignar publicación',exact:true})}).waitFor({state:'hidden'}); const resources=await (await page.request.get('" + origin + "/api/resources/" + company + "')).json(); const r=resources.resources.find(r=>r.values.name==='AUDIT-SMB pendiente'); if(!r)throw Error('Recurso perdido'); const p=await (await page.request.get('" + origin + "/api/asset-network-share-publications/" + company + "?resourceId='+r.id)).json(); if(p.publications.length)throw Error('Publicación no confirmada');")
|
||||
checks.append('link-later-keeps-unpublished-resource')
|
||||
create(client, 'AUDIT-SMB inactivo', active=False)
|
||||
code(client, "if(await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Asignar publicación',exact:true})}).count())throw Error('Alta inactiva ofrece publicación');")
|
||||
create(client, 'AUDIT-LIC licencia', smb=False)
|
||||
code(client, "if(await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Asignar publicación',exact:true})}).count())throw Error('Recurso ordinario ofrece publicación');")
|
||||
checks.extend(['inactive-share-skips-publication', 'ordinary-resource-skips-publication'])
|
||||
code(client, "const response=await page.request.get('" + origin + "/api/asset-network-share-publications/" + fixture['otherCompanies'][0] + "'); if(!response.ok()||(await response.json()).publications.length)throw Error('Aislamiento de compañía');")
|
||||
checks.append('other-company-has-no-publications')
|
||||
except Exception:
|
||||
capture(client, directory, 'publisher-failure')
|
||||
snapshot = tool(client, 'browser_snapshot', {})
|
||||
(directory / 'publisher-failure.json').write_text(json.dumps(snapshot, ensure_ascii=False))
|
||||
raise
|
||||
report = {'checks': checks, 'screenshots': screenshots, 'companies': 15,
|
||||
'principal': {'people': 50, 'assets': 51, 'resources': 70, 'profiles': 50, 'workstations': 50},
|
||||
'database': 'metis_desk_test', 'mcp': 'Docker stdio fijado'}
|
||||
(directory / 'publisher-report.json').write_text(json.dumps(report, ensure_ascii=False, indent=2) + '\n')
|
||||
print('Publicador SMB verificado: ' + str(len(checks)) + ' comprobaciones. Evidencias: ' + str(directory), flush=True)
|
||||
@ -301,6 +301,7 @@ def verify_browser(config, origin, fixture, directory, state, logout):
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--network-share-publisher", action="store_true", help="Alta de carpeta SMB y vinculación del activo publicador con datos sintéticos")
|
||||
parser.add_argument("--dialog-help", action="store_true", help="Ayuda contextual en diálogos reales con datos sintéticos y MCP")
|
||||
parser.add_argument("--check", action="store_true", help="Verifica y retira el entorno; sin esta opción espera hasta Ctrl+C")
|
||||
parser.add_argument("--global-applications", action="store_true", help="Recorrido R2.3 con catálogo habilitado solo tras migración desechable")
|
||||
@ -349,6 +350,11 @@ def main():
|
||||
"DB_NAME": "metis_desk_test", "DB_USER": "visual_audit", "DB_PASSWORD": database_password,
|
||||
"SESSION_SECRET": secrets.token_urlsafe(48), "NODE_ENV": "test", "APP_ORIGIN": origin,
|
||||
"PORT": str(api_port), "API_PROXY_TARGET": api.base}
|
||||
if options.network_share_publisher:
|
||||
runner = runpy.run_path(str(ROOT / "scripts/global-applications-ui-audit.py"))["run"]
|
||||
verifier = runpy.run_path(str(ROOT / "scripts/network-share-publisher-ui-audit.py"))["verify"]
|
||||
runner({**globals(), **locals()}, verifier=verifier)
|
||||
return
|
||||
if options.dialog_help:
|
||||
runner = runpy.run_path(str(ROOT / "scripts/global-applications-ui-audit.py"))["run"]
|
||||
verifier = runpy.run_path(str(ROOT / "scripts/dialog-help-ui-audit.py"))["verify"]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user