tasks: the host's task event reaches the page (toast); real-clock task and mode flows for a real model

This commit is contained in:
Niko1221 committed 2026-10-16 01:49:30 +02:00
1 parent daa45b1e37
commit 0edf5cb9fc
3 files changed
+89 -1

No files matched your search

+49
View File
@@ -0,0 +1,49 @@
/**
* The owner's "open my Downloads folder" in each Mode through the real app, with screenshots. STRATA_URL, SHOTS, MODEL (picker text).
* Manual: the folder is NOT opened until the one Approve card is approved; Plan: nothing changes and no card asks; Bypass: it just opens.
*/
import { chromium } from 'playwright-core';
import { existsSync, mkdirSync } from 'node:fs';
import { join, resolve } from 'node:path';
const BASE = process.env.STRATA_URL; if (!BASE) { console.error('STRATA_URL required'); process.exit(2); }
const SHOTS = resolve(process.env.SHOTS ?? 'mode-shots'); mkdirSync(SHOTS, { recursive: true });
const MODEL = process.env.MODEL ?? 'Cloud model';
const PROMPT = process.env.PROMPT ?? 'open my Downloads folder';
const CHROME = process.env.CHROME ?? ['C:/Program Files/Google/Chrome/Application/chrome.exe', '/usr/bin/google-chrome'].find(existsSync);
let failed = false; const check = (ok, label) => { console.log(`${ok ? 'ok ' : 'FAIL'} ${label}`); if (!ok) failed = true; };
const browser = await chromium.launch({ executablePath: CHROME, headless: true });
const page = await browser.newPage({ viewport: { width: 1400, height: 900 } });
const shot = async name => { await page.screenshot({ path: join(SHOTS, `${name}.png`) }); console.log(`shot ${name}.png`); };
const idle = async (ms = 600000) => page.waitForFunction(() => document.documentElement.dataset.streaming !== 'true', null, { timeout: ms });
try {
await page.goto(`${BASE}/#/chat`); await page.waitForTimeout(2000);
const skip = page.getByText('Go straight to the app'); if (await skip.count()) { await skip.click(); await page.waitForTimeout(500); }
for (const [mode, label] of [['manual', 'Manual'], ['plan', 'Plan'], ['bypass', 'Bypass permissions']]) {
await page.getByRole('button', { name: 'New chat' }).first().click(); await page.waitForTimeout(600);
await page.getByRole('button', { name: /Choose model/ }).first().click(); await page.waitForTimeout(300);
await page.getByRole('menuitemradio', { name: new RegExp(MODEL) }).first().click(); await page.waitForTimeout(300);
await page.getByRole('button', { name: /^Mode:/ }).first().click(); await page.waitForTimeout(300);
await page.getByRole('menuitemradio', { name: new RegExp(`^${label}`) }).first().click(); await page.waitForTimeout(300);
await page.locator('textarea').first().fill(PROMPT);
await page.getByRole('button', { name: 'Send' }).click();
let asked = 0, sawCard = false;
for (;;) {
const streaming = await page.evaluate(() => document.documentElement.dataset.streaming === 'true');
const card = page.locator('.toolcard[data-state="proposed"]');
if (await card.count()) {
if (!sawCard) { sawCard = true; await page.waitForTimeout(300); await shot(`${mode}-1-one-card-asks`); }
asked++; await card.first().getByRole('button', { name: 'Approve' }).click().catch(() => {});
}
if (!streaming && !(await card.count())) break;
await page.waitForTimeout(500);
}
await page.waitForTimeout(600); await shot(`${mode}-2-done`);
const text = await page.evaluate(() => document.body.innerText);
if (mode === 'manual') { check(sawCard, 'Manual: a single Approve/Deny card asked before the folder was opened'); check(!/sandbox|Constrained|language mode|access denied/i.test(text), 'Manual: no sandbox or PowerShell error text'); }
if (mode === 'plan') { check(!sawCard, 'Plan: no card asks (nothing can change)'); check(!/Constrained|language mode/i.test(text), 'Plan: no PowerShell language-mode noise'); }
if (mode === 'bypass') check(!sawCard, 'Bypass: it just opens, no question');
console.log(`${mode}: ${asked} approval click(s)`);
}
} catch (e) { failed = true; console.log('FAIL', e.message); await page.screenshot({ path: join(SHOTS, 'zz-failure.png') }).catch(() => {}); }
finally { await browser.close(); }
process.exit(failed ? 1 : 0);
+39
View File
@@ -0,0 +1,39 @@
/**
* Real-clock check of a scheduled task through the real app: creates a task due two minutes from now (the host's real scheduler runs it),
* keeps the Tasks page open to capture the toast, then shows Last result. node scripts/task-flow-s4b.mjs (STRATA_URL, SHOTS, MODE, PROMPT)
*/
import { chromium } from 'playwright-core';
import { existsSync, mkdirSync } from 'node:fs';
import { join, resolve } from 'node:path';
const BASE = process.env.STRATA_URL; if (!BASE) { console.error('STRATA_URL required'); process.exit(2); }
const SHOTS = resolve(process.env.SHOTS ?? 'task-shots'); mkdirSync(SHOTS, { recursive: true });
const CHROME = process.env.CHROME ?? ['C:/Program Files/Google/Chrome/Application/chrome.exe', '/usr/bin/google-chrome'].find(existsSync);
let failed = false; const check = (ok, label) => { console.log(`${ok ? 'ok ' : 'FAIL'} ${label}`); if (!ok) failed = true; };
const browser = await chromium.launch({ executablePath: CHROME, headless: true });
const page = await browser.newPage({ viewport: { width: 1400, height: 900 } });
const shot = async name => { await page.screenshot({ path: join(SHOTS, `${name}.png`) }); console.log(`shot ${name}.png`); };
try {
await page.goto(`${BASE}/#/tasks`); await page.waitForTimeout(2500);
const skip = page.getByText('Go straight to the app'); if (await skip.count()) { await skip.click(); await page.waitForTimeout(500); }
const due = new Date(Date.now() + 2 * 60_000);
const at = `${String(due.getHours()).padStart(2, '0')}:${String(due.getMinutes()).padStart(2, '0')}`;
const created = await (await fetch(`${BASE}/api/tasks`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ title: process.env.TITLE ?? 'Open my downloads folder', prompt: process.env.PROMPT ?? 'Open my Downloads folder and tell me how many items are in it.', frequency: 'daily', at, weekday: 1, timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, mode: process.env.MODE ?? 'bypass', enabled: true }) })).json();
console.log(`task ${created.id} due ${created.nextRunAt} (local ${at})`);
await page.reload(); await page.waitForTimeout(2000); await shot('01-task-scheduled');
let final = null; const t0 = Date.now();
for (let i = 0; i < 80 && !final; i++) {
await page.waitForTimeout(5000);
const t = (await (await fetch(`${BASE}/api/tasks`)).json()).tasks.find(x => x.id === created.id);
if (t.status === 'running' && !globalThis.sawRunning) { globalThis.sawRunning = true; console.log(`running after ${Math.round((Date.now() - t0) / 1000)} s`); await shot('02-task-running'); }
if (['done', 'error', 'cancelled'].includes(t.status)) final = t;
if (await page.locator('.st-toast').count() && !globalThis.sawToast) { globalThis.sawToast = true; await shot('03-toast'); }
}
check(!!final && final.status === 'done', `the task ran by itself and finished (${final?.status}, ${final?.error ?? ''})`);
console.log('result:', String(final?.output ?? '').replace(/\s+/g, ' ').slice(0, 400));
check(/What it did/.test(final?.output ?? ''), 'the result lists what it did');
await page.waitForTimeout(1500); await shot('04-toast-after');
await page.goto(`${BASE}/#/tasks/${created.id}`); await page.waitForTimeout(2500); await shot('05-last-result');
check(await page.getByText('Last result').count() >= 1, 'Last result is shown on the task page');
} catch (e) { failed = true; console.log('FAIL', e.message); await page.screenshot({ path: join(SHOTS, 'zz-failure.png') }).catch(() => {}); }
finally { await browser.close(); }
process.exit(failed ? 1 : 0);
+1 -1
View File
@@ -47,7 +47,7 @@ export interface StreamEvent {
export function onEvent(handler: (e: StreamEvent) => void, onStatus?: (s: 'live' | 'offline') => void) {
let source: EventSource;
let seenAt = Date.now();
const names = ['engine', 'monitor', 'models', 'image-models', 'runtime', 'providers', 'serving', 'download', 'log', 'setup', 'skills'];
const names = ['engine', 'monitor', 'models', 'image-models', 'runtime', 'providers', 'serving', 'download', 'log', 'setup', 'skills', 'task'];
const connect = () => {
source?.close();
seenAt = Date.now();