tests/constraints3.test.js
- import { describe, it, expect, vi } from "vitest";
- import { FIXTURES } from "./api.js";
- const { buildNotesModel, step3Voice, step3Params, step3TwoVoice } = FIXTURES;
- describe("modelNotes", () => {
-
- it("emits a well-formed CSP with include, var declarations and solve satisfy and no minimize", () => {
- const out = buildNotesModel(step3Params());
- expect(out).toContain('include "globals.mzn";');
- expect(out).toMatch(/^var/m);
- expect(out).toMatch(/^solve :: .* satisfy;$/m);
- expect(out).not.toContain("minimize");
- });
-
- it("supports scale, chord, atonal and minor pitch domains", () => {
- expect(
- buildNotesModel(
- step3Params({
- chords: ["I", "V"],
- bars: 99,
- voice: [step3Voice({ chord_tones: "chord" })],
- }),
- ),
- ).toContain("var");
- for (const v of [{ chord_tones: false }, { chord_tones: true }]) {
- expect(
- buildNotesModel(step3Params({ voice: [step3Voice(v)] })),
- ).toContain("var");
- }
- expect(buildNotesModel(step3Params({ mode: "minor" }))).toContain("var");
- });
-
- 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", () => {
- expect(buildNotesModel(step3Params())).not.toContain("== -2");
- const unguarded = buildNotesModel(
- step3Params({
- bars: 2,
- voice: [step3Voice({ allow_repetitions: false })],
- }),
- );
- expect(unguarded).toMatch(/^constraint note_0_\d+ != note_0_\d+;$/m);
- const guarded = buildNotesModel(
- step3Params({
- voices: 2,
- voice: [
- step3Voice({ allow_repetitions: false }),
- step3Voice({
- min_pitch: 48,
- max_pitch: 60,
- allow_repetitions: false,
- }),
- ],
- bars: 2,
- rest_pct: 25,
- forbid_unisons: true,
- dissonance: 0.1,
- }),
- );
- expect(guarded).toMatch(/bool2int\(note_/);
- expect(guarded).toMatch(/-> note_\d+_\d+ != note_\d+_\d+/);
- expect(guarded).toMatch(/-> abs\(note_\d+_\d+ - note_\d+_\d+\) <=/);
- expect(guarded).toMatch(
- /-> \(abs\(note_\d+_\d+ - note_\d+_\d+\) mod 12\) in /,
- );
- });
-
- it("emits cross-voice inequality and parallel-fifth and parallel-octave constraints when H12 and M14 toggles are active", () => {
- expect(buildNotesModel(step3TwoVoice({ forbid_unisons: true }))).toMatch(
- /note_0_\d+ != note_1_\d+/,
- );
- const m14 = buildNotesModel(
- step3TwoVoice({ forbid_parallels: true, bars: 2 }),
- );
- expect(m14).toMatch(/mod 12 == 7/);
- expect(m14).toMatch(/mod 12 == 0/);
- });
-
- it("emits H19 interval constraints across phi thresholds at <= 0.2, mid and >= 0.8; skips them at neutral 0.5", () => {
- for (const d of [0.1, 0.3, 0.7, 0.9]) {
- expect(buildNotesModel(step3TwoVoice({ dissonance: d }))).toMatch(
- /mod 12\) in \{/,
- );
- }
- expect(buildNotesModel(step3TwoVoice({ dissonance: 0.5 }))).not.toMatch(
- /mod 12\) in/,
- );
- });
-
- it("emits xi seeding with max_interval, anti-osc and empty-cs branches; covers filterByDissonance variants", () => {
- vi.spyOn(Math, "random").mockReturnValue(0.5);
- expect(buildNotesModel(step3Params({ seed_pct: 50, bars: 4 }))).toMatch(
- /constraint note_0_\d+ = \d+;/,
- );
- Math.random.mockRestore();
- vi.spyOn(Math, "random").mockReturnValue(0);
- expect(
- buildNotesModel(
- step3Params({
- voices: 3,
- voice: [
- step3Voice({ duration: "quarter" }),
- step3Voice({
- duration: "whole",
- min_pitch: 61,
- max_pitch: 61,
- chord_tones: false,
- }),
- step3Voice({ duration: "quarter" }),
- ],
- seed_pct: 50,
- dissonance: 0.1,
- rest_pct: 25,
- bars: 4,
- }),
- ),
- ).toContain("var");
- Math.random.mockRestore();
- expect(
- buildNotesModel(
- step3Params({
- voices: 2,
- voice: [
- step3Voice({ min_pitch: 60, max_pitch: 60, chord_tones: false }),
- step3Voice({ min_pitch: 61, max_pitch: 61, chord_tones: false }),
- ],
- dissonance: 0.1,
- }),
- ),
- ).not.toMatch(/abs\(note_\d+_\d+ - note_\d+_\d+\) mod 12\) in /);
- });
- });