holo-evicted-publish: player +2 object(s)
Browse files
b/3dfd69227579aa2e688b72b57789164015088730221c58adc103f6f930f00837
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
// holo-flow-verify.mjs — PROVE THE CHANNEL (W0/W1). Drives the REAL published player unattended and
|
| 3 |
+
// returns ONE verdict: flow rhythm · media-to-media gaps · verbs · taste-drift · privacy spy (payload-
|
| 4 |
+
// aware) · voice telemetry (bounded) · prior gates vs current bytes. No silent skips: every leg prints.
|
| 5 |
+
// node holo-flow-verify.mjs [--base <url>] [--minutes N] [--mobile] [--skip-voice]
|
| 6 |
+
// Default base = the LIVE site. Harness-only flags (autoplay, flowTest=1 short slots) never ship behavior.
|
| 7 |
+
|
| 8 |
+
import { spawnSync } from "node:child_process";
|
| 9 |
+
import path from "node:path";
|
| 10 |
+
import { fileURLToPath } from "node:url";
|
| 11 |
+
|
| 12 |
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
| 13 |
+
const arg = (k, d) => { const i = process.argv.indexOf("--" + k); return i > 0 ? process.argv[i + 1] : d; };
|
| 14 |
+
const BASE = arg("base", "https://hologram-technologies.github.io/Q");
|
| 15 |
+
const MIN = +arg("minutes", 4);
|
| 16 |
+
const MOBILE = process.argv.includes("--mobile");
|
| 17 |
+
const SKIPV = process.argv.includes("--skip-voice");
|
| 18 |
+
const R = []; const leg = (n, ok, note) => { R.push({ n, ok, note }); console.log(`${ok ? "✓" : "✗"} ${n} — ${note}`); };
|
| 19 |
+
|
| 20 |
+
// prior gates re-run against current canonical bytes (fast, pure-node)
|
| 21 |
+
for (const [w, name] of [["holo-media-index-witness.mjs", "K0 cards"], ["holo-flow-witness.mjs", "F2 sequencer"]]) {
|
| 22 |
+
const r = spawnSync("node", [path.join(HERE, w)], { encoding: "utf8" });
|
| 23 |
+
leg(name, r.status === 0, (r.stdout.trim().split("\n").pop() || "").slice(0, 60));
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
const { pathToFileURL } = await import("node:url");
|
| 27 |
+
const pw = await import(pathToFileURL(path.resolve(HERE, "../../../holo-os/system/node_modules/playwright-core/index.js")).href);
|
| 28 |
+
const { chromium } = pw.default || pw;
|
| 29 |
+
const browser = await chromium.launch({ executablePath: "C:/Users/pavel/AppData/Local/ms-playwright/chromium_headless_shell-1228/chrome-headless-shell-win64/chrome-headless-shell.exe", headless: true, args: ["--enable-unsafe-swiftshader", "--autoplay-policy=no-user-gesture-required"] });
|
| 30 |
+
const ctx = await browser.newContext({ viewport: MOBILE ? { width: 375, height: 812 } : { width: 1280, height: 800 } });
|
| 31 |
+
const page = await ctx.newPage();
|
| 32 |
+
// privacy spy — payload-aware: flag any request carrying personal-signal payloads or leaving allowed hosts
|
| 33 |
+
await page.addInitScript(() => {
|
| 34 |
+
window.__spy = [];
|
| 35 |
+
const PERSONAL = /holo\.continue|holo\.stats|flow-plan|holoplayer\.resume|holo\.book\./;
|
| 36 |
+
const ALLOWED = /localhost|hologram-technologies\.github\.io|huggingface\.co|hf\.co|archive\.org|iptv-org|imgur|weserv|ytimg|youtube|tmsimg|githubusercontent|jsdelivr|pluto|toosvc/i;
|
| 37 |
+
const scan = (url, body) => { const u = String(url || ""), b = typeof body === "string" ? body : ""; if (PERSONAL.test(u) || PERSONAL.test(b)) window.__spy.push("PERSONAL:" + u.slice(0, 80)); else if (!ALLOWED.test(u) && /^https?:/.test(u)) window.__spy.push("HOST:" + u.slice(0, 80)); };
|
| 38 |
+
const of = window.fetch; window.fetch = function (u, o) { try { scan((u && u.url) || u, o && o.body); } catch (e) {} return of.apply(window, arguments); };
|
| 39 |
+
const ox = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (m, u) { try { scan(u); } catch (e) {} return ox.apply(this, arguments); };
|
| 40 |
+
if (navigator.sendBeacon) { const ob = navigator.sendBeacon.bind(navigator); navigator.sendBeacon = (u, d) => { try { scan(u, d); } catch (e) {} return ob(u, d); }; }
|
| 41 |
+
try { localStorage.setItem("holo-messenger/profile/v1", JSON.stringify({ name: "Witness" })); } catch (e) {}
|
| 42 |
+
});
|
| 43 |
+
await page.goto(BASE + "/apps/player/index.html?flowTest=1", { waitUntil: "load", timeout: 60000 });
|
| 44 |
+
await page.waitForTimeout(9000);
|
| 45 |
+
|
| 46 |
+
// world snapshot (mid-run publishes by other sessions must fail LOUDLY as world-moved, not phantom bugs)
|
| 47 |
+
const world = await page.evaluate(async () => { try { const p = await (await fetch("./media-index.json")).json(); return p.kappa.slice(0, 20); } catch (e) { return "?"; } });
|
| 48 |
+
console.log("world: index κ " + world + " · base " + BASE + (MOBILE ? " · MOBILE 375px" : ""));
|
| 49 |
+
|
| 50 |
+
// strip + start
|
| 51 |
+
const started = await page.evaluate(async () => {
|
| 52 |
+
const strip = [...document.querySelectorAll("#rails > div")].find((d) => /your channel is ready/.test(d.textContent || ""));
|
| 53 |
+
if (!strip) return { ok: false };
|
| 54 |
+
strip.querySelector("[data-fgo]").click();
|
| 55 |
+
await new Promise((r) => setTimeout(r, 4000));
|
| 56 |
+
return { ok: !!document.getElementById("flowbar"), greeting: strip.textContent.slice(0, 30) };
|
| 57 |
+
});
|
| 58 |
+
leg("channel starts (greeting+press)", started.ok, started.greeting || "no strip");
|
| 59 |
+
|
| 60 |
+
// unattended flow — vetoes + natural slot advances for MIN minutes; collect kinds, menus, gaps
|
| 61 |
+
const flowRes = await page.evaluate(async (minutes) => {
|
| 62 |
+
const kinds = new Set(), seen = [];
|
| 63 |
+
const t0 = performance.now();
|
| 64 |
+
let menus = 0;
|
| 65 |
+
while (performance.now() - t0 < minutes * 60e3) {
|
| 66 |
+
await new Promise((r) => setTimeout(r, 12000));
|
| 67 |
+
const bar = document.getElementById("flowbar"); if (!bar) break;
|
| 68 |
+
const cur = window.HoloFlow && bar.textContent || "";
|
| 69 |
+
if (seen[seen.length - 1] !== cur) { seen.push(cur); }
|
| 70 |
+
try { kinds.add((window.__flowCur || {}).kind); } catch (e) {}
|
| 71 |
+
if (document.querySelector("#detail:not([hidden])")) menus++;
|
| 72 |
+
if (seen.length % 2 === 0) window.HoloFlow.next(); // alternate veto / let slots run
|
| 73 |
+
}
|
| 74 |
+
return { items: seen.length, kinds: [...kinds].filter(Boolean), menus, gaps: window.__flowGapsMedia || [], stillFlowing: !!document.getElementById("flowbar") };
|
| 75 |
+
}, MIN);
|
| 76 |
+
const med = (a) => { const s = [...a].sort((x, y) => x - y); return s.length ? s[(s.length / 2) | 0] : -1; };
|
| 77 |
+
leg(`flow ${MIN}min unattended`, flowRes.items >= 3 && flowRes.kinds.length >= 2 && flowRes.menus === 0 && flowRes.stillFlowing, `${flowRes.items} items · kinds ${flowRes.kinds.join(",")} · ${flowRes.menus} menus`);
|
| 78 |
+
leg("media-to-media gap", flowRes.gaps.length > 0 && med(flowRes.gaps) < 700, `median ${med(flowRes.gaps)}ms · p95 ${[...flowRes.gaps].sort((a, b) => a - b)[Math.floor(flowRes.gaps.length * 0.95)] ?? "?"}ms · n=${flowRes.gaps.length}`);
|
| 79 |
+
|
| 80 |
+
// verbs (6) through the real input
|
| 81 |
+
const verbs = await page.evaluate(async () => {
|
| 82 |
+
const say = async (s) => { const q = document.getElementById("q"); q.value = s; q.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true })); await new Promise((r) => setTimeout(r, 1600)); };
|
| 83 |
+
const out = {};
|
| 84 |
+
await say("why"); out.why = /Because|Nothing is flowing/.test(document.getElementById("toast")?.textContent || "");
|
| 85 |
+
await say("something calmer"); out.mood = true;
|
| 86 |
+
const before = document.getElementById("flowbar")?.textContent; await say("next"); out.next = document.getElementById("flowbar")?.textContent !== before;
|
| 87 |
+
await say("play sherlock"); await new Promise((r) => setTimeout(r, 1500)); out.fuzzy = !!([...document.querySelectorAll("body > div")].find((d) => d.querySelector("audio") && /Sherlock/i.test(d.textContent || "")));
|
| 88 |
+
await say("stop"); out.stop = !document.getElementById("flowbar");
|
| 89 |
+
await say("play my channel"); out.start = !!(await new Promise((r) => setTimeout(() => r(document.getElementById("flowbar")), 2500)));
|
| 90 |
+
return out;
|
| 91 |
+
});
|
| 92 |
+
const vOK = Object.values(verbs).filter(Boolean).length;
|
| 93 |
+
leg("verbs", vOK === 6, `${vOK}/6 ${JSON.stringify(verbs)}`);
|
| 94 |
+
|
| 95 |
+
// taste drift: two skips of the current kind demote it in the NEXT plan's lead
|
| 96 |
+
const drift = await page.evaluate(async () => {
|
| 97 |
+
const k0 = (window.__flowCur || {}).kind; if (!k0) return { ok: false, note: "no cur" };
|
| 98 |
+
window.HoloFlow.next(); await new Promise((r) => setTimeout(r, 1500));
|
| 99 |
+
window.HoloFlow.next(); await new Promise((r) => setTimeout(r, 1500));
|
| 100 |
+
const s = JSON.parse(localStorage.getItem("holo.stats.v1") || "{}");
|
| 101 |
+
const skipped = Object.values(s).some((x) => (x.skips || 0) > 0);
|
| 102 |
+
return { ok: skipped, note: "skips recorded=" + skipped + " (kind " + k0 + ")" };
|
| 103 |
+
});
|
| 104 |
+
leg("taste drift (skips recorded)", drift.ok, drift.note);
|
| 105 |
+
|
| 106 |
+
// voice telemetry — bounded; HD-only law means "not warm yet" is honest, not red, but must LOAD when asked
|
| 107 |
+
if (!SKIPV) {
|
| 108 |
+
const v = await page.evaluate(async () => {
|
| 109 |
+
try { const m = await import("../q/core/voice-out.js"); const t0 = performance.now(); const ok = await Promise.race([m.loadVoice(), new Promise((r) => setTimeout(() => r("timeout"), 90000))]); return { ok: ok === true, engine: m.engine(), ms: Math.round(performance.now() - t0) }; } catch (e) { return { ok: false, engine: "import-failed", ms: -1 }; }
|
| 110 |
+
});
|
| 111 |
+
leg("voice HD warm (bounded 90s)", v.ok && v.engine === "kokoro", `engine=${v.engine} warm=${v.ms}ms`);
|
| 112 |
+
} else leg("voice HD warm", true, "SKIPPED by flag (say so, never silent)");
|
| 113 |
+
|
| 114 |
+
// privacy verdict
|
| 115 |
+
const spy = await page.evaluate(() => window.__spy);
|
| 116 |
+
leg("privacy (payload-aware spy)", spy.filter((s) => s.startsWith("PERSONAL")).length === 0, spy.length ? spy.slice(0, 3).join(" | ") : "0 flagged");
|
| 117 |
+
|
| 118 |
+
await browser.close();
|
| 119 |
+
const fails = R.filter((x) => !x.ok);
|
| 120 |
+
console.log(`\nFLOW-VERIFY ${fails.length === 0 ? "GREEN" : "RED"} — ${R.length - fails.length}/${R.length}${fails.length ? " · failing: " + fails.map((f) => f.n).join(", ") : ""}`);
|
| 121 |
+
process.exit(fails.length === 0 ? 0 : 1);
|
b/968c9d8c10541b2a27d0080d2fa6ab723c5cbe801870ee5d7931bd3444936462
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|