@dgpholdings/greatoak-shared 1.2.87 → 1.2.88
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/dist/__mocks__/catalog.fixture.d.ts +2 -0
- package/dist/__mocks__/catalog.fixture.js +208 -0
- package/dist/__mocks__/exercises.mock.d.ts +4 -11
- package/dist/__mocks__/exercises.mock.js +82 -42
- package/dist/__mocks__/sessions.mock.d.ts +28 -0
- package/dist/__mocks__/sessions.mock.js +394 -0
- package/dist/__mocks__/testIds.d.ts +9 -0
- package/dist/__mocks__/testIds.js +13 -0
- package/dist/__mocks__/user.mock.js +3 -1
- package/dist/constants/goalJourney.d.ts +108 -0
- package/dist/constants/goalJourney.js +443 -0
- package/dist/constants/index.d.ts +1 -0
- package/dist/constants/index.js +1 -0
- package/dist/types/TApiUser.d.ts +2 -0
- package/dist/utils/adoptionEngine/scaleProPlan.util.js +9 -4
- package/dist/utils/constellation/computeNormalisedLoad.test.d.ts +1 -0
- package/dist/utils/constellation/computeNormalisedLoad.test.js +218 -0
- package/dist/utils/constellation/evaluateConstellation.js +1 -1
- package/dist/utils/constellation/evaluateConstellation.test.d.ts +1 -0
- package/dist/utils/constellation/evaluateConstellation.test.js +93 -0
- package/dist/utils/constellation/index.d.ts +1 -0
- package/dist/utils/constellation/index.js +4 -1
- package/dist/utils/constellation/starFoundation.test.d.ts +1 -0
- package/dist/utils/constellation/starFoundation.test.js +75 -0
- package/dist/utils/constellation/stars/consistency.test.d.ts +1 -0
- package/dist/utils/constellation/stars/consistency.test.js +94 -0
- package/dist/utils/constellation/stars/quality.test.d.ts +1 -0
- package/dist/utils/constellation/stars/quality.test.js +113 -0
- package/dist/utils/constellation/stars/recovery.test.d.ts +1 -0
- package/dist/utils/constellation/stars/recovery.test.js +131 -0
- package/dist/utils/constellation/strengthStar.test.d.ts +1 -0
- package/dist/utils/constellation/strengthStar.test.js +190 -0
- package/dist/utils/exerciseRecord/recordValidator.integration.test.js +1 -1
- package/dist/utils/exerciseRecord/recordValidator.js +1 -1
- package/dist/utils/exerciseRecord/recordValidator.test.js +8 -8
- package/dist/utils/scoringWorkout/scoringWorkout.integration.test.js +19 -19
- package/package.json +1 -1
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
// shared/src/utils/constellation/stars/recovery.test.ts
|
|
4
|
+
const starFoundation_1 = require("../starFoundation");
|
|
5
|
+
const recovery_1 = require("../stars/recovery");
|
|
6
|
+
const vitest_1 = require("vitest");
|
|
7
|
+
const NOW = Date.UTC(2026, 5, 1);
|
|
8
|
+
const day = (d) => NOW - d * 86400000;
|
|
9
|
+
// Exercises with muscles in specific zones (upper: pec/lat; lower: quad/ham/glute; core: abs).
|
|
10
|
+
const catalog = {
|
|
11
|
+
bench: {
|
|
12
|
+
exerciseId: "bench",
|
|
13
|
+
name: "Bench",
|
|
14
|
+
primaryMuscles: ["pectoralis-major"],
|
|
15
|
+
secondaryMuscles: ["deltoids-anterior"],
|
|
16
|
+
difficultyLevel: 3,
|
|
17
|
+
movementPattern: "push_horizontal",
|
|
18
|
+
status: "active",
|
|
19
|
+
},
|
|
20
|
+
squat: {
|
|
21
|
+
exerciseId: "squat",
|
|
22
|
+
name: "Squat",
|
|
23
|
+
primaryMuscles: ["quadriceps"],
|
|
24
|
+
secondaryMuscles: ["glutes-maximus"],
|
|
25
|
+
difficultyLevel: 3,
|
|
26
|
+
movementPattern: "squat",
|
|
27
|
+
status: "active",
|
|
28
|
+
},
|
|
29
|
+
row: {
|
|
30
|
+
exerciseId: "row",
|
|
31
|
+
name: "Row",
|
|
32
|
+
primaryMuscles: ["latissimus-dorsi"],
|
|
33
|
+
secondaryMuscles: [],
|
|
34
|
+
difficultyLevel: 3,
|
|
35
|
+
movementPattern: "pull_horizontal",
|
|
36
|
+
status: "active",
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
const scoredEx = (id, muscles) => ({
|
|
40
|
+
exerciseId: id,
|
|
41
|
+
score: 75,
|
|
42
|
+
muscleScores: Object.fromEntries(muscles.map((m) => [m, 70])),
|
|
43
|
+
});
|
|
44
|
+
const ctx = (scoredSessions, level = 1) => ({
|
|
45
|
+
sessions: [],
|
|
46
|
+
scoredSessions,
|
|
47
|
+
exerciseCatalog: catalog,
|
|
48
|
+
user: { weightKg: 80, gender: "male" },
|
|
49
|
+
now: NOW,
|
|
50
|
+
level,
|
|
51
|
+
});
|
|
52
|
+
(0, vitest_1.describe)("recovery — no data", () => {
|
|
53
|
+
(0, vitest_1.it)("empty history → noData gap, 0 brightness", () => {
|
|
54
|
+
const r = (0, starFoundation_1.resolveStar)(recovery_1.recoveryStar, ctx([]));
|
|
55
|
+
(0, vitest_1.expect)(r.brightness).toBe(0);
|
|
56
|
+
(0, vitest_1.expect)(r.gap.translationKey).toBe("constellation.recovery.gap.noData");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
(0, vitest_1.describe)("recovery — earns cycles from trained zones", () => {
|
|
60
|
+
(0, vitest_1.it)("a split lifter (alternating zones, spaced) earns recovery cycles", () => {
|
|
61
|
+
// train each zone, leave gaps so it recovers, train again → transitions
|
|
62
|
+
const s = [];
|
|
63
|
+
const sched = [
|
|
64
|
+
[40, "bench", ["pectoralis-major"]],
|
|
65
|
+
[37, "row", ["latissimus-dorsi"]],
|
|
66
|
+
[34, "squat", ["quadriceps"]],
|
|
67
|
+
[27, "bench", ["pectoralis-major"]],
|
|
68
|
+
[24, "row", ["latissimus-dorsi"]],
|
|
69
|
+
[21, "squat", ["quadriceps"]],
|
|
70
|
+
[14, "bench", ["pectoralis-major"]],
|
|
71
|
+
[11, "row", ["latissimus-dorsi"]],
|
|
72
|
+
[8, "squat", ["quadriceps"]],
|
|
73
|
+
];
|
|
74
|
+
for (const [d, id, m] of sched)
|
|
75
|
+
s.push({ date: day(d), exercises: [scoredEx(id, m)] });
|
|
76
|
+
const r = (0, starFoundation_1.resolveStar)(recovery_1.recoveryStar, ctx(s));
|
|
77
|
+
(0, vitest_1.expect)(r.brightness).toBeGreaterThan(0);
|
|
78
|
+
(0, vitest_1.expect)(r.currentState.value).toBeGreaterThanOrEqual(1);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
(0, vitest_1.describe)("recovery — penalises the daily grinder", () => {
|
|
82
|
+
(0, vitest_1.it)("training all zones every day → low/zero recovery", () => {
|
|
83
|
+
const s = [];
|
|
84
|
+
for (let d = 40; d >= 0; d--) {
|
|
85
|
+
s.push({
|
|
86
|
+
date: day(d),
|
|
87
|
+
exercises: [
|
|
88
|
+
scoredEx("bench", ["pectoralis-major"]),
|
|
89
|
+
scoredEx("squat", ["quadriceps"]),
|
|
90
|
+
scoredEx("row", ["latissimus-dorsi"]),
|
|
91
|
+
],
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const r = (0, starFoundation_1.resolveStar)(recovery_1.recoveryStar, ctx(s));
|
|
95
|
+
(0, vitest_1.expect)(r.brightness).toBeLessThan(50);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
(0, vitest_1.describe)("recovery — untrained zones earn no free credit", () => {
|
|
99
|
+
(0, vitest_1.it)("training only upper does not manufacture lower/core cycles", () => {
|
|
100
|
+
// only upper trained, spaced so it cycles a few times
|
|
101
|
+
const s = [];
|
|
102
|
+
for (const d of [40, 33, 26, 19, 12, 5])
|
|
103
|
+
s.push({
|
|
104
|
+
date: day(d),
|
|
105
|
+
exercises: [scoredEx("bench", ["pectoralis-major"])],
|
|
106
|
+
});
|
|
107
|
+
const r = (0, starFoundation_1.resolveStar)(recovery_1.recoveryStar, ctx(s));
|
|
108
|
+
// cycles come ONLY from the upper zone; if lower/core counted freely,
|
|
109
|
+
// brightness would be far higher. Bounded sanity check:
|
|
110
|
+
(0, vitest_1.expect)(r.currentState.value).toBeGreaterThanOrEqual(1);
|
|
111
|
+
(0, vitest_1.expect)(r.brightness).toBeLessThanOrEqual(100);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
(0, vitest_1.describe)("recovery — level scaling", () => {
|
|
115
|
+
(0, vitest_1.it)("same history is dimmer at L2 (needs more cycles)", () => {
|
|
116
|
+
const s = [];
|
|
117
|
+
const sched = [
|
|
118
|
+
[40, "bench", ["pectoralis-major"]],
|
|
119
|
+
[34, "squat", ["quadriceps"]],
|
|
120
|
+
[27, "bench", ["pectoralis-major"]],
|
|
121
|
+
[21, "squat", ["quadriceps"]],
|
|
122
|
+
[14, "bench", ["pectoralis-major"]],
|
|
123
|
+
[8, "squat", ["quadriceps"]],
|
|
124
|
+
];
|
|
125
|
+
for (const [d, id, m] of sched)
|
|
126
|
+
s.push({ date: day(d), exercises: [scoredEx(id, m)] });
|
|
127
|
+
const l1 = (0, starFoundation_1.resolveStar)(recovery_1.recoveryStar, ctx(s, 1));
|
|
128
|
+
const l2 = (0, starFoundation_1.resolveStar)(recovery_1.recoveryStar, ctx(s, 2));
|
|
129
|
+
(0, vitest_1.expect)(l2.brightness).toBeLessThanOrEqual(l1.brightness);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
// shared/src/utils/constellation/__tests__/strengthStars.test.ts
|
|
4
|
+
const starFoundation_1 = require("./starFoundation");
|
|
5
|
+
const push_1 = require("./stars/push");
|
|
6
|
+
const pull_1 = require("./stars/pull");
|
|
7
|
+
const lowerBody_1 = require("./stars/lowerBody");
|
|
8
|
+
const vitest_1 = require("vitest");
|
|
9
|
+
const NOW = Date.UTC(2026, 5, 1);
|
|
10
|
+
const day = (d) => NOW - d * 86400000;
|
|
11
|
+
const wr = (kg, reps) => ({ type: "weight-reps", kg, reps, isDone: true, isStrictMode: false });
|
|
12
|
+
const ro = (reps, aux = "0") => ({
|
|
13
|
+
type: "reps-only",
|
|
14
|
+
reps,
|
|
15
|
+
auxWeightKg: aux,
|
|
16
|
+
isDone: true,
|
|
17
|
+
isStrictMode: false,
|
|
18
|
+
});
|
|
19
|
+
const ex = (id, mp, over = {}) => ({
|
|
20
|
+
exerciseId: id,
|
|
21
|
+
name: id,
|
|
22
|
+
primaryMuscles: [],
|
|
23
|
+
secondaryMuscles: [],
|
|
24
|
+
difficultyLevel: 3,
|
|
25
|
+
movementPattern: mp,
|
|
26
|
+
status: "active",
|
|
27
|
+
...over,
|
|
28
|
+
});
|
|
29
|
+
const catalog = {
|
|
30
|
+
bench: ex("bench", "push_horizontal"),
|
|
31
|
+
ohp: ex("ohp", "push_vertical"),
|
|
32
|
+
row: ex("row", "pull_horizontal"),
|
|
33
|
+
pullup: ex("pullup", "pull_vertical", {
|
|
34
|
+
weightMultiplier: { male: 1.0, female: 0.95, default: 0.97 },
|
|
35
|
+
}),
|
|
36
|
+
squat: ex("squat", "squat"),
|
|
37
|
+
rdl: ex("rdl", "hinge"),
|
|
38
|
+
lunge: ex("lunge", "lunge"),
|
|
39
|
+
pistol: ex("pistol", "squat", {
|
|
40
|
+
weightMultiplier: { male: 0.95, female: 0.9, default: 0.9 },
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
const ctx = (sessions, user = {}, level = 1) => ({
|
|
44
|
+
sessions,
|
|
45
|
+
scoredSessions: [],
|
|
46
|
+
exerciseCatalog: catalog,
|
|
47
|
+
user: { weightKg: 80, gender: "male", ...user },
|
|
48
|
+
now: NOW,
|
|
49
|
+
level,
|
|
50
|
+
});
|
|
51
|
+
(0, vitest_1.describe)("push star — pattern filtering & thresholds", () => {
|
|
52
|
+
(0, vitest_1.it)("lights from a strong bench (L1 0.60 male)", () => {
|
|
53
|
+
const r = (0, starFoundation_1.resolveStar)(push_1.pushStar, ctx([
|
|
54
|
+
{
|
|
55
|
+
date: day(2),
|
|
56
|
+
exercises: [{ exerciseId: "bench", records: [wr("60", "5")] }],
|
|
57
|
+
},
|
|
58
|
+
]));
|
|
59
|
+
// 60x5 → 70/80 = 0.875 vs 0.60 → capped 100
|
|
60
|
+
(0, vitest_1.expect)(r.brightness).toBe(100);
|
|
61
|
+
(0, vitest_1.expect)(r.tier).toBe(3);
|
|
62
|
+
(0, vitest_1.expect)(r.gap.translationKey).toBe("constellation.push.gap.full");
|
|
63
|
+
});
|
|
64
|
+
(0, vitest_1.it)("counts vertical push (OHP) too", () => {
|
|
65
|
+
const r = (0, starFoundation_1.resolveStar)(push_1.pushStar, ctx([
|
|
66
|
+
{
|
|
67
|
+
date: day(2),
|
|
68
|
+
exercises: [{ exerciseId: "ohp", records: [wr("40", "5")] }],
|
|
69
|
+
},
|
|
70
|
+
]));
|
|
71
|
+
(0, vitest_1.expect)(r.brightness).toBeGreaterThan(0);
|
|
72
|
+
});
|
|
73
|
+
(0, vitest_1.it)("ignores pull movements", () => {
|
|
74
|
+
const r = (0, starFoundation_1.resolveStar)(push_1.pushStar, ctx([
|
|
75
|
+
{
|
|
76
|
+
date: day(2),
|
|
77
|
+
exercises: [{ exerciseId: "row", records: [wr("100", "5")] }],
|
|
78
|
+
},
|
|
79
|
+
]));
|
|
80
|
+
(0, vitest_1.expect)(r.brightness).toBe(0);
|
|
81
|
+
});
|
|
82
|
+
(0, vitest_1.it)("partial brightness below threshold gives a toGo gap", () => {
|
|
83
|
+
const r = (0, starFoundation_1.resolveStar)(push_1.pushStar, ctx([
|
|
84
|
+
{
|
|
85
|
+
date: day(2),
|
|
86
|
+
exercises: [{ exerciseId: "bench", records: [wr("25", "5")] }],
|
|
87
|
+
},
|
|
88
|
+
]));
|
|
89
|
+
// 25x5 → 29.2/80 = 0.365 vs 0.60 → ~61%
|
|
90
|
+
(0, vitest_1.expect)(r.brightness).toBeGreaterThan(0);
|
|
91
|
+
(0, vitest_1.expect)(r.brightness).toBeLessThan(100);
|
|
92
|
+
(0, vitest_1.expect)(r.gap.translationKey).toBe("constellation.push.gap.toGo");
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
(0, vitest_1.describe)("strength — gendered thresholds", () => {
|
|
96
|
+
(0, vitest_1.it)("female has a lower bar (same lift lights more)", () => {
|
|
97
|
+
const sessions = [
|
|
98
|
+
{
|
|
99
|
+
date: day(2),
|
|
100
|
+
exercises: [{ exerciseId: "bench", records: [wr("30", "5")] }],
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
const male = (0, starFoundation_1.resolveStar)(push_1.pushStar, ctx(sessions, { gender: "male" }));
|
|
104
|
+
const female = (0, starFoundation_1.resolveStar)(push_1.pushStar, ctx(sessions, { gender: "female" }));
|
|
105
|
+
(0, vitest_1.expect)(female.brightness).toBeGreaterThanOrEqual(male.brightness);
|
|
106
|
+
});
|
|
107
|
+
(0, vitest_1.it)("unmentioned gender resolves to the male (higher) bar", () => {
|
|
108
|
+
const sessions = [
|
|
109
|
+
{
|
|
110
|
+
date: day(2),
|
|
111
|
+
exercises: [{ exerciseId: "bench", records: [wr("30", "5")] }],
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
const male = (0, starFoundation_1.resolveStar)(push_1.pushStar, ctx(sessions, { gender: "male" }));
|
|
115
|
+
const unmentioned = (0, starFoundation_1.resolveStar)(push_1.pushStar, ctx(sessions, { gender: undefined }));
|
|
116
|
+
(0, vitest_1.expect)(unmentioned.brightness).toBe(male.brightness);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
(0, vitest_1.describe)("strength — level scaling", () => {
|
|
120
|
+
(0, vitest_1.it)("a fixed lift is dimmer at L2 than L1 (higher bar)", () => {
|
|
121
|
+
const sessions = [
|
|
122
|
+
{
|
|
123
|
+
date: day(2),
|
|
124
|
+
exercises: [{ exerciseId: "bench", records: [wr("40", "5")] }],
|
|
125
|
+
},
|
|
126
|
+
];
|
|
127
|
+
// 40x5 → 46.7/80 = 0.583. L1 push 0.60 → ~97%; L2 push 1.0 → ~58%
|
|
128
|
+
const l1 = (0, starFoundation_1.resolveStar)(push_1.pushStar, ctx(sessions, {}, 1));
|
|
129
|
+
const l2 = (0, starFoundation_1.resolveStar)(push_1.pushStar, ctx(sessions, {}, 2));
|
|
130
|
+
(0, vitest_1.expect)(l2.brightness).toBeLessThan(l1.brightness);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
(0, vitest_1.describe)("pull star", () => {
|
|
134
|
+
(0, vitest_1.it)("lights from rows", () => {
|
|
135
|
+
const r = (0, starFoundation_1.resolveStar)(pull_1.pullStar, ctx([
|
|
136
|
+
{
|
|
137
|
+
date: day(2),
|
|
138
|
+
exercises: [{ exerciseId: "row", records: [wr("50", "5")] }],
|
|
139
|
+
},
|
|
140
|
+
]));
|
|
141
|
+
(0, vitest_1.expect)(r.brightness).toBeGreaterThan(0);
|
|
142
|
+
});
|
|
143
|
+
(0, vitest_1.it)("bodyweight pull-ups light via weightMultiplier", () => {
|
|
144
|
+
const r = (0, starFoundation_1.resolveStar)(pull_1.pullStar, ctx([
|
|
145
|
+
{
|
|
146
|
+
date: day(2),
|
|
147
|
+
exercises: [{ exerciseId: "pullup", records: [ro("8")] }],
|
|
148
|
+
},
|
|
149
|
+
]));
|
|
150
|
+
(0, vitest_1.expect)(r.brightness).toBeGreaterThan(0);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
(0, vitest_1.describe)("lowerBody star — combined patterns & home reachability", () => {
|
|
154
|
+
(0, vitest_1.it)("squat, hinge, and lunge all count", () => {
|
|
155
|
+
for (const id of ["squat", "rdl", "lunge"]) {
|
|
156
|
+
const r = (0, starFoundation_1.resolveStar)(lowerBody_1.lowerBodyStar, ctx([
|
|
157
|
+
{
|
|
158
|
+
date: day(2),
|
|
159
|
+
exercises: [{ exerciseId: id, records: [wr("80", "5")] }],
|
|
160
|
+
},
|
|
161
|
+
]));
|
|
162
|
+
(0, vitest_1.expect)(r.brightness).toBeGreaterThan(0);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
(0, vitest_1.it)("HOME user reaches full lower body via bodyweight pistol squats", () => {
|
|
166
|
+
const r = (0, starFoundation_1.resolveStar)(lowerBody_1.lowerBodyStar, ctx([
|
|
167
|
+
{
|
|
168
|
+
date: day(2),
|
|
169
|
+
exercises: [{ exerciseId: "pistol", records: [ro("8")] }],
|
|
170
|
+
},
|
|
171
|
+
], { weightKg: 75 }));
|
|
172
|
+
// pistol 0.95*75=71.25; 1RM=71.25*(1+8/30)=90.25; /75=1.20 vs L1 1.0 → 100
|
|
173
|
+
(0, vitest_1.expect)(r.brightness).toBe(100);
|
|
174
|
+
});
|
|
175
|
+
(0, vitest_1.it)("takes the best across the three patterns", () => {
|
|
176
|
+
const sessions = [
|
|
177
|
+
{
|
|
178
|
+
date: day(5),
|
|
179
|
+
exercises: [{ exerciseId: "squat", records: [wr("50", "5")] }],
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
date: day(2),
|
|
183
|
+
exercises: [{ exerciseId: "rdl", records: [wr("100", "5")] }],
|
|
184
|
+
},
|
|
185
|
+
];
|
|
186
|
+
const r = (0, starFoundation_1.resolveStar)(lowerBody_1.lowerBodyStar, ctx(sessions));
|
|
187
|
+
// rdl 100x5 → 116.7/80 = 1.46 → capped 100
|
|
188
|
+
(0, vitest_1.expect)(r.brightness).toBe(100);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -41,7 +41,7 @@ const exercises_mock_1 = require("../../__mocks__/exercises.mock");
|
|
|
41
41
|
const mockTemplate = templateExercises_mock_1.mockTemplateExercisesDictionary.cardioFree;
|
|
42
42
|
const baseExercise = exercises_mock_1.mockExercisesDictionary[mockTemplate.exerciseId];
|
|
43
43
|
// Artificially disable enableDistance to prove regression safety
|
|
44
|
-
const hostileConfig =
|
|
44
|
+
const hostileConfig = { ...mockTemplate.config, enableDistance: false };
|
|
45
45
|
const result = (0, recordValidator_1.validateAndSanitizeRecord)(mockTemplate.initialRecords[0], baseExercise, hostileConfig);
|
|
46
46
|
(0, vitest_1.expect)(result.isValid).toBe(true);
|
|
47
47
|
// Distance is structurally required for cardio-free, the config cannot turn it off.
|
|
@@ -18,7 +18,7 @@ const validateAndSanitizeRecord = (record, baseExercise, config) => {
|
|
|
18
18
|
return { isValid: false, sanitizedRecord: null, errors };
|
|
19
19
|
}
|
|
20
20
|
// 3. Create a clean clone for sanitization
|
|
21
|
-
const sanitized =
|
|
21
|
+
const sanitized = { ...record };
|
|
22
22
|
// 4. Config-Driven Sanitization
|
|
23
23
|
// If a field is disabled in the TExerciseConfig, we remove it from the payload.
|
|
24
24
|
if (!config.enableRpe) {
|
|
@@ -85,7 +85,7 @@ const recordValidator_1 = require("./recordValidator");
|
|
|
85
85
|
(0, vitest_1.expect)(result.sanitizedRecord).toHaveProperty('reps', '0');
|
|
86
86
|
});
|
|
87
87
|
(0, vitest_1.it)('validates numeric integrity of reps-only and collects error', () => {
|
|
88
|
-
const baseRepExercise =
|
|
88
|
+
const baseRepExercise = { ...mockBaseExercise, recordType: 'reps-only' };
|
|
89
89
|
const record = { type: 'reps-only', isDone: true, isStrictMode: false, reps: 'abc', auxWeightKg: '20' };
|
|
90
90
|
const result = (0, recordValidator_1.validateAndSanitizeRecord)(record, baseRepExercise, mockConfigAllEnabled);
|
|
91
91
|
(0, vitest_1.expect)(result.isValid).toBe(false);
|
|
@@ -94,7 +94,7 @@ const recordValidator_1 = require("./recordValidator");
|
|
|
94
94
|
(0, vitest_1.expect)(result.errors[0]).toContain('Reps must be a parseable number');
|
|
95
95
|
});
|
|
96
96
|
(0, vitest_1.it)('zeroes out required auxWeightKg instead of deleting it on reps-only when disabled', () => {
|
|
97
|
-
const baseRepExercise =
|
|
97
|
+
const baseRepExercise = { ...mockBaseExercise, recordType: 'reps-only' };
|
|
98
98
|
const record = { type: 'reps-only', isDone: true, isStrictMode: false, reps: '10', auxWeightKg: '20' };
|
|
99
99
|
const result = (0, recordValidator_1.validateAndSanitizeRecord)(record, baseRepExercise, mockConfigAllDisabled);
|
|
100
100
|
(0, vitest_1.expect)(result.isValid).toBe(true);
|
|
@@ -102,14 +102,14 @@ const recordValidator_1 = require("./recordValidator");
|
|
|
102
102
|
(0, vitest_1.expect)(result.sanitizedRecord).toHaveProperty('auxWeightKg', '');
|
|
103
103
|
});
|
|
104
104
|
(0, vitest_1.it)('validates duration type successfully (Happy Path)', () => {
|
|
105
|
-
const baseDurationExercise =
|
|
105
|
+
const baseDurationExercise = { ...mockBaseExercise, recordType: 'duration' };
|
|
106
106
|
const record = { type: 'duration', isDone: true, isStrictMode: false, durationMmSs: '05:00', auxWeightKg: '0' };
|
|
107
107
|
const result = (0, recordValidator_1.validateAndSanitizeRecord)(record, baseDurationExercise, mockConfigAllEnabled);
|
|
108
108
|
(0, vitest_1.expect)(result.isValid).toBe(true);
|
|
109
109
|
(0, vitest_1.expect)(result.sanitizedRecord).toHaveProperty('durationMmSs', '05:00');
|
|
110
110
|
});
|
|
111
111
|
(0, vitest_1.it)('rejects cardio-machine records with zero duration', () => {
|
|
112
|
-
const baseMachineExercise =
|
|
112
|
+
const baseMachineExercise = { ...mockBaseExercise, recordType: 'cardio-machine' };
|
|
113
113
|
const record = { type: 'cardio-machine', isDone: true, isStrictMode: false, durationMmSs: '00:00' };
|
|
114
114
|
const result = (0, recordValidator_1.validateAndSanitizeRecord)(record, baseMachineExercise, mockConfigAllEnabled);
|
|
115
115
|
(0, vitest_1.expect)(result.isValid).toBe(false);
|
|
@@ -117,7 +117,7 @@ const recordValidator_1 = require("./recordValidator");
|
|
|
117
117
|
(0, vitest_1.expect)(result.errors[0]).toContain('Duration (MM:SS) must be present and non-zero');
|
|
118
118
|
});
|
|
119
119
|
(0, vitest_1.it)('strips optional fields from cardio-machine when disabled', () => {
|
|
120
|
-
const baseMachineExercise =
|
|
120
|
+
const baseMachineExercise = { ...mockBaseExercise, recordType: 'cardio-machine' };
|
|
121
121
|
const record = {
|
|
122
122
|
type: 'cardio-machine',
|
|
123
123
|
isDone: true,
|
|
@@ -139,7 +139,7 @@ const recordValidator_1 = require("./recordValidator");
|
|
|
139
139
|
(0, vitest_1.expect)(result.sanitizedRecord).toHaveProperty('durationMmSs', '15:00');
|
|
140
140
|
});
|
|
141
141
|
(0, vitest_1.it)('validates duration format for cardio-free', () => {
|
|
142
|
-
const baseCardioExercise =
|
|
142
|
+
const baseCardioExercise = { ...mockBaseExercise, recordType: 'cardio-free' };
|
|
143
143
|
// Missing durationMmSs
|
|
144
144
|
const record = { type: 'cardio-free', isDone: true, isStrictMode: false, distance: '5', durationMmSs: '00:00' };
|
|
145
145
|
const result = (0, recordValidator_1.validateAndSanitizeRecord)(record, baseCardioExercise, mockConfigAllEnabled);
|
|
@@ -148,7 +148,7 @@ const recordValidator_1 = require("./recordValidator");
|
|
|
148
148
|
(0, vitest_1.expect)(result.errors[0]).toContain('Duration (MM:SS) must be present and non-zero');
|
|
149
149
|
});
|
|
150
150
|
(0, vitest_1.it)('validates distance format for cardio-free', () => {
|
|
151
|
-
const baseCardioExercise =
|
|
151
|
+
const baseCardioExercise = { ...mockBaseExercise, recordType: 'cardio-free' };
|
|
152
152
|
const record = { type: 'cardio-free', isDone: true, isStrictMode: false, distance: 'invalid', durationMmSs: '15:00' };
|
|
153
153
|
const result = (0, recordValidator_1.validateAndSanitizeRecord)(record, baseCardioExercise, mockConfigAllEnabled);
|
|
154
154
|
(0, vitest_1.expect)(result.isValid).toBe(false);
|
|
@@ -156,7 +156,7 @@ const recordValidator_1 = require("./recordValidator");
|
|
|
156
156
|
(0, vitest_1.expect)(result.errors[0]).toContain('Distance must be a parseable number');
|
|
157
157
|
});
|
|
158
158
|
(0, vitest_1.it)('retains required distance on cardio-free even if enableDistance is false (Regression)', () => {
|
|
159
|
-
const baseCardioExercise =
|
|
159
|
+
const baseCardioExercise = { ...mockBaseExercise, recordType: 'cardio-free' };
|
|
160
160
|
const record = { type: 'cardio-free', isDone: true, isStrictMode: false, distance: '5', durationMmSs: '15:00' };
|
|
161
161
|
const result = (0, recordValidator_1.validateAndSanitizeRecord)(record, baseCardioExercise, mockConfigAllDisabled);
|
|
162
162
|
(0, vitest_1.expect)(result.isValid).toBe(true);
|
|
@@ -1,15 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __rest = (this && this.__rest) || function (s, e) {
|
|
3
|
-
var t = {};
|
|
4
|
-
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
5
|
-
t[p] = s[p];
|
|
6
|
-
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
7
|
-
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
8
|
-
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
9
|
-
t[p[i]] = s[p[i]];
|
|
10
|
-
}
|
|
11
|
-
return t;
|
|
12
|
-
};
|
|
13
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
3
|
const vitest_1 = require("vitest");
|
|
15
4
|
const index_1 = require("./index");
|
|
@@ -25,10 +14,10 @@ const user_mock_1 = require("../../__mocks__/user.mock");
|
|
|
25
14
|
// parseRecords should null-out the last done set's rest regardless of isStrictMode.
|
|
26
15
|
const records = template.initialRecords.map((r, i) => {
|
|
27
16
|
if (i === 1) {
|
|
28
|
-
const { restDurationSecs
|
|
29
|
-
return
|
|
17
|
+
const { restDurationSecs, ...rest } = r;
|
|
18
|
+
return { ...rest, isStrictMode: true };
|
|
30
19
|
}
|
|
31
|
-
return
|
|
20
|
+
return { ...r, isStrictMode: true, restDurationSecs: 120 };
|
|
32
21
|
});
|
|
33
22
|
// Direct parseRecords proof: last done set (index 1) must have null rest.
|
|
34
23
|
const parsed = (0, parseRecords_1.parseRecords)(records, exercise.timingGuardrails);
|
|
@@ -68,7 +57,11 @@ const user_mock_1 = require("../../__mocks__/user.mock");
|
|
|
68
57
|
const exercise = exercises_mock_1.mockExerciseWeightReps;
|
|
69
58
|
// restDurationSecs present → hasRestData = true → rest IS scored
|
|
70
59
|
// 600s > acceptableMax so parseRecords clamps to typical (120s) → optimal → 100
|
|
71
|
-
const records = template.initialRecords.map((r, i) => (
|
|
60
|
+
const records = template.initialRecords.map((r, i) => ({
|
|
61
|
+
...r,
|
|
62
|
+
isStrictMode: true,
|
|
63
|
+
restDurationSecs: i === 0 ? 600 : 120,
|
|
64
|
+
}));
|
|
72
65
|
const { qualityBreakdown, restDisciplineActive } = (0, index_1.calculateExerciseScoreV2)({
|
|
73
66
|
exercise,
|
|
74
67
|
record: records,
|
|
@@ -83,7 +76,11 @@ const user_mock_1 = require("../../__mocks__/user.mock");
|
|
|
83
76
|
// Set 0: 350s → outside acceptableMax (300s) → 50
|
|
84
77
|
// Set 1: 120s → optimal → 100
|
|
85
78
|
// avg = 75, plus stressRestBonus (5) = 80
|
|
86
|
-
const records = template.initialRecords.map((r, i) => (
|
|
79
|
+
const records = template.initialRecords.map((r, i) => ({
|
|
80
|
+
...r,
|
|
81
|
+
isStrictMode: true,
|
|
82
|
+
restDurationSecs: i === 0 ? 350 : 120,
|
|
83
|
+
}));
|
|
87
84
|
const { qualityBreakdown, restDisciplineActive } = (0, index_1.calculateExerciseScoreV2)({
|
|
88
85
|
exercise,
|
|
89
86
|
record: records,
|
|
@@ -158,7 +155,10 @@ const user_mock_1 = require("../../__mocks__/user.mock");
|
|
|
158
155
|
});
|
|
159
156
|
(0, vitest_1.it)("stretch-mobility = zero fatigue: muscleScores should be empty", () => {
|
|
160
157
|
const template = templateExercises_mock_1.mockTemplateExercisesDictionary.duration;
|
|
161
|
-
const exercise =
|
|
158
|
+
const exercise = {
|
|
159
|
+
...exercises_mock_1.mockExerciseDuration,
|
|
160
|
+
scoringSpecialHandling: "stretch-mobility",
|
|
161
|
+
};
|
|
162
162
|
const { muscleScores } = (0, index_1.calculateExerciseScoreV2)({
|
|
163
163
|
exercise,
|
|
164
164
|
record: template.initialRecords,
|
|
@@ -332,12 +332,12 @@ const user_mock_1 = require("../../__mocks__/user.mock");
|
|
|
332
332
|
const sedentaryResult = (0, index_1.calculateExerciseScoreV2)({
|
|
333
333
|
exercise: exercises_mock_1.mockExerciseWeightReps,
|
|
334
334
|
record: template.initialRecords,
|
|
335
|
-
user:
|
|
335
|
+
user: { ...user_mock_1.mockUser, fitnessLevel: "sedentary" },
|
|
336
336
|
});
|
|
337
337
|
const veryActiveResult = (0, index_1.calculateExerciseScoreV2)({
|
|
338
338
|
exercise: exercises_mock_1.mockExerciseWeightReps,
|
|
339
339
|
record: template.initialRecords,
|
|
340
|
-
user:
|
|
340
|
+
user: { ...user_mock_1.mockUser, fitnessLevel: "very-active" },
|
|
341
341
|
});
|
|
342
342
|
// sedentary: activityScale 0.70 → lower referenceMax → higher normalised fatigue
|
|
343
343
|
// very-active: activityScale 1.20 → higher referenceMax → lower normalised fatigue
|