Skip to content

[PROD RELEASE] AI Review/projects-api-v6/Updates & Fixes#77

Merged
kkartunov merged 58 commits intomasterfrom
develop
Mar 26, 2026
Merged

[PROD RELEASE] AI Review/projects-api-v6/Updates & Fixes#77
kkartunov merged 58 commits intomasterfrom
develop

Conversation

@jmgasper
Copy link
Copy Markdown
Contributor

@jmgasper jmgasper commented Mar 10, 2026

process.env.SUBMISSIONS_API_URL || "https://api.topcoder-dev.com/v5/submissions",
MEMBERS_API_URL: process.env.MEMBERS_API_URL || "https://api.topcoder-dev.com/v6/members",
REVIEW_SUMMATIONS_API_URL: process.env.REVIEW_SUMMATIONS_API_URL || "https://api.topcoder-dev.com/v6/reviewSummations",
REVIEWS_API_URL: process.env.REVIEWS_API_URL || "https://api.topcoder-dev.com",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
The REVIEWS_API_URL is set to https://api.topcoder-dev.com, which is inconsistent with the other API URLs that specify a version (e.g., /v5 or /v6). Consider adding a version to ensure consistency and avoid potential issues with future API changes.

@@ -4,5 +4,6 @@ AUTH0_URL=https://topcoder-dev.auth0.com/oauth/token
AUTH0_AUDIENCE=https://m2m.topcoder-dev.com/
DATABASE_URL="postgresql://topcoder:NUDvFEZzrey2x2og2!zn_kBzcaJ.fu_njAJcD*z2q@topcoder-services.ci8xwsszszsw.us-east-1.rds.amazonaws.com:5432/topcoder-services?schema=challenges"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ security]
The DATABASE_URL contains hardcoded credentials, which is a security risk. Consider using environment variables or a secure vault to manage sensitive information.

'2025-03-10T13:08:02.378Z',
'topcoder user'
)
ON CONFLICT DO NOTHING;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Using ON CONFLICT DO NOTHING can silently ignore conflicts without logging or handling them, which might lead to data inconsistencies or missed updates. Consider specifying a conflict target and handling the conflict appropriately to ensure data integrity.

* @param {Function} logDebugMessage optional logging function
*/
async addAIScreeningPhaseForChallengeCreation(challenge, prisma, logDebugMessage = () => {}) {
if (!challenge || !challenge.phases || !Array.isArray(challenge.reviewers)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
The function addAIScreeningPhaseForChallengeCreation mutates the challenge object in-place. Consider returning a new object or using a more functional approach to avoid potential side effects, which can improve maintainability.

);

if (!submissionPhaseName) {
throw new errors.BadRequestError(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 maintainability]
Throwing an error when no submission phase is found is correct, but consider logging this event before throwing to aid in debugging and tracing issues in production.

([_, phase]) => phase.name === "AI Screening"
);

if (!aiScreeningPhaseDefEntry) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 maintainability]
The error message 'AI Screening phase definition not found in the system' could be more informative by including the context or identifier of the challenge to help with debugging.


// Recalculate phase dates to keep timeline in sync
if (submissionPhase.scheduledEndDate) {
aiScreeningPhase.scheduledStartDate = submissionPhase.scheduledEndDate;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 performance]
The use of moment for date manipulation is fine, but consider using native JavaScript Date methods or a lighter library like date-fns if moment's full functionality is not needed, to reduce bundle size.

@@ -98,9 +100,27 @@ class ChallengePhaseHelper {
// to incorrectly push earlier phases forward. Sorting by template order prevents that.
const orderIndex = new Map();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 performance]
The variable orderIndex is created using new Map() but is populated using _.each. Consider using native JavaScript methods like forEach for consistency and potentially better performance.

