pi-gsd 2.0.1 → 2.0.3

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.
Files changed (66) hide show
  1. package/dist/pi-gsd-hooks.js +1533 -0
  2. package/dist/pi-gsd-tools.js +53 -52
  3. package/package.json +3 -5
  4. package/.gsd/extensions/pi-gsd-hooks.ts +0 -973
  5. package/src/cli.ts +0 -644
  6. package/src/commands/base.ts +0 -67
  7. package/src/commands/commit.ts +0 -22
  8. package/src/commands/config.ts +0 -71
  9. package/src/commands/frontmatter.ts +0 -51
  10. package/src/commands/index.ts +0 -76
  11. package/src/commands/init.ts +0 -43
  12. package/src/commands/milestone.ts +0 -37
  13. package/src/commands/phase.ts +0 -92
  14. package/src/commands/progress.ts +0 -71
  15. package/src/commands/roadmap.ts +0 -40
  16. package/src/commands/scaffold.ts +0 -19
  17. package/src/commands/state.ts +0 -102
  18. package/src/commands/template.ts +0 -52
  19. package/src/commands/verify.ts +0 -70
  20. package/src/commands/workstream.ts +0 -98
  21. package/src/commands/wxp.ts +0 -65
  22. package/src/lib/commands.ts +0 -1040
  23. package/src/lib/config.ts +0 -385
  24. package/src/lib/core.ts +0 -1167
  25. package/src/lib/frontmatter.ts +0 -462
  26. package/src/lib/init.ts +0 -517
  27. package/src/lib/milestone.ts +0 -290
  28. package/src/lib/model-profiles.ts +0 -272
  29. package/src/lib/phase.ts +0 -1012
  30. package/src/lib/profile-output.ts +0 -237
  31. package/src/lib/profile-pipeline.ts +0 -556
  32. package/src/lib/roadmap.ts +0 -378
  33. package/src/lib/schemas.ts +0 -290
  34. package/src/lib/security.ts +0 -176
  35. package/src/lib/state.ts +0 -1175
  36. package/src/lib/template.ts +0 -246
  37. package/src/lib/uat.ts +0 -289
  38. package/src/lib/verify.ts +0 -879
  39. package/src/lib/workstream.ts +0 -524
  40. package/src/output.ts +0 -45
  41. package/src/schemas/pi-gsd-settings.schema.json +0 -80
  42. package/src/schemas/wxp.xsd +0 -619
  43. package/src/schemas/wxp.zod.ts +0 -318
  44. package/src/wxp/__tests__/arguments.test.ts +0 -86
  45. package/src/wxp/__tests__/conditions.test.ts +0 -106
  46. package/src/wxp/__tests__/executor.test.ts +0 -95
  47. package/src/wxp/__tests__/helpers.ts +0 -26
  48. package/src/wxp/__tests__/integration.test.ts +0 -166
  49. package/src/wxp/__tests__/new-features.test.ts +0 -222
  50. package/src/wxp/__tests__/parser.test.ts +0 -159
  51. package/src/wxp/__tests__/paste.test.ts +0 -66
  52. package/src/wxp/__tests__/schema.test.ts +0 -120
  53. package/src/wxp/__tests__/security.test.ts +0 -87
  54. package/src/wxp/__tests__/shell.test.ts +0 -85
  55. package/src/wxp/__tests__/string-ops.test.ts +0 -25
  56. package/src/wxp/__tests__/variables.test.ts +0 -65
  57. package/src/wxp/arguments.ts +0 -89
  58. package/src/wxp/conditions.ts +0 -78
  59. package/src/wxp/executor.ts +0 -191
  60. package/src/wxp/index.ts +0 -191
  61. package/src/wxp/parser.ts +0 -198
  62. package/src/wxp/paste.ts +0 -51
  63. package/src/wxp/security.ts +0 -102
  64. package/src/wxp/shell.ts +0 -81
  65. package/src/wxp/string-ops.ts +0 -44
  66. package/src/wxp/variables.ts +0 -109
