Fix compressed draft saves provenance and Beira documentary continuity
tests / ci (push) Canceled after 0s
tests / ci (push) Canceled after 0s
This commit is contained in:
@@ -89,7 +89,7 @@ class InvestmentDiscoveryController extends Controller
|
||||
$data = $request->validate(['zone_id' => ['nullable', 'uuid'], 'from_dossier' => ['nullable', 'uuid']]);
|
||||
$dossierOrigin = isset($data['from_dossier']) ? InvestmentPortfolioController::visible()->whereKey($data['from_dossier'])->firstOrFail() : null;
|
||||
if ($dossierOrigin) {
|
||||
$linked = false;
|
||||
$linked = in_array($record->id, app(\App\Services\CorridorAgribusinessOpportunities::class)->portfolioOpportunityIds($dossierOrigin), true);
|
||||
foreach (['projects' => 'project_id', 'clusters' => 'cluster_id', 'corridors' => 'corridor_id'] as $relation => $foreign) {
|
||||
if ($dossierOrigin->$foreign && DB::table('opportunity_'.$relation)->where('opportunity_id', $record->id)->where($foreign, $dossierOrigin->$foreign)->exists()) {
|
||||
$linked = true;
|
||||
|
||||
@@ -105,6 +105,7 @@ class InvestmentPortfolioController extends Controller
|
||||
|
||||
$opportunities = Opportunity::published()->where(function ($query) use ($record) {
|
||||
$query->whereRaw('false');
|
||||
$query->orWhereIn('id', app(\App\Services\CorridorAgribusinessOpportunities::class)->portfolioOpportunityIds($record));
|
||||
foreach (['clusters' => 'cluster_id', 'corridors' => 'corridor_id', 'projects' => 'project_id'] as $relation => $foreign) {
|
||||
if ($record->$foreign) {
|
||||
$query->orWhereIn('id', DB::table('opportunity_'.$relation)->where($foreign, $record->$foreign)->select('opportunity_id'));
|
||||
|
||||
@@ -13,6 +13,17 @@ use Ramsey\Uuid\Uuid;
|
||||
final class CorridorAgribusinessOpportunities
|
||||
{
|
||||
public const SOURCE_HASH = '1473b06c48aa234e771660e3f2a51b9d2e4e1943bd52d878e0ae854ee6657055';
|
||||
|
||||
/** Documentary cross-reference only; keep the two source designations and master records intact. */
|
||||
public function portfolioOpportunityIds(\App\Models\InvestmentDossier $dossier): array
|
||||
{
|
||||
if (!$dossier->corridor_id || data_get($dossier->content, 'code') !== 'COR-02') return [];
|
||||
// COR-02 is Beira e Centro in investment-portfolio.json. The seven entries are explicitly
|
||||
// grouped under Corredor da Beira in the accepted source, not selected by province/name search.
|
||||
$keys = collect($this->entries())->where('corridor', 'Corredor da Beira')->pluck('key');
|
||||
return Opportunity::published()->whereIn('management_creation_key', $keys)
|
||||
->where('workflow_context->corridor_agribusiness->source_sha256', self::SOURCE_HASH)->pluck('id')->all();
|
||||
}
|
||||
public const LIMITATIONS = 'Promotor, implantação exacta, área, montante, financiamento, viabilidade e licenças não indicados nesta fonte. O corredor é um enquadramento documental; não identifica uma parcela nem confirma disponibilidade.';
|
||||
|
||||
public function entries(): array
|
||||
|
||||
@@ -292,6 +292,10 @@ class InvestmentCatalogue
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!empty($selected['corridor_id'])) {
|
||||
$dossier = InvestmentPortfolioController::visible()->where('corridor_id', $selected['corridor_id'])->first();
|
||||
if ($dossier) $ids['corridor_id'] = array_values(array_unique([...($ids['corridor_id'] ?? []), ...app(CorridorAgribusinessOpportunities::class)->portfolioOpportunityIds($dossier)]));
|
||||
}
|
||||
return $ids;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ final class InvestmentOpportunityChoices
|
||||
foreach (array_unique($ids) as $id) {
|
||||
$record = InvestmentPortfolioController::visible()->addSelect('version_no')->findOrFail($id);
|
||||
$kind = null;
|
||||
if (in_array($opportunity, app(CorridorAgribusinessOpportunities::class)->portfolioOpportunityIds($record), true)) $kind = 'corridor';
|
||||
foreach (['projects' => 'project_id', 'clusters' => 'cluster_id', 'corridors' => 'corridor_id'] as $relation => $foreign) {
|
||||
if ($record->$foreign && DB::table('opportunity_'.$relation)->where('opportunity_id', $opportunity)->where($foreign, $record->$foreign)->exists()) $kind = rtrim($relation, 's');
|
||||
}
|
||||
|
||||
@@ -632,6 +632,7 @@ final class InvestorContext
|
||||
'requirements_assessment' => $ctx['requirements_assessment'] ?? null,
|
||||
'path' => $ctx['path_applicability'] ?? null,
|
||||
'land_preparation' => $ctx['land_preparation'] ?? null,
|
||||
'field_provenance' => array_map(fn($origin) => Arr::only($origin, ['kind', 'title', 'version', 'context_revision']), $ctx['provenance'] ?? []),
|
||||
'preparation' => isset($ctx['preparation_assessment']) ? Arr::except($ctx['preparation_assessment'], ['questions']) : null,
|
||||
'fields' => Arr::only($f, ['investor_origin', 'tax_residency_country', 'entity_stage', 'currency', 'expected_start_date', 'investment_regime']),
|
||||
'notice' => 'Referências e declarações para preparação; sem reconhecimento automático de elegibilidade ou benefício.',
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# Rastreabilidade, utilização e formação
|
||||
|
||||
## Correcções da verificação de homologação — candidata QA, 23/10/2026
|
||||
|
||||
Referência: `c3f7c3ad`; pedido expresso da Tempo nas três prioridades. As 34 oportunidades já públicas e as provas aceites mantêm-se. Esta candidata não altera a homologação, que permanece em `7c1f5321`; sem migrations, novos papéis, publicação, importação ou alteração de dados activos.
|
||||
|
||||
1. **Gravação/idioma — causa de transporte reproduzida.** O Nginx do destino comprime JSON e enfraquece o ETag da representação; confirmado em leitura pública do questionário (o controlador produz uma versão forte e a resposta chega `W/"questionnaire-…"`, com gzip). O cliente do rascunho devolvia literalmente o cabeçalho ao servidor, que compara a versão forte exacta; os logs do dossier de teste mostram PUT 200 seguido de 412 e repetição após recuperação. Em QA isolado, um router de prova aplica gzip e enfraquece os ETags dos rascunhos para reproduzir esse transporte. A correcção normaliza apenas o formato conhecido `W/"sfd-UUID-vN"` antes de o usar; não aceita outra versão, não muda o controlo do servidor nem força retry/overwrite. No navegador QA, mudar o idioma para English, guardar e recarregar recuperou English com confirmação de gravação, título deliberado e escolhas preservados, sem falso conflito. A leitura pública comprova o transporte no destino; a correcção do rascunho ainda aguarda instalação/conferência desse delta na homologação. Sem consulta de credenciais ou bypass de sessão/RLS.
|
||||
|
||||
2. **Proveniência do relatório.** A origem existente por campo estava no dossier, mas não viajava no snapshot usado pelo relatório; as linhas tinham atribuições genéricas fixas ao utilizador. O snapshot passa a incluir apenas tipo/título/versão/revisão da proveniência, sem IDs de actores. O modelo comum de relatório/CSV usa também as assinaturas de herança já existentes: mostra origem na oportunidade/catálogo/dados do utilizador no dossier; uma alteração posterior no simulador é identificada com a origem anterior. Não muda valores nem reescreve relatórios emitidos; metadados ausentes de versões antigas não são inventados. Conferido no relatório QA: resumo herdado da oportunidade, título alterado posteriormente e referência da oportunidade/localização/associação conservadas. Captura `outputs/preservation-inventory-20261023/corrected-provenance-report-QA.png`.
|
||||
|
||||
3. **Beira → oportunidades.** A consulta da carteira só procurava pivots; o cadastro das 34 conserva a pertença documental no `workflow_context`, deliberadamente sem inferir relações com mestres. As fontes aceites distinguem COR-02 «Corredor da Beira e Centro» (`investment-portfolio.json`) e o grupo «Corredor da Beira» (`corridor-investment-opportunities.json`): sete entradas, não selecção por província. Esta correcção implementa uma referência documental delimitada entre essa ficha e esse grupo, sem renomear/fundir corredores, acrescentar pivots ou inferir implantação. A consulta exige a chave original e o hash da fonte aceite; outras oportunidades/corredores não entram por semelhança. A mesma resolução serve a ficha, a pesquisa por corredor, a validação da origem no detalhe e a associação escolhida no dossier. As sete fichas são Milho, trigo e arroz; Soja; Hortícolas; Sésamo; Açúcar; Florestas; Carne bovina — todas no Corredor da Beira. Não se estende esta correspondência aos outros corredores, clusters ou âncoras por inferência.
|
||||
|
||||
Validação delimitada: 26 testes frontend afectados passaram; novo teste de proveniência/ETag repetido após o ajuste de designações, dois aprovados. Três testes transaccionais de continuidade/Beira, 91 asserções, na base QA `simulator_fiscal_delta_20261021_test:55441`, incluindo sete resultados, pesquisa, detalhe/origem e recusa de outra oportunidade. Um teste adicional, cinco asserções, confirma que o servidor continua a recusar ETag fraco e versão antiga e aceita apenas a versão forte actual. Types/build aprovados. A conferência visual final da navegação da carteira QA foi interrompida pela automação do navegador; não se declara uma prova visual nova dessa ficha, coberta aqui pela resposta Inertia/API e validação de continuidade. Mantêm-se as provas anteriores. Matriz existente: evidências acrescidas em AUD-SIM-INT03/06/07, sem novo backlog. Aplicação transversal permanece parcial.
|
||||
|
||||
## Verificação do produto na homologação — 23/10/2026
|
||||
|
||||
Origem: pedido da Tempo de avaliar carteira → escolha → dossier → simulador → relatório, registando apenas diferenças e reutilizando as provas aceites. Revisão servida confirmada: `7c1f5321`. Sem alteração de código, permissões, originais ou publicações nesta passagem. Referências da matriz existente: AUD-SIM-INT03 (recuperação), AUD-SIM-INT06 (relatórios) e AUD-SIM-INT07 (gravação/erros).
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,3 +1,4 @@
|
||||
import { simulationDraftEtag } from '@/lib/simulation-draft-etag';
|
||||
import { SimulationPublishedIva, type PublishedIvaInput } from './simulation-published-iva';
|
||||
import { SimulationMonthlyIva } from './simulation-monthly-iva';
|
||||
import { inheritSimulatorFields, reusedInvestmentValue } from '@/lib/context-inheritance';
|
||||
@@ -1248,7 +1249,7 @@ export function SimulationForm() {
|
||||
if (payload.data) {
|
||||
if (cancelled) return;
|
||||
draftIdRef.current = payload.data.id;
|
||||
draftEtagRef.current = response.headers.get('ETag');
|
||||
draftEtagRef.current = simulationDraftEtag(response.headers.get('ETag'));
|
||||
setDraftVersionNo(payload.data.version_no);
|
||||
setDraftExpiresAt(payload.data.expires_at ?? null);
|
||||
if (canSavePersonalData)
|
||||
@@ -1370,7 +1371,7 @@ export function SimulationForm() {
|
||||
if (currentDraft) {
|
||||
draftIdRef.current = currentDraft.id;
|
||||
draftEtagRef.current =
|
||||
currentResponse.headers.get('ETag');
|
||||
simulationDraftEtag(currentResponse.headers.get('ETag'));
|
||||
sessionStorage.setItem(draftIdKey, currentDraft.id);
|
||||
}
|
||||
}
|
||||
@@ -1432,7 +1433,7 @@ export function SimulationForm() {
|
||||
);
|
||||
draftIdRef.current = payload.data.id;
|
||||
setServerFieldErrors({});
|
||||
draftEtagRef.current = response.headers.get('ETag');
|
||||
draftEtagRef.current = simulationDraftEtag(response.headers.get('ETag'));
|
||||
setDraftVersionNo(payload.data.version_no);
|
||||
lastSavedFingerprintRef.current = fingerprint;
|
||||
sessionStorage.setItem(draftIdKey, payload.data.id);
|
||||
@@ -1593,7 +1594,7 @@ export function SimulationForm() {
|
||||
)
|
||||
throw new Error('Não foi possível abrir este dossier privado.');
|
||||
draftIdRef.current = payload.data.id;
|
||||
draftEtagRef.current = response.headers.get('ETag');
|
||||
draftEtagRef.current = simulationDraftEtag(response.headers.get('ETag'));
|
||||
setDraftVersionNo(payload.data.version_no);
|
||||
setDraftExpiresAt(null);
|
||||
setSelectedAccountDraftId(payload.data.id);
|
||||
@@ -1656,7 +1657,7 @@ export function SimulationForm() {
|
||||
);
|
||||
const record = (payload as { data: ApiSimulationDraftV1 }).data;
|
||||
draftIdRef.current = record.id;
|
||||
draftEtagRef.current = response.headers.get('ETag');
|
||||
draftEtagRef.current = simulationDraftEtag(response.headers.get('ETag'));
|
||||
setDraftVersionNo(record.version_no);
|
||||
setDraftExpiresAt(null);
|
||||
setSelectedAccountDraftId(record.id);
|
||||
@@ -1756,7 +1757,7 @@ export function SimulationForm() {
|
||||
(payload as { expires_at: string }).expires_at,
|
||||
);
|
||||
draftEtagRef.current =
|
||||
response.headers.get('ETag') ?? draftEtagRef.current;
|
||||
simulationDraftEtag(response.headers.get('ETag')) ?? draftEtagRef.current;
|
||||
}
|
||||
setContinueLaterSaved(true);
|
||||
setSaveStatus(
|
||||
@@ -2375,7 +2376,7 @@ export function SimulationForm() {
|
||||
);
|
||||
const draftId = draftIdValue;
|
||||
draftIdRef.current = draftId;
|
||||
draftEtagRef.current = currentResponse.headers.get('ETag');
|
||||
draftEtagRef.current = simulationDraftEtag(currentResponse.headers.get('ETag'));
|
||||
if (!draftEtagRef.current)
|
||||
throw new Error(
|
||||
'O servidor não devolveu a versão actual do rascunho. Recarregue e tente novamente.',
|
||||
@@ -2409,7 +2410,7 @@ export function SimulationForm() {
|
||||
throw new Error(
|
||||
'O servidor devolveu uma confirmação de anexação inválida.',
|
||||
);
|
||||
draftEtagRef.current = response.headers.get('ETag');
|
||||
draftEtagRef.current = simulationDraftEtag(response.headers.get('ETag'));
|
||||
const sources = payload.data?.simulation_sources;
|
||||
if (Array.isArray(sources)) setSimulationSources(sources);
|
||||
setDraft((currentDraft) => ({
|
||||
|
||||
@@ -72,6 +72,26 @@ export function reusedInvestmentValue(scope: string, key: string, values: Record
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
/** Report the lineage without changing a stored answer or disclosing the tracking hash. */
|
||||
export function investmentFieldOrigin(values: Record<string, unknown>, key: string, fallback: string): string {
|
||||
try {
|
||||
const reuse = JSON.parse(String(values.context_reuse ?? ''));
|
||||
if (reuse.version !== 1 || !/^dossier:.+:simulator$/.test(reuse.scope) || !reuse.inherited?.[key]) return fallback;
|
||||
const snapshot = JSON.parse(String(values.context_applicability_snapshot ?? ''));
|
||||
const origin = snapshot.field_provenance?.[key];
|
||||
const source = origin?.kind === 'opportunity' ? `oportunidade «${origin.title ?? 'referência guardada'}»`
|
||||
: origin?.kind === 'process_declaration' || origin?.kind === 'dossier' ? 'dados introduzidos pelo utilizador no dossier'
|
||||
: origin?.kind === 'sector' ? `catálogo sectorial «${origin.title ?? 'referência guardada'}»`
|
||||
: origin?.kind === 'territory' ? `catálogo territorial «${origin.title ?? 'referência guardada'}»`
|
||||
: origin?.kind === 'linked_simulation' ? 'simulador ligado ao mesmo dossier'
|
||||
: 'dossier de investimento';
|
||||
const version = origin?.version ? `, versão ${origin.version}` : '';
|
||||
return reuse.inherited[key] === signature(values[key])
|
||||
? `Herdado de ${source}${version}`
|
||||
: `Alterado posteriormente pelo utilizador; origem anterior: ${source}${version}`;
|
||||
} catch { return fallback; }
|
||||
}
|
||||
|
||||
export function inheritSimulatorFields<T extends Record<string,unknown>>(draft:T): T {
|
||||
const source:Record<string,unknown>={};
|
||||
const plan=draft.business_plan_snapshot ? restoreBusinessPlanSnapshot(String(draft.business_plan_snapshot)) : null;
|
||||
|
||||
@@ -229,8 +229,8 @@ function makeSections(
|
||||
{
|
||||
field: 'Projecto',
|
||||
value: absent(text(draft, 'project_title')),
|
||||
origin: 'Informação introduzida no dossier',
|
||||
classification: 'Declarada pelo utilizador',
|
||||
origin: rows.find(row => row[1] === 'Título')?.[3] ?? 'Informação introduzida no dossier',
|
||||
classification: 'Origem indicada no dossier',
|
||||
},
|
||||
{
|
||||
field: 'Data de geração',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { displayValue } from './display-value.ts';
|
||||
import { investmentFieldOrigin } from './context-inheritance.ts';
|
||||
import { investmentLandPreparation } from './investment-land-preparation.ts';
|
||||
import { monthlyIvaInput, monthlyIvaKey, readMonthlyIvaRecord, readMonthlyIvaSettings } from './monthly-iva.ts';
|
||||
import { modeloAPurchaseReferences } from './iva-modelo-a.ts';
|
||||
@@ -705,7 +706,11 @@ export function buildProjectDossierRows(
|
||||
if (land.need === 'yes') rows.push(['Localização', 'Situação do terreno', String(draft.land_status_label ?? ({ owned: 'Próprio', duat: 'DUAT', leased: 'Arrendado', requested: 'Pedido em curso', unknown: 'Por determinar' } as Record<string, string>)[String(draft.land_status)] ?? 'Informação em falta'), 'Informação declarada; não comprova título ou disponibilidade']);
|
||||
else rows.push(['Localização', 'Preparação do terreno', land.need === 'no' ? 'Situação do terreno não aplicável à preparação actual; valores anteriores conservados.' : 'Definir se precisa de terra; pode continuar a preparar o investimento.', 'Sem inferência de dispensa legal']);
|
||||
}
|
||||
return rows;
|
||||
const lineageFields: Record<string, string> = {'Título':'project_title','Resumo':'project_summary','Sector':'sector_id','Província':'province_id','Distrito':'district_id','Posto administrativo':'admin_post_id','Localidade':'locality_id'};
|
||||
return rows.map(row => {
|
||||
const key = lineageFields[row[1]];
|
||||
return key && row[2] ? [row[0], row[1], row[2], investmentFieldOrigin(draft, key, row[3])] : row;
|
||||
});
|
||||
}
|
||||
|
||||
export function exportProjectDossierCsv(
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/** Compression can mark the representation weak; the draft version token itself is unchanged. */
|
||||
export function simulationDraftEtag(header: string | null): string | null {
|
||||
if (header && /^W\/"sfd-[0-9a-f-]{36}-v[1-9][0-9]*"$/i.test(header)) return header.slice(2);
|
||||
return header;
|
||||
}
|
||||
@@ -17,6 +17,41 @@ use Tests\TestCase;
|
||||
final class InvestmentPortfolioContinuityTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function test_draft_version_guard_still_rejects_a_real_stale_version(): void
|
||||
{
|
||||
$draft = new \App\Models\SimulationFormDraft;
|
||||
$draft->forceFill(['id'=>(string)Str::uuid(),'version_no'=>2]);
|
||||
$service=app(\App\Services\SimulationFormDrafts::class);
|
||||
$guard=new \ReflectionMethod($service,'requireMatch');
|
||||
$tag=$service->etag($draft);
|
||||
foreach (['W/'.$tag, str_replace('-v2','-v1',$tag)] as $invalid) {
|
||||
$request=\Illuminate\Http\Request::create('/','PUT'); $request->headers->set('If-Match',$invalid);
|
||||
try { $guard->invoke($service,$request,$draft); $this->fail('A mismatching version must be rejected'); }
|
||||
catch (\Symfony\Component\HttpKernel\Exception\HttpException $error) { $this->assertSame(412,$error->getStatusCode()); }
|
||||
}
|
||||
$request=\Illuminate\Http\Request::create('/','PUT'); $request->headers->set('If-Match',$tag);
|
||||
$this->assertNull($guard->invoke($service,$request,$draft));
|
||||
}
|
||||
|
||||
public function test_beira_documentary_membership_opens_the_same_published_opportunities_without_inferring_by_province(): void
|
||||
{
|
||||
$portfolio = InvestmentPortfolioController::visible()->where('content->code', 'COR-02')->firstOrFail();
|
||||
$service = app(CorridorAgribusinessOpportunities::class);
|
||||
$ids = $service->portfolioOpportunityIds($portfolio);
|
||||
$this->assertCount(7, $ids);
|
||||
$this->get('/carteira/'.$portfolio->id)->assertOk()->assertInertia(fn(Assert $page) => $page->has('opportunities', 7));
|
||||
$catalogueIds=app(\App\Services\InvestmentCatalogue::class)->query(['corridor_id'=>$portfolio->corridor_id])->pluck('opportunities.id')->all();
|
||||
$this->assertSame([], array_values(array_diff($ids,$catalogueIds)));
|
||||
$opportunity=Opportunity::findOrFail($ids[0]);
|
||||
$this->get('/oportunidades/'.$opportunity->slug.'?from_dossier='.$portfolio->id)->assertOk();
|
||||
$origins=app(InvestmentOpportunityChoices::class)->portfolioOrigins([$portfolio->id],$opportunity->id);
|
||||
$this->assertSame($portfolio->id,$origins[0]['id']);
|
||||
$this->assertSame('corridor',$origins[0]['kind']);
|
||||
$other=Opportunity::published()->whereNotIn('id',$ids)->firstOrFail();
|
||||
$this->get('/oportunidades/'.$other->slug.'?from_dossier='.$portfolio->id)->assertNotFound();
|
||||
$this->assertSame([], $service->portfolioOpportunityIds(InvestmentPortfolioController::visible()->whereNotNull('project_id')->firstOrFail()));
|
||||
}
|
||||
private User $actor;
|
||||
protected function setUp(): void
|
||||
{
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { inheritCompatibleFields } from '../../resources/js/lib/context-inheritance.ts';
|
||||
import { buildProjectDossierRows, exportProjectDossierCsv } from '../../resources/js/lib/project-dossier.ts';
|
||||
import { createProjectDossierReport, createProjectDossierPrintHtml } from '../../resources/js/lib/project-dossier-report.ts';
|
||||
|
||||
test('reports distinguish opportunity inheritance, dossier declarations and later edits without rewriting the snapshot', async()=>{
|
||||
const source={project_title:'Título do promotor',project_summary:'Resumo da oportunidade',sector_id:'1',sector_id_label:'Agronegócio',context_applicability_snapshot:JSON.stringify({version:1,field_provenance:{project_summary:{kind:'opportunity',title:'Florestas — Corredor da Beira',version:2},sector_id:{kind:'opportunity',title:'Florestas — Corredor da Beira',version:2},project_title:{kind:'process_declaration',context_revision:1}}})};
|
||||
const inherited=inheritCompatibleFields('dossier:A:simulator',{},source,null);
|
||||
const draft={...inherited.patch,context_reuse:inherited.metadata};
|
||||
let rows=buildProjectDossierRows(draft);
|
||||
assert.match(rows.find(r=>r[1]==='Resumo')[3],/Herdado de oportunidade «Florestas/);
|
||||
assert.match(rows.find(r=>r[1]==='Título')[3],/dados introduzidos pelo utilizador no dossier/);
|
||||
const edited={...draft,project_summary:'Resumo alterado no simulador'}; const before=JSON.stringify(edited);
|
||||
rows=buildProjectDossierRows(edited);
|
||||
assert.match(rows.find(r=>r[1]==='Resumo')[3],/Alterado posteriormente pelo utilizador; origem anterior: oportunidade/);
|
||||
assert.match(exportProjectDossierCsv(edited),/origem anterior: oportunidade/);
|
||||
const report=createProjectDossierReport(edited,{selectedSections:new Set(['summary','project'])});
|
||||
assert.match(await createProjectDossierPrintHtml(report,[],false),/Resumo alterado no simulador/);
|
||||
assert.equal(JSON.stringify(edited),before);
|
||||
assert.equal(buildProjectDossierRows({project_summary:'Legado'}).find(r=>r[1]==='Resumo')[3],'Declarado pelo utilizador');
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { simulationDraftEtag } from '../../resources/js/lib/simulation-draft-etag.ts';
|
||||
|
||||
test('compressed draft reads use the exact version token while genuinely stale versions stay different', () => {
|
||||
const tag='"sfd-6c07a968-7ff9-42fa-b252-072bb1acf221-v2"';
|
||||
assert.equal(simulationDraftEtag(`W/${tag}`),tag);
|
||||
assert.equal(simulationDraftEtag(tag),tag);
|
||||
assert.notEqual(simulationDraftEtag(`W/${tag}`),tag.replace('-v2','-v3'));
|
||||
assert.equal(simulationDraftEtag('W/"another-resource"'),'W/"another-resource"');
|
||||
assert.equal(simulationDraftEtag(null),null);
|
||||
});
|
||||
Reference in New Issue
Block a user