qualia-framework 3.6.0 → 4.0.0

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/tests/runner.js CHANGED
@@ -835,9 +835,11 @@ waves: 1
835
835
  it("init --force overwrites an existing project (preserves lifetime)", () => {
836
836
  const tmpDir = makeProject();
837
837
  try {
838
- // Seed lifetime via close-milestone first
838
+ // Seed lifetime via close-milestone first. --force bypasses the v4
839
+ // readiness guards (MILESTONE_NOT_READY) since this test doesn't
840
+ // exercise the verification flow — it's focused on lifetime preservation.
839
841
  const c = spawnSync(process.execPath, [
840
- path.join(BIN, "state.js"), "close-milestone",
842
+ path.join(BIN, "state.js"), "close-milestone", "--force",
841
843
  ], { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
842
844
  assert.equal(c.status, 0);
843
845
  const tBefore = JSON.parse(fs.readFileSync(path.join(tmpDir, ".planning", "tracking.json"), "utf8"));
@@ -863,8 +865,10 @@ waves: 1
863
865
  it("close-milestone refuses double-close (idempotency)", () => {
864
866
  const tmpDir = makeProject();
865
867
  try {
868
+ // First close uses --force to bypass v4 readiness guards — this test
869
+ // focuses on the ALREADY_CLOSED sentinel, not phase-verification gates.
866
870
  const r1 = spawnSync(process.execPath, [
867
- path.join(BIN, "state.js"), "close-milestone",
871
+ path.join(BIN, "state.js"), "close-milestone", "--force",
868
872
  ], { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
869
873
  assert.equal(r1.status, 0);
870
874
  const out1 = JSON.parse(r1.stdout);
@@ -878,6 +882,8 @@ waves: 1
878
882
  t.milestone = out1.closed_milestone; // rewind
879
883
  fs.writeFileSync(tFile, JSON.stringify(t, null, 2) + "\n");
880
884
 
885
+ // Second close (without --force) must fail with ALREADY_CLOSED, which
886
+ // is checked BEFORE the readiness guards in cmdCloseMilestone.
881
887
  const r2 = spawnSync(process.execPath, [
882
888
  path.join(BIN, "state.js"), "close-milestone",
883
889
  ], { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
@@ -893,7 +899,7 @@ waves: 1
893
899
  const tmpDir = makeProject();
894
900
  try {
895
901
  const r1 = spawnSync(process.execPath, [
896
- path.join(BIN, "state.js"), "close-milestone",
902
+ path.join(BIN, "state.js"), "close-milestone", "--force",
897
903
  ], { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
898
904
  assert.equal(r1.status, 0);
899
905
 
@@ -1002,6 +1008,165 @@ waves: 1
1002
1008
  }
1003
1009
  });
1004
1010
 
1011
+ // ─── v4.0.0: milestone readiness guards + milestones[] summary ─
1012
+ it("close-milestone refuses unverified phases (MILESTONE_NOT_READY)", () => {
1013
+ const tmpDir = makeProject();
1014
+ try {
1015
+ // No phases verified yet — close-milestone (without --force) must refuse.
1016
+ const r = spawnSync(process.execPath, [
1017
+ path.join(BIN, "state.js"), "close-milestone",
1018
+ ], { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
1019
+ assert.equal(r.status, 1);
1020
+ const out = JSON.parse(r.stdout);
1021
+ assert.equal(out.error, "MILESTONE_NOT_READY");
1022
+ } finally {
1023
+ fs.rmSync(tmpDir, { recursive: true, force: true });
1024
+ }
1025
+ });
1026
+
1027
+ it("close-milestone refuses single-phase milestones (MILESTONE_TOO_SMALL)", () => {
1028
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-single-"));
1029
+ try {
1030
+ const init = spawnSync(process.execPath, [
1031
+ path.join(BIN, "state.js"), "init",
1032
+ "--project", "SingleProject",
1033
+ "--phases", '[{"name":"Only","goal":"Y"}]',
1034
+ ], { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
1035
+ assert.equal(init.status, 0);
1036
+
1037
+ // Single-phase milestone — even if the phase were verified, the size
1038
+ // guard catches it first. A milestone needs ≥ 2 phases without --force.
1039
+ const r = spawnSync(process.execPath, [
1040
+ path.join(BIN, "state.js"), "close-milestone",
1041
+ ], { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
1042
+ assert.equal(r.status, 1);
1043
+ const out = JSON.parse(r.stdout);
1044
+ assert.equal(out.error, "MILESTONE_TOO_SMALL");
1045
+ } finally {
1046
+ fs.rmSync(tmpDir, { recursive: true, force: true });
1047
+ }
1048
+ });
1049
+
1050
+ it("close-milestone appends a summary to milestones[]", () => {
1051
+ const tmpDir = makeProject();
1052
+ try {
1053
+ const r = spawnSync(process.execPath, [
1054
+ path.join(BIN, "state.js"), "close-milestone", "--force",
1055
+ ], { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
1056
+ assert.equal(r.status, 0);
1057
+
1058
+ const tFile = path.join(tmpDir, ".planning", "tracking.json");
1059
+ const t = JSON.parse(fs.readFileSync(tFile, "utf8"));
1060
+ assert.ok(Array.isArray(t.milestones), "milestones[] must exist");
1061
+ assert.equal(t.milestones.length, 1);
1062
+ const m1 = t.milestones[0];
1063
+ assert.equal(m1.num, 1);
1064
+ assert.ok(m1.total_phases >= 2, "total_phases should reflect seeded phases");
1065
+ assert.ok(typeof m1.closed_at === "string" && m1.closed_at.length > 0);
1066
+ } finally {
1067
+ fs.rmSync(tmpDir, { recursive: true, force: true });
1068
+ }
1069
+ });
1070
+
1071
+ it("milestone summary captures cumulative tasks_completed, not current phase only", () => {
1072
+ const tmpDir = makeProject();
1073
+ try {
1074
+ // Simulate 2 phases each with 3 tasks verified pass. This bumps
1075
+ // lifetime.tasks_completed to 6. The milestone close summary should
1076
+ // reflect 6, not just 3 (the last phase's tasks_done).
1077
+ for (const phase of [1, 2]) {
1078
+ const planFile = path.join(tmpDir, ".planning", `phase-${phase}-plan.md`);
1079
+ fs.writeFileSync(planFile, `---
1080
+ phase: ${phase}
1081
+ goal: "x"
1082
+ tasks: 1
1083
+ waves: 1
1084
+ ---
1085
+
1086
+ ## Task 1 — x
1087
+ **Wave:** 1
1088
+ **Files:** x.ts
1089
+ **Depends on:** none
1090
+ **Acceptance Criteria:**
1091
+ - ok
1092
+ `);
1093
+ const verFile = path.join(tmpDir, ".planning", `phase-${phase}-verification.md`);
1094
+ // Plan → built → verified
1095
+ let r = spawnSync(process.execPath, [path.join(BIN, "state.js"), "transition", "--to", "planned", "--phase", String(phase)],
1096
+ { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
1097
+ assert.equal(r.status, 0, `planned transition failed for phase ${phase}: ${r.stderr || r.stdout}`);
1098
+ r = spawnSync(process.execPath, [path.join(BIN, "state.js"), "transition", "--to", "built", "--phase", String(phase), "--tasks-done", "3", "--tasks-total", "3"],
1099
+ { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
1100
+ assert.equal(r.status, 0, `built transition failed for phase ${phase}: ${r.stderr || r.stdout}`);
1101
+ fs.writeFileSync(verFile, "result: PASS");
1102
+ r = spawnSync(process.execPath, [path.join(BIN, "state.js"), "transition", "--to", "verified", "--phase", String(phase), "--verification", "pass"],
1103
+ { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
1104
+ assert.equal(r.status, 0, `verified transition failed for phase ${phase}: ${r.stderr || r.stdout}`);
1105
+ }
1106
+
1107
+ // Close milestone
1108
+ const r = spawnSync(process.execPath, [path.join(BIN, "state.js"), "close-milestone"],
1109
+ { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
1110
+ assert.equal(r.status, 0, `close-milestone failed: ${r.stderr || r.stdout}`);
1111
+
1112
+ const t = JSON.parse(fs.readFileSync(path.join(tmpDir, ".planning", "tracking.json"), "utf8"));
1113
+ assert.equal(t.lifetime.tasks_completed, 6, "lifetime should have 6 tasks (2 phases × 3 tasks)");
1114
+ assert.equal(t.milestones.length, 1);
1115
+ assert.equal(t.milestones[0].tasks_completed, 6, "milestone summary should cumulate all 6 tasks, not just the last phase's 3");
1116
+ assert.equal(t.milestones[0].phases_completed, 2);
1117
+ } finally {
1118
+ fs.rmSync(tmpDir, { recursive: true, force: true });
1119
+ }
1120
+ });
1121
+
1122
+ it("build_count bumps on each 'built' transition", () => {
1123
+ const tmpDir = makeProject();
1124
+ try {
1125
+ const tFile = path.join(tmpDir, ".planning", "tracking.json");
1126
+ const before = JSON.parse(fs.readFileSync(tFile, "utf8")).build_count || 0;
1127
+
1128
+ fs.writeFileSync(path.join(tmpDir, ".planning", "phase-1-plan.md"), `---
1129
+ phase: 1
1130
+ goal: "x"
1131
+ tasks: 1
1132
+ waves: 1
1133
+ ---
1134
+
1135
+ ## Task 1 — x
1136
+ **Wave:** 1
1137
+ **Files:** x.ts
1138
+ **Depends on:** none
1139
+ **Acceptance Criteria:**
1140
+ - ok
1141
+ `);
1142
+ spawnSync(process.execPath, [path.join(BIN, "state.js"), "transition", "--to", "planned", "--phase", "1"],
1143
+ { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
1144
+ const r = spawnSync(process.execPath, [path.join(BIN, "state.js"), "transition", "--to", "built", "--phase", "1", "--tasks-done", "1", "--tasks-total", "1"],
1145
+ { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
1146
+ assert.equal(r.status, 0);
1147
+
1148
+ const after = JSON.parse(fs.readFileSync(tFile, "utf8")).build_count || 0;
1149
+ assert.equal(after, before + 1, "build_count should bump on 'built' transition");
1150
+ } finally {
1151
+ fs.rmSync(tmpDir, { recursive: true, force: true });
1152
+ }
1153
+ });
1154
+
1155
+ it("check exposes milestones[] and milestone_name in output", () => {
1156
+ const tmpDir = makeProject();
1157
+ try {
1158
+ const r = spawnSync(process.execPath, [
1159
+ path.join(BIN, "state.js"), "check",
1160
+ ], { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
1161
+ assert.equal(r.status, 0);
1162
+ const out = JSON.parse(r.stdout);
1163
+ assert.ok(Array.isArray(out.milestones), "check must expose milestones[]");
1164
+ assert.ok(typeof out.milestone_name === "string", "check must expose milestone_name");
1165
+ } finally {
1166
+ fs.rmSync(tmpDir, { recursive: true, force: true });
1167
+ }
1168
+ });
1169
+
1005
1170
  // ─── v3.5.0: CRLF tolerance in parseStateMd ────────────
1006
1171
  it("parseStateMd tolerates CRLF line endings (Windows-edited STATE.md)", () => {
1007
1172
  const tmpDir = makeProject();