Home Manual Reference Source Test

tests/constraints3.test.js

  1. import { describe, it, expect, vi } from "vitest";
  2. import { FIXTURES } from "./api.js";
  3. const { buildNotesModel, step3Voice, step3Params, step3TwoVoice } = FIXTURES;
  4. /** @test {modelNotes} */
  5. describe("modelNotes", () => {
  6. /** @test {modelNotes} */
  7. it("emits a well-formed CSP with include, var declarations and solve satisfy and no minimize", () => {
  8. const out = buildNotesModel(step3Params());
  9. expect(out).toContain('include "globals.mzn";');
  10. expect(out).toMatch(/^var/m);
  11. expect(out).toMatch(/^solve :: .* satisfy;$/m);
  12. expect(out).not.toContain("minimize");
  13. });
  14. /** @test {modelNotes} */
  15. it("supports scale, chord, atonal and minor pitch domains", () => {
  16. expect(
  17. buildNotesModel(
  18. step3Params({
  19. chords: ["I", "V"],
  20. bars: 99,
  21. voice: [step3Voice({ chord_tones: "chord" })],
  22. }),
  23. ),
  24. ).toContain("var");
  25. for (const v of [{ chord_tones: false }, { chord_tones: true }]) {
  26. expect(
  27. buildNotesModel(step3Params({ voice: [step3Voice(v)] })),
  28. ).toContain("var");
  29. }
  30. expect(buildNotesModel(step3Params({ mode: "minor" }))).toContain("var");
  31. });
  32. /** @test {modelNotes} */
  33. it("emits bare constraints without sentinels when rest_pct = 0; emits holds and rests budget with sentinel-guarded H12, H19, M1 and M9 when rest_pct > 0", () => {
  34. expect(buildNotesModel(step3Params())).not.toContain("== -2");
  35. const unguarded = buildNotesModel(
  36. step3Params({
  37. bars: 2,
  38. voice: [step3Voice({ allow_repetitions: false })],
  39. }),
  40. );
  41. expect(unguarded).toMatch(/^constraint note_0_\d+ != note_0_\d+;$/m);
  42. const guarded = buildNotesModel(
  43. step3Params({
  44. voices: 2,
  45. voice: [
  46. step3Voice({ allow_repetitions: false }),
  47. step3Voice({
  48. min_pitch: 48,
  49. max_pitch: 60,
  50. allow_repetitions: false,
  51. }),
  52. ],
  53. bars: 2,
  54. rest_pct: 25,
  55. forbid_unisons: true,
  56. dissonance: 0.1,
  57. }),
  58. );
  59. expect(guarded).toMatch(/bool2int\(note_/);
  60. expect(guarded).toMatch(/-> note_\d+_\d+ != note_\d+_\d+/);
  61. expect(guarded).toMatch(/-> abs\(note_\d+_\d+ - note_\d+_\d+\) <=/);
  62. expect(guarded).toMatch(
  63. /-> \(abs\(note_\d+_\d+ - note_\d+_\d+\) mod 12\) in /,
  64. );
  65. });
  66. /** @test {modelNotes} */
  67. it("emits cross-voice inequality and parallel-fifth and parallel-octave constraints when H12 and M14 toggles are active", () => {
  68. expect(buildNotesModel(step3TwoVoice({ forbid_unisons: true }))).toMatch(
  69. /note_0_\d+ != note_1_\d+/,
  70. );
  71. const m14 = buildNotesModel(
  72. step3TwoVoice({ forbid_parallels: true, bars: 2 }),
  73. );
  74. expect(m14).toMatch(/mod 12 == 7/);
  75. expect(m14).toMatch(/mod 12 == 0/);
  76. });
  77. /** @test {modelNotes} */
  78. it("emits H19 interval constraints across phi thresholds at <= 0.2, mid and >= 0.8; skips them at neutral 0.5", () => {
  79. for (const d of [0.1, 0.3, 0.7, 0.9]) {
  80. expect(buildNotesModel(step3TwoVoice({ dissonance: d }))).toMatch(
  81. /mod 12\) in \{/,
  82. );
  83. }
  84. expect(buildNotesModel(step3TwoVoice({ dissonance: 0.5 }))).not.toMatch(
  85. /mod 12\) in/,
  86. );
  87. });
  88. /** @test {modelNotes} */
  89. it("emits xi seeding with max_interval, anti-osc and empty-cs branches; covers filterByDissonance variants", () => {
  90. vi.spyOn(Math, "random").mockReturnValue(0.5);
  91. expect(buildNotesModel(step3Params({ seed_pct: 50, bars: 4 }))).toMatch(
  92. /constraint note_0_\d+ = \d+;/,
  93. );
  94. Math.random.mockRestore();
  95. vi.spyOn(Math, "random").mockReturnValue(0);
  96. expect(
  97. buildNotesModel(
  98. step3Params({
  99. voices: 3,
  100. voice: [
  101. step3Voice({ duration: "quarter" }),
  102. step3Voice({
  103. duration: "whole",
  104. min_pitch: 61,
  105. max_pitch: 61,
  106. chord_tones: false,
  107. }),
  108. step3Voice({ duration: "quarter" }),
  109. ],
  110. seed_pct: 50,
  111. dissonance: 0.1,
  112. rest_pct: 25,
  113. bars: 4,
  114. }),
  115. ),
  116. ).toContain("var");
  117. Math.random.mockRestore();
  118. expect(
  119. buildNotesModel(
  120. step3Params({
  121. voices: 2,
  122. voice: [
  123. step3Voice({ min_pitch: 60, max_pitch: 60, chord_tones: false }),
  124. step3Voice({ min_pitch: 61, max_pitch: 61, chord_tones: false }),
  125. ],
  126. dissonance: 0.1,
  127. }),
  128. ),
  129. ).not.toMatch(/abs\(note_\d+_\d+ - note_\d+_\d+\) mod 12\) in /);
  130. });
  131. });