feat(geo): EA2-backed attendance sites + versioned seed scripts

Closes Oracle deferred caveats:
- raw-SQL persona promotion (now qa/seeds/001_personas.mjs, idempotent)
- no scripted multi-persona interaction (now qa/seeds/002_persona_
  interactions.mjs - HR onboarding tx, CEO budget tx, IT provisioning
  tx + 3 cross-persona chat threads, verified end-to-end)
- geo-attendance mock data (now src/lib/attendanceApi.ts pulls 8 sites
  via flow.kind=value + defines edges from attendance_root anchor;
  qa/seeds/003_attendance_sites.mjs seeds them idempotently)

Each site is a flow.kind=value doc with config.attendance = { lat,
lng, kind, status, who, city, label }. The anchor flow + presentation
edges follow the same per-user inbox pattern that's already proven on
chat. No hardcoded JS array left in src/scenes/GeoAttendance.tsx.
This commit is contained in:
2026-06-14 14:07:57 +04:00
parent 07eb93f67c
commit b5d1ea54b0
6 changed files with 551 additions and 39 deletions
+16 -9
View File
@@ -41,10 +41,11 @@ const b = await chromium.launch({ headless: true, args });
// HR opens a chat with CEO and sends a message
const hrWrites = await asPersona(b, { label: "HR", chipText: /Aisha/ }, async (p, writes) => {
// Go to chat
const chatTab = p.locator(".tab", { hasText: /Chat/i }).first();
await chatTab.click();
await p.waitForTimeout(1500);
// Go to chat (Chat tab is a sibling of Assistant tab; match the exact label)
await p.waitForTimeout(2000);
const chatTab = p.locator(".tab").filter({ hasText: /^\s*Chat\s*$/ }).first();
await chatTab.click({ timeout: 10000 });
await p.waitForTimeout(2000);
console.log("[HR] visible after Chat click:");
for (const b of await p.locator("button, .chat-thread-name").all()) {
const t = (await b.textContent())?.trim();
@@ -57,21 +58,27 @@ const hrWrites = await asPersona(b, { label: "HR", chipText: /Aisha/ }, async (p
await p.locator(".chat-new-row button").click();
await p.waitForTimeout(2000);
}
// Send a message
await p.waitForTimeout(1500);
const ta = p.locator(".chat-composer textarea").first();
if (await ta.count()) {
await ta.fill("Hey Mariana, store manager is requesting a new laptop. What's the approval cap this quarter?");
await p.locator(".chat-composer button", { hasText: /Send/i }).click();
await p.waitForTimeout(2500);
await p.waitForTimeout(400);
const sendBtn = p.locator(".chat-composer button", { hasText: /Send/i }).first();
console.log(`[HR] send disabled? ${await sendBtn.isDisabled()}`);
await sendBtn.click();
await p.waitForTimeout(3500);
} else {
console.log("[HR] no composer textarea found");
}
console.log(`[HR] writes so far: ${writes.length}`);
});
// CEO logs in and verifies the thread + message is visible
await asPersona(b, { label: "CEO", chipText: /Mariana/ }, async (p) => {
const chatTab = p.locator(".tab", { hasText: /Chat/i }).first();
await chatTab.click();
await p.waitForTimeout(2000);
const chatTab = p.locator(".tab").filter({ hasText: /^\s*Chat\s*$/ }).first();
await chatTab.click({ timeout: 10000 });
await p.waitForTimeout(2500);
const threadRows = await p.locator(".chat-thread-name").allTextContents();
console.log("[CEO] visible threads:", threadRows);
// Click the first thread that mentions Aisha