From efb6398b8ea76471ee25074fb6d6a0ef7d46985a Mon Sep 17 00:00:00 2001
From: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
Date: Thu, 11 Jan 2024 12:36:29 +0100
Subject: [PATCH] fix: sort challengeOrder when repairing meta (#53037)

---
 jest.config.js                                |   3 +-
 tools/challenge-helper-scripts/commands.ts    |  23 +-
 .../helpers/get-challenge-order.test.ts       | 180 ++++++-----
 .../helpers/get-file-name.test.ts             |  45 +--
 .../helpers/project-metadata.test.ts          | 280 ++++++++----------
 .../repair-meta.test.ts                       |  73 +++++
 tools/challenge-helper-scripts/repair-meta.ts |  23 +-
 tools/challenge-helper-scripts/utils.test.ts  |  44 +--
 8 files changed, 342 insertions(+), 329 deletions(-)
 create mode 100644 tools/challenge-helper-scripts/repair-meta.test.ts

diff --git a/jest.config.js b/jest.config.js
index 25f74d6b7dbd55..5d914b263a317d 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -21,5 +21,6 @@ module.exports = {
   roots: ['.', './client', './api-server'],
   transformIgnorePatterns: ['node_modules/.pnpm/(?!(nanoid|uuid)@)'],
   setupFilesAfterEnv: ['./jest.setup.js'],
-  testEnvironment: 'jsdom'
+  testEnvironment: 'jsdom',
+  watchPathIgnorePatterns: ['<rootDir>/__fixtures__.*']
 };
diff --git a/tools/challenge-helper-scripts/commands.ts b/tools/challenge-helper-scripts/commands.ts
index 1b7f6d37a04cef..b22c5c041586b2 100644
--- a/tools/challenge-helper-scripts/commands.ts
+++ b/tools/challenge-helper-scripts/commands.ts
@@ -1,6 +1,7 @@
 import fs from 'fs';
 import { getProjectPath } from './helpers/get-project-info';
-import { getMetaData } from './helpers/project-metadata';
+import { getMetaData, updateMetaData } from './helpers/project-metadata';
+import { getChallengeOrderFromFileTree } from './helpers/get-challenge-order';
 import {
   createStepFile,
   deleteStepFromMeta,
@@ -70,4 +71,22 @@ function createEmptySteps(num: number): void {
   console.log(`Successfully added ${num} steps`);
 }
 
-export { deleteStep, insertStep, createEmptySteps };
+const repairMeta = async () => {
+  const sortByStepNum = (a: string, b: string) =>
+    parseInt(a.split(' ')[1]) - parseInt(b.split(' ')[1]);
+
+  const challengeOrder = await getChallengeOrderFromFileTree();
+  if (!challengeOrder.every(({ title }) => /Step \d+/.test(title))) {
+    throw new Error(
+      'You can only run this command on project-based blocks with step files.'
+    );
+  }
+  const sortedChallengeOrder = challengeOrder.sort((a, b) =>
+    sortByStepNum(a.title, b.title)
+  );
+  const meta = getMetaData();
+  meta.challengeOrder = sortedChallengeOrder;
+  updateMetaData(meta);
+};
+
+export { deleteStep, insertStep, createEmptySteps, repairMeta };
diff --git a/tools/challenge-helper-scripts/helpers/get-challenge-order.test.ts b/tools/challenge-helper-scripts/helpers/get-challenge-order.test.ts
index fb6be7674cb928..7787988e8f77fa 100644
--- a/tools/challenge-helper-scripts/helpers/get-challenge-order.test.ts
+++ b/tools/challenge-helper-scripts/helpers/get-challenge-order.test.ts
@@ -6,120 +6,106 @@ import {
   getChallengeOrderFromMeta
 } from './get-challenge-order';
 
-const metaPath = join(
+const basePath = join(
   process.cwd(),
-  'curriculum',
-  'challenges',
-  '_meta',
-  'project'
+  '__fixtures__' + process.env.JEST_WORKER_ID
 );
+const commonPath = join(basePath, 'curriculum', 'challenges');
+
+const block = 'project-get-challenge-order';
+const metaPath = join(commonPath, '_meta', block);
 const superBlockPath = join(
-  process.cwd(),
-  'curriculum',
-  'challenges',
+  commonPath,
   'english',
-  'superblock'
+  'superblock-get-challenge-order'
 );
-const projectPath = join(superBlockPath, 'project');
+const projectPath = join(superBlockPath, block);
 
-const cleanFiles = () => {
-  try {
-    fs.rmSync(superBlockPath, { recursive: true });
-  } catch (err) {
-    console.log('Could not remove superblock mock folder. ');
-  }
-  try {
-    fs.rmSync(metaPath, { recursive: true });
-  } catch (err) {
-    console.log('Could not remove meta mock folder.');
-  }
-};
-
-describe('getChallengeOrderFromMeta helper', () => {
+describe('get-challenge-order helper', () => {
   beforeEach(() => {
-    fs.mkdirSync(superBlockPath);
-    fs.mkdirSync(projectPath);
-    fs.mkdirSync(metaPath);
-    fs.writeFileSync(
-      join(projectPath, 'this-is-a-challenge.md'),
-      '---\nid: 1\ntitle: This is a Challenge\n---',
-      'utf-8'
-    );
-    fs.writeFileSync(
-      join(projectPath, 'what-a-cool-thing.md'),
-      '---\nid: 100\ntitle: What a Cool Thing\n---',
-      'utf-8'
-    );
-    fs.writeFileSync(
-      join(projectPath, 'i-dunno.md'),
-      '---\nid: 2\ntitle: I Dunno\n---'
-    );
-    fs.writeFileSync(
-      join(metaPath, 'meta.json'),
-      `{
+    fs.mkdirSync(superBlockPath, { recursive: true });
+    fs.mkdirSync(projectPath, { recursive: true });
+    fs.mkdirSync(metaPath, { recursive: true });
+  });
+  describe('getChallengeOrderFromMeta helper', () => {
+    beforeEach(() => {
+      fs.writeFileSync(
+        join(projectPath, 'this-is-a-challenge.md'),
+        '---\nid: 1\ntitle: This is a Challenge\n---',
+        'utf-8'
+      );
+      fs.writeFileSync(
+        join(projectPath, 'what-a-cool-thing.md'),
+        '---\nid: 100\ntitle: What a Cool Thing\n---',
+        'utf-8'
+      );
+      fs.writeFileSync(
+        join(projectPath, 'i-dunno.md'),
+        '---\nid: 2\ntitle: I Dunno\n---'
+      );
+      fs.writeFileSync(
+        join(metaPath, 'meta.json'),
+        `{
       "id": "mock-id",
       "challengeOrder": [{"id": "1", "title": "This title is wrong"}, {"id": "2", "title": "I Dunno"}, {"id": "100", "title": "What a Cool Thing"}]}`,
-      'utf-8'
-    );
-  });
+        'utf-8'
+      );
+    });
 
-  it('should load the file order', () => {
-    process.env.CALLING_DIR = projectPath;
-    const challengeOrder = getChallengeOrderFromMeta();
-    expect(challengeOrder).toEqual([
-      { id: '1', title: 'This title is wrong' },
-      { id: '2', title: 'I Dunno' },
-      { id: '100', title: 'What a Cool Thing' }
-    ]);
+    it('should load the file order', () => {
+      process.env.CALLING_DIR = projectPath;
+      const challengeOrder = getChallengeOrderFromMeta();
+      expect(challengeOrder).toEqual([
+        { id: '1', title: 'This title is wrong' },
+        { id: '2', title: 'I Dunno' },
+        { id: '100', title: 'What a Cool Thing' }
+      ]);
+    });
   });
 
-  afterEach(() => {
-    delete process.env.CALLING_DIR;
-    cleanFiles();
-  });
-});
-
-describe('getChallengeOrderFromFileTree helper', () => {
-  beforeEach(() => {
-    fs.mkdirSync(superBlockPath);
-    fs.mkdirSync(projectPath);
-    fs.mkdirSync(metaPath);
-    fs.writeFileSync(
-      join(projectPath, 'step-001.md'),
-      '---\nid: a8d97bd4c764e91f9d2bda01\ntitle: Step 1\n---',
-      'utf-8'
-    );
-    fs.writeFileSync(
-      join(projectPath, 'step-002.md'),
-      '---\nid: a6b0bb188d873cb2c8729495\ntitle: Step 2\n---',
-      'utf-8'
-    );
-    fs.writeFileSync(
-      join(projectPath, 'step-003.md'),
-      '---\nid: a5de63ebea8dbee56860f4f2\ntitle: Step 3\n---'
-    );
-    fs.writeFileSync(
-      join(metaPath, 'meta.json'),
-      `{
+  describe('getChallengeOrderFromFileTree helper', () => {
+    beforeEach(() => {
+      fs.writeFileSync(
+        join(projectPath, 'step-001.md'),
+        '---\nid: a8d97bd4c764e91f9d2bda01\ntitle: Step 1\n---',
+        'utf-8'
+      );
+      fs.writeFileSync(
+        join(projectPath, 'step-002.md'),
+        '---\nid: a6b0bb188d873cb2c8729495\ntitle: Step 2\n---',
+        'utf-8'
+      );
+      fs.writeFileSync(
+        join(projectPath, 'step-003.md'),
+        '---\nid: a5de63ebea8dbee56860f4f2\ntitle: Step 3\n---'
+      );
+      fs.writeFileSync(
+        join(metaPath, 'meta.json'),
+        `{
       "id": "mock-id",
       "challengeOrder": [{"id": "a8d97bd4c764e91f9d2bda01", "title": "Step 1"}, {"id": "a6b0bb188d873cb2c8729495", "title": "Step 3"}, {"id": "a5de63ebea8dbee56860f4f2", "title": "Step 2"}]}`,
-      'utf-8'
-    );
-  });
+        'utf-8'
+      );
+    });
 
-  it('should load the file order', async () => {
-    expect.assertions(1);
-    process.env.CALLING_DIR = projectPath;
-    const challengeOrder = await getChallengeOrderFromFileTree();
-    expect(challengeOrder).toEqual([
-      { id: 'a8d97bd4c764e91f9d2bda01', title: 'Step 1' },
-      { id: 'a6b0bb188d873cb2c8729495', title: 'Step 2' },
-      { id: 'a5de63ebea8dbee56860f4f2', title: 'Step 3' }
-    ]);
+    it('should load the file order', async () => {
+      expect.assertions(1);
+      process.env.CALLING_DIR = projectPath;
+      const challengeOrder = await getChallengeOrderFromFileTree();
+      expect(challengeOrder).toEqual([
+        { id: 'a8d97bd4c764e91f9d2bda01', title: 'Step 1' },
+        { id: 'a6b0bb188d873cb2c8729495', title: 'Step 2' },
+        { id: 'a5de63ebea8dbee56860f4f2', title: 'Step 3' }
+      ]);
+    });
   });
-
   afterEach(() => {
-    cleanFiles();
     delete process.env.CALLING_DIR;
+    try {
+      fs.rmSync(basePath, { recursive: true });
+    } catch (err) {
+      console.log(err);
+      console.log('Could not remove fixtures folder.');
+    }
   });
 });
diff --git a/tools/challenge-helper-scripts/helpers/get-file-name.test.ts b/tools/challenge-helper-scripts/helpers/get-file-name.test.ts
index 813ca9e6e93f81..90a596107ea9e0 100644
--- a/tools/challenge-helper-scripts/helpers/get-file-name.test.ts
+++ b/tools/challenge-helper-scripts/helpers/get-file-name.test.ts
@@ -3,39 +3,22 @@ import { join } from 'path';
 
 import { getFileName } from './get-file-name';
 
-const metaPath = join(
+const basePath = join(
   process.cwd(),
-  'curriculum',
-  'challenges',
-  '_meta',
-  'project'
+  '__fixtures__' + process.env.JEST_WORKER_ID
 );
-const superBlockPath = join(
-  process.cwd(),
-  'curriculum',
-  'challenges',
-  'english',
-  'superblock'
-);
-const projectPath = join(superBlockPath, 'project');
+const commonPath = join(basePath, 'curriculum', 'challenges');
 
-const cleanFiles = () => {
-  try {
-    fs.rmSync(superBlockPath, { recursive: true });
-  } catch (err) {
-    console.log('Could not remove superblock mock folder. ');
-  }
-  try {
-    fs.rmSync(metaPath, { recursive: true });
-  } catch (err) {
-    console.log('Could not remove meta mock folder.');
-  }
-};
+const block = 'project-get-file-name';
+const metaPath = join(commonPath, '_meta', block);
+const superBlockPath = join(commonPath, 'english', 'superblock-get-file-name');
+const projectPath = join(superBlockPath, block);
 
 describe('getFileName helper', () => {
   beforeEach(() => {
-    fs.mkdirSync(superBlockPath);
-    fs.mkdirSync(projectPath);
+    fs.mkdirSync(superBlockPath, { recursive: true });
+    fs.mkdirSync(projectPath, { recursive: true });
+    fs.mkdirSync(metaPath, { recursive: true });
     fs.writeFileSync(
       join(projectPath, 'this-is-a-challenge.md'),
       '---\nid: a\ntitle: This is a Challenge\n---',
@@ -51,7 +34,6 @@ describe('getFileName helper', () => {
       '---\nid: c\ntitle: I Dunno\n---',
       'utf-8'
     );
-    fs.mkdirSync(metaPath);
     fs.writeFileSync(
       join(metaPath, 'meta.json'),
       `{
@@ -77,6 +59,11 @@ describe('getFileName helper', () => {
 
   afterEach(() => {
     delete process.env.CALLING_DIR;
-    cleanFiles();
+    try {
+      fs.rmSync(basePath, { recursive: true });
+    } catch (err) {
+      console.log(err);
+      console.log('Could not remove fixtures folder.');
+    }
   });
 });
diff --git a/tools/challenge-helper-scripts/helpers/project-metadata.test.ts b/tools/challenge-helper-scripts/helpers/project-metadata.test.ts
index 1f92c2326a85a9..d7cd0d7c99455f 100644
--- a/tools/challenge-helper-scripts/helpers/project-metadata.test.ts
+++ b/tools/challenge-helper-scripts/helpers/project-metadata.test.ts
@@ -6,212 +6,190 @@ import {
   validateMetaData
 } from './project-metadata';
 
-const metaPath = join(
+const basePath = join(
   process.cwd(),
-  'curriculum',
-  'challenges',
-  '_meta',
-  'project'
+  '__fixtures__' + process.env.JEST_WORKER_ID
 );
+const commonPath = join(basePath, 'curriculum', 'challenges');
+
+const block = 'project-project-metadata';
+const metaPath = join(commonPath, '_meta', block);
 const superBlockPath = join(
-  process.cwd(),
-  'curriculum',
-  'challenges',
+  commonPath,
   'english',
-  'superblock'
+  'superblock-project-metadata'
 );
-const projectPath = join(superBlockPath, 'project');
-
-const cleanFiles = () => {
-  try {
-    fs.rmSync(superBlockPath, { recursive: true });
-  } catch (err) {
-    console.log('Could not remove superblock mock folder. ');
-  }
-  try {
-    fs.rmSync(metaPath, { recursive: true });
-  } catch (err) {
-    console.log('Could not remove meta mock folder.');
-  }
-};
-
-describe('getProjectMetaPath helper', () => {
-  it('should return the meta path', () => {
-    const expected = join(metaPath, 'meta.json');
-
-    process.env.CALLING_DIR = projectPath;
-
-    expect(getProjectMetaPath()).toEqual(expected);
-  });
-
-  afterEach(() => {
-    cleanFiles();
-    delete process.env.CALLING_DIR;
-  });
-});
+const projectPath = join(superBlockPath, block);
 
-describe('getMetaData helper', () => {
+describe('project-metadata helper', () => {
   beforeEach(() => {
-    fs.mkdirSync(superBlockPath);
-    fs.mkdirSync(projectPath);
-    fs.writeFileSync(
-      join(projectPath, 'step-001.md'),
-      'Lorem ipsum...',
-      'utf-8'
-    );
-    fs.writeFileSync(
-      join(projectPath, 'step-002.md'),
-      'Lorem ipsum...',
-      'utf-8'
-    );
-    fs.writeFileSync(
-      join(projectPath, 'step-003.md'),
-      'Lorem ipsum...',
-      'utf-8'
-    );
-    fs.mkdirSync(metaPath);
-    fs.writeFileSync(
-      join(metaPath, 'meta.json'),
-      `{
-      "id": "mock-id",
-      "challengeOrder": [{"id": "1", "title": "Step 1"}, {"id": "2", "title": "Step 2"}, {"id": "1", "title": "Step 3"}]}`,
-      'utf-8'
-    );
-  });
-
-  it('should process requested file', () => {
-    const expected = {
-      id: 'mock-id',
-      challengeOrder: [
-        { id: '1', title: 'Step 1' },
-        { id: '2', title: 'Step 2' },
-        { id: '1', title: 'Step 3' }
-      ]
-    };
-    process.env.CALLING_DIR = projectPath;
-    expect(getMetaData()).toEqual(expected);
+    fs.mkdirSync(superBlockPath, { recursive: true });
+    fs.mkdirSync(projectPath, { recursive: true });
+    fs.mkdirSync(metaPath, { recursive: true });
   });
+  describe('getProjectMetaPath helper', () => {
+    it('should return the meta path', () => {
+      const expected = join(metaPath, 'meta.json');
 
-  it('should throw if file is not found', () => {
-    process.env.CALLING_DIR =
-      'curriculum/challenges/english/superblock/mick-priject';
+      process.env.CALLING_DIR = projectPath;
 
-    const errorPath = join(
-      'curriculum',
-      'challenges',
-      '_meta',
-      'mick-priject',
-      'meta.json'
-    );
-    expect(() => {
-      getMetaData();
-    }).toThrowError(
-      new Error(`ENOENT: no such file or directory, open '${errorPath}'`)
-    );
+      expect(getProjectMetaPath()).toEqual(expected);
+    });
   });
 
-  afterEach(() => {
-    cleanFiles();
-    delete process.env.CALLING_DIR;
+  describe('getMetaData helper', () => {
+    beforeEach(() => {
+      fs.writeFileSync(
+        join(projectPath, 'step-001.md'),
+        'Lorem ipsum...',
+        'utf-8'
+      );
+      fs.writeFileSync(
+        join(projectPath, 'step-002.md'),
+        'Lorem ipsum...',
+        'utf-8'
+      );
+      fs.writeFileSync(
+        join(projectPath, 'step-003.md'),
+        'Lorem ipsum...',
+        'utf-8'
+      );
+      fs.writeFileSync(
+        join(metaPath, 'meta.json'),
+        `{
+      "id": "mock-id",
+      "challengeOrder": [{"id": "1", "title": "Step 1"}, {"id": "2", "title": "Step 2"}, {"id": "1", "title": "Step 3"}]}`,
+        'utf-8'
+      );
+    });
+
+    it('should process requested file', () => {
+      const expected = {
+        id: 'mock-id',
+        challengeOrder: [
+          { id: '1', title: 'Step 1' },
+          { id: '2', title: 'Step 2' },
+          { id: '1', title: 'Step 3' }
+        ]
+      };
+      process.env.CALLING_DIR = projectPath;
+      expect(getMetaData()).toEqual(expected);
+    });
+
+    it('should throw if file is not found', () => {
+      process.env.CALLING_DIR =
+        'curriculum/challenges/english/superblock/mick-priject';
+
+      const errorPath = join(
+        'curriculum',
+        'challenges',
+        '_meta',
+        'mick-priject',
+        'meta.json'
+      );
+      expect(() => {
+        getMetaData();
+      }).toThrowError(
+        new Error(`ENOENT: no such file or directory, open '${errorPath}'`)
+      );
+    });
   });
-});
 
-describe('validateMetaData helper', () => {
-  it('should throw if a stepfile is missing', () => {
-    fs.mkdirSync(superBlockPath);
-    fs.mkdirSync(projectPath);
-    fs.writeFileSync(
-      join(projectPath, 'step-001.md'),
-      `---
+  describe('validateMetaData helper', () => {
+    it('should throw if a stepfile is missing', () => {
+      fs.writeFileSync(
+        join(projectPath, 'step-001.md'),
+        `---
 id: id-1
 title: Step 2
 challengeType: a
 dashedName: step-2
 ---
 `,
-      'utf-8'
-    );
-    fs.writeFileSync(
-      join(projectPath, 'step-003.md'),
-      `---
+        'utf-8'
+      );
+      fs.writeFileSync(
+        join(projectPath, 'step-003.md'),
+        `---
 id: id-3
 title: Step 3
 challengeType: c
 dashedName: step-3
 ---
 `,
-      'utf-8'
-    );
-    fs.mkdirSync(metaPath);
-    fs.writeFileSync(
-      join(metaPath, 'meta.json'),
-      `{
+        'utf-8'
+      );
+      fs.writeFileSync(
+        join(metaPath, 'meta.json'),
+        `{
       "id": "mock-id",
       "challengeOrder": [{"id": "1", "title": "Step 1"}, {"id": "2", "title": "Step 2"}, {"id": "1", "title": "Step 3"}]}`,
-      'utf-8'
-    );
+        'utf-8'
+      );
 
-    process.env.CALLING_DIR = projectPath;
+      process.env.CALLING_DIR = projectPath;
 
-    expect(() => validateMetaData()).toThrow(
-      `ENOENT: no such file or directory, access '${projectPath}/1.md'`
-    );
-  });
+      expect(() => validateMetaData()).toThrow(
+        `ENOENT: no such file or directory, access '${projectPath}/1.md'`
+      );
+    });
 
-  it('should throw if a step is present in the project, but not the meta', () => {
-    fs.mkdirSync(superBlockPath);
-    fs.mkdirSync(projectPath);
-    fs.writeFileSync(
-      join(projectPath, '1.md'),
-      `---
+    it('should throw if a step is present in the project, but not the meta', () => {
+      fs.writeFileSync(
+        join(projectPath, '1.md'),
+        `---
 id: id-1
 title: Step 2
 challengeType: a
 dashedName: step-2
 ---
 `,
-      'utf-8'
-    );
-    fs.writeFileSync(
-      join(projectPath, '2.md'),
-      `---
+        'utf-8'
+      );
+      fs.writeFileSync(
+        join(projectPath, '2.md'),
+        `---
 id: id-2
 title: Step 1
 challengeType: b
 dashedName: step-1
 ---
 `,
-      'utf-8'
-    );
-    fs.writeFileSync(
-      join(projectPath, '3.md'),
-      `---
+        'utf-8'
+      );
+      fs.writeFileSync(
+        join(projectPath, '3.md'),
+        `---
 id: id-3
 title: Step 3
 challengeType: c
 dashedName: step-3
 ---
 `,
-      'utf-8'
-    );
-    fs.mkdirSync(metaPath);
-    fs.writeFileSync(
-      join(metaPath, 'meta.json'),
-      `{
+        'utf-8'
+      );
+      fs.writeFileSync(
+        join(metaPath, 'meta.json'),
+        `{
       "id": "mock-id",
       "challengeOrder": [{"id": "1", "title": "Step 1"}, {"id": "2", "title": "Step 2"}, {"id": "1", "title": "Step 3"}]}`,
-      'utf-8'
-    );
+        'utf-8'
+      );
 
-    process.env.CALLING_DIR = projectPath;
+      process.env.CALLING_DIR = projectPath;
 
-    expect(() => validateMetaData()).toThrow(
-      `File ${projectPath}/3.md should be in the meta.json's challengeOrder`
-    );
+      expect(() => validateMetaData()).toThrow(
+        `File ${projectPath}/3.md should be in the meta.json's challengeOrder`
+      );
+    });
   });
-
   afterEach(() => {
     delete process.env.CALLING_DIR;
-    cleanFiles();
+    try {
+      fs.rmSync(basePath, { recursive: true });
+    } catch (err) {
+      console.log(err);
+      console.log('Could not remove fixtures folder.');
+    }
   });
 });
diff --git a/tools/challenge-helper-scripts/repair-meta.test.ts b/tools/challenge-helper-scripts/repair-meta.test.ts
new file mode 100644
index 00000000000000..63352b54ffd9c2
--- /dev/null
+++ b/tools/challenge-helper-scripts/repair-meta.test.ts
@@ -0,0 +1,73 @@
+import { join } from 'path';
+import fs from 'fs';
+
+import { repairMeta } from './commands';
+
+const basePath = join(
+  process.cwd(),
+  '__fixtures__' + process.env.JEST_WORKER_ID
+);
+const commonPath = join(basePath, 'curriculum', 'challenges');
+
+const metaPath = join(commonPath, '_meta', 'project-repair-meta');
+const superBlockPath = join(commonPath, 'english', 'superblock-repair-meta');
+const projectPath = join(superBlockPath, 'project-repair-meta');
+
+describe('Challenge utils helper scripts', () => {
+  beforeEach(() => {
+    process.env.CALLING_DIR = projectPath;
+    fs.mkdirSync(metaPath, { recursive: true });
+    fs.mkdirSync(superBlockPath, { recursive: true });
+    fs.mkdirSync(projectPath);
+  });
+
+  it('should restore the challenge order in the meta.json file', async () => {
+    fs.writeFileSync(
+      join(metaPath, 'meta.json'),
+      // all the challenges from step 1 to 30 in reverse order:
+      `{"challengeOrder": [${Array.from(
+        { length: 30 },
+        (_, i) => `{"id": "id-${i + 1}", "title": "Step ${30 - i}"}`
+      ).join(',')}]}`,
+      'utf-8'
+    );
+
+    // create all 30 challenges:
+    Array.from({ length: 30 }, (_, i) => {
+      fs.writeFileSync(
+        join(projectPath, `step-${i + 1}.md`),
+        `---
+id: id-${i + 1}
+title: Step ${30 - i}
+---
+`,
+        'utf-8'
+      );
+    });
+
+    // run the repair script:
+    await repairMeta();
+
+    // confirm that the meta.json file now has the correct challenge order:
+    const meta = JSON.parse(
+      fs.readFileSync(join(metaPath, 'meta.json'), 'utf-8')
+    ) as { challengeOrder: { id: string; title: string }[] };
+
+    expect(meta.challengeOrder).toEqual(
+      Array.from({ length: 30 }, (_, i) => ({
+        id: `id-${30 - i}`,
+        title: `Step ${i + 1}`
+      }))
+    );
+  });
+
+  afterEach(() => {
+    delete process.env.CALLING_DIR;
+    try {
+      fs.rmSync(basePath, { recursive: true });
+    } catch (err) {
+      console.log(err);
+      console.log('Could not remove fixtures folder.');
+    }
+  });
+});
diff --git a/tools/challenge-helper-scripts/repair-meta.ts b/tools/challenge-helper-scripts/repair-meta.ts
index 04515df489f6c4..d5a55829a696e5 100644
--- a/tools/challenge-helper-scripts/repair-meta.ts
+++ b/tools/challenge-helper-scripts/repair-meta.ts
@@ -1,22 +1,3 @@
-import { getMetaData, updateMetaData } from './helpers/project-metadata';
-import { getChallengeOrderFromFileTree } from './helpers/get-challenge-order';
+import { repairMeta } from './commands';
 
-const sortByStepNum = (a: string, b: string) =>
-  parseInt(a.split('-')[1]) - parseInt(b.split('-')[1]);
-
-const repairMeta = async () => {
-  const challengeOrder = await getChallengeOrderFromFileTree();
-  if (!challengeOrder.every(({ title }) => /Step \d+/.test(title))) {
-    throw new Error(
-      'You can only run this command on project-based blocks with step files.'
-    );
-  }
-  const sortedChallengeOrder = challengeOrder.sort((a, b) =>
-    sortByStepNum(a.title, b.title)
-  );
-  const meta = getMetaData();
-  meta.challengeOrder = sortedChallengeOrder;
-  updateMetaData(meta);
-};
-
-void (async () => await repairMeta())();
+void (() => repairMeta())();
diff --git a/tools/challenge-helper-scripts/utils.test.ts b/tools/challenge-helper-scripts/utils.test.ts
index 5aa598da2136de..9a4c4e9a3f2cb2 100644
--- a/tools/challenge-helper-scripts/utils.test.ts
+++ b/tools/challenge-helper-scripts/utils.test.ts
@@ -24,27 +24,25 @@ import {
   updateStepTitles
 } from './utils';
 
-const metaPath = join(
+const basePath = join(
   process.cwd(),
-  'curriculum',
-  'challenges',
-  '_meta',
-  'project'
+  '__fixtures__' + process.env.JEST_WORKER_ID
 );
-const superBlockPath = join(
-  process.cwd(),
-  'curriculum',
-  'challenges',
-  'english',
-  'superblock'
-);
-const projectPath = join(superBlockPath, 'project');
+const commonPath = join(basePath, 'curriculum', 'challenges');
+
+const block = 'utils-project';
+const metaPath = join(commonPath, '_meta', block);
+const superBlockPath = join(commonPath, 'english', 'utils-superblock');
+const projectPath = join(superBlockPath, block);
 
 describe('Challenge utils helper scripts', () => {
+  beforeEach(() => {
+    fs.mkdirSync(superBlockPath, { recursive: true });
+    fs.mkdirSync(projectPath, { recursive: true });
+    fs.mkdirSync(metaPath, { recursive: true });
+  });
   describe('createStepFile util', () => {
     it('should create next step and return its identifier', () => {
-      fs.mkdirSync(superBlockPath);
-      fs.mkdirSync(projectPath);
       fs.writeFileSync(
         join(projectPath, 'step-001.md'),
         'Lorem ipsum...',
@@ -81,8 +79,6 @@ describe('Challenge utils helper scripts', () => {
 
   describe('createChallengeFile util', () => {
     it('should create the challenge', () => {
-      fs.mkdirSync(superBlockPath);
-      fs.mkdirSync(projectPath);
       fs.writeFileSync(
         join(projectPath, 'fake-challenge.md'),
         'Lorem ipsum...',
@@ -110,7 +106,6 @@ describe('Challenge utils helper scripts', () => {
 
   describe('insertStepIntoMeta util', () => {
     it('should update the meta with a new file id and name', () => {
-      fs.mkdirSync(metaPath);
       fs.writeFileSync(
         join(metaPath, 'meta.json'),
         `{"id": "mock-id",
@@ -163,14 +158,11 @@ describe('Challenge utils helper scripts', () => {
 
   describe('updateStepTitles util', () => {
     it('should apply meta.challengeOrder to step files', () => {
-      fs.mkdirSync(metaPath);
       fs.writeFileSync(
         join(metaPath, 'meta.json'),
         `{"id": "mock-id", "challengeOrder": [{"id": "id-1", "title": "Step 1"}, {"id": "id-3", "title": "Step 2"}, {"id": "id-2", "title": "Step 3"}]}`,
         'utf-8'
       );
-      fs.mkdirSync(superBlockPath);
-      fs.mkdirSync(projectPath);
       fs.writeFileSync(
         join(projectPath, 'id-1.md'),
         `---
@@ -232,14 +224,10 @@ dashedName: step-3
   afterEach(() => {
     delete process.env.CALLING_DIR;
     try {
-      fs.rmSync(superBlockPath, { recursive: true });
-    } catch (err) {
-      console.log('Could not remove superblock mock folder. ');
-    }
-    try {
-      fs.rmSync(metaPath, { recursive: true });
+      fs.rmSync(basePath, { recursive: true });
     } catch (err) {
-      console.log('Could not remove meta mock folder.');
+      console.log(err);
+      console.log('Could not remove fixtures folder.');
     }
   });
 });