diff --git a/apps/presentation/dashboard/smoke/workspace-locale-browser-smoke.mjs b/apps/presentation/dashboard/smoke/workspace-locale-browser-smoke.mjs index 258dac84a4..f8c5ef92f4 100644 --- a/apps/presentation/dashboard/smoke/workspace-locale-browser-smoke.mjs +++ b/apps/presentation/dashboard/smoke/workspace-locale-browser-smoke.mjs @@ -97,6 +97,7 @@ export async function verifyWorkspaceLocales({ browser, url, installApi, outputD await page.getByRole("radio", { name: /Simplified Chinese/ }).click(); assert.equal(await page.evaluate(() => localStorage.getItem("loopx-pw-locale")), "zh-CN"); await page.reload({ waitUntil: "networkidle" }); + await page.getByRole("button", { name: "返回工作区", exact: true }).click(); await page.getByTestId("personal-goal-home").waitFor(); assert.equal(await page.locator("html").getAttribute("lang"), "zh-CN"); assert.match(await page.getByRole("region", { name: "控制面来源" }).innerText(), /本机/); @@ -105,6 +106,7 @@ export async function verifyWorkspaceLocales({ browser, url, installApi, outputD await page.getByRole("button", { name: /语言/ }).click(); await page.getByRole("radio", { name: /English/ }).click(); await page.reload({ waitUntil: "networkidle" }); + await page.getByRole("button", { name: "Back to workspace", exact: true }).click(); await page.getByTestId("personal-goal-home").waitFor(); assert.equal(await page.locator("html").getAttribute("lang"), "en"); assert.equal(await page.evaluate(() => localStorage.getItem("loopx-pw-locale")), "en"); diff --git a/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-page.tsx b/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-page.tsx index 555ceb5648..ba1ed3d00e 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-page.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-page.tsx @@ -1189,8 +1189,13 @@ export function PersonalWorkspacePage({ setSelection(target); } useEffect(() => { - const restore = () => setSelectionState({scope: currentSelectionScope.current.scope, - value: readSettingsRoute(new URL(window.location.href))}); + const restore = () => { + const target = readSettingsRoute(new URL(window.location.href)); + // Task history has its own route reader. Leaving settings may clear its + // selection, but an unrelated history event must not clear a restored task. + setSelectionState(current => target ? {scope: currentSelectionScope.current.scope, value: target} + : current?.value?.kind === "settings" ? null : current); + }; window.addEventListener("popstate", restore); return () => window.removeEventListener("popstate", restore); }, []); diff --git a/examples/personal-workspace-browser/agent-model-selection.mjs b/examples/personal-workspace-browser/agent-model-selection.mjs index dc5cfd3220..6ff70f13e3 100644 --- a/examples/personal-workspace-browser/agent-model-selection.mjs +++ b/examples/personal-workspace-browser/agent-model-selection.mjs @@ -72,7 +72,11 @@ export const agentModelSelectionScenario = { await assertReadableHeader(); assert.equal(discoveryCount, 0, "The steward binding does not start optional catalog discovery"); await managerControl.click(); - assert.equal(await page.locator(".personal-settings-tabs").getByRole("button", {name: "管家", exact: true}).getAttribute("aria-current"), "page"); + const settings = page.getByRole("region", {name: "设置", exact: true}); + await settings.getByRole("heading", {name: "管家", exact: true}).waitFor(); + assert.equal(await settings.getByRole("radio", {name: "本机设置", exact: true}).isChecked(), true); + assert.equal(await settings.locator(".personal-settings-mobile-category select").inputValue(), "steward", + "The phone model entry opens the machine-owned steward category"); await page.getByRole("button", {name: "返回工作区", exact: true}).click(); await page.reload({waitUntil: "networkidle"}); await managerControl.filter({hasText: managerBinding.model}).waitFor(); diff --git a/examples/personal-workspace-browser/assets/task-history-navigation/after-desktop.png b/examples/personal-workspace-browser/assets/task-history-navigation/after-desktop.png new file mode 100644 index 0000000000..df362d6853 Binary files /dev/null and b/examples/personal-workspace-browser/assets/task-history-navigation/after-desktop.png differ diff --git a/examples/personal-workspace-browser/assets/task-history-navigation/after-narrow.png b/examples/personal-workspace-browser/assets/task-history-navigation/after-narrow.png new file mode 100644 index 0000000000..a212ca2c01 Binary files /dev/null and b/examples/personal-workspace-browser/assets/task-history-navigation/after-narrow.png differ diff --git a/examples/personal-workspace-browser/assets/task-history-navigation/before-desktop.png b/examples/personal-workspace-browser/assets/task-history-navigation/before-desktop.png new file mode 100644 index 0000000000..a8b4d2a56c Binary files /dev/null and b/examples/personal-workspace-browser/assets/task-history-navigation/before-desktop.png differ diff --git a/examples/personal-workspace-browser/explore-composition-owner.mjs b/examples/personal-workspace-browser/explore-composition-owner.mjs index 98a9b77c85..82efe0ba7a 100644 --- a/examples/personal-workspace-browser/explore-composition-owner.mjs +++ b/examples/personal-workspace-browser/explore-composition-owner.mjs @@ -142,7 +142,7 @@ export const exploreCompositionOwnerScenario = { const { page, errors } = context; await page.getByRole("button", { name: "设置", exact: true }).click(); await page.locator(".personal-settings-tabs").getByRole("button", { name: "能力中心", exact: true }).click(); - await page.getByRole("radio", { name: "单个 Goal", exact: true }).check(); + await page.getByRole("radio", { name: "Goal 设置", exact: true }).check(); await page.getByRole("combobox", { name: "目标 Goal", exact: true }).selectOption(GOAL); const catalog = page.getByRole("navigation", { name: "Goal 能力目录" }); const openExplore = async () => { diff --git a/examples/personal-workspace-browser/goal-work-map.mjs b/examples/personal-workspace-browser/goal-work-map.mjs index 6a510ceca3..bd98e10133 100644 --- a/examples/personal-workspace-browser/goal-work-map.mjs +++ b/examples/personal-workspace-browser/goal-work-map.mjs @@ -180,6 +180,14 @@ export const goalWorkMapScenario = { assert.equal(await canvas.locator('.work-map-node[aria-pressed="true"]').count(), 0, "Closing details clears the shared task selection"); assert.equal(new URL(page.url()).searchParams.get("todo"), null, "Closing details clears the task route"); + await page.goBack({waitUntil: "domcontentloaded"}); + await drawer.getByText(finalRequirement, {exact: true}).waitFor(); + assert.equal(new URL(page.url()).searchParams.get("todo"), "todo_map_reserve"); + assert.deepEqual(await drawer.locator(".personal-task-request .personal-md > p").allTextContents(), requestParagraphs, + "Back restores the same complete request through its native owner"); + await page.goForward({waitUntil: "domcontentloaded"}); + await drawer.waitFor({state: "hidden"}); + assert.equal(new URL(page.url()).searchParams.get("todo"), null); const hiddenRegistry = `${authority.registry}.hidden`; const errorsBeforeLoss = desktop.errors.length; @@ -198,7 +206,12 @@ export const goalWorkMapScenario = { await page.getByRole("button", { name: /关闭详情/ }).click(); limits = { omitted_node_count: 3, topology_complete: false }; - await page.getByRole("button", { name: "刷新快照", exact: true }).click(); + const evidence = page.locator(".goal-overview-evidence"); + if (await evidence.getAttribute("open") === null) { + await evidence.locator(":scope > summary").focus(); + await page.keyboard.press("Enter"); + } + await evidence.getByRole("button", { name: "刷新快照", exact: true }).click(); await map.getByText("部分工作未出现在此地图中。").waitFor(); assert.equal(await page.locator("[role=alert]", { hasText: "工作区状态已在此快照之后变化" }).count(), 0); assert.ok(reads.length <= 4, `Delivery review re-read ${reads.length} times`); diff --git a/examples/personal-workspace-browser/goal-workspace-m2.mjs b/examples/personal-workspace-browser/goal-workspace-m2.mjs index 9366047454..71118e9c33 100644 --- a/examples/personal-workspace-browser/goal-workspace-m2.mjs +++ b/examples/personal-workspace-browser/goal-workspace-m2.mjs @@ -86,6 +86,7 @@ export const goalWorkspaceM2Scenario = { await page.locator('.personal-context-drawer').waitFor(); await now.locator('[data-todo-id="showcase-reconcile"][aria-pressed="true"]').waitFor(); assert.equal(await now.locator('[data-todo-id="showcase-reconcile"]').getAttribute('aria-pressed'),'true'); + await page.screenshot({path:resolve(outputDir,'task-history-restored-desktop.png'),animations:'disabled'}); await page.goForward({waitUntil:"domcontentloaded"}); await page.waitForURL(value => !value.searchParams.has("todo")); await page.locator('.personal-context-drawer').waitFor({state:"hidden"}); @@ -96,6 +97,11 @@ export const goalWorkspaceM2Scenario = { await page.screenshot({path:resolve(outputDir,'goal-workspace-m2-mobile.png'),animations:'disabled'}); // An enabled contract uses the native acceptance disclosure; the // fallback direction button only exists without a contract or plan. + const evidence = page.locator(".goal-overview-evidence"); + if (await evidence.getAttribute("open") === null) { + await evidence.locator(":scope > summary").focus(); + await page.keyboard.press("Enter"); + } const inspection = page.locator(".goal-overview .delivery-check-inspection"); const criteria = inspection.locator(".goal-overview-criteria"); await page.locator(".goal-overview .delivery-acceptance-contract .goal-overview-objective").waitFor({state:"visible"}); @@ -120,6 +126,7 @@ export const goalWorkspaceM2Scenario = { await railDialog.waitFor({state:"hidden"}); await page.locator('.personal-context-drawer').waitFor(); assert.equal(new URL(page.url()).searchParams.get("todo"),"showcase-map-columns"); + await page.screenshot({path:resolve(outputDir,'task-history-restored-narrow.png'),animations:'disabled'}); await page.locator('.personal-drawer-close').click(); await page.setViewportSize({width:1440,height:1000}); scale = true; diff --git a/examples/personal-workspace-browser/lark-cli-missing.mjs b/examples/personal-workspace-browser/lark-cli-missing.mjs index 4b30a7d9ec..48dc39f39e 100644 --- a/examples/personal-workspace-browser/lark-cli-missing.mjs +++ b/examples/personal-workspace-browser/lark-cli-missing.mjs @@ -57,9 +57,12 @@ export const larkCliMissingScenario = { // With lark-cli available again the same entries stay usable. await page.unroute(larkReads, answerCliMissing); await page.reload({ waitUntil: "networkidle" }); - await page.getByTestId("personal-goal-home").waitFor({ state: "visible" }); - await page.getByRole("button", { name: "设置", exact: true }).click(); - await page.getByRole("button", { name: "Lark", exact: true }).click(); + const restoredSettings = page.getByRole("region", { name: "设置", exact: true }); + await restoredSettings.waitFor({ state: "visible" }); + if (!await restoredSettings.getByRole("radio", { name: "本机设置", exact: true }).isChecked()) { + throw new Error("Reload did not retain the explicit device settings scope"); + } + await restoredSettings.getByRole("button", { name: "Lark", exact: true }).click(); await page.locator(".personal-lark-tabs").getByRole("button", { name: /^Lark Apps/u }).click(); if (await page.getByRole("button", { name: "新建 Lark App" }).isDisabled()) { throw new Error("New Lark App stayed disabled with lark-cli available"); diff --git a/examples/personal-workspace-browser/pr-review-agent-order.mjs b/examples/personal-workspace-browser/pr-review-agent-order.mjs index 910abbb749..029899c611 100644 --- a/examples/personal-workspace-browser/pr-review-agent-order.mjs +++ b/examples/personal-workspace-browser/pr-review-agent-order.mjs @@ -51,7 +51,7 @@ export const prReviewAgentOrderScenario = { const {page} = context; await page.getByRole("button", {name: "设置", exact: true}).click(); await page.locator(".personal-settings-tabs").getByRole("button", {name: "能力中心", exact: true}).click(); - await page.getByRole("radio", {name: "单个 Goal", exact: true}).check(); + await page.getByRole("radio", {name: "Goal 设置", exact: true}).check(); await page.getByRole("combobox", {name: "目标 Goal", exact: true}).selectOption("product-release"); await page.getByRole("navigation", {name: "Goal 能力目录"}).getByRole("button", {name: /Pull-request Review/}).click(); const detail = page.locator(".personal-capability-detail"); diff --git a/examples/personal-workspace-browser/replan-cadence.mjs b/examples/personal-workspace-browser/replan-cadence.mjs index d460e404cf..470e45d7ab 100644 --- a/examples/personal-workspace-browser/replan-cadence.mjs +++ b/examples/personal-workspace-browser/replan-cadence.mjs @@ -68,12 +68,13 @@ export const replanCadenceScenario = { if (!settled.ok()) throw new Error(await settled.text()); } - async function openMachineCadence() { - await page.getByRole("button", { name: "设置", exact: true }).click(); + async function selectMachineCadence() { + await page.getByRole("radio", { name: "本机设置", exact: true }).check(); await page.locator(".personal-settings-tabs").getByRole("button", { name: "能力中心", exact: true }).click(); await page.getByRole("navigation", { name: "机器能力目录" }).getByRole("button", { name: /Goal 复盘与步幅/ }).click(); } - await openMachineCadence(); + await page.getByRole("button", { name: "设置", exact: true }).click(); + await selectMachineCadence(); const unit = page.getByLabel(/^复核计数依据/); const count = page.getByLabel("两次复核间的数量", { exact: true }); const strategy = page.getByLabel(/^步幅策略/); @@ -90,7 +91,7 @@ export const replanCadenceScenario = { const preview = await request(base + "/preview", legacy); await request(base + "/apply", { ...legacy, expected_plan_revision: preview.plan_revision }); await page.reload({ waitUntil: "networkidle" }); - await openMachineCadence(); + await selectMachineCadence(); await waitForInputValue(count, "2"); if (await unit.inputValue() !== "completed_todos" || await count.inputValue() !== "2") throw new Error("Legacy cadence was reinterpreted"); let stored = (await request(base)).machine_configuration.namespaces.todo_replan_cadence; @@ -110,7 +111,7 @@ export const replanCadenceScenario = { if (stored.count_unit !== "effective_turns" || stored.count !== 3 || "completed_todos" in stored) throw new Error("Migration did not persist the selected unit"); if (stored.stride_strategy !== "evidence_conditioned_v1" || stored.schema_version !== "todo_replan_cadence_machine_defaults_v2") throw new Error("Machine stride version not recorded"); await page.screenshot({ path: resolve(outputDir, "replan-cadence-machine.png"), animations: "disabled" }); - await page.getByRole("radio", { name: "单个 Goal", exact: true }).check(); + await page.getByRole("radio", { name: "Goal 设置", exact: true }).check(); await page.getByRole("combobox", { name: "目标 Goal", exact: true }).selectOption("multi-agent-projection"); await page.getByRole("navigation", { name: "Goal 能力目录" }).getByRole("button", { name: /Goal 复盘与步幅/ }).click(); await waitForInputValue(count, "3"); @@ -148,11 +149,15 @@ export const replanCadenceScenario = { } async function reopenGoalCadence() { await page.reload({ waitUntil: "networkidle" }); - await page.getByRole("button", { name: "设置", exact: true }).click(); - await page.locator(".personal-settings-tabs").getByRole("button", { name: "能力中心", exact: true }).click(); - await page.getByRole("radio", { name: "单个 Goal", exact: true }).check(); - await page.getByRole("combobox", { name: "目标 Goal", exact: true }).selectOption("multi-agent-projection"); - await page.getByRole("navigation", { name: "Goal 能力目录" }).getByRole("button", { name: /Goal 复盘与步幅/ }).click(); + const english = await page.locator("html").getAttribute("lang") === "en"; + const settings = page.getByRole("region", { name: english ? "Settings" : "设置", exact: true }); + await settings.waitFor(); + if (!await settings.getByRole("radio", { name: english ? "One Goal" : "Goal 设置", exact: true }).isChecked() + || await settings.getByRole("combobox", { name: english ? "Target Goal" : "目标 Goal", exact: true }).inputValue() !== "multi-agent-projection") { + throw new Error("Reload did not restore the explicit Goal settings target"); + } + await settings.getByRole("navigation", { name: english ? "Goal capability catalog" : "Goal 能力目录" }) + .getByRole("button", { name: english ? /Goal review and stride/ : /Goal 复盘与步幅/ }).click(); } // Default context is visible to the Agent without an owner setup step. // Advanced opt-out persists without pinning cadence or changing stride. @@ -270,12 +275,7 @@ export const replanCadenceScenario = { if (await page.evaluate(() => document.documentElement.scrollWidth > innerWidth + 1)) throw new Error("Cadence editor overflowed"); await page.setViewportSize({ width: 1512, height: 982 }); await page.evaluate(() => localStorage.setItem("loopx-pw-locale", "en")); - await page.reload({ waitUntil: "networkidle" }); - await page.getByRole("button", { name: "Settings", exact: true }).click(); - await page.locator(".personal-settings-tabs").getByRole("button", { name: "Capability Center", exact: true }).click(); - await page.getByRole("radio", { name: "One Goal", exact: true }).check(); - await page.getByRole("combobox", { name: "Target Goal", exact: true }).selectOption("multi-agent-projection"); - await page.getByRole("navigation", { name: "Goal capability catalog" }).getByRole("button", { name: /Goal review and stride/ }).click(); + await reopenGoalCadence(); const englishUnit = page.getByLabel(/^Review after/); const englishCount = page.getByLabel("Number between reviews", { exact: true }); const englishStrategy = page.getByLabel(/^Stride strategy/); @@ -296,12 +296,7 @@ export const replanCadenceScenario = { await request(base + "/apply", { ...remove, expected_plan_revision: removalPreview.plan_revision }); const inherited = (await readGoalCadence()).effective_configuration; if (inherited.source !== "capability_default" || inherited.configuration.count_unit !== "effective_turns" || inherited.configuration.count !== 6) throw new Error("Removing device policy did not restore product default"); - await page.reload({ waitUntil: "networkidle" }); - await page.getByRole("button", { name: "Settings", exact: true }).click(); - await page.locator(".personal-settings-tabs").getByRole("button", { name: "Capability Center", exact: true }).click(); - await page.getByRole("radio", { name: "One Goal", exact: true }).check(); - await page.getByRole("combobox", { name: "Target Goal", exact: true }).selectOption("multi-agent-projection"); - await page.getByRole("navigation", { name: "Goal capability catalog" }).getByRole("button", { name: /Goal review and stride/ }).click(); + await reopenGoalCadence(); await waitForInputValue(englishCount, "6"); if (await englishUnit.inputValue() !== "effective_turns" || await englishStrategy.inputValue() !== "off") throw new Error("Goal editor did not restore product defaults"); await englishCount.fill("5"); diff --git a/examples/personal-workspace-browser/typed-actions.mjs b/examples/personal-workspace-browser/typed-actions.mjs index 8d0e569aaa..8b0a860afe 100644 --- a/examples/personal-workspace-browser/typed-actions.mjs +++ b/examples/personal-workspace-browser/typed-actions.mjs @@ -36,6 +36,23 @@ async function waitForSelectorCount(page, selector, expected, timeoutMs = 10_000 ); } +async function openEmptyFiles(page, navigation) { + const goalId = new URL(page.url()).searchParams.get("goalId"); + assert.ok(goalId, "Files must belong to the selected Goal"); + const inventoryRead = page.waitForResponse(response => { + const request = new URL(response.url()); + return request.pathname === "/api/chat/goal-results" && request.searchParams.get("goal_id") === goalId; + }); + await navigation.getByRole("button", {name: /^(Files|成果)$/, exact: true}).click(); + const response = await inventoryRead; + assert.equal(response.status(), 200); + assert.deepEqual(await response.json(), { + ok: true, items: [], total: 0, next_cursor: null, unavailable_count: 0, unavailable_todo_ids: [], + }, "Files must read the actual empty report inventory before omitting its extra section"); + await page.getByRole("region", {name: /^(Content artifacts|内容作品)$/, exact: true}).waitFor({state: "visible"}); + await page.waitForFunction(() => !document.querySelector('[data-goal-panel="files"] .goal-managed-results')); +} + function operationProposal({ id, title, lifecycleState, status, resultDelivery = null }) { const outcomeObserved = lifecycleState === "outcome_observed"; return { @@ -695,9 +712,8 @@ export const typedActionsScenario = { const englishGoalNavigation = page.getByRole("navigation", { name: "Goal view" }); await englishGoalNavigation.getByRole("button", { name: /^(Chat|对话)$/, exact: true }).click(); await page.locator('[data-goal-panel="chat"]').getByText("Agent is waiting for your decision", { exact: true }).first().waitFor({ state: "visible" }); - await englishGoalNavigation.getByRole("button", { name: /^(Files|成果)$/, exact: true }).click(); + await openEmptyFiles(page, englishGoalNavigation); if (await page.locator('[data-output-kind="evidence"]').count() !== 0) throw new Error("Run status appeared as a delivered file"); - await page.getByRole("region", { name: "Accepted team reports" }).getByText("No verifiable team reports yet.", { exact: true }).waitFor({ state: "visible" }); const englishProjectionText = await page.locator(".personal-workspace-main").innerText(); for (const forbidden of ["最近运行", "最近验证", "Agent 正在整理下一步", "Agent 正在推进当前 Goal", "Agent 等待你的决定"]) { if (englishProjectionText.includes(forbidden)) throw new Error(`English projection exposed Chinese UI copy ${forbidden}: ${englishProjectionText}`); @@ -845,10 +861,9 @@ export const typedActionsScenario = { const tasksTab = goalNavigation.getByRole("button", { name: /^(Tasks|任务)$/ }); await tasksTab.click(); await page.screenshot({ path: resolve(outputDir, "goal-tasks-loopx-theme.png"), fullPage: false, animations: "disabled" }); - await goalNavigation.getByRole("button", { name: /^(Files|成果)$/ }).click(); + await openEmptyFiles(page, goalNavigation); const publicFiles = page.locator(".personal-files-list > button"); if (await publicFiles.count() !== 0) throw new Error("A status or run observation appeared as a delivered file"); - await page.getByRole("region", { name: "已验收的团队报告" }).getByText("暂无可核验的团队报告。", { exact: true }).waitFor({ state: "visible" }); await page.screenshot({ path: resolve(outputDir, "goal-files-loopx-theme.png"), fullPage: false, animations: "disabled" }); await goalNavigation.getByRole("button", { name: /^(Chat|对话)$/ }).click(); await page.locator(".personal-channel-timeline").waitFor({ state: "visible" }); diff --git a/pyproject.toml b/pyproject.toml index ab2d653c43..444fa689ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -195,8 +195,8 @@ include = ["loopx*"] [tool.pytest.ini_options] markers = ["stage2c_e2e: real process Stage 2C correctness and recovery acceptance"] -# Private snapshots and packaging copies are not source tests; optional provider tests run in ark-turn.yml. -norecursedirs = [".local", "build", "dist", "deprecate", "packages/loopx-ark-turn"] +# Private snapshots and packaging copies are not source tests; optional provider suites have dedicated workflows. +norecursedirs = [".local", "build", "dist", "deprecate", "packages/loopx-ark-turn", "packages/loopx-memt-turn"] [tool.coverage.run] source = ["loopx"] diff --git a/tests/control_plane_ts/content_digest_single_owner.test.ts b/tests/control_plane_ts/content_digest_single_owner.test.ts index 0417686ba3..4adffe2fbd 100644 --- a/tests/control_plane_ts/content_digest_single_owner.test.ts +++ b/tests/control_plane_ts/content_digest_single_owner.test.ts @@ -93,6 +93,8 @@ const CANONICAL_CONSUMERS = [ "control_plane/agents/supervisor_event_append.ts", "control_plane/capabilities/change_quality_validation.ts", "control_plane/capabilities/content_media.ts", + "control_plane/capabilities/content_media_generation.ts", + "control_plane/capabilities/content_media_image.ts", "control_plane/capabilities/explore_execution_observation.ts", "control_plane/capabilities/explore_result_page.ts", "control_plane/capabilities/external_evidence.ts", diff --git a/tests/test_python_ci_workflow.py b/tests/test_python_ci_workflow.py index fef989bf2b..c7cb9bb462 100644 --- a/tests/test_python_ci_workflow.py +++ b/tests/test_python_ci_workflow.py @@ -17,6 +17,33 @@ WORKFLOW = (WORKFLOW_ROOT / ".github/workflows/python-tests.yml").read_text(encoding="utf-8") +@pytest.mark.parametrize("provider", ["loopx-ark-turn", "loopx-memt-turn"]) +def test_core_collection_keeps_optional_provider_tests_explicit(tmp_path: Path, provider: str) -> None: + (tmp_path / "pyproject.toml").write_text( + (WORKFLOW_ROOT / "pyproject.toml").read_text(encoding="utf-8"), encoding="utf-8", + ) + (tmp_path / "test_core.py").write_text("def test_core_contract(): pass\n", encoding="utf-8") + optional = tmp_path / "packages" / provider / "tests" + optional.mkdir(parents=True) + (optional / "test_provider.py").write_text("def test_optional_contract(): pass\n", encoding="utf-8") + (optional / "conftest.py").write_text( + "from pathlib import Path\nPath(__file__).with_name('dependency-imported').touch()\n", encoding="utf-8", + ) + args = [sys.executable, "-m", "pytest", "--collect-only", "-q"] + options = {"cwd": tmp_path, "env": {**os.environ, "PYTEST_DISABLE_PLUGIN_AUTOLOAD": "1"}, + "capture_output": True, "text": True, "timeout": 15, "check": False} + core = subprocess.run(args, **options) + assert core.returncode == 0, core.stdout + core.stderr + assert "test_core_contract" in core.stdout + assert "test_optional_contract" not in core.stdout, "Core discovery must not import optional provider dependencies" + assert not (optional / "dependency-imported").exists() + explicit = subprocess.run([*args, str(optional.relative_to(tmp_path))], **options) + assert explicit.returncode == 0, explicit.stdout + explicit.stderr + assert "test_optional_contract" in explicit.stdout, "The dedicated provider workflow must still collect its suite" + assert (optional / "dependency-imported").exists() + assert "test_core_contract" not in explicit.stdout + + def test_dashboard_acceptance_and_kernel_checks_run_independently() -> None: kernel = WORKFLOW.split(" kernel-static-checks:\n", 1)[1].split( " dashboard-browser:\n", 1,