@@ -1,166 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import type { Mock } from "vitest";
3
- import path from "node:path";
4
-
5
- vi.mock("node:child_process", () => ({
6
- execFileSync: vi.fn().mockReturnValue("shell-output\n"),
7
- }));
8
-
9
- import { processWxp, WxpProcessingError } from "../index.js";
10
- import type { WxpSecurityConfig } from "../../schemas/wxp.zod.js";
11
- import { execFileSync } from "node:child_process";
12
-
13
- const PROJECT_ROOT = "/project";
14
- const PKG_ROOT = "/pkg";
15
- const TRUSTED_DIR = "/project/.pi/gsd/workflows";
16
- const VIRTUAL_FILE = path.join(TRUSTED_DIR, "test.md");
17
-
18
- const cfg: WxpSecurityConfig = {
19
- trustedPaths: [{ position: "absolute", path: TRUSTED_DIR }],
20
- untrustedPaths: [],
21
- shellAllowlist: ["pi-gsd-tools", "git", "node", "cat", "ls", "echo", "find"],
22
- shellBanlist: [],
23
- shellTimeoutMs: 30_000,
24
- };
25
-
26
- describe("processWxp — integration (TST-02)", () => {
27
- beforeEach(() => {
28
- (execFileSync as Mock).mockReturnValue("shell-output\n");
29
- });
30
-
31
- it("Fixture 1: shell output captured and paste tag replaced", () => {
32
- const content = [
33
- "<gsd-execute>",
34
- " <shell command=\"pi-gsd-tools\">",
35
- " <args><arg string=\"state\" /><arg string=\"json\" /></args>",
36
- " <outs><out type=\"string\" name=\"state\" /></outs>",
37
- " </shell>",
38
- "</gsd-execute>",
39
- "State: <gsd-paste name=\"state\" />",
40
- ].join("\n");
41
-
42
- const result = processWxp(content, VIRTUAL_FILE, cfg, PROJECT_ROOT, PKG_ROOT);
43
- expect(result).toContain("State: shell-output");
44
- expect(result).not.toMatch(/<gsd-paste/);
45
- expect(result).not.toMatch(/<gsd-execute>/);
46
- });
47
-
48
- it("Fixture 2: <gsd-paste> inside code fence is unchanged (WXP-01)", () => {
49
- const content = [
50
- "Example:",
51
- "```",
52
- '<gsd-paste name="x" />',
53
- "```",
54
- ].join("\n");
55
- // No variables — would throw WxpProcessingError if paste ran outside fence
56
- const result = processWxp(content, VIRTUAL_FILE, cfg, PROJECT_ROOT, PKG_ROOT);
57
- expect(result).toContain('<gsd-paste name="x" />');
58
- });
59
-
60
- it("Fixture 3: if/starts-with condition branches correctly", () => {
61
- const content = [
62
- "<gsd-arguments>",
63
- ' <arg name="auto-chain-active" type="flag" flag="--auto" optional />',
64
- "</gsd-arguments>",
65
- "<gsd-execute>",
66
- " <if>",
67
- " <condition><equals>",
68
- ' <left name="auto-chain-active" />',
69
- ' <right type="boolean" value="false" />',
70
- " </equals></condition>",
71
- " <then>",
72
- ' <shell command="pi-gsd-tools">',
73
- ' <args><arg string="config-set" /><arg string="workflow._auto_chain_active" /><arg name="auto-chain-active" /></args>',
74
- " <outs><suppress-errors /></outs>",
75
- " </shell>",
76
- " </then>",
77
- " </if>",
78
- "</gsd-execute>",
79
- ].join("\n");
80
-
81
- (execFileSync as Mock).mockReturnValue("ok\n");
82
- // Without --auto flag: auto-chain-active = false → condition true → shell runs
83
- const result = processWxp(content, VIRTUAL_FILE, cfg, PROJECT_ROOT, PKG_ROOT, "");
84
- expect(execFileSync).toHaveBeenCalled();
85
- expect(result.trim()).toBe("");
86
- });
87
-
88
- it("Fixture 5: variable collision gets owner-prefixed", async () => {
89
- const { createVariableStore } = await import("../variables.js");
90
- const store = createVariableStore();
91
- store.set("result", "from-a", "file-a");
92
- store.set("result", "from-b", "file-b");
93
- expect(store.get("result")).toBeUndefined();
94
- expect(store.get("file-a:result")).toBe("from-a");
95
- expect(store.get("file-b:result")).toBe("from-b");
96
- });
97
-
98
- it("Fixture 6a: untrusted path throws WxpProcessingError", () => {
99
- expect(() =>
100
- processWxp("content", "/untrusted/file.md", cfg, PROJECT_ROOT, PKG_ROOT),
101
- ).toThrow(WxpProcessingError);
102
- });
103
-
104
- it("Fixture 6b: .planning/ path throws WxpProcessingError", () => {
105
- expect(() =>
106
- processWxp("content", "/project/.planning/STATE.md", cfg, PROJECT_ROOT, PKG_ROOT),
107
- ).toThrow(WxpProcessingError);
108
- });
109
-
110
- it("Fixture 6c: undefined paste variable throws WxpProcessingError", () => {
111
- const content = '<gsd-paste name="missing" />';
112
- expect(() =>
113
- processWxp(content, VIRTUAL_FILE, cfg, PROJECT_ROOT, PKG_ROOT),
114
- ).toThrow(WxpProcessingError);
115
- });
116
-
117
- it("Fixture 6d: non-allowlisted command throws WxpProcessingError", () => {
118
- const content = [
119
- "<gsd-execute>",
120
- ' <shell command="bash">',
121
- ' <args><arg string="-c" /><arg string="echo hi" /></args>',
122
- " <outs><out type=\"string\" name=\"out\" /></outs>",
123
- " </shell>",
124
- "</gsd-execute>",
125
- ].join("\n");
126
- expect(() =>
127
- processWxp(content, VIRTUAL_FILE, cfg, PROJECT_ROOT, PKG_ROOT),
128
- ).toThrow(WxpProcessingError);
129
- });
130
-
131
- it("WxpProcessingError contains variable namespace", () => {
132
- try {
133
- processWxp('<gsd-paste name="gone" />', VIRTUAL_FILE, cfg, PROJECT_ROOT, PKG_ROOT);
134
- } catch (err) {
135
- expect(err).toBeInstanceOf(WxpProcessingError);
136
- expect((err as WxpProcessingError).message).toContain("Variable Namespace");
137
- }
138
- });
139
-
140
- it("Fixture 7: output contains zero <gsd-* tags outside code fences (TST-03)", () => {
141
- const content = [
142
- "<gsd-execute>",
143
- ' <shell command="echo">',
144
- ' <args><arg string="hello" /></args>',
145
- ' <outs><out type="string" name="val" /></outs>',
146
- " </shell>",
147
- "</gsd-execute>",
148
- '<gsd-paste name="val" />',
149
- "",
150
- "```",
151
- '<gsd-paste name="val" />',
152
- "```",
153
- ].join("\n");
154
-
155
- (execFileSync as Mock).mockReturnValue("hello\n");
156
- const result = processWxp(content, VIRTUAL_FILE, cfg, PROJECT_ROOT, PKG_ROOT);
157
- const withoutFences = result.replace(/^```[^\n]*\n[\s\S]*?^```/gm, "FENCE");
158
- expect(withoutFences.match(/<gsd-(?!-)[^>]*>/g)).toBeNull();
159
- });
160
-
161
- it("gsd-version tags are informational and do not cause errors", () => {
162
- const content = '<gsd-version v="1.12.4" />\n# Hello';
163
- const result = processWxp(content, VIRTUAL_FILE, cfg, PROJECT_ROOT, PKG_ROOT);
164
- expect(result).toContain("# Hello");
165
- });
166
- });
@@ -1,222 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import type { Mock } from "vitest";
3
-
4
- vi.mock("node:child_process", () => ({
5
- execFileSync: vi.fn().mockReturnValue("ok\n"),
6
- }));
7
-
8
- import { createVariableStore } from "../variables.js";
9
- import { executeBlock } from "../executor.js";
10
- import { evaluateCondExprNode } from "../conditions.js";
11
- import type { WxpExecContext } from "../../schemas/wxp.zod.js";
12
- import { execFileSync } from "node:child_process";
13
- import { x } from "./helpers.js";
14
-
15
- const makeCtx = (onDisplay = vi.fn()): WxpExecContext => ({
16
- config: {
17
- trustedPaths: [],
18
- untrustedPaths: [],
19
- shellAllowlist: ["pi-gsd-tools", "git", "echo", "cat", "node", "ls", "find"],
20
- shellBanlist: [],
21
- shellTimeoutMs: 30_000,
22
- },
23
- projectRoot: "/project",
24
- pkgRoot: "/pkg",
25
- onDisplay,
26
- });
27
-
28
- function exec(...children: ReturnType<typeof x>[]) {
29
- return x("gsd-execute", {}, children);
30
- }
31
-
32
- describe("conditions — new operators", () => {
33
- it("not-equals: true when values differ", () => {
34
- const vars = createVariableStore();
35
- vars.set("status", "complete");
36
- expect(evaluateCondExprNode(
37
- x("not-equals", {}, [x("left", { name: "status" }), x("right", { value: "pending" })]),
38
- vars,
39
- )).toBe(true);
40
- });
41
-
42
- it("less-than (numeric)", () => {
43
- const vars = createVariableStore();
44
- vars.set("n", "3");
45
- expect(evaluateCondExprNode(
46
- x("less-than", {}, [x("left", { name: "n", type: "number" }), x("right", { type: "number", value: "5" })]),
47
- vars,
48
- )).toBe(true);
49
- });
50
-
51
- it("greater-than-or-equal", () => {
52
- const vars = createVariableStore();
53
- vars.set("n", "5");
54
- expect(evaluateCondExprNode(
55
- x("greater-than-or-equal", {}, [x("left", { name: "n", type: "number" }), x("right", { type: "number", value: "5" })]),
56
- vars,
57
- )).toBe(true);
58
- });
59
-
60
- it("contains", () => {
61
- const vars = createVariableStore();
62
- vars.set("init", "@file:/tmp/out.json");
63
- expect(evaluateCondExprNode(
64
- x("contains", {}, [x("left", { name: "init" }), x("right", { value: "@file:" })]),
65
- vars,
66
- )).toBe(true);
67
- });
68
-
69
- it("<and>: all children must be true", () => {
70
- const vars = createVariableStore();
71
- vars.set("a", "1"); vars.set("b", "2");
72
- expect(evaluateCondExprNode(x("and", {}, [
73
- x("equals", {}, [x("left", { name: "a" }), x("right", { value: "1" })]),
74
- x("equals", {}, [x("left", { name: "b" }), x("right", { value: "2" })]),
75
- ]), vars)).toBe(true);
76
- expect(evaluateCondExprNode(x("and", {}, [
77
- x("equals", {}, [x("left", { name: "a" }), x("right", { value: "1" })]),
78
- x("equals", {}, [x("left", { name: "b" }), x("right", { value: "99" })]),
79
- ]), vars)).toBe(false);
80
- });
81
-
82
- it("<or>: any child true is sufficient", () => {
83
- const vars = createVariableStore();
84
- vars.set("x", "hello");
85
- expect(evaluateCondExprNode(x("or", {}, [
86
- x("equals", {}, [x("left", { name: "x" }), x("right", { value: "nope" })]),
87
- x("equals", {}, [x("left", { name: "x" }), x("right", { value: "hello" })]),
88
- ]), vars)).toBe(true);
89
- });
90
-
91
- it("nested <and> inside <or>", () => {
92
- const vars = createVariableStore();
93
- vars.set("status", "pending"); vars.set("phase", "3");
94
- expect(evaluateCondExprNode(x("or", {}, [
95
- x("equals", {}, [x("left", { name: "status" }), x("right", { value: "complete" })]),
96
- x("and", {}, [
97
- x("equals", {}, [x("left", { name: "status" }), x("right", { value: "pending" })]),
98
- x("greater-than-or-equal", {}, [x("left", { name: "phase", type: "number" }), x("right", { type: "number", value: "2" })]),
99
- ]),
100
- ]), vars)).toBe(true);
101
- });
102
- });
103
-
104
- describe("<display>", () => {
105
- beforeEach(() => vi.clearAllMocks());
106
-
107
- it("emits via onDisplay with {varname} interpolation", () => {
108
- const onDisplay = vi.fn();
109
- const vars = createVariableStore();
110
- vars.set("phase", "3"); vars.set("phase-name", "WXP Foundation");
111
- executeBlock(exec(x("display", { msg: "GSD ► PHASE {phase} — {phase-name}", level: "info" })), vars, makeCtx(onDisplay));
112
- expect(onDisplay).toHaveBeenCalledWith("GSD ► PHASE 3 — WXP Foundation", "info");
113
- });
114
-
115
- it("resolves dot-notation {item.status}", () => {
116
- const onDisplay = vi.fn();
117
- const vars = createVariableStore();
118
- vars.set("phase", JSON.stringify({ status: "complete", name: "Test" }));
119
- executeBlock(exec(x("display", { msg: "Status: {phase.status}" })), vars, makeCtx(onDisplay));
120
- expect(onDisplay).toHaveBeenCalledWith("Status: complete", "info");
121
- });
122
- });
123
-
124
- describe("<json-parse>", () => {
125
- it("extracts a top-level key", () => {
126
- const vars = createVariableStore();
127
- vars.set("data", JSON.stringify({ phase_number: "3" }));
128
- executeBlock(exec(x("json-parse", { src: "data", path: "$.phase_number", out: "phase" })), vars, makeCtx());
129
- expect(vars.get("phase")).toBe("3");
130
- });
131
-
132
- it("extracts an array for <for-each>", () => {
133
- const vars = createVariableStore();
134
- vars.set("progress", JSON.stringify({ phases: [{ number: "1" }, { number: "2" }] }));
135
- executeBlock(exec(x("json-parse", { src: "progress", path: "$.phases", out: "phases" })), vars, makeCtx());
136
- const arr = vars.getArray("phases");
137
- expect(arr).toHaveLength(2);
138
- expect(JSON.parse(arr![0]).number).toBe("1");
139
- });
140
- });
141
-
142
- describe("<for-each>", () => {
143
- beforeEach(() => vi.clearAllMocks());
144
-
145
- it("iterates array and runs body for each item", () => {
146
- const onDisplay = vi.fn();
147
- const vars = createVariableStore();
148
- vars.setArray("items", [JSON.stringify({ name: "Alpha" }), JSON.stringify({ name: "Beta" })]);
149
- executeBlock(exec(
150
- x("for-each", { var: "items", item: "item" }, [
151
- x("display", { msg: "Item: {item.name}", level: "info" }),
152
- ]),
153
- ), vars, makeCtx(onDisplay));
154
- expect(onDisplay).toHaveBeenCalledTimes(2);
155
- expect(onDisplay).toHaveBeenNthCalledWith(1, "Item: Alpha", "info");
156
- expect(onDisplay).toHaveBeenNthCalledWith(2, "Item: Beta", "info");
157
- });
158
-
159
- it("<where> filters items", () => {
160
- const onDisplay = vi.fn();
161
- const vars = createVariableStore();
162
- vars.setArray("phases", [
163
- JSON.stringify({ number: "1", status: "complete" }),
164
- JSON.stringify({ number: "2", status: "pending" }),
165
- JSON.stringify({ number: "3", status: "pending" }),
166
- ]);
167
- executeBlock(exec(
168
- x("for-each", { var: "phases", item: "phase" }, [
169
- x("where", {}, [
170
- x("not-equals", {}, [x("left", { name: "phase.status" }), x("right", { value: "complete" })]),
171
- ]),
172
- x("display", { msg: "{phase.number}" }),
173
- ]),
174
- ), vars, makeCtx(onDisplay));
175
- expect(onDisplay).toHaveBeenCalledTimes(2);
176
- expect(onDisplay).toHaveBeenNthCalledWith(1, "2", "info");
177
- });
178
-
179
- it("<sort-by> sorts numerically", () => {
180
- const onDisplay = vi.fn();
181
- const vars = createVariableStore();
182
- vars.setArray("phases", [
183
- JSON.stringify({ number: "3" }), JSON.stringify({ number: "1" }), JSON.stringify({ number: "2" }),
184
- ]);
185
- executeBlock(exec(
186
- x("for-each", { var: "phases", item: "phase" }, [
187
- x("sort-by", { key: "number", type: "number", order: "asc" }),
188
- x("display", { msg: "{phase.number}" }),
189
- ]),
190
- ), vars, makeCtx(onDisplay));
191
- expect(onDisplay).toHaveBeenNthCalledWith(1, "1", "info");
192
- expect(onDisplay).toHaveBeenNthCalledWith(3, "3", "info");
193
- });
194
-
195
- it("missing array is silently skipped", () => {
196
- const vars = createVariableStore();
197
- expect(() => executeBlock(exec(
198
- x("for-each", { var: "nonexistent", item: "x" }, []),
199
- ), vars, makeCtx())).not.toThrow();
200
- });
201
- });
202
-
203
- describe("variables — dot notation and arrays", () => {
204
- it("resolve dot-notation accesses JSON property", () => {
205
- const vars = createVariableStore();
206
- vars.set("item", JSON.stringify({ status: "complete", number: "5" }));
207
- expect(vars.resolve("item.status")).toBe("complete");
208
- expect(vars.resolve("item.number")).toBe("5");
209
- });
210
-
211
- it("setArray/getArray round-trips", () => {
212
- const vars = createVariableStore();
213
- vars.setArray("arr", ["a", "b", "c"]);
214
- expect(vars.getArray("arr")).toEqual(["a", "b", "c"]);
215
- });
216
-
217
- it("getArray falls back to parsing JSON scalar", () => {
218
- const vars = createVariableStore();
219
- vars.set("data", JSON.stringify(["x", "y"]));
220
- expect(vars.getArray("data")).toEqual(["x", "y"]);
221
- });
222
- });
@@ -1,159 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { extractWxpTags, extractCodeFenceRegions } from "../parser.js";
3
- import { executeNode } from "../executor.js";
4
- import { createVariableStore } from "../variables.js";
5
- import type { WxpExecContext } from "../../schemas/wxp.zod.js";
6
- import { x } from "./helpers.js";
7
-
8
- const ctx: WxpExecContext = {
9
- config: {
10
- trustedPaths: [],
11
- untrustedPaths: [],
12
- shellAllowlist: ["pi-gsd-tools", "git", "cat", "ls", "echo", "node", "find"],
13
- shellBanlist: [],
14
- shellTimeoutMs: 30_000,
15
- },
16
- projectRoot: "/project",
17
- pkgRoot: "/pkg",
18
- onDisplay: () => {},
19
- };
20
-
21
- describe("extractCodeFenceRegions", () => {
22
- it("returns empty for content with no fences", () => {
23
- expect(extractCodeFenceRegions("hello world")).toEqual([]);
24
- });
25
-
26
- it("identifies a fenced region", () => {
27
- const content = "before\n```\ncode\n```\nafter";
28
- const regions = extractCodeFenceRegions(content);
29
- expect(regions).toHaveLength(1);
30
- expect(content.slice(regions[0][0], regions[0][1])).toContain("code");
31
- });
32
- });
33
-
34
- describe("extractWxpTags — code-fence skip (WXP-01)", () => {
35
- it("does NOT parse <gsd-paste> inside a code fence", () => {
36
- const content = "before\n```\n<gsd-paste name=\"x\" />\n```\nafter";
37
- const pastes = extractWxpTags(content).filter((t) => t.node.tag === "gsd-paste");
38
- expect(pastes).toHaveLength(0);
39
- });
40
-
41
- it("DOES parse <gsd-paste> outside a code fence", () => {
42
- const content = 'text\n<gsd-paste name="x" />\nmore';
43
- const pastes = extractWxpTags(content).filter((t) => t.node.tag === "gsd-paste");
44
- expect(pastes).toHaveLength(1);
45
- expect(pastes[0].node.attrs["name"]).toBe("x");
46
- });
47
- });
48
-
49
- describe("extractWxpTags — gsd-execute with shell children", () => {
50
- it("parses nested shell/args/outs structure", () => {
51
- const content = [
52
- "<gsd-execute>",
53
- " <shell command=\"pi-gsd-tools\">",
54
- " <args><arg string=\"init\" /><arg string=\"execute-phase\" /><arg name=\"phase\" wrap='\"' /></args>",
55
- " <outs><out type=\"string\" name=\"init\" /></outs>",
56
- " </shell>",
57
- "</gsd-execute>",
58
- ].join("\n");
59
-
60
- const tags = extractWxpTags(content);
61
- expect(tags).toHaveLength(1);
62
- expect(tags[0].node.tag).toBe("gsd-execute");
63
-
64
- const shell = tags[0].node.children.find((c) => c.tag === "shell");
65
- expect(shell).toBeDefined();
66
- expect(shell?.attrs["command"]).toBe("pi-gsd-tools");
67
-
68
- const args = shell?.children.find((c) => c.tag === "args");
69
- expect(args?.children.filter((c) => c.tag === "arg")).toHaveLength(3);
70
-
71
- const outs = shell?.children.find((c) => c.tag === "outs");
72
- expect(outs?.children.find((c) => c.tag === "out")?.attrs["name"]).toBe("init");
73
- });
74
- });
75
-
76
- describe("extractWxpTags — gsd-arguments", () => {
77
- it("parses typed positionals and flag with settings", () => {
78
- const content = [
79
- "<gsd-arguments>",
80
- " <settings><keep-extra-args /></settings>",
81
- " <arg name=\"phase\" type=\"number\" />",
82
- " <arg name=\"auto\" type=\"flag\" flag=\"--auto\" optional />",
83
- " <arg name=\"user-text\" type=\"string\" optional />",
84
- "</gsd-arguments>",
85
- ].join("\n");
86
-
87
- const tags = extractWxpTags(content);
88
- expect(tags[0].node.tag).toBe("gsd-arguments");
89
- const argDefs = tags[0].node.children.filter((c) => c.tag === "arg");
90
- expect(argDefs).toHaveLength(3);
91
- expect(argDefs.find((a) => a.attrs["type"] === "flag")?.attrs["flag"]).toBe("--auto");
92
- expect(tags[0].node.children.find((c) => c.tag === "settings")
93
- ?.children.some((c) => c.tag === "keep-extra-args")).toBe(true);
94
- });
95
- });
96
-
97
- describe("extractWxpTags — gsd-include", () => {
98
- it("parses self-closing include", () => {
99
- const content = `<gsd-include path="other.md" />`;
100
- const tags = extractWxpTags(content);
101
- expect(tags[0].node.attrs["path"]).toBe("other.md");
102
- });
103
-
104
- it("parses include-arguments attribute", () => {
105
- const content = `<gsd-include path="other.md" include-arguments />`;
106
- expect("include-arguments" in extractWxpTags(content)[0].node.attrs).toBe(true);
107
- });
108
-
109
- it("parses include with arg mappings (INC-02)", () => {
110
- const content = [
111
- `<gsd-include path="other.md">`,
112
- ` <gsd-arguments>`,
113
- ` <arg name="my-phase" as="phase" />`,
114
- ` </gsd-arguments>`,
115
- `</gsd-include>`,
116
- ].join("\n");
117
- const tag = extractWxpTags(content)[0];
118
- const mappingArg = tag.node.children
119
- .find((c) => c.tag === "gsd-arguments")
120
- ?.children.find((c) => c.tag === "arg");
121
- expect(mappingArg?.attrs["name"]).toBe("my-phase");
122
- expect(mappingArg?.attrs["as"]).toBe("phase");
123
- });
124
- });
125
-
126
- describe("extractWxpTags — gsd-version", () => {
127
- it("parses version tag", () => {
128
- const tag = extractWxpTags(`<gsd-version v="1.12.4" />`)[0];
129
- expect(tag.node.attrs["v"]).toBe("1.12.4");
130
- });
131
-
132
- it("parses do-not-update", () => {
133
- const tag = extractWxpTags(`<gsd-version v="1.0.0" do-not-update />`)[0];
134
- expect("do-not-update" in tag.node.attrs).toBe(true);
135
- });
136
- });
137
-
138
- describe("if node with PRD condition structure — executed directly", () => {
139
- it("evaluates equals with left/right operands and runs then-branch", () => {
140
- const vars = createVariableStore();
141
- vars.set("auto-chain-active", "false");
142
- const displayed: string[] = [];
143
- const testCtx = { ...ctx, onDisplay: (m: string) => displayed.push(m) };
144
-
145
- executeNode(x("if", {}, [
146
- x("condition", {}, [
147
- x("equals", {}, [
148
- x("left", { name: "auto-chain-active" }),
149
- x("right", { type: "boolean", value: "false" }),
150
- ]),
151
- ]),
152
- x("then", {}, [
153
- x("display", { msg: "condition was true" }),
154
- ]),
155
- ]), vars, testCtx);
156
-
157
- expect(displayed).toContain("condition was true");
158
- });
159
- });
@@ -1,66 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { applyPaste, WxpPasteError } from "../paste.js";
3
- import { createVariableStore } from "../variables.js";
4
-
5
- describe("applyPaste (WXP-06)", () => {
6
- it("replaces a paste tag with the variable value", () => {
7
- const vars = createVariableStore();
8
- vars.set("greeting", "Hello World");
9
- const result = applyPaste('Say: <gsd-paste name="greeting" />', vars);
10
- expect(result).toBe("Say: Hello World");
11
- });
12
-
13
- it("replaces multiple paste tags", () => {
14
- const vars = createVariableStore();
15
- vars.set("a", "foo");
16
- vars.set("b", "bar");
17
- const result = applyPaste('<gsd-paste name="a" /> and <gsd-paste name="b" />', vars);
18
- expect(result).toBe("foo and bar");
19
- });
20
-
21
- it("throws WxpPasteError immediately on undefined variable (no partial output)", () => {
22
- const vars = createVariableStore();
23
- vars.set("defined", "ok");
24
- const content = '<gsd-paste name="defined" /> <gsd-paste name="missing" />';
25
- expect(() => applyPaste(content, vars)).toThrow(WxpPasteError);
26
- });
27
-
28
- it("WxpPasteError contains the variable name and snapshot", () => {
29
- const vars = createVariableStore();
30
- vars.set("existing", "val");
31
- try {
32
- applyPaste('<gsd-paste name="nope" />', vars);
33
- } catch (err) {
34
- expect(err).toBeInstanceOf(WxpPasteError);
35
- expect((err as WxpPasteError).variableName).toBe("nope");
36
- expect((err as WxpPasteError).variableSnapshot).toMatchObject({ existing: "val" });
37
- }
38
- });
39
-
40
- it("does NOT replace paste tag inside a code fence (dead-zone skip)", () => {
41
- const vars = createVariableStore();
42
- vars.set("x", "REPLACED");
43
- const content = "```\n<gsd-paste name=\"x\" />\n```\nafter";
44
- const result = applyPaste(content, vars);
45
- expect(result).toContain('<gsd-paste name="x" />');
46
- expect(result).not.toContain("REPLACED");
47
- });
48
-
49
- it("replaces paste tag outside fence but not inside fence", () => {
50
- const vars = createVariableStore();
51
- vars.set("val", "VALUE");
52
- const content = "```\n<gsd-paste name=\"val\" />\n```\n<gsd-paste name=\"val\" />";
53
- const result = applyPaste(content, vars);
54
- // Inside fence: unchanged
55
- expect(result).toContain('<gsd-paste name="val" />');
56
- // Outside fence: replaced
57
- const outsidePart = result.split("```\n").pop() ?? "";
58
- expect(outsidePart).toBe("VALUE");
59
- });
60
-
61
- it("returns content unchanged when there are no paste tags", () => {
62
- const vars = createVariableStore();
63
- const content = "No tags here";
64
- expect(applyPaste(content, vars)).toBe(content);
65
- });
66
- });