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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 19 additions & 2 deletions examples/personal-workspace-browser/research-history-backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import signal
import tempfile
import sys
from urllib.parse import urlparse
from urllib.parse import parse_qs, urlencode, urlparse

from loopx.capabilities.explore.result_log import validate_explore_result_event
from loopx.chat_action_store import ChatActionStore
Expand Down Expand Up @@ -106,8 +106,25 @@ def main():
metrics = {"history_reads": 0, "mutation_attempts": 0}

class Handler(ChatRequestHandler):
def _send_json(self, payload, *, status=200):
parsed = urlparse(self.path)
if status == 200 and parsed.path == "/status.json" and "run_history" in payload:
goal = parse_qs(parsed.query).get("goal_id", [""])[0]
query = "?" + urlencode({"goal_id": goal}) if goal else ""
payload = {**payload, "local_dashboard_api": {
**(payload.get("local_dashboard_api") or {}),
"read_only_sources": [{"id": "research-preview", "label": "Research preview",
"status_url": f"http://127.0.0.1:{self.server.server_port}/status.json{query}"}],
}}
return super()._send_json(payload, status=status)

def do_GET(self):
path = urlparse(self.path).path
parsed = urlparse(self.path)
path = parsed.path
if path == "/status.json" and parse_qs(parsed.query).get("view") == ["workspace-directory"]:
# Like the runtime workspace reader, use the full-status
# fallback: directory-only hydration omits source identity.
return self._send_error("Use the captured source's full status read.", status=404)
if path == "/fixture/readback":
return self._send_json({"ok": True, **metrics,
"source_unchanged": all(path.read_bytes() == value for path, value in original.items()),
Expand Down
25 changes: 25 additions & 0 deletions examples/personal-workspace-browser/research-history.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,24 @@ export const researchHistoryScenario = {
const nativePage = await browser.newPage({viewport: {width: 1280, height: 900}});
try {
await nativePage.addInitScript(() => localStorage.setItem('loopx-pw-locale', 'zh-CN'));
// A captured research reader has no execution services. Its home must
// identify the read-only source, rather than diagnose the owner's machine.
await nativePage.goto(`${nativeOwner.base}/chat/?view=overview&statusUrl=%2Fstatus.json`, {waitUntil: 'domcontentloaded'});
await nativePage.locator('.personal-home-goal-card').filter({hasText: 'Synthetic research'}).waitFor();
await nativePage.locator('.personal-status-source-meta .is-connected').waitFor();
assert.match(await nativePage.locator('.personal-status-source').innerText(), /Research preview/);
assert.equal(await nativePage.getByTestId('runtime-readiness').count(), 0, 'Reading captured research must not ask the owner to install an Agent runtime');
assert.equal(await nativePage.locator('.personal-channel-composer').count(), 0, 'The read-only source offers no execution composer');
await nativePage.screenshot({path: resolve(outputDir, 'research-readonly-home-desktop.png'), animations: 'disabled'});
await nativePage.setViewportSize({width: 390, height: 844});
assert(await nativePage.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1));
await nativePage.screenshot({path: resolve(outputDir, 'research-readonly-home-mobile.png'), animations: 'disabled'});
await nativePage.setViewportSize({width: 1280, height: 900});
const query = new URLSearchParams({goalId: 'synthetic-research-readback', view: 'research', statusUrl: '/status.json?goal_id=synthetic-research-readback'});
await nativePage.goto(`${nativeOwner.base}/chat/?${query}`, {waitUntil: 'domcontentloaded'});
const native = nativePage.getByRole('region', {name: '原生测量', exact: true});
await native.waitFor();
assert.match(await nativePage.locator('.personal-status-source').innerText(), /Research preview/, 'Goal-scoped reads preserve the read-only source');
assert.match(await native.innerText(), /2 \/ 3.*项有效结果/s);
assert.equal(await native.locator('[data-native-point="best_valid"] strong').textContent(), '3');
assert.equal(await native.locator('[data-native-point="initial"] strong').textContent(), '7');
Expand Down Expand Up @@ -164,6 +178,13 @@ export const researchHistoryScenario = {
const readback = await (await nativePage.request.get(`${nativeOwner.base}/fixture/readback`)).json();
assert.equal(readback.source_unchanged, true);
assert.equal(readback.mutation_attempts, 0);
for (const method of ['POST', 'PUT', 'DELETE']) {
const refused = await nativePage.request.fetch(`${nativeOwner.base}/api/chat/turns`, {method, data: {agent_id: 'codex', text: 'Synthetic request'}});
assert.equal(refused.status(), 405, 'Source presentation never grants execution writes');
}
const fenced = await (await nativePage.request.get(`${nativeOwner.base}/fixture/readback`)).json();
assert.equal(fenced.source_unchanged, true);
assert.equal(fenced.mutation_attempts, 3);
} finally {
await nativePage.close();
await nativeOwner.close();
Expand Down Expand Up @@ -405,6 +426,10 @@ export const researchHistoryScenario = {
// the status endpoint; navigation must reach the frontend server.
await page.route(url => url.pathname === "/status.json", async route => {
const response = await fetchWorkspaceBackend(route, owner.base);
if (new URL(route.request().url()).searchParams.get('view') === 'workspace-directory') {
assert.equal(response.status(), 404, 'Captured readers use the full-status fallback with source identity');
return route.fulfill({response});
}
assert.equal(response.status(), 200);
await route.fulfill({response});
});
Expand Down
Loading