const submissionPhase = submissionPhaseName
? _.find(challengePhases, (phase) => phase.name === submissionPhaseName)
: null;
const submissionOrderIndex = _.isNil(submissionPhase)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 readability]
The use of _.isNil to check for null or undefined is correct, but consider using submissionPhase === null for clarity since submissionPhase is explicitly set to null when not found.

return updatedPhase;
});

const aiScreeningPhase = _.find(updatedPhases, (phase) => phase.name === "AI Screening");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The logic for updating the predecessor of aiScreeningPhase and review phases assumes that updateSubmissionPhase is not null. Ensure that this logic is covered by tests to prevent potential issues if updateSubmissionPhase is unexpectedly null.


- name: Run Trivy scanner in repo mode
uses: aquasecurity/trivy-action@0.33.1
uses: aquasecurity/trivy-action@latest
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
Using @latest for the Trivy action can lead to unexpected behavior if a breaking change is introduced in future versions. Consider specifying a specific version to ensure consistent behavior.

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

View 6 additional findings in Devin Review.

Open in Devin Review

Comment on lines +611 to +638
// Recalculate phase dates to keep timeline in sync
if (submissionPhase.scheduledEndDate) {
aiScreeningPhase.scheduledStartDate = submissionPhase.scheduledEndDate;
aiScreeningPhase.scheduledEndDate = moment(aiScreeningPhase.scheduledStartDate)
.add(aiScreeningPhase.duration, "seconds")
.toDate()
.toISOString();

logDebugMessage(
`AI screening phase dates calculated (start=${aiScreeningPhase.scheduledStartDate}, end=${aiScreeningPhase.scheduledEndDate})`
);

// Update dates for review phases that now depend on AI Screening
reviewPhases.forEach((phase) => {
if (_.isNil(phase.actualStartDate)) {
phase.scheduledStartDate = aiScreeningPhase.scheduledEndDate;
if (phase.duration) {
phase.scheduledEndDate = moment(phase.scheduledStartDate)
.add(phase.duration, "seconds")
.toDate()
.toISOString();

logDebugMessage(
`Updated ${phase.name} phase dates (start=${phase.scheduledStartDate}, end=${phase.scheduledEndDate})`
);
}
}
});
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Incomplete phase date cascade after AI Screening insertion causes stale downstream dates

When addAIScreeningPhaseForChallengeCreation inserts the AI Screening phase during challenge activation (src/services/ChallengeService.js:2725-2737), it only recalculates dates for the AI Screening phase itself and its immediate successor review phases (lines 624-638). Downstream phases whose predecessor chain passes through a review phase (e.g., Appeals → depends on Review, Appeals Response → depends on Appeals, Post-Mortem, etc.) retain their stale scheduledStartDate/scheduledEndDate values that were computed by populatePhasesForChallengeUpdate before the AI Screening was inserted. This means those downstream phases will have dates that overlap with or precede the shifted review phase, producing an incorrect timeline that is persisted via syncChallengePhases (src/services/ChallengeService.js:3031-3038).

Example of the stale date problem

Before AI Screening insertion (dates from populatePhasesForChallengeUpdate):

  • Submission: ends T+3d
  • Review: starts T+3d, ends T+6d
  • Appeals: starts T+6d, ends T+7d

After AI Screening insertion (only Review shifted):

  • Submission: ends T+3d
  • AI Screening: starts T+3d, ends T+3d+4h
  • Review: starts T+3d+4h, ends T+6d+4h ← updated
  • Appeals: starts T+6d, ends T+7d ← NOT updated, overlaps with Review
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +241 to +243
if (Array.isArray(challenge.reviewers) && challenge.reviewers.length > 0) {
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Duplicate early-return guard check in applyDefaultMemberReviewersForChallengeCreation

The exact same guard check if (Array.isArray(challenge.reviewers) && challenge.reviewers.length > 0) { return; } appears twice in succession at lines 237-239 and 241-243. The second check is dead code that can never be reached (if the first check passes, the function already returned). This is harmless but suggests a copy-paste error.

Suggested change
if (Array.isArray(challenge.reviewers) && challenge.reviewers.length > 0) {
return;
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

updatedBy String

@@index([abbreviation])
@@index([legacyId])
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ performance]
Indexing on legacyId and isLegacy might not be necessary if these fields are not frequently queried. Consider evaluating the query patterns to ensure these indexes are justified, as unnecessary indexes can impact write performance.

updatedBy String

@@index([legacyId])
@@index([isLegacy])
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ performance]
Similar to the ChallengeType model, ensure that indexing on isLegacy is necessary based on query patterns. Unnecessary indexes can degrade performance by increasing the time taken for write operations.

process.env.REVIEW_SUMMATIONS_API_URL || "https://api.topcoder-dev.com/v6/reviewSummations",
REVIEWS_API_URL: process.env.REVIEWS_API_URL || "https://api.topcoder-dev.com",
TC_AI_API_URL: process.env.TC_AI_API_URL || "https://api.topcoder-dev.com/v6/ai",
RESOURCES_API_URL: process.env.RESOURCES_API_URL || "http://localhost:4000/v5/resources",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
The RESOURCES_API_URL is set to http://localhost:4000/v5/resources, which might be intended for local development. Ensure this is not mistakenly included in a production configuration.

*/
function triggerChallengeReviewContextGeneration(challengeId) {
if (isGenerationInProgress(challengeId)) {
const existingRunId = getRunId(challengeId);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 performance]
The function isGenerationInProgress is called twice for the same challengeId within this function. Consider storing the result in a variable to avoid redundant calls and improve performance.


const cancelUrl = `${tcAiApiUrl}/workflows/${workflowId}/runs/${runId}/cancel`;
await axios.post(
cancelUrl,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Consider handling potential errors from the axios.post call to cancelUrl. Currently, if this request fails, it will not be caught, which could lead to unhandled promise rejections.

while (Date.now() - startTime < WORKFLOW_POLL_TIMEOUT_MS) {
try {
await sleep(WORKFLOW_POLL_INTERVAL_MS);
const statusResponse = await axios.get(workflowStatusUrl, {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The axios.get call within the polling loop does not handle non-2xx status codes explicitly. Consider adding a validateStatus function to handle unexpected status codes gracefully.

`[ChallengeReviewContextHelper] Polling error for workflow ${runId}: ${pollErr.message}`,
);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
The polling loop does not have a mechanism to break out if the axios.get call consistently fails due to network issues. Consider adding a retry limit to avoid an infinite loop in case of persistent network failures.

let existingContext = null;

try {
const getResponse = await axios.get(contextGetUrl, {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The axios.get call for checking existing context uses a validateStatus function that allows all status codes below 500. Consider narrowing this range to handle unexpected status codes more explicitly.

* @param {string} runId - The workflow run ID (optional, can be set later)
*/
function markGenerationStarted(challengeId, runId = null) {
generationCache.set(challengeId, { timestamp: Date.now(), runId });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Consider adding a check to ensure challengeId is not null or undefined before setting it in the generationCache. This will prevent potential issues with invalid keys in the cache.

* @param {string} runId - The workflow run ID
*/
function setRunId(challengeId, runId) {
const entry = generationCache.get(challengeId);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 maintainability]
It might be beneficial to handle the case where challengeId is not found in the generationCache by logging a warning or error. This can help in debugging scenarios where setRunId is called with an invalid challengeId.

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 21 additional findings in Devin Review.

Open in Devin Review

Comment on lines +61 to +75
clearGeneration(challengeId);
}

markGenerationStarted(challengeId);
logger.info(
`[ChallengeReviewContextHelper] Triggering context generation for challenge ${challengeId}`,
);

runReviewContextGenerationAsync(challengeId).catch((err) => {
logger.error(
`[ChallengeReviewContextHelper] Context generation failed for challenge ${challengeId}: ${err.message}`,
err,
);
clearGeneration(challengeId);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Old async run's clearGeneration call will erase the new run's guard entry

In triggerChallengeReviewContextGeneration, when a new generation is triggered while one is already in progress, the old runReviewContextGenerationAsync promise remains alive in the background. After clearGeneration(challengeId) at line 61 removes the old entry and markGenerationStarted(challengeId) at line 64 creates a new entry for the replacement run, the old run's promise will eventually complete (succeed, fail, or timeout) and call clearGeneration(challengeId) (at src/common/challenge-review-context-helper.js:74, :225, or :283), which deletes the new run's guard entry. This allows a subsequent update to start yet another concurrent generation while the replacement run is still active, defeating the deduplication guard.

Prompt for agents
In src/common/challenge-review-context-helper.js, the race condition occurs because the old runReviewContextGenerationAsync promise's completion handler calls clearGeneration(challengeId), which wipes the new run's guard entry. To fix this, add a generation counter or nonce to the guard so that clearGeneration only clears the entry if it matches the current generation. In review-context-generation-guard.js, change the generationCache to store a monotonically increasing nonce alongside the timestamp and runId. markGenerationStarted should return the nonce. clearGeneration should accept a nonce parameter and only delete the entry if the stored nonce matches the passed nonce. Then in challenge-review-context-helper.js, capture the nonce returned by markGenerationStarted (line 64) and pass it through to runReviewContextGenerationAsync so that its clearGeneration calls use the correct nonce. This ensures the old run's cleanup cannot interfere with the new run's guard.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

}
} catch (saveErr) {
logger.error(
`[ChallengeReviewContextHelper] Failed to save context for challenge ${challengeId}: ${JSON.stringify(saveErr, null, 2)}`,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ security]
Using JSON.stringify to log errors can expose sensitive information if the error object contains any. Consider logging only the necessary parts of the error message to avoid potential security risks.

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 24 additional findings in Devin Review.

Open in Devin Review

Comment on lines +117 to +128
let token;
try {
token = await m2mHelper.getM2MToken();
} catch (tokenErr) {
logger.error(
`[ChallengeReviewContextHelper] Failed to get M2M token for challenge ${challengeId}: ${tokenErr.message}`,
);
clearGeneration(challengeId);
return;
}

const authHeader = { Authorization: `Bearer ${token}` };
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Stale M2M token used across 15+ minute polling window causes silent context save failure

In runReviewContextGenerationAsync, a single M2M token is obtained at line 119 and stored in authHeader. This same token is then reused across the entire workflow: creating a run, polling for up to 15 minutes (WORKFLOW_POLL_TIMEOUT_MS), and finally saving the context to the review API (lines 238-274). If the M2M token expires during the long polling period, all subsequent API calls (including polling and the critical final context save) will fail. During polling, expired-token errors are silently caught as network errors (lines 210-218) and polling continues fruitlessly until timeout. The workflow result is then lost even if the workflow completed successfully on the server side.

Prompt for agents
In src/common/challenge-review-context-helper.js, the runReviewContextGenerationAsync function obtains a single M2M token at line 119 and stores it in authHeader at line 128. This token is reused for all subsequent API calls including a polling loop that can last up to 15 minutes. Re-fetch the M2M token before critical API calls: (1) before the polling loop or at least periodically within it, and (2) before the final context save (around line 238). You can do this by calling m2mHelper.getM2MToken() again to get a fresh/refreshed token and rebuilding the authHeader object.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@kkartunov kkartunov requested a review from Copilot March 25, 2026 06:09
@kkartunov kkartunov changed the title [DO NOT MERGE] March 2026 prod release for projects-api-v6 [PROD RELEASE] AI Review/projects-api-v6/Updates & Fixes Mar 25, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR packages the March 2026 production release changes for projects-api-v6/challenge-api-v6, focusing on legacy track/type visibility, AI-review-driven “AI Screening” phase behavior, and updated access controls/config wiring.

Changes:

  • Add legacyId/isLegacy flags for challenge types/tracks (schema, migrations, seeds, search filters, and unit tests).
  • Introduce AI Screening phase seeding and enforce AI-review completion checks before closing AI Screening / progressing phases; shift successor schedules on manual close.
  • Refactor default reviewer AI settings to use aiConfigTemplateId and integrate Reviews API config creation; expand challenge edit route access to include Talent Manager.

Reviewed changes

Copilot reviewed 33 out of 37 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
test/unit/ChallengeTypeService.legacy.test.js Adds unit coverage for default legacy filtering and legacy metadata on type creation.
test/unit/ChallengeTrackService.test.js Updates tests for track aliasing plus legacy filter behavior.
test/unit/ChallengeService.test.js Adds tests for default AI config application when reviewers aren’t provided.
test/unit/ChallengeRoutes.test.js Verifies Talent Manager role has access to challenge create/edit routes.
test/unit/ChallengePhaseService.test.js Adds test ensuring successor schedules shift to actual end time when closing a phase.
test/testHelper.js Updates seeded track enum value to canonical DEVELOPMENT.
src/services/DefaultChallengeReviewerService.js Renames default AI reviewer field to aiConfigTemplateId in payload normalization and Joi schemas.
src/services/ChallengeTypeService.js Adds legacyId + isLegacy support (search/filtering, validation, uniqueness checks).
src/services/ChallengeTrackService.js Adds isLegacy filtering and schema support; normalizes track aliases.
src/services/ChallengeService.js Adds AI Screening closure guard, default AI config application, and AI review config creation callouts.
src/services/ChallengePhaseService.js Adds escalation checks, AI Screening close validation, and successor schedule shifting logic.
src/scripts/seed/Phase.json Seeds the new AI Screening phase definition.
src/scripts/seed/ChallengeType.json Seeds legacy challenge types and adds isLegacy/legacyId fields.
src/scripts/seed/ChallengeTrack.json Normalizes track enum values and seeds isLegacy.
src/routes.js Refactors repeated access role lists and adds Talent Manager to challenge editor routes.
src/common/project-helper.js Requests members explicitly from projects-api-v6.
src/common/phase-helper.js Adjusts ordering and predecessor wiring for AI Screening in phase updates.
src/common/helper.js Adds getAIReviewConfigByChallengeId via Reviews API.
src/common/challenge-helper.js Adds default reviewer application helpers and AI Screening phase injection + AI config/template integration.
prisma/schema.prisma Adds legacyId/isLegacy to types, isLegacy to tracks; renames DefaultChallengeReviewer field.
prisma/migrations/20260316090000_add_legacy_type_visibility_flags/migration.sql Migration for legacy visibility flags + seed insertions.
prisma/migrations/20260306100000_add_ai_screening_phase/migration.sql Migration to insert the AI Screening phase definition.
prisma/migrations/20260225100000_rename_default_ai_workflow_id/migration.sql Renames DefaultChallengeReviewer column to aiConfigTemplateId.
packages/challenge-prisma-client/wasm.js Regenerates prisma client artifacts reflecting schema updates.
packages/challenge-prisma-client/schema.prisma Updates generated prisma schema (legacy fields, isLegacy, funChallenge, indices).
packages/challenge-prisma-client/package.json Updates generated prisma client package name/hash.
packages/challenge-prisma-client/index-browser.js Updates browser prisma client artifacts reflecting schema updates.
docs/swagger.yaml Documents new query params/fields for legacy filtering and track/type legacy metadata.
docs/prod.env Adds REVIEWS_API_URL env var to production env template.
docs/dev.env Adds REVIEWS_API_URL env var for dev template.
config/default.js Adds REVIEWS_API_URL and TC_AI_API_URL configuration defaults; formatting cleanup.
app-constants.js Adds TalentManager role constant.
.github/workflows/trivy.yaml Bumps Trivy action version.
.circleci/config.yml Updates CircleCI branch filters for workflows.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1924 to +1928
await challengeHelper.createAIReviewConfigsForChallengeCreation(
ret.id,
aiReviewConfigsForCreation,
debugLog,
);
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createAIReviewConfigsForChallengeCreation is invoked after the challenge DB record is created. If the reviews API call fails (network/4xx/5xx), createChallenge will throw but the challenge will already exist, which can lead to orphaned records and non-idempotent retries. Consider wrapping challenge creation + AI config creation in a transaction/compensation flow (e.g., delete the challenge on failure), or treat AI config creation as best-effort (log + continue) and reconcile asynchronously.

Suggested change
await challengeHelper.createAIReviewConfigsForChallengeCreation(
ret.id,
aiReviewConfigsForCreation,
debugLog,
);
try {
await challengeHelper.createAIReviewConfigsForChallengeCreation(
ret.id,
aiReviewConfigsForCreation,
debugLog,
);
} catch (err) {
logger.error(
`createChallenge: failed to create AI review configs for challengeId=${ret.id} ${buildLogContext()}: ${err.message}`,
);
logger.debug(err.stack);
}

Copilot uses AI. Check for mistakes.
('f9570d73-4f26-4d3a-b804-7f6f2f4b85e9', 'BUG_HUNT', 'Legacy develop subtype retained for historical stats references.', 'LBGH', 120),
('65d2d4d4-a0e2-4f5d-94df-570f0b084439', 'CONTENT_CREATION', 'Legacy develop subtype retained for historical stats references.', 'LCTC', 146)
) AS seed("id", "name", "description", "abbreviation", "legacyId")
WHERE NOT EXISTS (
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The WHERE NOT EXISTS clause checks for existing entries by name or legacyId. Ensure that this logic aligns with the intended uniqueness constraints of the ChallengeType table. If both fields are meant to be unique, this is correct; otherwise, consider revising to prevent unintended data exclusion.

return null;
}

const markup = _.toNumber(rawMarkup);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Consider using Number(rawMarkup) instead of _.toNumber(rawMarkup) for converting to a number. Lodash's toNumber is generally used for broader type coercion, which might not be necessary here and could introduce unexpected behavior if rawMarkup is not a string or number.

}

const markup = _.toNumber(rawMarkup);
if (!Number.isFinite(markup)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The check !Number.isFinite(markup) might not catch all invalid cases if markup is NaN. Consider using Number.isNaN(markup) to explicitly handle NaN values.

})

it('preserves decimal billing markup returned by projects api', async () => {
m2mHelper.getM2MToken = async () => 'test-token'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
Overriding m2mHelper.getM2MToken directly in the test can lead to side effects if other tests rely on the original implementation. Consider using a mocking library like sinon to stub this method instead.


it('preserves decimal billing markup returned by projects api', async () => {
m2mHelper.getM2MToken = async () => 'test-token'
axios.get = async () => ({
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
Overriding axios.get directly in the test can lead to side effects if other tests rely on the original implementation. Consider using a mocking library like nock or sinon to stub this method instead.

})

it('converts legacy whole-percentage billing markup to decimal format', async () => {
m2mHelper.getM2MToken = async () => 'test-token'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
Overriding m2mHelper.getM2MToken directly in the test can lead to side effects if other tests rely on the original implementation. Consider using a mocking library like sinon to stub this method instead.


it('converts legacy whole-percentage billing markup to decimal format', async () => {
m2mHelper.getM2MToken = async () => 'test-token'
axios.get = async () => ({
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
Overriding axios.get directly in the test can lead to side effects if other tests rely on the original implementation. Consider using a mocking library like nock or sinon to stub this method instead.

@kkartunov kkartunov merged commit 3888786 into master Mar 26, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants