Tracks the app that was already in place (capture, treatment, outreach, WhatsApp integration, server, frontend, tests, docs) so future feature work can branch and use isolated worktrees.
27 KiB
Commercial Outreach Pilot Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build a supervised, measurable WhatsApp prospecting operation for GTI Vendas and G-Delivery with approved daily queues, gradual number verification, inbound-reply pauses, a manual commercial funnel, and conservative scheduling.
Architecture: Add a campaign layer that references existing leads, verification, opt-out, and provider-send records without duplicating their authority. Queue preparation, scheduling, execution, inbound handling, pipeline state, metrics, and UI each live in focused modules with transactional boundaries at approval, reservation, reply, and stage changes.
Tech Stack: Node.js 24 ESM, node:test, SQLite DatabaseSync, Baileys 7.0.0-rc14, browser JavaScript, Playwright browser acceptance tests.
Global Constraints
- Work only in
MAQUINA COMERCIAL PESSOAL. - Products are exactly
GTI VendasandG-Delivery. - The sender is
Gustavo - Representante Comercial do Grupo GTI. - Do not put price, links, CNPJ, source-database details, unsupported features, unapproved integrations, technical deadlines, or guaranteed results in the initial message.
- Every initial message contains a simple refusal mechanism.
- The pilot has no follow-up.
- Campaign order is G-Delivery food delivery, GTI Vendas food/markets, then GTI Vendas retail/services.
- Operating windows are Monday-Friday
09:30-11:30and14:00-17:00local time. - Daily campaign progression is
10,12,15, and18; weekly increases require explicit approval. - Campaign sends use random
12-25minute intervals and at most four attempts in a rolling hour. - Manual and automatic daily limits are separate and displayed separately.
- Previously unverified checks per day are capped at
ceil(1.5 * approved campaign daily limit). - Any inbound message pauses the equivalent phone/lead; no automatic commercial reply is sent.
- Unknown delivery is terminal and never retried automatically.
- Automation starts disabled after every server restart.
- Do not send real messages from automated tests.
- Do not create a Git commit unless the user explicitly requests one.
Task 1: Campaign Persistence and Fixed Catalog
Files:
- Modify:
src/db.mjs - Create:
src/outreach/campaign-store.mjs - Create:
src/outreach/campaign-catalog.mjs - Test:
tests/campaign-store.test.mjs - Modify:
tests/db.test.mjs - Modify:
tests/personal-branding.test.mjs
Interfaces:
-
Produces:
CAMPAIGN_PRODUCTS,CAMPAIGN_STAGES,CAMPAIGN_DEFINITIONS -
Produces:
seedCampaigns(db),listCampaigns(db),getCampaign(db, id) -
Produces:
getLeadCommercialState(db, leadId),setLeadCommercialStage(db, input) -
Produces migrations v15 tables consumed by all later tasks
-
Step 1: Write failing migration and seed tests
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map((r) => r.name);
assert.ok(tables.includes('outreach_campaigns'));
assert.ok(tables.includes('outreach_campaign_variants'));
assert.ok(tables.includes('outreach_campaign_queue'));
assert.ok(tables.includes('outreach_inbound_replies'));
assert.ok(tables.includes('outreach_lead_states'));
assert.ok(tables.includes('outreach_stage_events'));
assert.ok(tables.includes('outreach_appointments'));
seedCampaigns(db);
assert.deepEqual(listCampaigns(db).map((c) => c.produto), ['G-Delivery', 'GTI Vendas', 'GTI Vendas']);
- Step 2: Run tests and verify RED
Run: node --test tests/campaign-store.test.mjs tests/db.test.mjs tests/personal-branding.test.mjs
Expected: FAIL because v15, campaign catalog, and campaign helpers do not exist and branding tests still reject approved Grupo GTI names.
- Step 3: Append migration v15
Add these table responsibilities and constraints in one appended migration:
CREATE TABLE outreach_campaigns (
id INTEGER PRIMARY KEY,
code TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
produto TEXT NOT NULL CHECK (produto IN ('GTI Vendas','G-Delivery')),
status TEXT NOT NULL DEFAULT 'paused' CHECK (status IN ('draft','paused','ready','running','completed')),
pause_reason TEXT,
week_number INTEGER NOT NULL DEFAULT 1 CHECK (week_number BETWEEN 1 AND 4),
approved_daily_limit INTEGER NOT NULL DEFAULT 10 CHECK (approved_daily_limit IN (10,12,15,18)),
segments_json TEXT NOT NULL,
windows_json TEXT NOT NULL DEFAULT '[{"start":"09:30","end":"11:30"},{"start":"14:00","end":"17:00"}]',
interval_min_ms INTEGER NOT NULL DEFAULT 720000,
interval_max_ms INTEGER NOT NULL DEFAULT 1500000,
hourly_limit INTEGER NOT NULL DEFAULT 4,
verification_multiplier REAL NOT NULL DEFAULT 1.5,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT
);
CREATE TABLE outreach_campaign_variants (
id INTEGER PRIMARY KEY,
campaign_id INTEGER NOT NULL REFERENCES outreach_campaigns(id) ON DELETE CASCADE,
code TEXT NOT NULL CHECK (code IN ('A','B','C')),
mensagem TEXT NOT NULL,
ativo INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(campaign_id, code)
);
CREATE TABLE outreach_campaign_queue (
id INTEGER PRIMARY KEY,
campaign_id INTEGER NOT NULL REFERENCES outreach_campaigns(id) ON DELETE CASCADE,
lead_id INTEGER NOT NULL REFERENCES leads(id) ON DELETE CASCADE,
variant_id INTEGER NOT NULL REFERENCES outreach_campaign_variants(id),
numero TEXT NOT NULL,
segmento TEXT NOT NULL,
rendered_message TEXT NOT NULL,
selection_reason TEXT NOT NULL,
status TEXT NOT NULL CHECK (status IN ('preview','approved','scheduled','reserved','sent','cancelled','unknown','error')),
scheduled_at TEXT,
outreach_send_id INTEGER REFERENCES outreach_sends(id),
cancel_reason TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT
);
CREATE UNIQUE INDEX idx_campaign_queue_active_lead
ON outreach_campaign_queue(campaign_id, lead_id)
WHERE status IN ('preview','approved','scheduled','reserved','sent','unknown');
CREATE INDEX idx_campaign_queue_due ON outreach_campaign_queue(status, scheduled_at);
CREATE TABLE outreach_inbound_replies (
id INTEGER PRIMARY KEY,
provider_message_id TEXT NOT NULL UNIQUE,
lead_id INTEGER REFERENCES leads(id) ON DELETE SET NULL,
numero TEXT NOT NULL,
received_at TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE outreach_lead_states (
lead_id INTEGER PRIMARY KEY REFERENCES leads(id) ON DELETE CASCADE,
stage TEXT NOT NULL,
first_reply_at TEXT,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE outreach_stage_events (
id INTEGER PRIMARY KEY,
lead_id INTEGER NOT NULL REFERENCES leads(id) ON DELETE CASCADE,
campaign_id INTEGER REFERENCES outreach_campaigns(id) ON DELETE SET NULL,
stage TEXT NOT NULL,
note TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE outreach_appointments (
id INTEGER PRIMARY KEY,
lead_id INTEGER NOT NULL REFERENCES leads(id) ON DELETE CASCADE,
campaign_id INTEGER REFERENCES outreach_campaigns(id) ON DELETE SET NULL,
kind TEXT NOT NULL CHECK (kind IN ('demonstracao','teste_g_delivery')),
participant_name TEXT,
company_name TEXT,
preferred_day TEXT,
preferred_time TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
Add lead-linked campaign tables to resetLeadsBase deletion order; preserve opt-outs.
- Step 4: Implement catalog and store helpers
Define three campaigns with exact codes gdelivery-food, gti-food, gti-retail; exact segment arrays from the specification; exact products; and six unique catalog variants. Seed A/B/C G-Delivery rows into gdelivery-food and seed the same A/B/C GTI Vendas texts into both GTI campaigns, producing nine campaign-owned rows from six unique approved texts. seedCampaigns inserts missing campaign/variant rows but does not overwrite an edited status, week, or approved limit.
- Step 5: Run focused tests and verify GREEN
Run: node --test tests/campaign-store.test.mjs tests/db.test.mjs tests/personal-branding.test.mjs
Expected: PASS; migration remains idempotent; reset deletes lead-linked campaign records and preserves opt-outs.
Task 2: Safe Campaign Message Rendering
Files:
- Create:
src/outreach/campaign-messages.mjs - Test:
tests/campaign-messages.test.mjs
Interfaces:
-
Produces:
naturalBusinessName(lead): string -
Produces:
managementFocus(segment): string -
Produces:
renderCampaignMessage(template, lead): string -
Produces:
variantForLead(variants, leadId): object -
Step 1: Write failing rendering tests
assert.equal(naturalBusinessName({ empresa: '61.590.668 A ARAUJO', legal_name: 'A ARAUJO LTDA' }), '');
assert.equal(naturalBusinessName({ empresa: 'Pizzaria Central' }), 'Pizzaria Central');
assert.equal(managementFocus('restaurantes'), 'vendas, estoque, comandas e emissão fiscal');
assert.equal(variantForLead([{ code: 'A' }, { code: 'B' }, { code: 'C' }], 4).code, 'B');
assert.doesNotMatch(renderCampaignMessage(template, lead), /CNPJ|R\$|http/i);
- Step 2: Run test and verify RED
Run: node --test tests/campaign-messages.test.mjs
Expected: FAIL because the focused campaign renderer does not exist.
- Step 3: Implement deterministic safe rendering
Use leadId % activeVariants.length for stable A/B/C assignment. Replace {foco_gestao} and the opening greeting. When the trade name is natural, render Olá, pessoal da {nome_fantasia}, tudo bem?; otherwise preserve Olá, tudo bem? or Olá! according to the approved variant. Reject a rendered initial message when it contains a URL, a CNPJ-shaped 14-digit number, R$, or lacks both Gustavo and Grupo GTI or the refusal sentence não volto a entrar em contato.
- Step 4: Run focused tests and verify GREEN
Run: node --test tests/campaign-messages.test.mjs tests/scoring.test.mjs
Expected: PASS.
Task 3: Gradual Verification and Queue Preview
Files:
- Create:
src/outreach/campaign-queue.mjs - Modify:
src/outreach/campaign-store.mjs - Test:
tests/campaign-queue.test.mjs
Interfaces:
-
Produces:
prepareDailyQueue({ db, whatsapp, campaignId, now }): Promise<{checked, previewed, skipped}> -
Produces:
listCampaignQueue(db, campaignId, status?) -
Consumes exact helpers from Tasks 1-2 and existing verification/opt-out helpers
-
Step 1: Write failing eligibility and cap tests
Cover campaign segment order, no prior product send, latest confirmed equivalent number, opt-out, inbound-replied state, active queue dedupe, stable variants, and exact verification cap:
const summary = await prepareDailyQueue({ db, whatsapp, campaignId, now });
assert.ok(summary.checked <= Math.ceil(campaign.approved_daily_limit * 1.5));
assert.ok(summary.previewed <= campaign.approved_daily_limit);
assert.equal(new Set(queue.map((q) => q.lead_id)).size, queue.length);
- Step 2: Run test and verify RED
Run: node --test tests/campaign-queue.test.mjs
Expected: FAIL because no campaign eligibility/preparation service exists.
- Step 3: Implement sparse candidate selection and verification
Query only the active campaign's segment list, ordered by prioridade ASC, score DESC, id ASC, and exclude prior same-product outreach_sends, replies, opt-outs, and active queue rows in SQL. Process already-confirmed candidates first. Call whatsapp.checkNumber sequentially for at most ceil(1.5 * approved_daily_limit) unverified candidates, persist every result, and never retry erro_temporario in the same preparation run.
Insert preview rows transactionally with immutable variant ID, rendered text, normalized number, segment, and a selection reason such as gdelivery-food:prioridade-1.
- Step 4: Verify no full-base scan
Add a test with 10,000 out-of-segment leads and inspect EXPLAIN QUERY PLAN; require idx_leads_segmento or another selective campaign index and bounded candidate results.
- Step 5: Run focused tests and verify GREEN
Run: node --test tests/campaign-queue.test.mjs tests/db.test.mjs
Expected: PASS with no provider send calls.
Task 4: Scheduling, Approval, and Campaign API
Files:
- Create:
src/outreach/campaign-schedule.mjs - Create:
src/server/routes/campaigns.mjs - Modify:
src/server/index.mjs - Test:
tests/campaign-schedule.test.mjs - Test:
tests/campaign-routes.test.mjs
Interfaces:
-
Produces:
generateDailySchedule({ date, count, windows, minMs, maxMs, hourlyLimit, random }): Date[] -
Produces routes under
/api/campaigns -
Consumes
prepareDailyQueueand campaign-store queue transitions -
Step 1: Write failing schedule tests
Assert Monday-Friday only, exact windows, 12-25 minute requested spacing, no more than four timestamps in any rolling 60 minutes, deterministic injected random, lunch break, and at most 18 slots.
- Step 2: Run schedule tests and verify RED
Run: node --test tests/campaign-schedule.test.mjs
Expected: FAIL because schedule generation does not exist.
- Step 3: Implement schedule generation
Advance through both windows in local time. For each random interval, enforce candidate >= fourthMostRecent + 60 minutes before accepting. If no valid slot remains, return fewer timestamps; never spill into lunch, another date, or weekend.
- Step 4: Write failing route tests
Required admin routes:
GET /api/campaigns
GET /api/campaigns/:id
POST /api/campaigns/:id/prepare
GET /api/campaigns/:id/queue
POST /api/campaigns/:id/approve
DELETE /api/campaigns/:id/queue/:queueId
POST /api/campaigns/:id/start
POST /api/campaigns/:id/pause
PUT /api/campaigns/:id/week
Assert non-admin mutation is 403; approval accepts explicit queue IDs only; deletion cannot remove reserved/sent rows; start requires at least one scheduled row and WhatsApp READY; week changes map exactly 1->10, 2->12, 3->15, 4->18 and never advance implicitly.
- Step 5: Implement transactional transitions and routes
Approval schedules selected preview rows and cancels unselected rows with cancel_reason='removed_by_operator'. Campaign start changes ready|paused -> running; pause stores a required reason. Return manual and campaign daily usage as separate fields.
- Step 6: Run route tests and verify GREEN
Run: node --test tests/campaign-schedule.test.mjs tests/campaign-routes.test.mjs tests/server-whatsapp-integration.test.mjs
Expected: PASS.
Task 5: Due Queue Worker, Recovery, and Separate Limits
Files:
- Create:
src/outreach/campaign-worker.mjs - Modify:
src/db.mjs - Modify:
src/outreach/scheduler.mjs - Modify:
src/outreach/run.mjs - Modify:
scripts/servidor.mjs - Modify:
scripts/enviar-outreach.mjs - Test:
tests/campaign-worker.test.mjs - Modify:
tests/scheduler.test.mjs - Modify:
tests/server-shutdown.test.mjs
Interfaces:
-
Produces:
runDueCampaignQueue({ db, whatsapp, now, signal, log }): Promise<summary> -
Produces:
pauseCampaignsAfterRestart(db): number -
Produces:
reserveCampaignQueueSend(db, { queueId, now }): { queueId, sendId } -
Changes
reserveOutreachSendso its manual limit query counts onlyorigem='manual'; automatic campaign limits are enforced byreserveCampaignQueueSend -
Step 1: Write failing separate-limit and due-item tests
Assert manual sends count only origem='manual'; campaign sends count only the campaign's automatic attempts. Due worker sends only scheduled_at <= now, never preview/approved/future rows, and handles one due row at a time.
- Step 2: Run worker tests and verify RED
Run: node --test tests/campaign-worker.test.mjs tests/outreach-routes.test.mjs tests/scheduler.test.mjs
Expected: FAIL on missing worker and shared-origin limit.
- Step 3: Implement atomic campaign reservation
Inside whatsapp.sendText.beforeSend, run one transaction that re-reads queue/campaign/lead state and requires: campaign running, queue scheduled and due, READY connection, confirmed number, no equivalent opt-out, no inbound reply, no same-product active send, automatic count below campaign limit, and rolling-hour attempts below four. Transition queue to reserved and create outreach_sends together; return both IDs.
In afterSend, finalize provider history and queue status exactly once. Map delivery unknown to queue unknown, definite error to error, and success to sent.
- Step 4: Replace raw-lead scheduler execution
Run the due worker every 60 seconds while retaining overlap prevention and abort-aware shutdown. Make runOutreachBatch and scripts/enviar-outreach.mjs consume the same approved due queue; neither may select raw leads.
- Step 5: Implement restart safety
Before scheduler start, set every running campaign to paused with pause_reason='server_restart', set legacy outreach.ativo=false, recover reserved queue rows by joining outreach_sends, and preserve scheduled rows without sending until explicit start.
- Step 6: Run focused tests and verify GREEN
Run: node --test tests/campaign-worker.test.mjs tests/scheduler.test.mjs tests/server-shutdown.test.mjs tests/outreach-routes.test.mjs tests/outreach.test.mjs
Expected: PASS; no raw-lead automatic path remains.
Task 6: Inbound Reply Detection and Send-Race Protection
Files:
- Modify:
src/whatsapp/manager.mjs - Create:
src/outreach/inbound.mjs - Modify:
scripts/servidor.mjs - Modify:
src/outreach/campaign-worker.mjs - Test:
tests/whatsapp-manager.test.mjs - Test:
tests/inbound-outreach.test.mjs
Interfaces:
-
Adds manager option
onInboundMessage(event) -
Event:
{ messageId: string, jid: string, number: string, receivedAt: string } -
Produces:
recordInboundReply(db, event): { inserted, leadId, cancelled } -
Step 1: Write failing manager event tests
Emit messages.upsert and assert only new, non-fromMe, one-to-one @s.whatsapp.net messages produce one callback. Ignore groups, broadcasts, status JIDs, duplicates, and messages without an ID.
- Step 2: Run manager tests and verify RED
Run: node --test tests/whatsapp-manager.test.mjs
Expected: FAIL because inbound events are not subscribed.
- Step 3: Implement provider-neutral inbound event publication
Do not classify text or send a reply. Normalize only the sender number and timestamp; preserve existing outbound messages.update behavior.
- Step 4: Write failing persistence/race tests
For an inbound number, find the latest campaign send across equivalent variants, insert by unique provider ID, set current stage respondeu, set first_reply_at once, append a stage event, and cancel every preview/approved/scheduled row for equivalent numbers. Reprocessing the same provider ID is idempotent.
Block a worker in beforeSend, record an inbound reply, then release it; assert provider sendMessage is never called.
- Step 5: Implement inbound transaction and startup injection
Inject onInboundMessage: (event) => recordInboundReply(db, event) in scripts/servidor.mjs. Log persistence failures without throwing through the provider EventEmitter.
- Step 6: Run focused tests and verify GREEN
Run: node --test tests/whatsapp-manager.test.mjs tests/inbound-outreach.test.mjs tests/campaign-worker.test.mjs
Expected: PASS and no automatic outbound reply.
Task 7: Manual Pipeline and Appointment API
Files:
- Modify:
src/outreach/campaign-store.mjs - Modify:
src/server/routes/campaigns.mjs - Modify:
src/treatment/queries.mjs - Test:
tests/pipeline.test.mjs - Test:
tests/campaign-routes.test.mjs
Interfaces:
-
Produces:
listPendingReplies(db) -
Produces:
setLeadCommercialStage(db, { leadId, campaignId, stage, note }) -
Produces:
saveAppointment(db, input) -
Adds current stage/history/appointments to
getLead -
Step 1: Write failing stage and appointment tests
Accept only the 11 specification stages. Every change updates current state and appends history in one transaction. opt_out requires addOptOut for the equivalent phone. demonstracao_agendada requires participant, company, preferred day, and preferred time. teste_g_delivery_iniciado creates a test appointment without demo time requirements.
- Step 2: Run tests and verify RED
Run: node --test tests/pipeline.test.mjs tests/campaign-routes.test.mjs
Expected: FAIL because stage/appointment APIs do not exist.
- Step 3: Implement routes and enrichment
GET /api/campaigns/replies/pending
PUT /api/campaigns/leads/:leadId/stage
POST /api/campaigns/leads/:leadId/appointments
Return stage history newest first, appointments newest first, and provider/send history without duplicating ACK fields.
- Step 4: Run focused tests and verify GREEN
Run: node --test tests/pipeline.test.mjs tests/campaign-routes.test.mjs tests/personal-backend.test.mjs
Expected: PASS.
Task 8: Campaign Metrics and Automatic Pause Policies
Files:
- Create:
src/outreach/campaign-metrics.mjs - Modify:
src/outreach/campaign-worker.mjs - Modify:
src/outreach/campaign-store.mjs - Modify:
src/server/routes/campaigns.mjs - Test:
tests/campaign-metrics.test.mjs
Interfaces:
-
Produces:
campaignMetrics(db, campaignId, filters?) -
Produces:
evaluateCampaignHealth(db, campaignId): { pause, reason } -
Step 1: Write failing metric tests
Cover previewed, approved, removed, attempted, provider statuses, replies, first-response time, positive/negative/opt-out, demos, tests, sales, and grouping by product/segment/variant.
- Step 2: Write failing pause tests
Exact policies:
unknownCount > 0 // unknown_outcome
attemptsToday >= 10 && failureRateToday > 0.10 // high_failure_rate
contacts >= 20 && negativeOrOptOutRate > 0.10 // high_negative_rate
contacts >= 50 && replyRate < 0.05 // low_reply_rate
Assert equality at 10% does not pause because the requirement says exceeds 10%. Assert an automatic pause never changes itself back to running.
- Step 3: Run tests and verify RED
Run: node --test tests/campaign-metrics.test.mjs
Expected: FAIL because campaign metrics/policies do not exist.
- Step 4: Implement metrics and policy hooks
Evaluate health after every send finalization and manual stage change. On pause, store the reason and cancel no historical rows; future scheduled rows remain visible but ineligible until an explicit resume. A variant receives eligibleForComparison=true only at 40 sends for that exact campaign/segment/variant; declare a winner only when all active compared variants satisfy that threshold.
- Step 5: Add metrics route and verify GREEN
Route: GET /api/campaigns/:id/metrics?segmento=&variant=
Run: node --test tests/campaign-metrics.test.mjs tests/campaign-routes.test.mjs tests/campaign-worker.test.mjs
Expected: PASS.
Task 9: Supervised Campaign Browser Workspace
Files:
- Modify:
public/index.html - Create:
public/js/campanhas.js - Modify:
public/js/app.js - Modify:
public/js/tratamento.js - Modify:
public/css/app.css - Modify:
tests/personal-branding.test.mjs - Create:
tests/campaign-ui.test.mjs
Interfaces:
-
Adds top-level
Campanhastab -
Consumes campaign, queue, reply, stage, appointment, and metrics routes
-
Does not construct outbound text in the browser
-
Step 1: Write failing UI behavior tests
Using the existing VM DOM harness, test campaign cards, week/limit display, prepare action, preview rows with exact server-rendered text, per-row removal, explicit approval, start/pause controls, separate manual/automatic usage, pending replies, stage classification, appointment form, metrics filters, and a simulated post-server-restart response showing paused with reason server_restart.
- Step 2: Run tests and verify RED
Run: node --test tests/campaign-ui.test.mjs tests/personal-branding.test.mjs
Expected: FAIL because no campaign workspace exists.
- Step 3: Build the focused campaign tab
Keep existing visual language. Use separate sections for campaign status, daily preview, pending replies, funnel, and metrics. Require confirmation before start, weekly increase, and opt-out. Show pause reasons prominently. Never expose an editable campaign message after approval; removal is allowed, mutation is not.
- Step 4: Extend lead detail
Show current stage, first reply, stage history, campaign/send history, and appointments. Keep manual send available but do not prefill it with browser-generated generic campaign copy.
- Step 5: Add responsive behavior and verify GREEN
At widths below 760 px, campaign cards stack, tables remain horizontally scrollable, action controls become full-width, and no fixed-width dialog exceeds the viewport.
Run: node --test tests/campaign-ui.test.mjs tests/personal-branding.test.mjs
Expected: PASS; node --check public/js/campanhas.js also passes.
Task 10: Real Browser Acceptance and Final Verification
Files:
- Modify:
package.json - Modify:
package-lock.json - Create:
playwright.config.mjs - Create:
tests/browser/server.mjs - Create:
tests/browser/campaign-pilot.spec.mjs
Interfaces:
-
Adds
npm run test:browser -
Browser test server uses an isolated temporary SQLite database and fake WhatsApp manager
-
Never connects to or sends through the real provider
-
Step 1: Install the browser test dependency
Run: npm.cmd install --save-dev @playwright/test
Run: npx.cmd playwright install chromium
- Step 2: Create isolated browser test server
Open a temporary database, seed admin/campaigns/sample leads/verifications, inject a fake READY manager that records sends in memory, and start createAppServer on 127.0.0.1:3911. Delete the temporary database on shutdown.
- Step 3: Write desktop and mobile acceptance tests
Configure Chromium projects at 1440x900 and 390x844. Seed one pending inbound reply in the isolated test database before browser startup. Test login, campaign prepare, preview removal, approval, explicit start, separate limits, pause reason, pending reply classification, demonstration scheduling, and metrics rendering. Assert fake provider send count remains zero unless the test advances a due scheduled item explicitly.
- Step 4: Run all automated verification
Run: npm.cmd test
Expected: all Node tests pass.
Run: npm.cmd run test:browser
Expected: desktop and mobile projects pass with no console errors.
Run: npm.cmd audit --omit=dev
Expected: zero production vulnerabilities.
- Step 5: Restart and perform non-sending smoke checks
Verify migration v15, server-restart campaign pause, WhatsApp status, campaign preview, manual and automatic counters, queue approval, pending-reply UI, and mobile layout. Do not start a real campaign or send a real message without Gustavo's explicit recipient-level authorization.