mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
merge main into B147; keep optional sinal body for v2 status
Align with B134 engagement write path; preserve B147 sinal empty-motivo (collection + zod) required by the status-strip confirm rite.
This commit is contained in:
commit
a2553e1f03
15 changed files with 293 additions and 20 deletions
64
docs/plans/motivo-opcional-tendencia-e-nivel-impl.md
Normal file
64
docs/plans/motivo-opcional-tendencia-e-nivel-impl.md
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# Impl: Motivo opcional — tendência (wizard) + nível; remover voltar atrás do DB (B134)
|
||||
|
||||
Status: aprovado
|
||||
Atualizado em: 2026-08-02
|
||||
Issue: #288
|
||||
Intenção: docs/plans/motivo-opcional-tendencia-e-nivel.md
|
||||
Appetite restante: herdado (~1 dia eng)
|
||||
|
||||
## Leitura da intenção
|
||||
|
||||
- **Outcome:** staff grava tendência ou nível sem texto obrigatório; Motivo permanece opcional nos dois ritos; `reversalSignals` some da UI, write path e snapshots persistidos.
|
||||
- **O que NÃO negociar:** histerese/override/choque triangulado intactos; `leader` lockdown; sem auto-save no nível; motivo vazio grava `null` (não preserva anterior); dados históricos de `reversalSignals` apagados do DB.
|
||||
- **O que reavaliar:** não há coluna dedicada — `reversalSignals` vive só no JSON `allocation_decision.snapshot`; migration é data-only, sem alterar `AllocationDecision` config.
|
||||
|
||||
## Abordagem recomendada
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
UI[Popover / wizard] --> schema[municipalityEngagementLevelSchema]
|
||||
schema --> action[setMunicipalityEngagementLevelRecord]
|
||||
action --> mun[municipality.levelNote nullable]
|
||||
action --> ad[allocationDecision snapshot sem reversalSignals]
|
||||
mig[migration SQL] --> ad
|
||||
```
|
||||
|
||||
**Opções consideradas:** A remover só da UI/schema | B migration JSON strip + schema/UI/action
|
||||
**Recomendação:** B — aceite exige apagar do banco, não só esconder.
|
||||
**Rejeitadas:** A (órfãos no snapshot); tornar `rationale` opcional na collection (escopo desnecessário — string vazia basta quando motivo ausente).
|
||||
|
||||
### Componentes / mudanças
|
||||
|
||||
- **`municipalityEngagementLevelSchema`** (`src/lib/schemas/municipality.ts`): `note` → `trimmedNullableText`; remover `reversalSignals`.
|
||||
- **`setMunicipalityEngagementLevelRecord`** (`actions/municipality.ts`): snapshot sem `reversalSignals`; `rationale: note ?? ''`.
|
||||
- **`MunicipalityListLevelControl`**: um campo Motivo (opcional); `canSubmit` só exige movimento + override quando violações.
|
||||
- **`WizardTrendNoteStep`**: remover `required`; label com “(opcional)”.
|
||||
- **Migration:** `20260802_*_strip_engagement_reversal_signals` — `UPDATE allocation_decision SET snapshot = snapshot - 'reversalSignals' WHERE …`.
|
||||
- **Access / Consent:** inalterado (`canManageMunicipalityEngagementLevel`).
|
||||
|
||||
### Dados → forma
|
||||
|
||||
- Tendência: schema já usa `trimmedNullableText` — só UI wizard tinha `required` HTML.
|
||||
- Nível: `levelNote` nullable no município; `allocationDecision.rationale` string (vazia se sem motivo).
|
||||
|
||||
## Fases verificáveis
|
||||
|
||||
1. **Schema+server** — schema, action, migration, int test com motivo vazio + snapshot sem reversalSignals.
|
||||
2. **UI** — labels “(opcional)”, remover segundo textarea.
|
||||
3. **Gates** — `pnpm gate:fast`; push via `pnpm push`.
|
||||
|
||||
## Rabbit holes / Não escopo (engenharia)
|
||||
|
||||
- Alterar copy do glossário `campaignIntelligenceConcepts` (prosa conceitual, não o campo).
|
||||
- `rationale` required na collection — string vazia, não relaxar schema Payload.
|
||||
|
||||
## Riscos e mitigação
|
||||
|
||||
- **Rationale vazio rejeitado pelo Payload:** int test com `note` omitido; se falhar, usar `''` explicitamente (textarea required aceita vazio).
|
||||
- **Migration idempotente:** operador JSONB `-` só onde chave existe; log de row count.
|
||||
|
||||
## Aceite de engenharia
|
||||
|
||||
- [x] Aceite de produto da intenção ainda coberto
|
||||
- [x] Invariantes AGENTS/engineering-standards
|
||||
- [x] Testes de domínio previstos (int engagement level + migration unit)
|
||||
61
docs/plans/sidebar-organizacoes-impl.md
Normal file
61
docs/plans/sidebar-organizacoes-impl.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# Impl: Sidebar — entrada para Organizações
|
||||
|
||||
Status: aprovado
|
||||
Atualizado em: 2026-08-02
|
||||
Issue: #313
|
||||
Intenção: docs/plans/sidebar-organizacoes.md
|
||||
Appetite restante: ~0,25 dia eng (um item de nav; sem migration)
|
||||
|
||||
## Leitura da intenção
|
||||
|
||||
- **Outcome:** Staff (coordenador, assessor, candidato) descobre `/campanha/organizacoes` pela sidebar desktop e Sheet mobile; liderança não vê o item; destino ativo em lista, detalhe e `/nova`.
|
||||
- **O que NÃO negociar:** lockdown de `leader`; não redesenhar sidebar; não duplicar lista; href canônico `/campanha/organizacoes`.
|
||||
- **O que reavaliar:** hipótese de importar `ORGANIZATIONS_LIST_PATH` — `nav.ts` usa literais (precedente Conceitos) para não puxar módulos ao client bundle; manter literal.
|
||||
|
||||
## Abordagem recomendada
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
staffNav["staffNav + Organizações"] --> getCampaignNav
|
||||
getCampaignNav --> CampaignSidebar
|
||||
getCampaignNav --> MobileSheet["Sheet mobile"]
|
||||
isCampaignNavActive --> Active["lista / slug / nova"]
|
||||
```
|
||||
|
||||
**Opções consideradas:** A) item após Lideranças | B) após Dobradinhas | C) antes de Apoiadores
|
||||
**Recomendação:** **A** — afinidade domínio orgs ↔ lideranças; alinhado à recomendação da intenção.
|
||||
**Rejeitadas:** B/C — sem ganho de descoberta; A é o modelo mental acordado.
|
||||
|
||||
### Componentes / mudanças
|
||||
|
||||
- **`staffNav`** (`src/components/campaign/shell/nav.ts`): inserir `{ title: 'Organizações', href: '/campanha/organizacoes', icon: LandmarkIcon }` logo após Lideranças; import `LandmarkIcon` de lucide-react (instituições — distinto de `HandshakeIcon` e `Building2` usado em município no dashboard).
|
||||
- **Migration:** sem migration
|
||||
- **Access / Consent:** nenhum — `getCampaignNav` já exclui `leader`; staff vê por default (mesmo que Lideranças/Demandas)
|
||||
- **UI:** Impeccable B — encaixe no shell existente; `CampaignSidebar` e Sheet já consomem `getCampaignNav`; `isCampaignNavActive` já cobre paths aninhados (`/nova`, `[slug]`)
|
||||
|
||||
### Dados → forma
|
||||
|
||||
N/A — só navegação.
|
||||
|
||||
## Fases verificáveis
|
||||
|
||||
1. **Nav** — uma linha em `staffNav` + ícone
|
||||
2. **Testes** — unit: href presente para staff, ausente para leader; `isCampaignNavActive` em lista/detalhe/nova
|
||||
3. **Gates** — `pnpm gate:fast`; entrega `pnpm push`
|
||||
|
||||
## Rabbit holes / Não escopo (engenharia)
|
||||
|
||||
- Reordenar demais itens além do encaixe após Lideranças
|
||||
- Exportar constante de href (só `MUNICIPALITY_NAV_HREF` tem consumidor especial B18)
|
||||
- `staffSecondaryNav` — destino de trabalho, não referência
|
||||
|
||||
## Riscos e mitigação
|
||||
|
||||
- **Leader vê link:** mitigado — `getCampaignNav('leader')` retorna `leaderNav` separado
|
||||
- **Active state em `/nova`:** mitigado — `isCampaignNavActive` usa `pathname.startsWith(\`${href}/\`)`
|
||||
|
||||
## Aceite de engenharia
|
||||
|
||||
- [x] Aceite de produto da intenção ainda coberto
|
||||
- [x] Invariantes AGENTS/engineering-standards
|
||||
- [x] Testes de domínio previstos (unit) para nav + active state
|
||||
|
|
@ -151,10 +151,6 @@ export const setMunicipalityEngagementLevelRecord = async (
|
|||
) => {
|
||||
const { municipality, level, note, triangulatedShock, override } =
|
||||
municipalityEngagementLevelSchema.parse(input)
|
||||
const levelNote = note ?? null
|
||||
// allocationDecision.rationale is required; empty Motivo still leaves an
|
||||
// auditable row without inventing coordinator prose.
|
||||
const rationale = levelNote ?? 'Sem motivo registrado.'
|
||||
|
||||
return withPayloadTransaction(
|
||||
payload,
|
||||
|
|
@ -194,7 +190,7 @@ export const setMunicipalityEngagementLevelRecord = async (
|
|||
const updated = await payload.update({
|
||||
collection: 'municipality',
|
||||
id: municipality,
|
||||
data: { engagementLevel: level, levelNote },
|
||||
data: { engagementLevel: level, levelNote: note },
|
||||
depth: 0,
|
||||
user: currentActor,
|
||||
overrideAccess: false,
|
||||
|
|
@ -207,7 +203,7 @@ export const setMunicipalityEngagementLevelRecord = async (
|
|||
municipality,
|
||||
patternId: ENGAGEMENT_LEVEL_PATTERN_ID,
|
||||
outcome: 'movimento',
|
||||
rationale,
|
||||
rationale: note ?? '',
|
||||
// Only the values the decision was taken on — no document dumps
|
||||
// (the collection caps the serialized snapshot at 16 KB).
|
||||
snapshot: {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,13 @@ const validateAllocationDecision: CollectionBeforeValidateHook = ({
|
|||
throw new APIError('Informe a leitura alternativa ao descartar a sugestão.', 400)
|
||||
}
|
||||
|
||||
if (
|
||||
nextData.outcome !== 'movimento' &&
|
||||
(typeof nextData.rationale !== 'string' || !nextData.rationale.trim())
|
||||
) {
|
||||
throw new APIError('Informe a justificativa.', 400)
|
||||
}
|
||||
|
||||
try {
|
||||
const serializedSnapshot = JSON.stringify(nextData.snapshot)
|
||||
if (
|
||||
|
|
@ -116,7 +123,8 @@ export const AllocationDecision: CollectionConfig = {
|
|||
name: 'rationale',
|
||||
type: 'textarea',
|
||||
label: 'Justificativa',
|
||||
required: true,
|
||||
// E14 movimento may omit motivo (B134); triage outcomes validate in beforeValidate.
|
||||
required: false,
|
||||
maxLength: 2000,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -45,8 +45,9 @@ type MunicipalityListLevelControlProps = {
|
|||
|
||||
/**
|
||||
* E14 — unlike the trend and estimate cells next to it, this one submits
|
||||
* explicitly: a movement may need an override / triangulated shock. Motivo is
|
||||
* optional (B134/B147). Auto-saving would skip the confirmation rite.
|
||||
* explicitly: a movement may need an override, and the motivo is optional.
|
||||
* Auto-saving half of that would file an incomplete decision under the
|
||||
* coordinator's name.
|
||||
*/
|
||||
export const MunicipalityListLevelControl = ({
|
||||
municipalityID,
|
||||
|
|
@ -170,7 +171,7 @@ export const MunicipalityListLevelControl = ({
|
|||
{
|
||||
municipalityId: municipalityID,
|
||||
level: draftLevel,
|
||||
note: note.trim() ? note : null,
|
||||
note,
|
||||
triangulatedShock,
|
||||
override,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ export const WizardTrendNoteStep = ({
|
|||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label htmlFor="wizard-trend-note" className="text-sm font-medium">
|
||||
Por que mudar a tendência?
|
||||
Por que mudar a tendência? (opcional)
|
||||
</label>
|
||||
<Textarea
|
||||
id="wizard-trend-note"
|
||||
|
|
@ -103,7 +103,6 @@ export const WizardTrendNoteStep = ({
|
|||
onChange={(event) => setNote(event.target.value)}
|
||||
rows={5}
|
||||
maxLength={2000}
|
||||
required
|
||||
disabled={isPending}
|
||||
autoFocus
|
||||
className="min-h-28"
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
HandshakeIcon,
|
||||
HomeIcon,
|
||||
InboxIcon,
|
||||
LandmarkIcon,
|
||||
LayoutDashboardIcon,
|
||||
MapIcon,
|
||||
MapPinIcon,
|
||||
|
|
@ -37,6 +38,7 @@ const staffNav: CampaignNavItem[] = [
|
|||
{ title: 'Municípios', href: MUNICIPALITY_NAV_HREF, icon: MapPinIcon },
|
||||
{ title: 'Territórios', href: '/campanha/territorios', icon: MapIcon },
|
||||
{ title: 'Lideranças', href: '/campanha/liderancas', icon: HandshakeIcon },
|
||||
{ title: 'Organizações', href: '/campanha/organizacoes', icon: LandmarkIcon },
|
||||
{ title: 'Dobradinhas', href: '/campanha/dobradinhas', icon: Users2Icon },
|
||||
{ title: 'Atividades', href: '/campanha/atividades', icon: CalendarDaysIcon },
|
||||
{ title: 'Demandas', href: '/campanha/demandas', icon: InboxIcon },
|
||||
|
|
|
|||
|
|
@ -89,9 +89,8 @@ export const MUNICIPALITY_ENGAGEMENT_LEVEL_UNRESTRICTED_MESSAGE =
|
|||
'Somente a coordenação geral ou o candidato move o nível de envolvimento.'
|
||||
|
||||
/**
|
||||
* E14 + B134 — a movement still records hysteresis/override in the snapshot;
|
||||
* Motivo is optional free text (empty OK). "O que faria voltar atrás" left the
|
||||
* write model (B147 lands the policy soft-dep; historical snapshot scrub stays B134).
|
||||
* E14 — a movement records the new level plus an optional motivo; hysteresis,
|
||||
* triangulated shock and override stay on the write path (B134).
|
||||
*/
|
||||
export const municipalityEngagementLevelSchema = z.object({
|
||||
municipality: positiveRelationshipId,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
/**
|
||||
* Degenerate omnibox for lists that only expose free-text search.
|
||||
* Pure / client-safe. (Organizações graduated to a typed omnibox — chips + apply remain for search-only surfaces / B128 pins.)
|
||||
* Degenerate omnibox for lists that only expose free-text search (B128:
|
||||
* organizações). Pure / client-safe.
|
||||
*/
|
||||
import { type CampaignListOmniboxChip } from '@/lib/campaignListOmnibox'
|
||||
import {
|
||||
filterOmniboxSuggestionSeeds,
|
||||
type CampaignListOmniboxChip,
|
||||
type CampaignListOmniboxSuggestion,
|
||||
} from '@/lib/campaignListOmnibox'
|
||||
|
||||
export type SearchOnlyListState = {
|
||||
q?: string
|
||||
|
|
@ -19,6 +23,9 @@ export const buildSearchOnlyOmniboxChips = (
|
|||
state: SearchOnlyListState,
|
||||
): CampaignListOmniboxChip[] => (state.q ? [{ id: 'q', label: chipLabel('Busca', state.q) }] : [])
|
||||
|
||||
export const buildSearchOnlyOmniboxSuggestions = (query: string): CampaignListOmniboxSuggestion[] =>
|
||||
filterOmniboxSuggestionSeeds([], query)
|
||||
|
||||
export const applySearchOnlyOmniboxSuggestion = <T extends SearchOnlyListState>({
|
||||
state,
|
||||
suggestionId,
|
||||
|
|
@ -34,3 +41,28 @@ export const applySearchOnlyOmniboxSuggestion = <T extends SearchOnlyListState>(
|
|||
}
|
||||
return { kind: 'url', state }
|
||||
}
|
||||
|
||||
export const removeSearchOnlyOmniboxChip = <T extends SearchOnlyListState>({
|
||||
state,
|
||||
chipId,
|
||||
withPageReset,
|
||||
}: {
|
||||
state: T
|
||||
chipId: string
|
||||
withPageReset: (next: T) => T
|
||||
}): SearchOnlyOmniboxAction<T> => {
|
||||
if (chipId === 'q') {
|
||||
return { kind: 'url', state: withPageReset({ ...state, q: undefined }) }
|
||||
}
|
||||
return { kind: 'url', state }
|
||||
}
|
||||
|
||||
export const clearSearchOnlyOmnibox = <T extends SearchOnlyListState>({
|
||||
cleared,
|
||||
}: {
|
||||
state: T
|
||||
cleared: T
|
||||
}): SearchOnlyOmniboxAction<T> => ({
|
||||
kind: 'clear',
|
||||
state: cleared,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
import { MigrateDownArgs, MigrateUpArgs, sql } from '@payloadcms/db-postgres'
|
||||
|
||||
export async function up({ db }: MigrateUpArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
DO $$
|
||||
DECLARE
|
||||
affected integer;
|
||||
BEGIN
|
||||
UPDATE allocation_decision
|
||||
SET snapshot = snapshot - 'reversalSignals'
|
||||
WHERE outcome = 'movimento'
|
||||
AND snapshot ? 'reversalSignals';
|
||||
|
||||
GET DIAGNOSTICS affected = ROW_COUNT;
|
||||
RAISE NOTICE 'strip_engagement_reversal_signals: removed reversalSignals from % row(s)', affected;
|
||||
END $$;
|
||||
`)
|
||||
}
|
||||
|
||||
export async function down({ db }: MigrateDownArgs): Promise<void> {
|
||||
// Historical reversal text was deliberately dropped (B134); it cannot be restored.
|
||||
await db.execute(sql`SELECT 1`)
|
||||
}
|
||||
|
|
@ -38,6 +38,7 @@ import * as migration_20260730_010601_simplify_municipality_signal_fields from '
|
|||
import * as migration_20260730_043306_simplify_leadership_fields from './20260730_043306_simplify_leadership_fields';
|
||||
import * as migration_20260731_014319_add_campaign_notifications from './20260731_014319_add_campaign_notifications';
|
||||
import * as migration_20260801_062558_add_campaign_vote_summary_snapshot from './20260801_062558_add_campaign_vote_summary_snapshot';
|
||||
import * as migration_20260802_230000_strip_engagement_reversal_signals from './20260802_230000_strip_engagement_reversal_signals';
|
||||
|
||||
export const migrations = [
|
||||
{
|
||||
|
|
@ -240,4 +241,9 @@ export const migrations = [
|
|||
down: migration_20260801_062558_add_campaign_vote_summary_snapshot.down,
|
||||
name: '20260801_062558_add_campaign_vote_summary_snapshot'
|
||||
},
|
||||
{
|
||||
up: migration_20260802_230000_strip_engagement_reversal_signals.up,
|
||||
down: migration_20260802_230000_strip_engagement_reversal_signals.down,
|
||||
name: '20260802_230000_strip_engagement_reversal_signals',
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ describe('municipality engagement level (E14)', () => {
|
|||
expect(decision.snapshot).not.toHaveProperty('reversalSignals')
|
||||
})
|
||||
|
||||
it('accepts an empty motivo and still writes the movement', async () => {
|
||||
it('records a movement with an empty motivo (B134)', async () => {
|
||||
const fixtures = campaignFixtures()
|
||||
const coordinator = await fixtures.createCampaignUser('coordinator')
|
||||
const municipality = await fixtures.getMunicipality()
|
||||
|
|
@ -85,7 +85,9 @@ describe('municipality engagement level (E14)', () => {
|
|||
expect(decisions.docs).toHaveLength(1)
|
||||
const decision = decisions.docs[0]!
|
||||
fixtures.own('allocationDecision', decision.id)
|
||||
expect(decision.rationale).toBe('Sem motivo registrado.')
|
||||
expect(decision.rationale).toBe('')
|
||||
expect(decision.snapshot).toMatchObject({ from: null, to: 'n1' })
|
||||
expect(decision.snapshot).not.toHaveProperty('reversalSignals')
|
||||
})
|
||||
|
||||
it('holds a movement that breaks the rules and writes nothing', async () => {
|
||||
|
|
@ -158,6 +160,7 @@ describe('municipality engagement level (E14)', () => {
|
|||
// against undoing a decision, not against deepening it.
|
||||
violations: ['dois-movimentos-no-mes'],
|
||||
})
|
||||
expect(latest.snapshot).not.toHaveProperty('reversalSignals')
|
||||
})
|
||||
|
||||
it('accepts a two-level jump when a triangulated shock is declared', async () => {
|
||||
|
|
@ -194,6 +197,7 @@ describe('municipality engagement level (E14)', () => {
|
|||
triangulatedShock: true,
|
||||
overridden: false,
|
||||
})
|
||||
expect(decisions.docs[0]!.snapshot).not.toHaveProperty('reversalSignals')
|
||||
})
|
||||
|
||||
it('keeps the ladder with unrestricted staff — the advisor proposes, it does not move', async () => {
|
||||
|
|
|
|||
34
tests/unit/campaignNav.unit.spec.ts
Normal file
34
tests/unit/campaignNav.unit.spec.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// @vitest-environment node
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { getCampaignNav, isCampaignNavActive } from '@/components/campaign/shell/nav'
|
||||
import { ORGANIZATIONS_LIST_PATH } from '@/lib/campaignQuickActionPaths'
|
||||
|
||||
describe('organizations sidebar entry', () => {
|
||||
it('is offered to staff after Lideranças', () => {
|
||||
const hrefs = getCampaignNav('coordinator').map((item) => item.href)
|
||||
const leadershipIndex = hrefs.indexOf('/campanha/liderancas')
|
||||
const organizationsIndex = hrefs.indexOf(ORGANIZATIONS_LIST_PATH)
|
||||
|
||||
expect(organizationsIndex).toBeGreaterThan(-1)
|
||||
expect(organizationsIndex).toBe(leadershipIndex + 1)
|
||||
})
|
||||
|
||||
it('is visible to advisor and candidate', () => {
|
||||
expect(getCampaignNav('advisor').map((item) => item.href)).toContain(ORGANIZATIONS_LIST_PATH)
|
||||
expect(getCampaignNav('candidate').map((item) => item.href)).toContain(ORGANIZATIONS_LIST_PATH)
|
||||
})
|
||||
|
||||
it('is hidden from leaders', () => {
|
||||
expect(getCampaignNav('leader').map((item) => item.href)).not.toContain(ORGANIZATIONS_LIST_PATH)
|
||||
})
|
||||
|
||||
it('marks list, detail, and nova as active', () => {
|
||||
expect(isCampaignNavActive(ORGANIZATIONS_LIST_PATH, ORGANIZATIONS_LIST_PATH)).toBe(true)
|
||||
expect(isCampaignNavActive('/campanha/organizacoes/sindmed', ORGANIZATIONS_LIST_PATH)).toBe(
|
||||
true,
|
||||
)
|
||||
expect(isCampaignNavActive('/campanha/organizacoes/nova', ORGANIZATIONS_LIST_PATH)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
@ -3,6 +3,9 @@ import { describe, expect, it } from 'vitest'
|
|||
import {
|
||||
applySearchOnlyOmniboxSuggestion,
|
||||
buildSearchOnlyOmniboxChips,
|
||||
buildSearchOnlyOmniboxSuggestions,
|
||||
clearSearchOnlyOmnibox,
|
||||
removeSearchOnlyOmniboxChip,
|
||||
} from '@/lib/searchOnlyListOmnibox'
|
||||
import {
|
||||
applyActivityOmniboxSuggestion,
|
||||
|
|
@ -264,6 +267,21 @@ describe('list omnibox adapters (B128)', () => {
|
|||
withPageReset: (next) => ({ ...next, page: 1 }),
|
||||
})
|
||||
expect(applied).toEqual({ kind: 'url', state: { page: 1, q: 'ana' } })
|
||||
|
||||
expect(buildSearchOnlyOmniboxSuggestions('x')[0]?.id).toBe('q:x')
|
||||
|
||||
const removed = removeSearchOnlyOmniboxChip({
|
||||
state: { q: 'sindicato', page: 2 },
|
||||
chipId: 'q',
|
||||
withPageReset: (next) => ({ ...next, page: 1 }),
|
||||
})
|
||||
expect(removed).toEqual({ kind: 'url', state: { page: 1, q: undefined } })
|
||||
|
||||
const cleared = clearSearchOnlyOmnibox({
|
||||
state: { q: 'sindicato' },
|
||||
cleared: {},
|
||||
})
|
||||
expect(cleared).toEqual({ kind: 'clear', state: {} })
|
||||
})
|
||||
|
||||
it('advisor toggles municipality portfolio filter and suggests by name', () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
// @vitest-environment node
|
||||
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const migration = readFileSync(
|
||||
new URL(
|
||||
'../../src/migrations/20260802_230000_strip_engagement_reversal_signals.ts',
|
||||
import.meta.url,
|
||||
),
|
||||
'utf8',
|
||||
)
|
||||
|
||||
describe('strip engagement reversal signals migration (B134)', () => {
|
||||
it('removes reversalSignals only from movimento snapshots', () => {
|
||||
expect(migration).toContain("snapshot = snapshot - 'reversalSignals'")
|
||||
expect(migration).toContain("outcome = 'movimento'")
|
||||
expect(migration).toContain("snapshot ? 'reversalSignals'")
|
||||
expect(migration).toContain('GET DIAGNOSTICS affected = ROW_COUNT')
|
||||
})
|
||||
|
||||
it('does not attempt to restore dropped data on down', () => {
|
||||
const down = migration.indexOf('export async function down')
|
||||
expect(migration.indexOf('reversalSignals', down)).toBe(-1)
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue