Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Run with an existing Ego Page and the dashboard's Vite origin:
// const {checkFootageWatch} = await import("/checkout/apps/presentation/dashboard/smoke/content-footage-watch-checks.mjs");
// console.log(await checkFootageWatch(page, origin));
import assert from "node:assert/strict";

export async function checkFootageWatch(page, origin) {
const load = async () => {
await page.goto(origin + "/smoke/content-footage-watch-smoke.html");
await page.waitForFunction(() => window.footageWatchFixture && document.body.innerText.includes("A bounded observation"));
};
await load();
const initial = await page.snapshot();
const invoke = (method, value) => page.evaluate(async ({method, value}) => {
await window.footageWatchFixture[method](value);
return window.footageWatchFixture.read();
}, {method, value});
const read = () => page.evaluate(() => window.footageWatchFixture.read());
const start = async () => {
await page.click("loc=role:button[name='Check for up to 5 minutes']");
await page.waitForFunction(() => document.body.innerText.includes("Stop checking"));
await invoke("advance", 0);
};
const checks = state => state.calls.filter(call => call.action === "generation_check");
await invoke("advance", 30000);
assert.equal(checks(await read()).length, 0, "A saved running task must not start observation on mount");

await load(); await start();
let state = await invoke("advance", 299000);
assert.ok(state.text.includes("Stop checking") && checks(state).length > 0);
state = await invoke("advance", 2000);
assert.ok(!state.text.includes("Stop checking"), "Expire at five elapsed minutes even while the sixteen-second GET is pending");
const atExpiry = checks(state).length;
state = await invoke("advance", 310000);
assert.equal(checks(state).length, atExpiry, "No new query after the deadline");
assert.ok(checks(state).every(call => call.at < 300000));
const deadlineResult = {queries: atExpiry, lastQueryAt: checks(state).at(-1).at};

await start();
state = await invoke("advance", 27000);
assert.ok(checks(state).length > atExpiry, "Explicit restart gets a fresh window");
await page.click("loc=role:button[name='Stop checking']");
await page.waitForFunction(() => !document.body.innerText.includes("Stop checking"));
const stoppedAt = checks(state).length;
state = await invoke("advance", 310000);
assert.equal(checks(state).length, stoppedAt, "Manual stop retains the cloud task without more observation");

for (const reason of ["terminal", "unreadable", "changeGoal", "unmount"]) {
await load(); await start(); await invoke("advance", 11000);
await invoke(reason);
// Let the pending response be observed before advancing beyond that state.
if (reason === "terminal" || reason === "unreadable") {
await invoke("advance", 16000);
await page.waitForFunction(() => !document.body.innerText.includes("Stop checking"));
}
state = await invoke("advance", 310000);
assert.equal(checks(state).length, 1, reason + " stops further automatic queries");
assert.ok(!state.text.includes("Stop checking"));
assert.ok(state.calls.every(call => ["generation_list", "generation_check"].includes(call.action)),
"Stopping observation must not submit or cancel a cloud task");
}
await load();
return {ok: true, boundary: "actual React component, synthetic elapsed clock and transport", deadlineResult,
verified: ["mount reads only", "elapsed deadline while busy", "no late queries", "restart", "manual stop",
"terminal", "read failure", "Goal change", "unmount", "no submit/cancel"], initial};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Actual React component; synthetic transport and a deterministic elapsed clock.
// This fixture cannot issue network requests or qualify a live model/provider.
import React from "react";
import {flushSync} from "react-dom";
import {createRoot} from "react-dom/client";
import {ContentGeneratedFootage} from "../src/features/personal-workspace/content-generated-footage";
import {WorkspaceI18nProvider} from "../src/features/personal-workspace/i18n";

const realTimeout = window.setTimeout.bind(window);
let now = 0, sequence = 0, root, goal = "watch-a", remoteStatus = "running", failRead = false;
let calls = [];
const timers = new Map();
const flush = async () => {
await new Promise(resolve => realTimeout(resolve, 0));
// Drain actual React commits/effects before moving the independent clock.
flushSync(() => {});
};
const schedule = (callback, delay, period, args) => {
const id = ++sequence;
timers.set(id, {at: now + Math.max(0, Number(delay) || 0), period, callback, args});
return id;
};
window.setTimeout = (callback, delay, ...args) => schedule(callback, delay, null, args);
window.setInterval = (callback, delay, ...args) => schedule(callback, delay, Math.max(1, Number(delay) || 1), args);
window.clearTimeout = window.clearInterval = id => timers.delete(id);
Object.defineProperty(performance, "now", {value: () => now});
localStorage.setItem("loopx-pw-locale", "en");

function library() {
return {ok: true, provider: {status: "ready"}, generations: [{
generation_id: "gen_watch", revision: 1, title: "A bounded observation", status: remoteStatus,
seconds: 5, resolution: "720p", prompt: "Synthetic observation only", model: "synthetic",
task_id: "cgt-synthetic-watch", error_code: null, estimate: {cny: 0},
reported_usage_estimate_cny: null, quote: {source_url: "https://example.com", checked_at: "synthetic"},
watermark_requested: true, references: [], result: null,
}]};
}

window.fetch = async (_url, options) => {
const body = JSON.parse(options.body);
calls.push({action: body.action, goal: body.goal_id, at: now});
if (body.action === "generation_check") {
// Longer than the component's ten-second observation interval.
await new Promise(resolve => setTimeout(resolve, 16000));
return {ok: true, json: async () => ({ok: true})};
}
if (body.action === "generation_list") {
if (failRead) return {ok: false};
return {ok: true, json: async () => library()};
}
throw new Error("Unexpected effect in read-only observation fixture: " + body.action);
};

function mount() {
root?.unmount();
root = createRoot(document.getElementById("root"));
root.render(React.createElement(WorkspaceI18nProvider, null,
React.createElement(ContentGeneratedFootage, {goalId: goal})));
}

window.footageWatchFixture = {
async reset() {
root?.unmount(); root = null;
timers.clear(); now = 0; calls = []; goal = "watch-a"; remoteStatus = "running"; failRead = false;
mount(); await flush(); await flush();
},
async advance(milliseconds) {
const end = now + milliseconds;
let steps = 0;
while (true) {
const next = [...timers].filter(([, timer]) => timer.at <= end).sort((a, b) => a[1].at - b[1].at)[0];
if (!next) break;
if (++steps > 2000) throw new Error("Synthetic timer budget exceeded");
const [id, timer] = next;
now = timer.at;
if (timer.period === null) timers.delete(id); else timer.at += timer.period;
timer.callback(...timer.args);
await flush(); await flush();
}
now = end; await flush(); await flush();
},
async terminal() {remoteStatus = "succeeded";},
async unreadable() {failRead = true;},
async changeGoal() {goal = "watch-b"; mount(); await flush(); await flush();},
async unmount() {root.unmount(); root = null; await flush();},
read() {return {now, calls: [...calls], text: document.body.innerText};},
};
await window.footageWatchFixture.reset();
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!doctype html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Footage observation regression</title></head>
<body><div id="root"></div><script type="module" src="./content-footage-watch-fixture.mjs"></script></body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const capabilityCopy: Record<WorkspaceLocale, Record<string, LocalizedCopy>> = {
en: {
codex_chat: {displayName: "Codex Goal conversation defaults", description: "Choose the native Codex profile, model, speed tier and reasoning effort for new Goal conversations. Existing conversations keep their original history. This does not start or reconfigure an active Agent Loop, worker or steward."},
decision_context: {displayName: "Find evidence", description: "Find advisory evidence with a temporary private profile. OpenViking and BM25 are retrieval implementations; this page does not save a binding or enable automatic memory."},
content_ops: {displayName: "Content production", description: "Prepare source-backed outlines and local videos. Review the result before sharing; rendering uses an optional local extension."},
content_ops: {displayName: "Content production", description: "Prepare source-backed outlines, compose local videos or generate cloud images and footage with the optional media provider. Review the result before sharing."},
goal_storage: { displayName: "New Goal authority", description: "New Goals default to canonical SQLite with hard leases. Choose storage and execution policy for future Goals. Existing Goals require a separate backed-up migration." },
manager_runtime: {
displayName: "Runtime",
Expand Down Expand Up @@ -83,7 +83,7 @@ const capabilityCopy: Record<WorkspaceLocale, Record<string, LocalizedCopy>> = {
"zh-CN": {
codex_chat: {displayName: "Codex Goal 对话默认值", description: "分别选择 Codex 配置来源、模型、速度档和推理强度,作用于本机的新 Goal 对话。已有对话保留原历史;不会启动或重配正在运行的 Agent Loop、工作 Agent 或管家。"},
decision_context: {displayName: "资料召回", description: "通过临时私有配置查找参考证据。OpenViking、BM25 是检索实现;本页不保存绑定,也不启用自动记忆。"},
content_ops: {displayName: "内容制作", description: "准备有来源的提纲与本机视频,检查作品后再分享;视频制作使用可选本机扩展。"},
content_ops: {displayName: "内容制作", description: "准备有来源的提纲,使用可选媒体扩展制作本机视频、生成云端图像与镜头,检查作品后再分享。"},
goal_storage: { displayName: "新 Goal 的权威存储", description: "新 Goal 默认使用 canonical SQLite 与 hard lease;可选择之后新 Goal 的存储与执行策略。已有 Goal 仍需单独备份、迁移。" },
manager_runtime: {
displayName: "运行环境",
Expand Down
Loading
Loading