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
Expand Up @@ -5,8 +5,10 @@ import {mkdtemp, mkdir, readFile, writeFile, rm} from "node:fs/promises";
import {tmpdir} from "node:os";
import {resolve} from "node:path";
import {createInterface} from "node:readline";
import {createElement} from "react";
import {renderToStaticMarkup} from "react-dom/server";
import {resolveTestPython} from "../../../../scripts/test-python.mjs";
import {readAnalysisSnapshot} from "../src/features/personal-workspace/goal-research-analysis.tsx";
import {readAnalysisSnapshot, ResearchAnalysisPanel} from "../src/features/personal-workspace/goal-research-analysis.tsx";

// A language-specific cache miss must not hide the same registered pack's
// older, unspecified-language result or turn a read into model execution.
Expand Down Expand Up @@ -86,6 +88,25 @@ try {
failOriginal = false;
assert.deepEqual((await read()).originalPack, original, "The ordinary read retries recovery without execution");

// A failed/in-flight observation is unknown; only a successful empty read
// supports the absence message. Keep the existing read retry as recovery.
for (const zh of [true, false]) {
const empty = {...pack, availability: "available", selection: {exhaustive: false}, episode_count: 0};
const controller = {zh, pack: empty, requestPack: empty, location: {open: true}, panel: {current: null},
readOnly: true, loading: false, snapshot: {catalog: {}}, read: async () => {}, close: () => {}};
const noSaved = zh ? "此来源尚无已保存的分析。" : "This source has no saved analysis yet.";
const render = state => renderToStaticMarkup(createElement(ResearchAnalysisPanel, {analysis: {...controller, ...state}}));
assert.ok(render({}).includes(noSaved), "A successful empty source retains its ordinary absence state");
for (const state of [{loading: true}, {readError: "Source unavailable"}, {snapshot: {catalog: {}, originalReadError: "Original unavailable"}}]) {
const html = render(state);
assert.ok(!html.includes(noSaved), "An incomplete source read must not claim that no saved analysis exists");
if (!state.loading) {
assert.ok(html.includes('role="alert"'));
assert.ok(html.includes(zh ? 'aria-label="重试读取"' : 'aria-label="Retry reading"'));
}
}
}

for (const options of [{current: () => false}, {signal: AbortSignal.abort()}]) {
reset("zh");
value = await read(true, undefined, options.signal, options.current);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,8 @@ export function ResearchAnalysisPanel({analysis: a, onHistory}: {analysis: Contr
{a.pending || a.recordedRunning ? <><p role="status">{a.pending ? <LoaderCircle size={15} aria-hidden className="analysis-spin"/> : null}{a.pending ? (zh ? "正在处理分析请求…" : "Processing analysis request…") : (zh ? "上次记录仍待分析结果;当前执行状态尚未核验。" : "The last receipt awaits a result; current execution is unverified.")}</p><button type="button" onClick={() => void a.cancel()} disabled={a.readOnly || a.cancelPending || currentJob?.cancel_requested}>{currentJob?.cancel_requested ? (zh ? "已请求取消" : "Cancellation requested") : a.cancelPending ? (zh ? "正在请求取消…" : "Requesting cancellation…") : (zh ? "取消分析" : "Cancel analysis")}</button></>
: a.savedOriginal && !a.readOnly ? <button className="analysis-primary" type="button" disabled={!available} onClick={() => void a.execute(failed)}><Sparkles size={15} aria-hidden/>{zh ? "用中文分析所选范围" : "Analyze selected scope in English"}</button>
: result ? <button type="button" onClick={() => void a.read().catch(() => undefined)}><RefreshCw size={14} aria-hidden/>{zh ? "刷新结果" : "Refresh result"}</button>
: a.readOnly ? <><p className={sourceAvailable ? undefined : "analysis-warning"}>{sourceAvailable ? (zh ? "此来源尚无已保存的分析。" : "This source has no saved analysis yet.") : reasonCopy(p.reason_code, zh)}</p><button type="button" onClick={() => void a.read().catch(() => undefined)}>{zh ? "刷新结果" : "Refresh result"}</button></>
: a.readOnly ? (a.loading || a.readError || a.snapshot?.originalReadError ? null
: <><p className={sourceAvailable ? undefined : "analysis-warning"}>{sourceAvailable ? (zh ? "此来源尚无已保存的分析。" : "This source has no saved analysis yet.") : reasonCopy(p.reason_code, zh)}</p><button type="button" onClick={() => void a.read().catch(() => undefined)}>{zh ? "刷新结果" : "Refresh result"}</button></>)
: <button className="analysis-primary" type="button" disabled={!available} onClick={() => void a.execute(failed)}><Sparkles size={15} aria-hidden/>{failed ? (zh ? "重试分析" : "Retry analysis") : p.continuation ? analysisActionLabel(p, zh) : (zh ? "分析所选范围" : "Analyze selected scope")}</button>}
{(!result || a.savedOriginal) && !a.readOnly && !a.pending && !a.recordedRunning ? <small>{a.requestPack?.continuation?.can_reuse ? (zh ? "复用已有判读,无需新模型调用。" : "Reuses existing judgments without a new model call.") : zh ? "使用本机 Codex 账户,单独产生分析用量;浏览不调用 Agent。" : "Uses the local Codex account with separate analysis usage. Browsing never invokes an Agent."}</small> : null}
</div> : null}
Expand Down
18 changes: 4 additions & 14 deletions apps/presentation/dashboard/src/views/dashboard-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3876,21 +3876,11 @@ export function DashboardPage() {
if (search.statusUrl && source.kind === "example") {
return;
}
// Empty active rows and failed history do not establish Goal absence.
// Wait for the complete source before discarding a stopped-Goal bookmark.
if (source.kind === "url" && goalArchiveLoadState.phase !== "ready") return;
const goalIds = new Set(goalRows.map((row) => row.goal.id));
if (goalRows.length === 0) {
if (search.goalId) {
void navigate({
replace: true,
search: (current) => ({
...current,
goalId: "",
}),
});
}
return;
}
if (search.goalId && !goalIds.has(search.goalId)
&& goalArchiveLoadState.phase !== "loading") {
if (search.goalId && !goalIds.has(search.goalId)) {
void navigate({
replace: true,
search: (current) => ({
Expand Down
87 changes: 74 additions & 13 deletions examples/workspace-progressive-loading-browser-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -84,19 +84,14 @@ try {
await page.waitForFunction(() => [...document.querySelectorAll('.personal-goal-link')].find((el) => el.textContent.includes('ready project'))?.textContent.match(/加载|Loading/) === null);
retryFails = false;
revisionMismatch = true;
const beforeRevisionRetry = requested.filter((id) => id === "retry").length;
const revisionRead = page.waitForResponse((response) => new URL(response.url()).searchParams.get("goal_id") === "retry"
&& requested.filter((id) => id === "retry").length >= beforeRevisionRetry + 2);
await page.getByTestId("goal-status-loading").getByRole("button").click();
while (requested.filter((id) => id === "retry").length < 2) {
await new Promise((done) => setTimeout(done, 10));
}
await revisionRead;
await page.getByTestId("goal-status-loading").getByRole("button").waitFor();
revisionMismatch = false;
const clickedRevisionRetry = await page.evaluate(() => {
const button = document.querySelector('[data-testid="goal-status-loading"] button');
if (!button) return false;
button.click();
return true;
});
assert.equal(clickedRevisionRetry, true, "revision error should keep a manual retry button mounted");
await page.getByTestId("goal-status-loading").getByRole("button").click();
await page.getByTestId("goal-status-loading").waitFor({ state: "hidden" });
assert.ok(requested.filter((id) => id === "ready").length >= 2, "a transient service error should recover automatically");
assert.ok(!requested.includes("archived"), "stopped history must not compete with the active first screen");
Expand Down Expand Up @@ -159,7 +154,7 @@ try {
// different Goal still fails. Directory lifecycle identity is unchanged.
await accessPage.locator(".personal-goal-link").filter({ hasText: "ready project" }).click();
await accessPage.locator(".personal-goal-tabs").getByRole("button", { name: locale === "en" ? "Tasks" : "任务", exact: true }).click();
await accessPage.getByText(readyText, { exact: true }).waitFor();
await accessPage.locator('[data-goal-panel="tasks"]').getByText(readyText, { exact: true }).waitFor();
const history = accessPage.getByTestId("completed-task-lane");
await history.getByText(historyText, { exact: true }).waitFor();
await accessPage.getByRole("button", { name: locale === "en" ? "List" : "列表", exact: true }).click();
Expand All @@ -170,7 +165,7 @@ try {
readyText = "Review updated public evidence";
historyText = "Updated completed public evidence";
await accessPage.getByRole("button", { name: locale === "en" ? "Refresh status" : "刷新状态", exact: true }).click();
await accessPage.getByText(readyText, { exact: true }).waitFor({ timeout: 5_000 });
await accessPage.locator('[data-goal-panel="tasks"]').getByText(readyText, { exact: true }).waitFor({ timeout: 5_000 });
await history.getByText(historyText, { exact: true }).waitFor();
assert.equal(await history.getByText("Completed public evidence", { exact: true }).count(), 0);
assert.equal(await expandHistory.getAttribute("aria-expanded"), "true", "refresh preserves list expansion");
Expand All @@ -187,8 +182,74 @@ try {
assert.deepEqual(counts, { ready: 3, access: 4 }, "a failed re-read exposes its error instead of presenting the retained snapshot as current");
await accessPage.close();
}
// An active-first fallback is incomplete even when it contains no Goals.
// Loading or failed stopped history cannot disprove a bookmarked Goal.
for (const activeIds of [[], ["ready"]]) {
const archivePage = await browser.newPage({ viewport: { width: 1440, height: 960 } });
archivePage.on("pageerror", (error) => errors.push(error.message));
let releaseArchive;
const archiveGate = new Promise((done) => { releaseArchive = done; });
let archiveFails = true;
let mutations = 0;
const scoped = (scope) => {
const ids = scope === "stopped" ? ["archived"] : activeIds;
const payload = snapshot("archived");
payload.run_history.goals = ids.flatMap((id) => snapshot(id).run_history.goals);
payload.attention_queue.items = [];
payload.todo_index.items = [];
payload.goal_projection = { schema_version: "loopx_goal_projection_scope_v0", scope,
complete: false, projected_goal_count: ids.length, registry_goal_count: activeIds.length + 1,
registry_revision: directory.registry_revision };
return payload;
};
await archivePage.route("**/status.json*", async (route) => {
const query = new URL(route.request().url()).searchParams;
if (query.has("view")) return route.fulfill({ status: 404 });
const scope = query.get("goal_activation");
assert.ok(scope === "active" || scope === "stopped");
if (scope === "stopped") {
await archiveGate;
if (archiveFails) return route.fulfill({ status: 503, json: { ok: false } });
}
return route.fulfill({ json: scoped(scope) });
});
await archivePage.route("**/api/**", (route) => {
if (route.request().method() !== "GET") mutations++;
return route.fulfill({ status: 503, json: { ok: false } });
});
try {
await archivePage.goto(`${origin}/chat/?goalId=archived&view=research`);
await archivePage.locator(".personal-stopped-goals").waitFor();
await archivePage.evaluate(() => new Promise((done) => requestAnimationFrame(() => requestAnimationFrame(done))));
await archivePage.screenshot({ path: resolve(out, `stopped-loading-${activeIds.length}.png`) });
assert.equal(new URL(archivePage.url()).searchParams.get("goalId"), "archived", "pending history must preserve the deep link, including an empty active projection");
releaseArchive();
await archivePage.locator(".personal-stopped-goal-error button").waitFor();
assert.equal(new URL(archivePage.url()).searchParams.get("goalId"), "archived", "failed history is unknown, not an absent Goal");
archiveFails = false;
await archivePage.locator(".personal-stopped-goal-error button").click();
await archivePage.locator(".personal-channel-title h1").filter({ hasText: "archived project" }).waitFor();
assert.equal(new URL(archivePage.url()).searchParams.get("view"), "research");
assert.match(await archivePage.locator(".personal-channel-title h1").innerText(), /archived project/);
assert.match(await archivePage.locator('.personal-goal-tabs button[aria-current="page"]').innerText(), /研究|Research/);
await archivePage.screenshot({ path: resolve(out, `stopped-recovered-${activeIds.length}.png`) });
await archivePage.setViewportSize({ width: 390, height: 844 });
assert.ok(await archivePage.locator(".personal-channel-title h1").isVisible());
await archivePage.screenshot({ path: resolve(out, `stopped-recovered-${activeIds.length}-narrow.png`) });
await archivePage.setViewportSize({ width: 1440, height: 960 });
await archivePage.reload();
await archivePage.locator(".personal-channel-title h1").filter({ hasText: "archived project" }).waitFor();
assert.equal(new URL(archivePage.url()).searchParams.get("goalId"), "archived", "reload preserves the stopped Goal without activating it");
await archivePage.goto(`${origin}/chat/?goalId=absent&view=research`);
await archivePage.waitForURL((url) => !url.searchParams.get("goalId"));
assert.equal(mutations, 0, "bookmark recovery must remain read-only");
} finally {
releaseArchive();
await archivePage.close();
}
}
assert.deepEqual(errors, []);
console.log(JSON.stringify({ ok: true, directory_ms: directoryMs, peak_concurrent_requests: peak, checks: ["directory-before-slow-goal", "ready-peer-usable", "queue-progress", "isolated-failure", "registry-revision-fence", "retry", "service-restart-recovery", "lazy-stopped-goals", "access-no-auto-retry", "access-manual-recovery-preserves-peers", "full-refresh-observes-external-todo", "full-refresh-invalidates-completed-history", "failed-reread-replaces-stale-snapshot", "bilingual-access", "mobile", "no-render-errors"] }));
console.log(JSON.stringify({ ok: true, directory_ms: directoryMs, peak_concurrent_requests: peak, checks: ["directory-before-slow-goal", "ready-peer-usable", "queue-progress", "isolated-failure", "registry-revision-fence", "retry", "service-restart-recovery", "lazy-stopped-goals", "access-no-auto-retry", "access-manual-recovery-preserves-peers", "full-refresh-observes-external-todo", "full-refresh-invalidates-completed-history", "failed-reread-replaces-stale-snapshot", "stopped-deep-link-loading-error-retry-reload", "absent-goal-after-complete-read", "read-only-bookmark-recovery", "bilingual-access", "mobile", "no-render-errors"] }));
} finally {
releaseSlow();
await cleanupBrowserSmoke({ browser, server, fixturePaths: [] });
Expand Down
Loading