@nanobpm/nano-workforce 0.139.0 → 0.139.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/pages/delivery-graphs/library.mount.js +15 -5
- package/pages/delivery-graphs/mount.js +14 -5
- package/pages/delivery-graphs/staged.mount.js +17 -7
- package/pages/delivery-graphs.page.json +9 -9
- package/test/delivery-graphs-embed.test.ts +39 -16
- package/test/delivery-graphs-import.test.ts +28 -10
- package/test/delivery-graphs-library-embed.test.ts +44 -14
- package/test/delivery-graphs-staged-embed.test.ts +63 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [0.139.2](https://github.com/nanobpm/nano-workforce/compare/v0.139.1...v0.139.2) (2026-08-24)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* **delivery-graphs:** stop the in-flight grid headers wrapping to 0-width columns ([#538](https://github.com/nanobpm/nano-workforce/issues/538)) ([#539](https://github.com/nanobpm/nano-workforce/issues/539)) ([c52e85b](https://github.com/nanobpm/nano-workforce/commit/c52e85bf38edef680914df0336ff95c98a364059))
|
|
6
|
+
|
|
7
|
+
## [0.139.1](https://github.com/nanobpm/nano-workforce/compare/v0.139.0...v0.139.1) (2026-08-24)
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **delivery-graphs:** anchor App-View door defaults to import.meta.url ([#536](https://github.com/nanobpm/nano-workforce/issues/536)) ([#537](https://github.com/nanobpm/nano-workforce/issues/537)) ([58cd57e](https://github.com/nanobpm/nano-workforce/commit/58cd57e7f35937a9d1720e2fd4d938f70ce8f304)), closes [#279](https://github.com/nanobpm/nano-workforce/issues/279) [#467](https://github.com/nanobpm/nano-workforce/issues/467) [#279-only](https://github.com/nanobpm/nano-workforce/issues/279-only) [#279](https://github.com/nanobpm/nano-workforce/issues/279) [#279](https://github.com/nanobpm/nano-workforce/issues/279)
|
|
12
|
+
|
|
1
13
|
## [0.139.0](https://github.com/nanobpm/nano-workforce/compare/v0.138.3...v0.139.0) (2026-08-24)
|
|
2
14
|
|
|
3
15
|
### Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.139.
|
|
3
|
+
"version": "0.139.2",
|
|
4
4
|
"description": "Nano Workforce — an Agent Graph Orchestration application for Agentic SDLC: durable BPMN processes that coordinate a graph of AI agents across the software delivery lifecycle.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "main.ts",
|
|
@@ -15,9 +15,18 @@
|
|
|
15
15
|
|
|
16
16
|
import { DG_COMPOSE_FILL_MESSAGE } from "./mount.js";
|
|
17
17
|
|
|
18
|
-
// The read behind the list: every saved library entry, newest first
|
|
19
|
-
//
|
|
20
|
-
|
|
18
|
+
// The read behind the list: every saved library entry, newest first. Anchored to THIS MODULE's url
|
|
19
|
+
// (import.meta.url), NOT the document base. The library App-View shell (library-embed.html /
|
|
20
|
+
// library-standalone.html) — and therefore this mount.js — is served ONE DIRECTORY DEEP at
|
|
21
|
+
// `<appMount>/delivery-graphs/`, while the API is a sibling of that dir at `<appMount>/app/api/…`. A
|
|
22
|
+
// document-base-relative default (`"app/api/delivery-graph/library"`) resolves against the
|
|
23
|
+
// `…/delivery-graphs/` shell base to `…/delivery-graphs/app/api/delivery-graph/library` → 404 on EVERY
|
|
24
|
+
// surface (standalone, local urban-SPA App-View, and the Studio console App-View, which never injects
|
|
25
|
+
// window.__NANO_APP_VIEW__ so this default is what runs). A leading-slash absolute is worse still:
|
|
26
|
+
// through Studio it resolves against the console ORIGIN, not the app-view base (#279). `../app/api/…`
|
|
27
|
+
// off import.meta.url steps up out of `/delivery-graphs/` and lands on `<appMount>/app/api/…` on all
|
|
28
|
+
// surfaces regardless of the document base — the same fix the preview/stage doors ship (#467/#536).
|
|
29
|
+
const DEFAULT_LIBRARY_URL = new URL("../app/api/delivery-graph/library", import.meta.url).href;
|
|
21
30
|
|
|
22
31
|
// How often the list re-polls so a freshly-saved (or just-deleted) entry appears (or drops off)
|
|
23
32
|
// without a manual refresh — mirrors the 5s cadence the staged list uses.
|
|
@@ -145,10 +154,11 @@ export function mountDeliveryGraphLibrary(host, config = {}) {
|
|
|
145
154
|
...(config.hookSecret && isSameOrigin(url) ? { "x-hook-secret": config.hookSecret } : {}),
|
|
146
155
|
});
|
|
147
156
|
|
|
148
|
-
// The delete door is the per-entry path under the list door: DELETE
|
|
157
|
+
// The delete door is the per-entry path under the list door: DELETE .../delivery-graph/library/<id>.
|
|
149
158
|
// Append the id to the path only, preserving any query/hash on the configured door so a
|
|
150
159
|
// `?library=` override carrying a search string or fragment still resolves the per-entry URL.
|
|
151
|
-
//
|
|
160
|
+
// Derived by string-appending to `libraryUrl` (no fresh `new URL`) so it inherits the same
|
|
161
|
+
// module-anchored (`../app/api/…`, #467/#536) resolution as the list door it hangs off.
|
|
152
162
|
const entryUrl = (id) => {
|
|
153
163
|
const [beforeHash, hash = ""] = libraryUrl.split("#");
|
|
154
164
|
const [path, search = ""] = beforeHash.split("?");
|
|
@@ -15,14 +15,23 @@
|
|
|
15
15
|
// View) and standalone on a phone — only the host element and injected endpoint config differ. The app
|
|
16
16
|
// has no browser build step, so this consumes the preview/stage doors straight off the wire.
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
// Door defaults are anchored to THIS MODULE's url (import.meta.url), NOT the document base. The compose
|
|
19
|
+
// shell (embed.html / standalone.html) — and therefore this mount.js — is served ONE DIRECTORY DEEP at
|
|
20
|
+
// `<appMount>/delivery-graphs/`, while the preview/stage doors are siblings of that dir at
|
|
21
|
+
// `<appMount>/app/api/…`. A document-base-relative default resolves against the `…/delivery-graphs/`
|
|
22
|
+
// shell base → `…/delivery-graphs/app/api/…` → 404 on every surface (the Studio console App-View never
|
|
23
|
+
// injects window.__NANO_APP_VIEW__, so the default is what runs); a leading-slash absolute resolves
|
|
24
|
+
// against the console ORIGIN through Studio (#279). `../app/api/…` off import.meta.url steps up out of
|
|
25
|
+
// `/delivery-graphs/` onto `<appMount>/app/api/…` on all surfaces — the cockpit's #467 fix.
|
|
26
|
+
const DEFAULT_PREVIEW_URL = new URL("../app/api/actions/delivery-graph/preview", import.meta.url).href;
|
|
27
|
+
const DEFAULT_STAGE_URL = new URL("../app/api/actions/delivery-graph/stage", import.meta.url).href;
|
|
20
28
|
|
|
21
29
|
// The filesystem IMPORT door (issue #524, epic #519 S5). The Import control below reads a chosen
|
|
22
30
|
// `.json` file's text client-side and POSTs it here; the door validates + compiles it and persists it
|
|
23
|
-
// to the library with `source: imported`.
|
|
24
|
-
// resolution class — a
|
|
25
|
-
|
|
31
|
+
// to the library with `source: imported`. Module-anchored off import.meta.url like the preview/stage
|
|
32
|
+
// defaults (App-View #279/#467/#536 resolution class — a document-base-relative default 404s under the
|
|
33
|
+
// `…/delivery-graphs/` shell base, a leading-slash absolute resolves against the console origin).
|
|
34
|
+
const DEFAULT_IMPORT_URL = new URL("../app/api/actions/delivery-graph/library/import", import.meta.url).href;
|
|
26
35
|
|
|
27
36
|
// The INBOUND reuse-fill seam (issue #523, epic #519 S4). Until now the compose textarea (`#dg-json`)
|
|
28
37
|
// had NO inbound prefill path — its value was set only by "Load example" or the operator typing. The
|
|
@@ -17,21 +17,31 @@
|
|
|
17
17
|
// demand×supply board (pages/board/mount.js): the SAME module mounts embedded in the console (App View)
|
|
18
18
|
// and standalone — only the host element and injected endpoint config differ.
|
|
19
19
|
|
|
20
|
-
// The
|
|
21
|
-
//
|
|
22
|
-
|
|
20
|
+
// The door defaults are anchored to THIS MODULE's url (import.meta.url), NOT the document base. The
|
|
21
|
+
// staged App-View shell (staged-embed.html / staged-standalone.html) — and therefore this mount.js —
|
|
22
|
+
// is served ONE DIRECTORY DEEP at `<appMount>/delivery-graphs/`, while the API is a sibling of that
|
|
23
|
+
// dir at `<appMount>/app/api/…`. A document-base-relative default (`"app/api/delivery-graph/staged"`)
|
|
24
|
+
// resolves against the `…/delivery-graphs/` shell base to `…/delivery-graphs/app/api/delivery-graph/
|
|
25
|
+
// staged` → 404 on EVERY surface (standalone, local urban-SPA App-View, and the Studio console
|
|
26
|
+
// App-View, which never injects window.__NANO_APP_VIEW__ so this default is what runs) — the
|
|
27
|
+
// "Could not load staged proposals." bug. A leading-slash absolute is worse still: through Studio it
|
|
28
|
+
// resolves against the console ORIGIN, not the app-view base (#279). `../app/api/…` off
|
|
29
|
+
// import.meta.url steps up out of `/delivery-graphs/` and lands on `<appMount>/app/api/…` on all
|
|
30
|
+
// three surfaces regardless of the document base — the same fix the cockpit shipped for #467.
|
|
31
|
+
// The read behind the list: every live staged proposal, newest first.
|
|
32
|
+
const DEFAULT_STAGED_URL = new URL("../app/api/delivery-graph/staged", import.meta.url).href;
|
|
23
33
|
// The operator dispatch door: POST { digest } → launches the staged graph engine-natively (#460).
|
|
24
|
-
const DEFAULT_DISPATCH_URL = "app/api/actions/delivery-graph/dispatch";
|
|
34
|
+
const DEFAULT_DISPATCH_URL = new URL("../app/api/actions/delivery-graph/dispatch", import.meta.url).href;
|
|
25
35
|
// The operator dismiss door: POST { digest } → discards a staged proposal as noise, flipping it to the
|
|
26
36
|
// terminal `dismissed` status so it drops off the staged list (#520). Launches nothing.
|
|
27
|
-
const DEFAULT_DISMISS_URL = "app/api/actions/delivery-graph/dismiss";
|
|
37
|
+
const DEFAULT_DISMISS_URL = new URL("../app/api/actions/delivery-graph/dismiss", import.meta.url).href;
|
|
28
38
|
// The read-only DI preview door: recompiles a staged proposal's BPMN (with diagram interchange) so its
|
|
29
39
|
// generated diagram can be rendered in the host explorer BEFORE dispatch. No deploy, no dispatch.
|
|
30
|
-
const DEFAULT_PROPOSAL_BPMN_URL = "app/api/actions/delivery-graph/proposal-bpmn";
|
|
40
|
+
const DEFAULT_PROPOSAL_BPMN_URL = new URL("../app/api/actions/delivery-graph/proposal-bpmn", import.meta.url).href;
|
|
31
41
|
// The save-to-library door: POST { name, digest } → copies this staged proposal's already-stored graph
|
|
32
42
|
// into the reusable library (issue #523, save-from-digest → source `from-staged`). Persists a library
|
|
33
43
|
// entry; it never dispatches or re-stages, so the #460 operator boundary holds.
|
|
34
|
-
const DEFAULT_SAVE_LIBRARY_URL = "app/api/actions/delivery-graph/library/save";
|
|
44
|
+
const DEFAULT_SAVE_LIBRARY_URL = new URL("../app/api/actions/delivery-graph/library/save", import.meta.url).href;
|
|
35
45
|
|
|
36
46
|
// How often the list re-polls the read door so a freshly-staged (or just-dispatched) proposal appears
|
|
37
47
|
// (or drops off) without a manual refresh — mirrors the 5s cadence the old declarative grid used.
|
|
@@ -128,15 +128,15 @@
|
|
|
128
128
|
{ "label": "All", "filter": [] }
|
|
129
129
|
],
|
|
130
130
|
"columns": [
|
|
131
|
-
{ "field": "title", "template": "{{title}}", "header": "Graph", "subtitleField": "run_key", "truncate": true, "width": "
|
|
132
|
-
{ "field": "status", "header": "Status", "link": { "kind": "processExplorer", "keyField": "process_key" } },
|
|
133
|
-
{ "field": "process_key", "header": "Instance", "width": "
|
|
134
|
-
{ "field": "phase", "header": "Phase", "truncate": true, "width": "
|
|
135
|
-
{ "field": "node_count", "header": "Nodes" },
|
|
136
|
-
{ "field": "human_node_count", "header": "Human" },
|
|
137
|
-
{ "field": "side_effect_count", "header": "
|
|
138
|
-
{ "field": "created_at", "header": "Dispatched", "width": "
|
|
139
|
-
{ "field": "updated_at", "header": "Updated", "width": "
|
|
131
|
+
{ "field": "title", "template": "{{title}}", "header": "Graph", "subtitleField": "run_key", "truncate": true, "width": "20%", "link": { "kind": "page", "page": "delivery-graph-detail", "keyField": "run_key" } },
|
|
132
|
+
{ "field": "status", "header": "Status", "truncate": true, "width": "12%", "link": { "kind": "processExplorer", "keyField": "process_key" } },
|
|
133
|
+
{ "field": "process_key", "header": "Instance", "width": "9%", "truncate": true, "link": { "kind": "processExplorer", "keyField": "process_key" } },
|
|
134
|
+
{ "field": "phase", "header": "Phase", "truncate": true, "width": "15%" },
|
|
135
|
+
{ "field": "node_count", "header": "Nodes", "width": "5%" },
|
|
136
|
+
{ "field": "human_node_count", "header": "Human", "width": "5%" },
|
|
137
|
+
{ "field": "side_effect_count", "header": "Effects", "width": "7%" },
|
|
138
|
+
{ "field": "created_at", "header": "Dispatched", "width": "11%", "truncate": true, "format": "datetime" },
|
|
139
|
+
{ "field": "updated_at", "header": "Updated", "width": "11%", "truncate": true, "format": "datetime" }
|
|
140
140
|
],
|
|
141
141
|
"detail": {
|
|
142
142
|
"fields": [
|
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
// Stage are SEPARATE operator actions (#516): Preview compiles without persisting; Stage persists a
|
|
7
7
|
// proposal. Dispatch is deliberately NOT in this view (issue #460): it is an OPERATOR row-action on the
|
|
8
8
|
// Staged proposals grid on the same page. This test pins the wiring so it can't silently regress: the
|
|
9
|
-
// sidecars exist, mount.js hits the preview + stage doors with
|
|
10
|
-
// App-View resolution class —
|
|
11
|
-
//
|
|
12
|
-
//
|
|
9
|
+
// sidecars exist, mount.js hits the preview + stage doors with MODULE-ANCHORED defaults (`new
|
|
10
|
+
// URL("../app/api/…", import.meta.url)`, the #467 App-View resolution class — this module is served one
|
|
11
|
+
// dir deep under `/delivery-graphs/`, so a document-base-relative or leading-slash default 404s; see
|
|
12
|
+
// test/delivery-graphs-staged-embed.test.ts and #536), it renders each preview facet, its compose panel
|
|
13
|
+
// is collapsible, and it exposes NO dispatch/approval affordance (the self-approval hole #460 closes).
|
|
13
14
|
import { test } from "node:test";
|
|
14
|
-
import { assert } from "#test-assert";
|
|
15
|
+
import { assert, assertEquals } from "#test-assert";
|
|
15
16
|
import { readFileSync } from "node:fs";
|
|
16
17
|
import { parseAndCompileText } from "../app/deliveryGraphTextIngress.ts";
|
|
17
18
|
import { EXAMPLE_GRAPH } from "../pages/delivery-graphs/mount.js";
|
|
@@ -24,15 +25,41 @@ const STANDALONE_HTML = readFileSync(`${DIR}/standalone.html`, "utf8");
|
|
|
24
25
|
const CSS = readFileSync(`${DIR}/delivery-graphs.css`, "utf8");
|
|
25
26
|
const PAGE_JSON = readFileSync(`${ROOT}pages/delivery-graphs.page.json`, "utf8");
|
|
26
27
|
|
|
27
|
-
//
|
|
28
|
-
|
|
28
|
+
// The REAL served location of mount.js on each surface: one directory deep under `/delivery-graphs/`.
|
|
29
|
+
const STANDALONE_MOUNT = "http://127.0.0.1:3000/delivery-graphs/mount.js";
|
|
30
|
+
const STUDIO_MOUNT = "http://studio-host:8080/console/app-view/Workforce/delivery-graphs/mount.js";
|
|
31
|
+
|
|
32
|
+
// Pull the module-anchored default spec out of `const <name> = config.<field> ?? <CONST>;` where the
|
|
33
|
+
// CONST is declared `const <CONST> = new URL("<spec>", import.meta.url).href;`.
|
|
34
|
+
function defaultSpec(name: string): string {
|
|
29
35
|
const m = MOUNT_JS.match(new RegExp(`${name}\\s*=\\s*config\\.\\w+\\s*\\?\\?\\s*(\\w+);`));
|
|
30
36
|
assert(m, `mount.js must default ${name} from config with a fallback constant`);
|
|
31
|
-
const constM = MOUNT_JS.match(
|
|
32
|
-
|
|
37
|
+
const constM = MOUNT_JS.match(
|
|
38
|
+
new RegExp(`const ${m![1]}\\s*=\\s*new URL\\(\\s*"([^"]*)"\\s*,\\s*import\\.meta\\.url\\s*\\)\\s*\\.href`),
|
|
39
|
+
);
|
|
40
|
+
assert(
|
|
41
|
+
constM,
|
|
42
|
+
`mount.js must default ${m![1]} to new URL("<spec>", import.meta.url) so the door is anchored to the ` +
|
|
43
|
+
`module's own served location, not the document base (#467/#536)`,
|
|
44
|
+
);
|
|
33
45
|
return constM![1];
|
|
34
46
|
}
|
|
35
47
|
|
|
48
|
+
// A module-anchored door default must (a) not be absolute (#279), (b) step up out of /delivery-graphs/
|
|
49
|
+
// (#467), and (c) resolve onto <appMount>/app/api/… both standalone AND inside the Studio iframe.
|
|
50
|
+
function assertModuleAnchored(name: string, endpoint: string): void {
|
|
51
|
+
const spec = defaultSpec(name);
|
|
52
|
+
assert(!spec.startsWith("/"), `default ${name} spec "${spec}" must not be absolute (resolves against console origin, #279)`);
|
|
53
|
+
assert(spec.startsWith("../"), `default ${name} spec "${spec}" must step up out of /delivery-graphs/ (#467)`);
|
|
54
|
+
assert(spec.endsWith(endpoint), `default ${name} spec "${spec}" must hit the ${endpoint} door`);
|
|
55
|
+
assertEquals(new URL(spec, STANDALONE_MOUNT).href, `http://127.0.0.1:3000/${endpoint}`, `default ${name} must resolve to the app root (#467)`);
|
|
56
|
+
assertEquals(
|
|
57
|
+
new URL(spec, STUDIO_MOUNT).href,
|
|
58
|
+
`http://studio-host:8080/console/app-view/Workforce/${endpoint}`,
|
|
59
|
+
`default ${name} must resolve under the app-view base, not the console origin (#279) nor the /delivery-graphs/ base (#467/#536)`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
36
63
|
test("#441: the delivery-graphs App View mounts the same module standalone and embedded", () => {
|
|
37
64
|
assert(/mountDeliveryGraphs/.test(MOUNT_JS), "mount.js must export mountDeliveryGraphs");
|
|
38
65
|
for (const [file, html] of [["embed.html", EMBED_HTML], ["standalone.html", STANDALONE_HTML]] as const) {
|
|
@@ -41,13 +68,9 @@ test("#441: the delivery-graphs App View mounts the same module standalone and e
|
|
|
41
68
|
}
|
|
42
69
|
});
|
|
43
70
|
|
|
44
|
-
test("#516: mount.js wires SEPARATE preview and stage doors
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
assert(!previewUrl.startsWith("/"), `default previewUrl "${previewUrl}" must be base-relative (App-View #279 resolution class)`);
|
|
48
|
-
const stageUrl = defaultUrl("stageUrl");
|
|
49
|
-
assert(stageUrl.endsWith("actions/delivery-graph/stage"), `stageUrl default "${stageUrl}" must hit the stageDeliveryGraph door`);
|
|
50
|
-
assert(!stageUrl.startsWith("/"), `default stageUrl "${stageUrl}" must be base-relative (App-View #279 resolution class)`);
|
|
71
|
+
test("#516/#467: mount.js wires SEPARATE preview and stage doors, module-anchored to the app root", () => {
|
|
72
|
+
assertModuleAnchored("previewUrl", "app/api/actions/delivery-graph/preview");
|
|
73
|
+
assertModuleAnchored("stageUrl", "app/api/actions/delivery-graph/stage");
|
|
51
74
|
// Preview and Stage are distinct buttons wired to distinct actions.
|
|
52
75
|
assert(/id="dg-preview"/.test(MOUNT_JS) && /id="dg-stage"/.test(MOUNT_JS), "mount.js must render distinct Preview and Stage buttons");
|
|
53
76
|
assert(/submit\(previewUrl,\s*false\)/.test(MOUNT_JS), "the Preview button must submit to the preview door WITHOUT staging");
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// epic #519 S5). The Import control is ADDED into the PRE-EXISTING compose mount (pages/delivery-graphs/
|
|
3
3
|
// mount.js — the one #523 reshaped), alongside #523's inbound reuse-fill seam. It must: render an
|
|
4
4
|
// `<input type=file accept=.json>`, read the picked file's text client-side, POST it to the
|
|
5
|
-
// importToLibrary door (
|
|
5
|
+
// importToLibrary door (module-anchored relative spec, App-View #279 + #467/#536 resolution class), route a successful import back
|
|
6
6
|
// through #523's SINGLE `fillComposer()` seam (no second inbound fill path), and render path-qualified
|
|
7
7
|
// compile errors inline on a 400. This test pins that wiring so it can't silently regress.
|
|
8
8
|
import { test } from "node:test";
|
|
@@ -16,12 +16,22 @@ const CSS = readFileSync(`${DIR}/delivery-graphs.css`, "utf8");
|
|
|
16
16
|
const EMBED_HTML = readFileSync(`${DIR}/embed.html`, "utf8");
|
|
17
17
|
const STANDALONE_HTML = readFileSync(`${DIR}/standalone.html`, "utf8");
|
|
18
18
|
|
|
19
|
-
// Pull the
|
|
20
|
-
|
|
19
|
+
// Pull the module-anchored default URL *spec* (the "<spec>" literal inside
|
|
20
|
+
// `new URL("<spec>", import.meta.url)`, e.g. "../app/api/…") out of
|
|
21
|
+
// `const <name> = config.<name> ?? <CONST>;` where the CONST is declared
|
|
22
|
+
// `const <CONST> = new URL("<spec>", import.meta.url).href;`. This is the unresolved
|
|
23
|
+
// relative spec, not a resolved URL string — hence `Spec`, not `Url`.
|
|
24
|
+
function defaultSpec(name: string): string {
|
|
21
25
|
const m = MOUNT_JS.match(new RegExp(`${name}\\s*=\\s*config\\.\\w+\\s*\\?\\?\\s*(\\w+);`));
|
|
22
26
|
assert(m, `mount.js must default ${name} from config with a fallback constant`);
|
|
23
|
-
const constM = MOUNT_JS.match(
|
|
24
|
-
|
|
27
|
+
const constM = MOUNT_JS.match(
|
|
28
|
+
new RegExp(`const ${m![1]}\\s*=\\s*new URL\\(\\s*"([^"]*)"\\s*,\\s*import\\.meta\\.url\\s*\\)\\s*\\.href`),
|
|
29
|
+
);
|
|
30
|
+
assert(
|
|
31
|
+
constM,
|
|
32
|
+
`mount.js must default ${m![1]} to new URL("<spec>", import.meta.url) so the door is anchored ` +
|
|
33
|
+
`to the module's own served location, not the document base (#467/#536)`,
|
|
34
|
+
);
|
|
25
35
|
return constM![1];
|
|
26
36
|
}
|
|
27
37
|
|
|
@@ -31,13 +41,21 @@ test("#524: the compose mount renders a file-input Import control accepting .jso
|
|
|
31
41
|
assert(/accept="[^"]*\.json[^"]*"/.test(MOUNT_JS), "the Import file input must accept .json files");
|
|
32
42
|
});
|
|
33
43
|
|
|
34
|
-
test("#524: Import wires the importToLibrary door (
|
|
35
|
-
const
|
|
44
|
+
test("#524: Import wires the importToLibrary door (module-anchored relative), reading the file text client-side", () => {
|
|
45
|
+
const importSpec = defaultSpec("importUrl");
|
|
46
|
+
assert(
|
|
47
|
+
importSpec.endsWith("actions/delivery-graph/library/import"),
|
|
48
|
+
`importUrl default spec "${importSpec}" must hit the importToLibrary door`,
|
|
49
|
+
);
|
|
50
|
+
assert(!importSpec.startsWith("/"), `default importUrl spec "${importSpec}" must be relative, not absolute (App-View #279 + #467/#536 resolution class)`);
|
|
51
|
+
// Pin the MODULE-anchored invariant, not merely "not absolute": the spec must step UP out of the
|
|
52
|
+
// /delivery-graphs/ shell base with "../" (resolved against import.meta.url), so a regression back to a
|
|
53
|
+
// document-base-relative "app/api/…" — which 404s under the shell base (#467/#536) — fails the guard.
|
|
36
54
|
assert(
|
|
37
|
-
|
|
38
|
-
`importUrl
|
|
55
|
+
importSpec.startsWith("../"),
|
|
56
|
+
`default importUrl spec "${importSpec}" must be module-anchored with a "../" prefix (steps out of the ` +
|
|
57
|
+
`/delivery-graphs/ shell base off import.meta.url), not document-base-relative (#467/#536)`,
|
|
39
58
|
);
|
|
40
|
-
assert(!importUrl.startsWith("/"), `default importUrl "${importUrl}" must be base-relative (App-View #279 resolution class)`);
|
|
41
59
|
// The file's text is read CLIENT-SIDE and POSTed as graphJson to the import door.
|
|
42
60
|
assert(/\.text\(\)/.test(MOUNT_JS), "mount.js must read the selected file's text client-side via File.text()");
|
|
43
61
|
assert(/post\(importUrl,\s*\{\s*graphJson:/.test(MOUNT_JS), "the Import handler must POST the file text as graphJson to the import door");
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
// compose mount's NEW inbound fill seam over the shared `deliveryGraph.compose.fill` host-bridge
|
|
7
7
|
// message. Plus a Save-to-library affordance on the staged App-View (save-from-digest) and a
|
|
8
8
|
// Save-to-library row action on the dispatched/history grid. This test pins that wiring so it cannot
|
|
9
|
-
// silently regress: the sidecars mount the same module, the door defaults are
|
|
10
|
-
// App-View resolution class), Reuse posts the ONE shared fill message, Delete hits the per-entry door,
|
|
9
|
+
// silently regress: the sidecars mount the same module, the door defaults are module-anchored off
|
|
10
|
+
// import.meta.url (the #279/#467/#536 App-View resolution class), Reuse posts the ONE shared fill message, Delete hits the per-entry door,
|
|
11
11
|
// the compose mount adds the inbound fill listener, and the page carries the new Library App-View node.
|
|
12
12
|
import { test } from "node:test";
|
|
13
13
|
import { assert, assertEquals, assertStringIncludes } from "#test-assert";
|
|
@@ -23,15 +23,21 @@ const EMBED_HTML = readFileSync(`${DIR}/library-embed.html`, "utf8");
|
|
|
23
23
|
const STANDALONE_HTML = readFileSync(`${DIR}/library-standalone.html`, "utf8");
|
|
24
24
|
const PAGE_JSON = readFileSync(`${ROOT}pages/delivery-graphs.page.json`, "utf8");
|
|
25
25
|
|
|
26
|
-
// Pull
|
|
27
|
-
|
|
26
|
+
// Pull a MODULE-ANCHORED default spec out of `const <name> = config.<field> ?? <CONST>;` where the
|
|
27
|
+
// CONST is declared `const <CONST> = new URL("<spec>", import.meta.url).href;` (#467/#536).
|
|
28
|
+
function defaultSpec(src: string, name: string): string {
|
|
28
29
|
const m = src.match(new RegExp(`${name}\\s*=\\s*config\\.\\w+\\s*\\?\\?\\s*(\\w+);`));
|
|
29
30
|
assert(m, `mount must default ${name} from config with a fallback constant`);
|
|
30
|
-
const constM = src.match(
|
|
31
|
-
|
|
31
|
+
const constM = src.match(
|
|
32
|
+
new RegExp(`const ${m![1]}\\s*=\\s*new URL\\(\\s*"([^"]*)"\\s*,\\s*import\\.meta\\.url\\s*\\)\\s*\\.href`),
|
|
33
|
+
);
|
|
34
|
+
assert(
|
|
35
|
+
constM,
|
|
36
|
+
`mount must default ${m![1]} to new URL("<spec>", import.meta.url) so the door is anchored to the ` +
|
|
37
|
+
`module's own served location, not the document base (#467/#536)`,
|
|
38
|
+
);
|
|
32
39
|
return constM![1];
|
|
33
40
|
}
|
|
34
|
-
|
|
35
41
|
test("#523: the Library App-View mounts the same module standalone and embedded", () => {
|
|
36
42
|
assert(/mountDeliveryGraphLibrary/.test(LIBRARY_JS), "library.mount.js must export mountDeliveryGraphLibrary");
|
|
37
43
|
for (const [file, html] of [["library-embed.html", EMBED_HTML], ["library-standalone.html", STANDALONE_HTML]] as const) {
|
|
@@ -52,10 +58,11 @@ test("#523: the page binds the Library node to the library App-View sidecars", (
|
|
|
52
58
|
assert(library?.props?.standalone === "./delivery-graphs/library-standalone.html", "it has the library standalone sidecar");
|
|
53
59
|
});
|
|
54
60
|
|
|
55
|
-
test("#523/#
|
|
56
|
-
const
|
|
57
|
-
assert(
|
|
58
|
-
assert(!
|
|
61
|
+
test("#523/#467/#536: the Library list door default is module-anchored and hits the listLibrary door", () => {
|
|
62
|
+
const spec = defaultSpec(LIBRARY_JS, "libraryUrl");
|
|
63
|
+
assert(spec.endsWith("delivery-graph/library"), `libraryUrl default "${spec}" must hit the listLibrary door`);
|
|
64
|
+
assert(!spec.startsWith("/"), `default libraryUrl "${spec}" must not be absolute (App-View #279 resolution class)`);
|
|
65
|
+
assert(spec.startsWith("../"), `default libraryUrl "${spec}" must step up out of /delivery-graphs/ (module-anchored, #467/#536)`);
|
|
59
66
|
// The list read consumes the door's `entries` array (one row per saved entry).
|
|
60
67
|
assert(/body\.entries/.test(LIBRARY_JS), "library.mount.js must render one row per `entries[]` item the listLibrary door returns");
|
|
61
68
|
});
|
|
@@ -98,15 +105,38 @@ test("#523: the compose mount exposes an INBOUND reuse-fill seam (message → #d
|
|
|
98
105
|
});
|
|
99
106
|
|
|
100
107
|
test("#523: Save-to-library on the staged App-View posts save-from-digest", () => {
|
|
101
|
-
const
|
|
102
|
-
assert(
|
|
103
|
-
assert(!
|
|
108
|
+
const spec = defaultSpec(STAGED_JS, "saveLibraryUrl");
|
|
109
|
+
assert(spec.endsWith("actions/delivery-graph/library/save"), `saveLibraryUrl default "${spec}" must hit the saveToLibrary door`);
|
|
110
|
+
assert(!spec.startsWith("/"), `default saveLibraryUrl "${spec}" must not be absolute (App-View #279 resolution class)`);
|
|
111
|
+
assert(spec.startsWith("../"), `default saveLibraryUrl "${spec}" must step up out of /delivery-graphs/ (module-anchored, #467/#536)`);
|
|
104
112
|
assert(/data-save-library=/.test(STAGED_JS), "staged.mount.js must render a per-row Save-to-library affordance carrying the digest");
|
|
105
113
|
// Save-from-digest: it posts { name, digest } — it must NOT compile or stage a raw graph (the #460
|
|
106
114
|
// operator boundary the staged view enforces stays intact).
|
|
107
115
|
assert(/post\(saveLibraryUrl,\s*\{\s*name:[^}]*digest:/.test(STAGED_JS), "Save-to-library must POST { name, digest } (save-from-digest) to the save door");
|
|
108
116
|
});
|
|
109
117
|
|
|
118
|
+
test("#538: the in-flight grid's column widths never over-allocate (no 0-width column, no header char-wrap)", () => {
|
|
119
|
+
// Regression guard: the grid is `table-layout:fixed` with `overflow-wrap:anywhere` headers, so a
|
|
120
|
+
// column squeezed to 0 width wraps its title one character per line (a ~250px-tall header row). That
|
|
121
|
+
// happens when the declared widths over-allocate. Pin the contract: every data column carries a
|
|
122
|
+
// percentage width, and their sum leaves headroom (>=5%) for the implicit row-action column — so no
|
|
123
|
+
// column can collapse to 0.
|
|
124
|
+
const page = JSON.parse(PAGE_JSON) as { nodes: Array<Record<string, any>> };
|
|
125
|
+
const grid = page.nodes.find((n) => n.id === "delivery-graphs-inflight");
|
|
126
|
+
assert(grid, "the page must carry the delivery-graphs-inflight grid");
|
|
127
|
+
const columns = (grid?.props?.columns ?? []) as Array<Record<string, any>>;
|
|
128
|
+
assert(columns.length > 0, "the grid must declare columns");
|
|
129
|
+
let sum = 0;
|
|
130
|
+
for (const col of columns) {
|
|
131
|
+
const w = col.width;
|
|
132
|
+
assert(typeof w === "string" && /^\d+(\.\d+)?%$/.test(w), `column "${col.header}" must declare a percentage width so it can't collapse to 0 (got ${JSON.stringify(w)})`);
|
|
133
|
+
const parsed = Number.parseFloat(w);
|
|
134
|
+
assert(parsed > 0, `column "${col.header}" must declare a positive width so it can't collapse to 0 (got ${JSON.stringify(w)})`);
|
|
135
|
+
sum += parsed;
|
|
136
|
+
}
|
|
137
|
+
assert(sum <= 95, `declared column widths sum to ${sum}% — they must leave >=5% for the row-action column so nothing squeezes to 0 (#538)`);
|
|
138
|
+
});
|
|
139
|
+
|
|
110
140
|
test("#523: Save-to-library is offered on a dispatched/history grid row (save-from-dispatched)", () => {
|
|
111
141
|
const page = JSON.parse(PAGE_JSON) as { nodes: Array<Record<string, any>> };
|
|
112
142
|
const grid = page.nodes.find((n) => n.id === "delivery-graphs-inflight");
|
|
@@ -6,10 +6,21 @@
|
|
|
6
6
|
// Dispatch button and NO way to see the graph. The staged App-View (pages/delivery-graphs/staged.*)
|
|
7
7
|
// closes that: per row it offers Preview-DI (over the nano-navigate bridge) AND Dispatch. This test
|
|
8
8
|
// pins the wiring so it cannot silently regress: the sidecars exist and mount the same module, the door
|
|
9
|
-
// defaults are
|
|
10
|
-
//
|
|
9
|
+
// defaults are MODULE-ANCHORED (`new URL("../app/api/…", import.meta.url)`, the #467 App-View
|
|
10
|
+
// resolution class — see below), it drives the DI-preview bridge, and it posts the dispatch by digest.
|
|
11
|
+
//
|
|
12
|
+
// #279/#467 resolution class: the staged shell (staged-embed.html / staged-standalone.html) — and thus
|
|
13
|
+
// staged.mount.js — is served ONE DIRECTORY DEEP at `<appMount>/delivery-graphs/`, while the API lives
|
|
14
|
+
// at the app root, a sibling of `/delivery-graphs/`: `<appMount>/app/api/…`. A leading-slash absolute
|
|
15
|
+
// default resolves against the console iframe ORIGIN, not the app-view base (#279). A document-base-
|
|
16
|
+
// relative default (`"app/api/…"`) resolves against the `…/delivery-graphs/` shell base to
|
|
17
|
+
// `…/delivery-graphs/app/api/…` → 404 on EVERY surface (the Studio console never injects
|
|
18
|
+
// window.__NANO_APP_VIEW__, so the default runs) — the "Could not load staged proposals." bug (#536).
|
|
19
|
+
// The only correct default is anchored to staged.mount.js's OWN url (import.meta.url): `../app/api/…`
|
|
20
|
+
// steps up out of `/delivery-graphs/` onto `<appMount>/app/api/…` on all surfaces. This test pins that
|
|
21
|
+
// resolution against the REAL, `/delivery-graphs/`-deep module url so neither regression can return.
|
|
11
22
|
import { test } from "node:test";
|
|
12
|
-
import { assert } from "#test-assert";
|
|
23
|
+
import { assert, assertEquals } from "#test-assert";
|
|
13
24
|
import { readFileSync } from "node:fs";
|
|
14
25
|
|
|
15
26
|
const ROOT = decodeURIComponent(new URL("../", import.meta.url).pathname);
|
|
@@ -19,15 +30,55 @@ const EMBED_HTML = readFileSync(`${DIR}/staged-embed.html`, "utf8");
|
|
|
19
30
|
const STANDALONE_HTML = readFileSync(`${DIR}/staged-standalone.html`, "utf8");
|
|
20
31
|
const PAGE_JSON = readFileSync(`${ROOT}pages/delivery-graphs.page.json`, "utf8");
|
|
21
32
|
|
|
22
|
-
//
|
|
23
|
-
|
|
33
|
+
// The REAL served location of staged.mount.js on each surface: one directory deep under
|
|
34
|
+
// `/delivery-graphs/`.
|
|
35
|
+
const STANDALONE_MOUNT = "http://127.0.0.1:3000/delivery-graphs/staged.mount.js";
|
|
36
|
+
const STUDIO_MOUNT = "http://studio-host:8080/console/app-view/Workforce/delivery-graphs/staged.mount.js";
|
|
37
|
+
|
|
38
|
+
// Pull the module-anchored default spec out of `const <name> = config.<field> ?? <CONST>;` where the
|
|
39
|
+
// CONST is declared `const <CONST> = new URL("<spec>", import.meta.url).href;`.
|
|
40
|
+
function defaultSpec(name: string): string {
|
|
24
41
|
const m = MOUNT_JS.match(new RegExp(`${name}\\s*=\\s*config\\.\\w+\\s*\\?\\?\\s*(\\w+);`));
|
|
25
42
|
assert(m, `staged.mount.js must default ${name} from config with a fallback constant`);
|
|
26
|
-
const constM = MOUNT_JS.match(
|
|
27
|
-
|
|
43
|
+
const constM = MOUNT_JS.match(
|
|
44
|
+
new RegExp(`const ${m![1]}\\s*=\\s*new URL\\(\\s*"([^"]*)"\\s*,\\s*import\\.meta\\.url\\s*\\)\\s*\\.href`),
|
|
45
|
+
);
|
|
46
|
+
assert(
|
|
47
|
+
constM,
|
|
48
|
+
`staged.mount.js must default ${m![1]} to new URL("<spec>", import.meta.url) so the door is anchored ` +
|
|
49
|
+
`to the module's own served location, not the document base (#467/#536)`,
|
|
50
|
+
);
|
|
28
51
|
return constM![1];
|
|
29
52
|
}
|
|
30
53
|
|
|
54
|
+
// A module-anchored door default must (a) not be absolute (#279), (b) step up out of /delivery-graphs/
|
|
55
|
+
// (#467), and (c) resolve onto <appMount>/app/api/… both standalone AND inside the Studio iframe.
|
|
56
|
+
function assertModuleAnchored(name: string, endpoint: string): void {
|
|
57
|
+
const spec = defaultSpec(name);
|
|
58
|
+
assert(
|
|
59
|
+
!spec.startsWith("/"),
|
|
60
|
+
`default ${name} spec "${spec}" must not be absolute: a leading-slash path resolves against the ` +
|
|
61
|
+
`iframe ORIGIN (console :8080), not the app-view base, so every fetch 404s (#279)`,
|
|
62
|
+
);
|
|
63
|
+
assert(
|
|
64
|
+
spec.startsWith("../"),
|
|
65
|
+
`default ${name} spec "${spec}" must step up out of /delivery-graphs/ (import.meta.url points at ` +
|
|
66
|
+
`<appMount>/delivery-graphs/staged.mount.js; the API is a sibling at <appMount>/app/api/…) (#467)`,
|
|
67
|
+
);
|
|
68
|
+
assert(spec.endsWith(endpoint), `default ${name} spec "${spec}" must hit the ${endpoint} door`);
|
|
69
|
+
assertEquals(
|
|
70
|
+
new URL(spec, STANDALONE_MOUNT).href,
|
|
71
|
+
`http://127.0.0.1:3000/${endpoint}`,
|
|
72
|
+
`default ${name} must resolve to the app root, not the /delivery-graphs/ shell base (#467)`,
|
|
73
|
+
);
|
|
74
|
+
assertEquals(
|
|
75
|
+
new URL(spec, STUDIO_MOUNT).href,
|
|
76
|
+
`http://studio-host:8080/console/app-view/Workforce/${endpoint}`,
|
|
77
|
+
`default ${name} must resolve under the app-view base the console proxies, not the console origin ` +
|
|
78
|
+
`root (#279) nor the /delivery-graphs/ shell base (#467/#536)`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
31
82
|
test("#511: the staged App-View mounts the same module standalone and embedded", () => {
|
|
32
83
|
assert(/mountStagedProposals/.test(MOUNT_JS), "staged.mount.js must export mountStagedProposals");
|
|
33
84
|
for (const [file, html] of [["staged-embed.html", EMBED_HTML], ["staged-standalone.html", STANDALONE_HTML]] as const) {
|
|
@@ -48,25 +99,19 @@ test("#511: the page binds the Staged proposals node to the staged App-View side
|
|
|
48
99
|
assert(staged?.props?.standalone === "./delivery-graphs/staged-standalone.html", "it has the staged standalone sidecar");
|
|
49
100
|
});
|
|
50
101
|
|
|
51
|
-
test("#511/#279: the staged list door default is
|
|
52
|
-
|
|
53
|
-
assert(url.endsWith("delivery-graph/staged"), `stagedUrl default "${url}" must hit the listStagedProposals door`);
|
|
54
|
-
assert(!url.startsWith("/"), `default stagedUrl "${url}" must be base-relative (App-View #279 resolution class)`);
|
|
102
|
+
test("#511/#279/#467: the staged list door default is module-anchored to the app root", () => {
|
|
103
|
+
assertModuleAnchored("stagedUrl", "app/api/delivery-graph/staged");
|
|
55
104
|
});
|
|
56
105
|
|
|
57
106
|
test("#511: DI preview — the staged view wires the proposal-bpmn door and bridges the XML to the explorer", () => {
|
|
58
|
-
|
|
59
|
-
assert(url.endsWith("actions/delivery-graph/proposal-bpmn"), `proposalBpmnUrl default "${url}" must hit the previewProposalBpmn door`);
|
|
60
|
-
assert(!url.startsWith("/"), `default proposalBpmnUrl "${url}" must be base-relative`);
|
|
107
|
+
assertModuleAnchored("proposalBpmnUrl", "app/api/actions/delivery-graph/proposal-bpmn");
|
|
61
108
|
assert(/data-preview-di=/.test(MOUNT_JS), "staged.mount.js must render a per-row Preview-DI affordance carrying the digest");
|
|
62
109
|
assert(/target:\s*"definitionPreview"/.test(MOUNT_JS), "staged.mount.js must post nano-navigate to the definitionPreview target");
|
|
63
110
|
assert(/params:\s*\{\s*xml:/.test(MOUNT_JS), "staged.mount.js must carry the compiled BPMN xml in the bridge message");
|
|
64
111
|
});
|
|
65
112
|
|
|
66
113
|
test("#460/#511: Dispatch is the operator's launch — posts the digest to the dispatch door, and never compiles/stages", () => {
|
|
67
|
-
|
|
68
|
-
assert(url.endsWith("actions/delivery-graph/dispatch"), `dispatchUrl default "${url}" must hit the dispatchDeliveryGraph door`);
|
|
69
|
-
assert(!url.startsWith("/"), `default dispatchUrl "${url}" must be base-relative`);
|
|
114
|
+
assertModuleAnchored("dispatchUrl", "app/api/actions/delivery-graph/dispatch");
|
|
70
115
|
assert(/data-dispatch=/.test(MOUNT_JS), "staged.mount.js must render a per-row Dispatch affordance carrying the digest");
|
|
71
116
|
assert(/window\.confirm\(/.test(MOUNT_JS), "Dispatch must confirm before launching (dispatch authorises side effects)");
|
|
72
117
|
// Operator-only: this surface dispatches a digest that is ALREADY staged — it must not compile or
|
|
@@ -77,9 +122,7 @@ test("#460/#511: Dispatch is the operator's launch — posts the digest to the d
|
|
|
77
122
|
});
|
|
78
123
|
|
|
79
124
|
test("#520: Dismiss is the operator's discard — posts the digest to the dismiss door, behind a confirm, and launches nothing", () => {
|
|
80
|
-
|
|
81
|
-
assert(url.endsWith("actions/delivery-graph/dismiss"), `dismissUrl default "${url}" must hit the dismissProposal door`);
|
|
82
|
-
assert(!url.startsWith("/"), `default dismissUrl "${url}" must be base-relative (App-View #279 resolution class)`);
|
|
125
|
+
assertModuleAnchored("dismissUrl", "app/api/actions/delivery-graph/dismiss");
|
|
83
126
|
assert(/data-dismiss=/.test(MOUNT_JS), "staged.mount.js must render a per-row Dismiss affordance carrying the digest");
|
|
84
127
|
// Dismiss is a one-way discard off the staged list — confirm before it drops the proposal.
|
|
85
128
|
assert(/window\.confirm\(DISMISS_CONFIRM\)/.test(MOUNT_JS), "staged.mount.js must confirm before dismissing (a one-way discard off the staged list)");
|