Task nodes carry wait_kind (external | human | both | unqualified, null unless wait): the hub field wins, otherwise derived from needs_human and dependency edges (task-level depends_on, or a workplan dependency row whose target the task cites). Workplan nodes carry blocked_kind (human | external | none, null unless blocked). Task-level depends_on becomes a task -> workplan/task depends_on edge; depends_on edges carry edge_kind commitment (cyan #0891b2), human gates are marked on the node (humanGate, rose #be123c). A needs_human=true query / "Needs human" toggle / --needs-human flag keeps only blocked_kind=human workplans and their human-wait tasks; the parameter survives in the shareable URL and mode switch like include_residuals. Fabric stays a read of State Hub. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: sonnet Assistant-Process: 237582@bnt-lap001 Assistant-Session: f2b3d9f1-8fb9-4b9c-bc2b-837ec5dfc826
81 lines
4 KiB
Python
81 lines
4 KiB
Python
"""Execute the browser transition against a small graph/DOM harness."""
|
|
import shutil
|
|
import subprocess
|
|
|
|
import pytest
|
|
|
|
from railiance_fabric.graph_explorer_ui import graph_explorer_page
|
|
|
|
|
|
def test_graph_switch_replaces_data_and_retains_previous_graph_on_failure(tmp_path):
|
|
if not shutil.which("node"):
|
|
pytest.skip("Node.js is required for the UI transition test")
|
|
page = graph_explorer_page()
|
|
transition = page.split(" let graphRequest = 0;", 1)[1].split(
|
|
" const handleFilterChange", 1
|
|
)[0]
|
|
script = r'''
|
|
const assert = require("node:assert/strict");
|
|
let graphMode = "full", includeResidualsParam = false, needsHumanParam = false, activeMode = "full";
|
|
const coordinationUrl = (includeResiduals, needsHuman) =>
|
|
`/exports/graph-explorer?mode=coordination${includeResiduals ? "&include_residuals=true" : ""}${needsHuman ? "&needs_human=true" : ""}`;
|
|
let selected, selectedAnchor, selectedZoneId, focusSet, manualOverrides, filterRules;
|
|
let editingRuleId, currentProfileId, allEdgeTypes;
|
|
const layerColors = {}, allNodeTypes = [], nodeTypeLabels = {};
|
|
const collapsedZoneSnapshots = new Map(), zoneContainerState = new Map(), zoneSummaries = new Map();
|
|
const modeSelect = {}, profileSelect = {}, nodeTypeFilter = {}, edgeTypeFilter = {};
|
|
const popup = {style: {}}, profileSummary = {};
|
|
const controls = {};
|
|
const document = {getElementById(id) {return controls[id] ||= {style: {}};}};
|
|
let data = [{data: {id: "old"}}], urls = [], updated = 0;
|
|
const cy = {elements: () => ({remove() {data = [];}}), add(elements) {data.push(...elements);}};
|
|
const renderChecklist = () => {}, refreshRuleBuilder = () => {}, renderRules = () => {};
|
|
const syncFilterSummaries = () => {}, applyFilters = () => {}, runLayout = () => {};
|
|
const updateUrlState = () => {updated++;}, showDetails = () => {};
|
|
const updateProfileControls = () => {}, updateProfileSummary = () => {};
|
|
const applyViewState = () => {};
|
|
let response = {ok: true, json: async () => ({elements: [{data: {id: "workplan:a"}}]})};
|
|
let fetch = async (url) => {urls.push(url); return response;};
|
|
let graphRequest = 0;
|
|
'''
|
|
script += transition + r'''
|
|
(async () => {
|
|
await switchGraph("coordination", true);
|
|
assert.equal(data[0].data.id, "workplan:a");
|
|
assert.equal(modeSelect.value, "coordination");
|
|
assert.equal(controls["include-residuals-wrap"].style.display, "");
|
|
assert.match(urls[0], /include_residuals=true/);
|
|
assert.doesNotMatch(urls[0], /needs_human/);
|
|
await switchGraph("coordination", true, null, true);
|
|
assert.match(urls[1], /include_residuals=true&needs_human=true/);
|
|
assert.equal(needsHumanParam, true);
|
|
assert.equal(controls["needs-human"].checked, true);
|
|
assert.equal(controls["needs-human-wrap"].style.display, "");
|
|
await switchGraph("coordination", true, null, false);
|
|
assert.equal(needsHumanParam, false);
|
|
response = {ok: false, status: 503};
|
|
await switchGraph("full");
|
|
assert.equal(data[0].data.id, "workplan:a");
|
|
assert.equal(modeSelect.value, "coordination");
|
|
assert.match(profileSummary.textContent, /503/);
|
|
response = {ok: true, json: async () => ({elements: []})};
|
|
await switchGraph("by-access-zone");
|
|
assert.deepEqual(data, []);
|
|
assert.equal(activeMode, "by-access-zone");
|
|
assert.equal(graphMode, "full");
|
|
assert.equal(controls["include-residuals-wrap"].style.display, "none");
|
|
assert.equal(controls["needs-human-wrap"].style.display, "none");
|
|
let resolveOld;
|
|
fetch = () => new Promise(resolve => {resolveOld = resolve;});
|
|
const oldRequest = switchGraph("coordination");
|
|
fetch = async () => ({ok: true, json: async () => ({elements: [{data: {id: "latest"}}]})});
|
|
await switchGraph("full");
|
|
resolveOld({ok: true, json: async () => ({elements: [{data: {id: "stale"}}]})});
|
|
await oldRequest;
|
|
assert.equal(data[0].data.id, "latest");
|
|
assert.equal(updated, 5);
|
|
})().catch(error => {console.error(error); process.exitCode = 1;});
|
|
'''
|
|
path = tmp_path / "switch.cjs"
|
|
path.write_text(script)
|
|
subprocess.run(["node", str(path)], check=True, capture_output=True, text=True)
|