@arcote.tech/arc-process 0.7.28

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.
@@ -0,0 +1,244 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { buildProcessModel } from "../src/model/build";
3
+ import { collapseContexts, type CollapseOptions } from "../src/model/collapse";
4
+ import { layoutProcess, type DiagramNode } from "../src/layout/layout";
5
+ import { COL_GAP_X, STEP_W } from "../src/layout/measure";
6
+ import {
7
+ EMPTY_VIEW_STATE,
8
+ initialViewState,
9
+ rootKey,
10
+ toggleGroup,
11
+ type ProcessViewState,
12
+ } from "../src/state/view-state";
13
+ import { arcProfile, resolveArcRef, toProcessGraph } from "../src/profiles/arc";
14
+ import type { Occurrence, ProcessGraph, StepOccurrence } from "../src/model/types";
15
+
16
+ function el(id: string, kind: string, extra: Record<string, unknown> = {}) {
17
+ return { id, name: id, kind, ...extra };
18
+ }
19
+
20
+ const graph = toProcessGraph({
21
+ elements: [
22
+ el("cmd", "command", { queryDeps: ["v"], mutationDeps: ["e"] }),
23
+ el("v", "view"),
24
+ el("e", "event"),
25
+ el("l", "listener"),
26
+ ],
27
+ edges: [
28
+ { source: "cmd", target: "v", kind: "queries" },
29
+ { source: "cmd", target: "e", kind: "mutates" },
30
+ { source: "l", target: "e", kind: "handles" },
31
+ ],
32
+ });
33
+
34
+ function occ(occId: string, props: Record<string, string>): StepOccurrence {
35
+ return { type: "step", occId, ref: resolveArcRef(props), label: occId };
36
+ }
37
+
38
+ function run(
39
+ occurrences: Occurrence[],
40
+ state: ProcessViewState = EMPTY_VIEW_STATE,
41
+ g: ProcessGraph = graph,
42
+ collapseOpts?: Partial<CollapseOptions>,
43
+ ) {
44
+ const model = buildProcessModel({ graph: g, profile: arcProfile, occurrences });
45
+ const collapse = collapseContexts(model, {
46
+ contexts: g.contexts ?? [],
47
+ state,
48
+ defaultCollapsed: false,
49
+ ...collapseOpts,
50
+ });
51
+ return {
52
+ model,
53
+ collapse,
54
+ layout: layoutProcess({ model, collapse, graph: g, profile: arcProfile, state }),
55
+ };
56
+ }
57
+
58
+ const nodeById = (nodes: DiagramNode[], id: string) => nodes.find((n) => n.id === id)!;
59
+
60
+ describe("layoutProcess — tracki i kierunki", () => {
61
+ test("sekwencja: x rośnie z depth, wspólny y", () => {
62
+ const { layout } = run([occ("o1", { command: "cmd" }), occ("o2", { event: "e" })]);
63
+ const a = nodeById(layout.nodes, "o1");
64
+ const b = nodeById(layout.nodes, "o2");
65
+ expect(a.y).toBe(b.y);
66
+ expect(b.x - a.x).toBeGreaterThanOrEqual(STEP_W);
67
+ });
68
+
69
+ test("rozwinięcie W (queries) stawia kartę na zachód od kroku", () => {
70
+ const state = toggleGroup(EMPTY_VIEW_STATE, rootKey("o1"), "queries");
71
+ const { layout } = run([occ("o1", { command: "cmd" })], state);
72
+ const step = nodeById(layout.nodes, "o1");
73
+ const card = layout.nodes.find((n) => n.type === "attachment")!;
74
+ expect(card).toBeDefined();
75
+ const stepCx = step.x + step.w / 2;
76
+ const cardCx = card.x + card.w / 2;
77
+ expect(cardCx).toBeCloseTo(stepCx - COL_GAP_X, 5);
78
+ // Krawędź wchodzi DO kroku (queries intoNode).
79
+ const edge = layout.edges.find((e) => e.source === card.id && e.target === "o1");
80
+ expect(edge).toBeDefined();
81
+ });
82
+
83
+ test("override side=SE przesuwa kolumnę na wschód POD krok", () => {
84
+ const o: StepOccurrence = { ...occ("o1", { command: "cmd" }), sides: { mutates: "SE" } };
85
+ const state = toggleGroup(EMPTY_VIEW_STATE, rootKey("o1"), "mutates");
86
+ const { layout } = run([o], state);
87
+ const step = nodeById(layout.nodes, "o1");
88
+ const card = layout.nodes.find((n) => n.type === "attachment")!;
89
+ const stepCx = step.x + step.w / 2;
90
+ expect(card.x + card.w / 2).toBeCloseTo(stepCx + COL_GAP_X, 5);
91
+ expect(card.y).toBeGreaterThan(step.y + step.h); // pod krokiem
92
+ });
93
+
94
+ test("track sizing: rozwinięcie E odsuwa następną kolumnę (brak nachodzenia)", () => {
95
+ // o2 = v (view) — e pozostaje doczepką (nie connectorem), więc otwarcie
96
+ // grupy mutates na o1 rezerwuje pasmo E kolumny depth 0.
97
+ const occs = [occ("o1", { command: "cmd" }), occ("o2", { view: "v" })];
98
+ const closed = run(occs);
99
+ const state = toggleGroup(EMPTY_VIEW_STATE, rootKey("o1"), "mutates");
100
+ const open = run(occs, state);
101
+ const shift =
102
+ nodeById(open.layout.nodes, "o2").x - nodeById(closed.layout.nodes, "o2").x;
103
+ expect(shift).toBeGreaterThan(0); // kolumna o2 przesunięta w prawo
104
+ });
105
+
106
+ test("notatka renderuje się nad krokiem (side N)", () => {
107
+ const occurrences: Occurrence[] = [
108
+ occ("o1", { command: "cmd" }),
109
+ { type: "note", occId: "n1", text: "notka", side: "N" },
110
+ ];
111
+ const { layout } = run(occurrences);
112
+ const step = nodeById(layout.nodes, "o1");
113
+ const note = layout.nodes.find((n) => n.type === "note")!;
114
+ expect(note).toBeDefined();
115
+ expect(note.y + note.h).toBeLessThanOrEqual(step.y);
116
+ });
117
+
118
+ test("rekurencja: karteczka otwiera własną grupę głębiej", () => {
119
+ // cmd -queries-> v; v jest czytane przez... l -handles-> e? Zbudujmy: v queriedBy cmd (visible), więc
120
+ // użyjemy e: cmd -mutates-> e (karta E), e handledBy l (rekurencyjnie na karcie).
121
+ let state = toggleGroup(EMPTY_VIEW_STATE, rootKey("o1"), "mutates");
122
+ state = toggleGroup(state, `${rootKey("o1")}/mutates:e`, "handledBy");
123
+ const { layout } = run([occ("o1", { command: "cmd" })], state);
124
+ const cards = layout.nodes.filter((n) => n.type === "attachment");
125
+ expect(cards.length).toBe(2); // e + l (rekurencyjnie)
126
+ const eCard = cards.find((c) => (c.data.node as { id: string }).id === "e")!;
127
+ const lCard = cards.find((c) => (c.data.node as { id: string }).id === "l")!;
128
+ expect(lCard).toBeDefined();
129
+ // l doczepione względem karty e (W od e).
130
+ expect(lCard.x).toBeLessThan(eCard.x);
131
+ const edge = layout.edges.find(
132
+ (e) => e.source === lCard.id && e.target === eCard.id,
133
+ );
134
+ expect(edge).toBeDefined();
135
+ });
136
+
137
+ test("Entry na torze ujemnym renderuje się nad osią + krawędź continues", () => {
138
+ const occurrences: Occurrence[] = [
139
+ {
140
+ ...occ("e1", { listener: "l" }),
141
+ path: [{ type: "entry", id: "en1", title: "Wejście" }],
142
+ },
143
+ { type: "continue", to: "p", path: [{ type: "entry", id: "en1" }] },
144
+ { ...occ("m1", { command: "cmd" }), path: [{ type: "step", id: "s1", phaseId: "p" }] },
145
+ ];
146
+ const { layout } = run(occurrences);
147
+ const entry = nodeById(layout.nodes, "e1");
148
+ const main = nodeById(layout.nodes, "m1");
149
+ expect(entry.y).toBeLessThan(main.y);
150
+ const cont = layout.edges.find((e) => e.id.startsWith("cont:"));
151
+ expect(cont).toBeDefined();
152
+ expect(cont!.source).toBe("e1");
153
+ expect(cont!.target).toBe("m1");
154
+ });
155
+ });
156
+
157
+ describe("collapse kontekstów", () => {
158
+ const ctxGraph = toProcessGraph({
159
+ elements: [
160
+ el("send", "command", { contextPath: ["ctx_leads", "ctx_chat"] }),
161
+ el("answer", "listener", { contextPath: ["ctx_leads", "ctx_chat"] }),
162
+ el("score", "listener", { contextPath: ["ctx_leads"] }),
163
+ ],
164
+ edges: [],
165
+ contexts: [
166
+ { id: "ctx_leads", path: ["ctx_leads"], name: "leads" },
167
+ { id: "ctx_chat", path: ["ctx_leads", "ctx_chat"], name: "chat", title: "Czat AI" },
168
+ ],
169
+ });
170
+
171
+ test("przyległe kroki zwiniętego kontekstu zlewają się w jeden box", () => {
172
+ const occurrences = [
173
+ occ("o1", { command: "send" }),
174
+ occ("o2", { listener: "answer" }),
175
+ occ("o3", { listener: "score" }),
176
+ ];
177
+ const { collapse, layout } = run(occurrences, EMPTY_VIEW_STATE, ctxGraph, {
178
+ defaultCollapsed: true,
179
+ homeContextId: "ctx_leads",
180
+ });
181
+ // send+answer (ctx_chat, obcy) → box; score (ctx_leads, domowy) zostaje.
182
+ expect(collapse.groups.length).toBe(1);
183
+ expect(collapse.groups[0].context.id).toBe("ctx_chat");
184
+ expect(collapse.groups[0].memberOccIds).toEqual(["o1", "o2"]);
185
+ const stepNodes = layout.nodes.filter((n) => n.type === "step");
186
+ expect(stepNodes.length).toBe(2); // box + score
187
+ const box = stepNodes.find((n) => (n.data.isContext as boolean) === true)!;
188
+ expect((n(box).step as { label: string }).label).toBe("Czat AI");
189
+
190
+ function n(x: DiagramNode) {
191
+ return x.data as { step: unknown };
192
+ }
193
+ });
194
+
195
+ test("stan czytelnika otwiera box → kroki wracają + obrys context-group", () => {
196
+ const occurrences = [occ("o1", { command: "send" }), occ("o2", { listener: "answer" })];
197
+ const state: ProcessViewState = {
198
+ expanded: {},
199
+ collapsedContexts: { ctx_chat: false },
200
+ };
201
+ const { collapse, layout } = run(occurrences, state, ctxGraph, {
202
+ defaultCollapsed: true,
203
+ homeContextId: "ctx_leads",
204
+ });
205
+ expect(collapse.groups.length).toBe(0);
206
+ const box = layout.nodes.find((n) => n.type === "context-group");
207
+ expect(box).toBeDefined();
208
+ const o1 = nodeById(layout.nodes, "o1");
209
+ expect(o1.x).toBeGreaterThan(box!.x);
210
+ expect(o1.y).toBeGreaterThan(box!.y);
211
+ });
212
+
213
+ test("relacje do wnętrza boxa re-targetowane (dedup)", () => {
214
+ const g = toProcessGraph({
215
+ elements: [
216
+ el("send", "command", { contextPath: ["ctx_chat"], mutationDeps: [] }),
217
+ el("answer", "listener", { contextPath: ["ctx_chat"] }),
218
+ el("out", "view"),
219
+ ],
220
+ edges: [
221
+ { source: "send", target: "out", kind: "mutates" },
222
+ { source: "answer", target: "out", kind: "mutates" },
223
+ ],
224
+ contexts: [{ id: "ctx_chat", path: ["ctx_chat"], name: "chat" }],
225
+ });
226
+ const occurrences = [
227
+ occ("o1", { command: "send" }),
228
+ occ("o2", { listener: "answer" }),
229
+ occ("o3", { view: "out" }),
230
+ ];
231
+ const { layout } = run(occurrences, EMPTY_VIEW_STATE, g, { defaultCollapsed: true });
232
+ // Obie relacje mutates z wnętrza boxa → jedna krawędź box→out na kind.
233
+ const rel = layout.edges.filter((e) => e.id.startsWith("rel:"));
234
+ expect(rel.length).toBe(1);
235
+ expect(rel[0].source.startsWith("grp:ctx_chat")).toBe(true);
236
+ });
237
+
238
+ test("initialViewState otwiera grupy z expand", () => {
239
+ const o: StepOccurrence = { ...occ("o1", { command: "cmd" }), expand: ["mutates"] };
240
+ const model = buildProcessModel({ graph, profile: arcProfile, occurrences: [o] });
241
+ const state = initialViewState(model);
242
+ expect(state.expanded[rootKey("o1")]).toEqual(["mutates"]);
243
+ });
244
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "../../../tsconfig.json",
3
+ "include": ["src/**/*", "tests/**/*"]
4
+ }