Skip to content

feat: Requirement edits lack optimistic-concurrency protection - #101

Merged
johlju merged 45 commits into
viscalyx:mainfrom
johlju:fix/a4-april
Apr 25, 2026
Merged

feat: Requirement edits lack optimistic-concurrency protection#101
johlju merged 45 commits into
viscalyx:mainfrom
johlju:fix/a4-april

Conversation

@johlju

@johlju johlju commented Apr 25, 2026

Copy link
Copy Markdown
Member

Description

  • Where: app/api/requirements/[id]/route.ts,
    lib/dal/requirements.ts,
    lib/requirements/service.ts
  • Issue: Editing a draft updates the latest version row by id only.
    A second editor can submit stale data and silently overwrite the first
    editor's changes. The request payload has no edited_at/version precondition
    and the API has no 409 Conflict response shape for stale edits.
  • Fix: Define the edit precondition contract first, then require either
    the current edited_at value or a new integer row version in edit requests.
    Apply the precondition in the UPDATE, return 409 Conflict with the latest
    snapshot on mismatch, and teach the UI to show a refresh/compare prompt.

Screenshots (if applicable)

Related Issues

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactoring (no functional changes)
  • Performance improvement (improves performance without changing functionality)
  • Dependency update (updating libraries or tools)

Testing

  • npm run check passes locally
  • All existing tests still pass
  • Manual testing completed
  • UI tested on desktop and mobile (if applicable)

Checklist

  • Documentation updated as needed

Checklist

  • Code follows the project style guidelines (Biome)
  • Tests added/updated as needed
  • Self-review of code completed
  • Comments added for complex logic
  • No hardcoded strings (use translations if i18n is added)

This change is Reviewable

johlju added 15 commits April 24, 2026 17:22
…rrency control

- Added `revision_token` column to `requirement_versions` table to manage optimistic concurrency.
- Updated database schema documentation to reflect the new column and its constraints.
- Enhanced TypeORM entity to include `revisionToken`.
- Created migration to add `revision_token` to the existing `requirement_versions` table.
- Updated various schemas in the server code to include `revisionToken` in requirement version outputs and mutations.
- Modified tests to ensure migrations are correctly registered and executed.
@coderabbitai

coderabbitai Bot commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds optimistic-concurrency for requirement edits by introducing per-version revisionToken, requiring baseVersionId + baseRevisionToken on edits, rotating tokens on mutations, surfacing structured 409 stale-edit conflicts with latest snapshot, and updating DAL, service, API, UI, types, migrations, tests, docs, MCP schemas, and CI/tooling.

Changes

Cohort / File(s) Summary
Database migrations & schema
typeorm/migrations/0001_initial_sqlserver.mjs, typeorm/migrations/0002_requirement_version_revision_token.mjs, docs/database-schema.md
Adds revision_token column (uniqueidentifier) with NEWID() default and unique index; documents optimistic-concurrency and revised version lifecycle.
DAL — concurrency & token rotation
lib/dal/requirements.ts
Adds baseVersionId/baseRevisionToken preconditions, GUID validation, SERIALIZABLE transaction with UPDLOCK/HOLDLOCK, rotates revision_token on edits/status changes, and throws structured conflict errors on stale preconditions.
Service layer & types
lib/requirements/service.ts, lib/requirements/types.ts, lib/typeorm/entities/requirement-version.ts
Accepts base preconditions in mutation input, exposes revisionToken on versions, catches DAL conflicts to attach latest snapshot and rethrows as conflict; updates entity/type signatures.
MCP server schemas & tool registrations
lib/mcp/server.ts, .github/instructions/mcp-tool-contract.instructions.md
Introduces shared response schemas; tightens input/output validation and .superRefine rules (requires base tokens for edits); adds/updates requirement/suggestion/AI tool schemas and descriptions.
API route & client wiring
app/api/requirements/[id]/route.ts, app/[locale]/requirements/[id]/edit/edit-requirement-client.tsx, app/[locale]/requirements/requirements-client.tsx
Maps/propagates baseRevisionToken/baseVersionId; edit page captures and passes base tokens and exposes onRefreshLatest; list mapping includes revisionToken on version rows.
UI — RequirementForm & localization
components/RequirementForm.tsx, messages/en.json, messages/sv.json
Form accepts baseRevisionToken/baseVersionId and onRefreshLatest; includes base fields in edit requests; parses structured 409 conflicts to show stale-edit alert with view-latest/reload actions; adds i18n keys.
TypeORM & list view
lib/requirements/list-view.ts
Adds optional revisionToken to RequirementRow.version.
Migrations tooling & CI env
scripts/db-sqlserver-admin.mjs, scripts/__tests__/db-sqlserver-admin.test.mjs, .env.sqlserver.ci
Registers new revision-token migration and updates tests to assert migration list; CI example env adds SA and read-only DB credential entries.
Docs & quality/specs
docs/lifecycle-workflow.md, docs/version-lifecycle-dates.md, docs/mcp-server-user-guide.md, docs/mcp-server-contributor-guide.md, .github/skills/run-spec-audit/references/scrutiny-areas.md, tests/quality/QUALITY.md
Documents required history fetch and base preconditions for edits, stale-edit rejection semantics, Scenario 11 draft concurrency, and updates MCP user/contributor guidance.
Tests — unit, functional, quality
tests/unit/*, tests/quality/functional.test.ts, tests/quality/QUALITY.md
Adds revisionToken to fixtures; updates unit/functional tests to pass/validate base preconditions, conflict handling, MCP schema changes, and adds Scenario 11 and DAL/service concurrency tests.
Build/tooling & small updates
package.json, .github/workflows/quality-checks.yml, .github/workflows/integration-tests.yml, CONTRIBUTING.md, .devcontainer/*, cspell.jsonc
Adds dotenv-linter scripts and workflow steps, updates check/fix scripts, adjusts CI env handling for SQL Server, installs dotenv-linter in devcontainer, and adds HOLDLOCK/UPDLOCK to spell dictionary.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as Edit Client
    participant API as API Route
    participant Service
    participant DAL
    participant DB

    User->>UI: Open edit (load latest)
    UI->>API: GET requirement history
    API->>Service: getRequirement / history
    Service->>DAL: getLatestVersionLite / getRequirementById
    DAL->>DB: SELECT latest version (includes revision_token)
    DB-->>DAL: latest version + revision_token
    DAL-->>Service: version with revisionToken
    Service-->>API: requirement
    API-->>UI: requirement (includes baseVersionId, baseRevisionToken)

    User->>UI: Submit edits (includes baseVersionId, baseRevisionToken)
    UI->>API: PUT /requirements/[id] with baseVersionId, baseRevisionToken, changes
    API->>Service: manageRequirement(edit, payload)
    Service->>DAL: editRequirement(id, baseVersionId, baseRevisionToken, changes)
    DAL->>DB: BEGIN TRANSACTION (SERIALIZABLE)
    DAL->>DB: SELECT ... WITH (UPDLOCK, HOLDLOCK) to lock latest
    DB-->>DAL: current latest version + revision_token
    alt base matches latest
        DAL->>DB: UPDATE requirement_versions SET ..., revision_token=NEWID() WHERE id=@latestId AND revision_token=CONVERT(uniqueidentifier, `@baseRevisionToken`)
        DB-->>DAL: rowsAffected = 1
        DAL->>DB: COMMIT
        DAL-->>Service: updated version (new revisionToken)
        Service-->>API: 200 OK with updated data
        API-->>UI: 200 OK
    else mismatch (stale)
        DAL->>DB: ROLLBACK
        DAL->>DB: SELECT latest snapshot
        DB-->>DAL: latest snapshot
        DAL-->>Service: throw conflictError(reason='stale_requirement_edit', latest=...)
        Service-->>API: 409 Conflict with structured payload
        API-->>UI: 409 Conflict + latest snapshot
        UI->>User: Show stale-edit alert (view latest / reload)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description identifies the problem and solution but is missing critical sections: Type of Change is unchecked, Testing checklist items are unchecked despite substantial test additions, and Testing/Documentation/Code review checkboxes are incomplete. Complete the Type of Change (select 'New feature'), check all applicable Testing boxes (npm run check, existing tests, manual testing), mark Documentation/Code review checkboxes, and document test coverage added for optimistic concurrency (Scenario 11).
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding optimistic-concurrency protection (revision tokens and base version tracking) to requirement edits to prevent stale overwrites.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Apr 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.57143% with 30 lines in your changes missing coverage. Please review.
✅ Project coverage is 57.35%. Comparing base (fda1bf4) to head (988fedf).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
lib/mcp/server.ts 56.75% 16 Missing ⚠️
lib/dal/requirements.ts 77.77% 8 Missing ⚠️
components/RequirementForm.tsx 90.47% 4 Missing ⚠️
lib/requirements/service.ts 92.85% 1 Missing ⚠️
lib/typeorm/entities/requirement-version.ts 0.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #101      +/-   ##
==========================================
+ Coverage   56.80%   57.35%   +0.54%     
==========================================
  Files         257      257              
  Lines       16880    17013     +133     
  Branches     6376     6554     +178     
==========================================
+ Hits         9589     9757     +168     
+ Misses       7178     7143      -35     
  Partials      113      113              
Files with missing lines Coverage Δ
...requirements/[id]/edit/edit-requirement-client.tsx 90.00% <100.00%> (+0.81%) ⬆️
app/[locale]/requirements/requirements-client.tsx 83.06% <ø> (ø)
app/api/requirements/[id]/route.ts 86.11% <100.00%> (+0.81%) ⬆️
lib/requirements/list-view.ts 88.88% <ø> (ø)
scripts/db-sqlserver-admin.mjs 58.44% <ø> (ø)
lib/requirements/service.ts 53.64% <92.85%> (+0.63%) ⬆️
lib/typeorm/entities/requirement-version.ts 50.00% <0.00%> (-50.00%) ⬇️
components/RequirementForm.tsx 52.91% <90.47%> (+9.50%) ⬆️
lib/dal/requirements.ts 32.25% <77.77%> (+12.75%) ⬆️
lib/mcp/server.ts 59.63% <56.75%> (+1.70%) ⬆️
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (7)
typeorm/migrations/0002_requirement_version_revision_token.mjs (1)

1-8: Use a UNIQUE constraint here to mirror the entity metadata.

lib/typeorm/entities/requirement-version.ts models uq_requirement_versions_revision_token as a unique constraint, but this migration creates a unique index with the same name. SQL Server treats those as different schema objects, so future metadata-based diffs can think the constraint is still missing.

Suggested fix
 const UP_STATEMENTS = [
-  "IF COL_LENGTH(N'dbo.requirement_versions', N'revision_token') IS NULL\nBEGIN\n  ALTER TABLE [requirement_versions] ADD [revision_token] uniqueidentifier NULL;\n  UPDATE [requirement_versions] SET [revision_token] = NEWID() WHERE [revision_token] IS NULL;\n  ALTER TABLE [requirement_versions] ALTER COLUMN [revision_token] uniqueidentifier NOT NULL;\n  ALTER TABLE [requirement_versions] ADD CONSTRAINT [df_requirement_versions_revision_token] DEFAULT NEWID() FOR [revision_token];\n  CREATE UNIQUE INDEX [uq_requirement_versions_revision_token] ON [requirement_versions] ([revision_token]);\nEND",
+  "IF COL_LENGTH(N'dbo.requirement_versions', N'revision_token') IS NULL\nBEGIN\n  ALTER TABLE [requirement_versions] ADD [revision_token] uniqueidentifier NULL;\n  UPDATE [requirement_versions] SET [revision_token] = NEWID() WHERE [revision_token] IS NULL;\n  ALTER TABLE [requirement_versions] ALTER COLUMN [revision_token] uniqueidentifier NOT NULL;\n  ALTER TABLE [requirement_versions] ADD CONSTRAINT [df_requirement_versions_revision_token] DEFAULT NEWID() FOR [revision_token];\n  ALTER TABLE [requirement_versions] ADD CONSTRAINT [uq_requirement_versions_revision_token] UNIQUE ([revision_token]);\nEND",
 ]
 
 const DOWN_STATEMENTS = [
-  "IF EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'uq_requirement_versions_revision_token' AND object_id = OBJECT_ID(N'dbo.requirement_versions')) DROP INDEX [uq_requirement_versions_revision_token] ON [requirement_versions];",
+  "IF EXISTS (SELECT 1 FROM sys.key_constraints WHERE name = N'uq_requirement_versions_revision_token' AND parent_object_id = OBJECT_ID(N'dbo.requirement_versions')) ALTER TABLE [requirement_versions] DROP CONSTRAINT [uq_requirement_versions_revision_token];",
   "IF EXISTS (SELECT 1 FROM sys.default_constraints WHERE name = N'df_requirement_versions_revision_token' AND parent_object_id = OBJECT_ID(N'dbo.requirement_versions')) ALTER TABLE [requirement_versions] DROP CONSTRAINT [df_requirement_versions_revision_token];",
   "IF COL_LENGTH(N'dbo.requirement_versions', N'revision_token') IS NOT NULL ALTER TABLE [requirement_versions] DROP COLUMN [revision_token];",
 ]

As per coding guidelines, "Always pass an explicit name to TypeORM @Index/@Unique decorators and to migration SQL CREATE INDEX / ALTER TABLE … ADD CONSTRAINT."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@typeorm/migrations/0002_requirement_version_revision_token.mjs` around lines
1 - 8, The migration creates a UNIQUE INDEX named
uq_requirement_versions_revision_token but the entity
(lib/typeorm/entities/requirement-version.ts) models it as a UNIQUE constraint,
so change the migration to create a UNIQUE constraint instead: in UP_STATEMENTS
replace the CREATE UNIQUE INDEX line with an ALTER TABLE [requirement_versions]
ADD CONSTRAINT [uq_requirement_versions_revision_token] UNIQUE
([revision_token]); and in DOWN_STATEMENTS remove the DROP INDEX line and add a
conditional DROP CONSTRAINT for uq_requirement_versions_revision_token (use
sys.objects/sys.constraints or IF EXISTS (SELECT 1 FROM sys.objects WHERE name =
N'uq_requirement_versions_revision_token' ...) ALTER TABLE
[requirement_versions] DROP CONSTRAINT
[uq_requirement_versions_revision_token];) so the constraint name exactly
matches the entity metadata.
lib/dal/requirements.ts (4)

880-908: Duplicated requirementAreaId update across both edit branches.

The same UPDATE requirements SET requirement_area_id = @0WHERE id =@1`` block now appears in the draft path (L880-885) and the published-creates-new-draft path (L903-908). Extracting a small helper inside the transaction keeps the SQL definition in one place and makes future column additions to that side-effect (e.g., updated_at) easier.

♻️ Sketch
+    const updateAreaIfRequested = async () => {
+      if (data.requirementAreaId == null) return
+      await tx.query(
+        `UPDATE requirements SET requirement_area_id = `@0` WHERE id = `@1``,
+        [data.requirementAreaId, requirementId],
+      )
+    }
@@
-      if (data.requirementAreaId != null) {
-        await tx.query(
-          `UPDATE requirements SET requirement_area_id = `@0` WHERE id = `@1``,
-          [data.requirementAreaId, requirementId],
-        )
-      }
+      await updateAreaIfRequested()
@@
-    if (data.requirementAreaId != null) {
-      await tx.query(
-        `UPDATE requirements SET requirement_area_id = `@0` WHERE id = `@1``,
-        [data.requirementAreaId, requirementId],
-      )
-    }
+    await updateAreaIfRequested()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/dal/requirements.ts` around lines 880 - 908, The update of
requirement_area_id is duplicated in both edit branches; refactor by extracting
a small helper inside the transaction (e.g., an inner async function or local
helper like updateRequirementArea) that takes tx, requirementId, and
data.requirementAreaId and performs the tx.query(`UPDATE requirements SET
requirement_area_id = `@0` WHERE id = `@1``, [...]); replace both occurrences in the
branches (the block near insertVersionJoinsSqlServer/current.id branch and the
block before getNextVersionNumberSqlServer) with calls to that helper so the SQL
is defined in one place and can be extended (e.g., adding updated_at) centrally.

755-795: Add explicit return types to the new helpers.

normalizeBaseVersionId, normalizeBaseRevisionToken, and staleRequirementEditError rely on inference. As per coding guidelines (lib/**/*.ts: "Explicitly specify return types for all functions in utilities"), each should declare its return type so refactors elsewhere can't silently widen the contract.

♻️ Proposed signatures
-function normalizeBaseVersionId(value: number | null | undefined) {
+function normalizeBaseVersionId(value: number | null | undefined): number {
@@
-function normalizeBaseRevisionToken(value: string | null | undefined) {
+function normalizeBaseRevisionToken(value: string | null | undefined): string {
@@
-function staleRequirementEditError(
-  requirementId: number,
-  baseVersionId: number,
-  latestVersionId: number,
-) {
+function staleRequirementEditError(
+  requirementId: number,
+  baseVersionId: number,
+  latestVersionId: number,
+): ReturnType<typeof conflictError> {

As per coding guidelines: "Explicitly specify return types for all functions in utilities".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/dal/requirements.ts` around lines 755 - 795, The three helpers lack
explicit return types; add them: declare normalizeBaseVersionId(value: number |
null | undefined): number, normalizeBaseRevisionToken(value: string | null |
undefined): string, and staleRequirementEditError(requirementId: number,
baseVersionId: number, latestVersionId: number): ReturnType<typeof
conflictError> (or the specific error type your project uses) so their contracts
are explicit and future refactors can't widen returns; update the function
signatures for normalizeBaseVersionId, normalizeBaseRevisionToken, and
staleRequirementEditError accordingly.

826-839: Consider running the stale-edit check before the lifecycle status checks.

If a stale editor's base is mismatched AND the requirement has since moved to REVIEW/ARCHIVED, the user currently sees 'Cannot edit a requirement in Review status' / 'Cannot edit an archived requirement…' instead of the stale-edit conflict that the PR added the refresh/compare UI flow for. That's still technically correct, but it bypasses the new reason: 'stale_requirement_edit' payload (with latestVersionId) the UI is now wired to handle. Surfacing the stale conflict first gives consistent UX whenever the user's base is out-of-date, regardless of where the requirement subsequently landed.

♻️ Suggested ordering
-    if (current.statusId === STATUS_REVIEW) {
-      throw conflictError('Cannot edit a requirement in Review status')
-    }
-    if (current.statusId === STATUS_ARCHIVED) {
-      throw conflictError(
-        'Cannot edit an archived requirement — restore it first',
-      )
-    }
     if (
       current.id !== baseVersionId ||
       current.revisionToken !== baseRevisionToken
     ) {
       throw staleRequirementEditError(requirementId, baseVersionId, current.id)
     }
+    if (current.statusId === STATUS_REVIEW) {
+      throw conflictError('Cannot edit a requirement in Review status')
+    }
+    if (current.statusId === STATUS_ARCHIVED) {
+      throw conflictError(
+        'Cannot edit an archived requirement — restore it first',
+      )
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/dal/requirements.ts` around lines 826 - 839, Move the stale-edit
validation to run before lifecycle status checks: instead of first checking
current.statusId against STATUS_REVIEW/STATUS_ARCHIVED, first compare current.id
and current.revisionToken with baseVersionId and baseRevisionToken and throw
staleRequirementEditError(requirementId, baseVersionId, current.id) when they
mismatch; keep the existing status checks (throwing conflictError for
STATUS_REVIEW and STATUS_ARCHIVED) but execute them only after the stale check
so the stale_requirement_edit payload (including latestVersionId) is
consistently returned to the UI.

691-691: editedAt is selected and mapped but never read.

VersionLite.editedAt, the new edited_at AS editedAt column in getLatestVersionLite, and the corresponding map at L738 are not consumed by any caller (editRequirement, deleteDraftVersion, transitionStatus). Per the commit history, editedAt was the original optimistic-concurrency precondition before the switch to revision_token; this looks like a leftover. Either remove it to keep the lite read minimal, or leave a brief comment explaining the intentional retention.

Also applies to: 717-717, 738-738

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/dal/requirements.ts` at line 691, VersionLite.editedAt is an unused
leftover: remove the editedAt property from the VersionLite type and stop
selecting/mapping edited_at AS editedAt in getLatestVersionLite and its map (the
mapping around the current editedAt assignment), since callers (editRequirement,
deleteDraftVersion, transitionStatus) do not read it; alternatively, if you
intentionally want to keep it, add a brief comment near the VersionLite
declaration and the getLatestVersionLite mapping noting it's retained for
historical/compatibility reasons — pick one approach and update any tests or
usages accordingly.
tests/unit/mcp-http.test.ts (2)

639-697: Edit-mock test pair correctly exercises the new precondition contract.

accepts normReferenceIds in manage_requirement now passes baseRevisionToken/baseVersionId and asserts they reach manageRequirement, matching the new superRefine requirement in lib/mcp/server.ts (lines 821-846). The companion rejects the old references field … test still validates strict() unknown-field rejection — note that with the new superRefine, this request also lacks baseVersionId/baseRevisionToken, so multiple validation errors will be raised; the /unrecognized/i regex still matches because .strict() issues are surfaced alongside superRefine ones. Behavior is fine, but if a future Zod change reorders/short-circuits errors, this test could become brittle. Consider including the base fields here so the test is unambiguously about the unknown references field.

♻️ Suggested tightening so the test isolates the unknown-field rejection
       arguments: {
         operation: 'edit',
         uniqueId: 'INT0001',
         requirement: {
+          baseRevisionToken: '11111111-1111-4111-8111-111111111111',
+          baseVersionId: 10,
           description: 'Updated description',
           references: [1],
         },
       },
       name: 'requirements_manage_requirement',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/mcp-http.test.ts` around lines 639 - 697, The second test "rejects
the old references field in manage_requirement due to strict schema" is
currently ambiguous because it also omits baseRevisionToken/baseVersionId and
thus triggers the superRefine precondition; update that test to include the
required baseRevisionToken and baseVersionId in the request body so the failure
is isolated to the unknown "references" field. Locate the test by the it()
description or the tool name "requirements_manage_requirement" and add the same
baseRevisionToken/baseVersionId values used in the "accepts normReferenceIds in
manage_requirement" test so the assertion that content[0]?.text matches
/unrecognized/i only reflects the strict() unknown-field rejection.

221-303: Consider splitting the omnibus tool-schema assertion test.

This single test block now asserts on input/output schemas for six different tools (requirements_manage_requirement, requirements_query_catalog, requirements_get_requirement, requirements_transition_requirement, requirements_list_improvement_suggestions, requirements_manage_improvement_suggestion, requirements_generate_requirements) plus performs the original resource-read flow. When any one schema drifts, the failure name will be misleading ('lists the MCP tools and serves the requirement detail resource') and the developer has to scan ~80 assertions to find the actual mismatch.

For "Chill" mode this isn't a blocker, but extracting per-tool it(...) blocks (or a describe('tool schemas', ...) group) would localize failures and document each tool's contract independently.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/mcp-http.test.ts` around lines 221 - 303, The test "lists the MCP
tools and serves the requirement detail resource" bundles schema assertions for
many tools which obscures failures; split it into focused tests (or a
describe('tool schemas') group) by creating separate it blocks that each locate
the specific tool (e.g., requirements_manage_requirement,
requirements_query_catalog, requirements_get_requirement,
requirements_transition_requirement, requirements_list_improvement_suggestions,
requirements_manage_improvement_suggestion, requirements_generate_requirements)
and move only that tool's input/output/description assertions into that block,
leaving the original resource-read flow (the requirement detail assertions) as
its own test; ensure each new test still calls createClient() or reuses the
setup to obtain tools and uses the same symbol lookups (tools.tools.find(...)
for the tool.name) so failures are localized per tool.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/skills/run-spec-audit/references/scrutiny-areas.md:
- Around line 60-71: Update the Scenario 11 heading in QUALITY.md so it exactly
matches the test name string used in functional.test.ts: replace the current
title-case heading text with the exact test name "Scenario 11: stale draft edits
are rejected before replacing latest content" to ensure verbatim equality with
the test selector.

In `@components/RequirementForm.tsx`:
- Around line 244-249: The stale-conflict UI assumes staleConflict.latest exists
and fires onRefreshLatest() without awaiting or error handling; update the
rendering and actions to be resilient by: only render the "View latest" button
and build latestConflictHref when staleConflict?.latest is present (use
latestConflictVersion and latestConflictTarget references) and otherwise hide or
disable that action; introduce an isRefreshing state flag, await
onRefreshLatest() (wrap in try/catch) and set isRefreshing to disable
conflicting controls (disabled={isRefreshing}) and show inline loading feedback
(e.g., {isRefreshing ? t('loading') : t('view latest')}); apply the same pattern
for the other stale-conflict controls referenced in the same component (the
block around lines 347-385).

In `@docs/database-schema.md`:
- Line 1286: The Mermaid Index Relationship Diagram is missing a self-edge for
the new unique index uq_requirement_versions_revision_token; update the
diagram's graph LR to add a self-edge from the requirement_versions node (RV)
back to itself using the same convention as other unique indexes, e.g. RV --
"uq_requirement_versions_revision_token\n(revision_token)" --> RV so the new
index appears in the diagram alongside the existing unique indexes.

In `@docs/version-lifecycle-dates.md`:
- Around line 51-54: Update the stale paragraph that instructs callers to send
edited_at as the optimistic concurrency token: change it to describe the new
contract using baseVersionId and baseRevisionToken as the precondition tokens.
Reference the implemented normalized fields (baseVersionId and
baseRevisionToken) and remove or replace any mention of edited_at so the doc
matches the behavior enforced by normalizeBaseVersionId /
normalizeBaseRevisionToken and the rest of the codebase.

In `@lib/dal/requirements.ts`:
- Line 815: Add explicit return type annotations to the three helper functions:
annotate normalizeBaseVersionId(...) with : number, annotate
normalizeBaseRevisionToken(...) with : string, and annotate
staleRequirementEditError(...) with the same return type as conflictError()
(i.e., the return type of conflictError). Update each function signature to
include these explicit return types so TypeScript enforces the expected types.

In `@tests/unit/requirements-id-route.test.ts`:
- Around line 24-32: The mocked toHttpErrorPayload used in the tests returns
status 400 and leaves code undefined for plain Errors, but production maps plain
Errors to { code: 'internal', status: 500 } in lib/requirements/service.ts;
update the mock (toHttpErrorPayload) so it defaults to status: err.status ?? 500
and code: err.code ?? 'internal' (i.e., set the fallback for code to 'internal'
and the fallback for status to 500) to align test failure-path assertions with
production behavior.

---

Nitpick comments:
In `@lib/dal/requirements.ts`:
- Around line 880-908: The update of requirement_area_id is duplicated in both
edit branches; refactor by extracting a small helper inside the transaction
(e.g., an inner async function or local helper like updateRequirementArea) that
takes tx, requirementId, and data.requirementAreaId and performs the
tx.query(`UPDATE requirements SET requirement_area_id = `@0` WHERE id = `@1``,
[...]); replace both occurrences in the branches (the block near
insertVersionJoinsSqlServer/current.id branch and the block before
getNextVersionNumberSqlServer) with calls to that helper so the SQL is defined
in one place and can be extended (e.g., adding updated_at) centrally.
- Around line 755-795: The three helpers lack explicit return types; add them:
declare normalizeBaseVersionId(value: number | null | undefined): number,
normalizeBaseRevisionToken(value: string | null | undefined): string, and
staleRequirementEditError(requirementId: number, baseVersionId: number,
latestVersionId: number): ReturnType<typeof conflictError> (or the specific
error type your project uses) so their contracts are explicit and future
refactors can't widen returns; update the function signatures for
normalizeBaseVersionId, normalizeBaseRevisionToken, and
staleRequirementEditError accordingly.
- Around line 826-839: Move the stale-edit validation to run before lifecycle
status checks: instead of first checking current.statusId against
STATUS_REVIEW/STATUS_ARCHIVED, first compare current.id and
current.revisionToken with baseVersionId and baseRevisionToken and throw
staleRequirementEditError(requirementId, baseVersionId, current.id) when they
mismatch; keep the existing status checks (throwing conflictError for
STATUS_REVIEW and STATUS_ARCHIVED) but execute them only after the stale check
so the stale_requirement_edit payload (including latestVersionId) is
consistently returned to the UI.
- Line 691: VersionLite.editedAt is an unused leftover: remove the editedAt
property from the VersionLite type and stop selecting/mapping edited_at AS
editedAt in getLatestVersionLite and its map (the mapping around the current
editedAt assignment), since callers (editRequirement, deleteDraftVersion,
transitionStatus) do not read it; alternatively, if you intentionally want to
keep it, add a brief comment near the VersionLite declaration and the
getLatestVersionLite mapping noting it's retained for historical/compatibility
reasons — pick one approach and update any tests or usages accordingly.

In `@tests/unit/mcp-http.test.ts`:
- Around line 639-697: The second test "rejects the old references field in
manage_requirement due to strict schema" is currently ambiguous because it also
omits baseRevisionToken/baseVersionId and thus triggers the superRefine
precondition; update that test to include the required baseRevisionToken and
baseVersionId in the request body so the failure is isolated to the unknown
"references" field. Locate the test by the it() description or the tool name
"requirements_manage_requirement" and add the same
baseRevisionToken/baseVersionId values used in the "accepts normReferenceIds in
manage_requirement" test so the assertion that content[0]?.text matches
/unrecognized/i only reflects the strict() unknown-field rejection.
- Around line 221-303: The test "lists the MCP tools and serves the requirement
detail resource" bundles schema assertions for many tools which obscures
failures; split it into focused tests (or a describe('tool schemas') group) by
creating separate it blocks that each locate the specific tool (e.g.,
requirements_manage_requirement, requirements_query_catalog,
requirements_get_requirement, requirements_transition_requirement,
requirements_list_improvement_suggestions,
requirements_manage_improvement_suggestion, requirements_generate_requirements)
and move only that tool's input/output/description assertions into that block,
leaving the original resource-read flow (the requirement detail assertions) as
its own test; ensure each new test still calls createClient() or reuses the
setup to obtain tools and uses the same symbol lookups (tools.tools.find(...)
for the tool.name) so failures are localized per tool.

In `@typeorm/migrations/0002_requirement_version_revision_token.mjs`:
- Around line 1-8: The migration creates a UNIQUE INDEX named
uq_requirement_versions_revision_token but the entity
(lib/typeorm/entities/requirement-version.ts) models it as a UNIQUE constraint,
so change the migration to create a UNIQUE constraint instead: in UP_STATEMENTS
replace the CREATE UNIQUE INDEX line with an ALTER TABLE [requirement_versions]
ADD CONSTRAINT [uq_requirement_versions_revision_token] UNIQUE
([revision_token]); and in DOWN_STATEMENTS remove the DROP INDEX line and add a
conditional DROP CONSTRAINT for uq_requirement_versions_revision_token (use
sys.objects/sys.constraints or IF EXISTS (SELECT 1 FROM sys.objects WHERE name =
N'uq_requirement_versions_revision_token' ...) ALTER TABLE
[requirement_versions] DROP CONSTRAINT
[uq_requirement_versions_revision_token];) so the constraint name exactly
matches the entity metadata.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: caf7338f-913c-44ec-b7fd-128175b891c2

📥 Commits

Reviewing files that changed from the base of the PR and between fda1bf4 and 867448b.

📒 Files selected for processing (35)
  • .github/instructions/mcp-tool-contract.instructions.md
  • .github/skills/run-spec-audit/references/scrutiny-areas.md
  • app/[locale]/requirements/[id]/edit/edit-requirement-client.tsx
  • app/[locale]/requirements/requirements-client.tsx
  • app/api/requirements/[id]/route.ts
  • components/RequirementForm.tsx
  • cspell.jsonc
  • docs/database-schema.md
  • docs/lifecycle-workflow.md
  • docs/mcp-server-contributor-guide.md
  • docs/mcp-server-user-guide.md
  • docs/version-lifecycle-dates.md
  • lib/dal/requirements.ts
  • lib/mcp/server.ts
  • lib/requirements/list-view.ts
  • lib/requirements/service.ts
  • lib/requirements/types.ts
  • lib/typeorm/entities/requirement-version.ts
  • messages/en.json
  • messages/sv.json
  • scripts/__tests__/db-sqlserver-admin.test.mjs
  • scripts/db-sqlserver-admin.mjs
  • tests/quality/QUALITY.md
  • tests/quality/functional.test.ts
  • tests/unit/edit-requirement-client.test.tsx
  • tests/unit/mcp-http.test.ts
  • tests/unit/requirement-detail-client.test.tsx
  • tests/unit/requirement-form.test.tsx
  • tests/unit/requirements-client.test.tsx
  • tests/unit/requirements-dal.test.ts
  • tests/unit/requirements-id-route.test.ts
  • tests/unit/requirements-service.test.ts
  • tests/unit/version-detail-client.test.tsx
  • typeorm/migrations/0001_initial_sqlserver.mjs
  • typeorm/migrations/0002_requirement_version_revision_token.mjs

Comment thread .github/skills/run-spec-audit/references/scrutiny-areas.md
Comment thread components/RequirementForm.tsx Outdated
Comment thread docs/database-schema.md
Comment thread docs/version-lifecycle-dates.md Outdated
Comment thread lib/dal/requirements.ts
Comment thread tests/unit/requirements-id-route.test.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
tests/unit/mcp-http.test.ts (2)

221-378: Optional: extract a small helper for the repeated tool-lookup boilerplate.

The eight tool schemas cases all repeat createClient → listTools → tools.find(...) → close. A tiny helper would tighten the suite and reduce drift if more tools are added.

♻️ Sketch
+    async function getTool(name: string) {
+      const { client, transport } = await createClient()
+      const { tools } = await client.listTools()
+      const tool = tools.find(t => t.name === name)
+      return {
+        tool,
+        tools,
+        cleanup: async () => {
+          await client.close()
+          await transport.close()
+        },
+      }
+    }

Each case then becomes const { tool, cleanup } = await getTool('...'); ...; await cleanup().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/mcp-http.test.ts` around lines 221 - 378, The tests repeat
createClient → listTools → tools.find(...) → close boilerplate across the "tool
schemas" suite; add a small async helper (e.g., getTool) that calls
createClient(), awaits client.listTools(), finds the tool by name (using
tools.find), and returns { tool, cleanup } where cleanup closes both client and
transport; replace each test's repeated sequence with a call to
getTool('requirements_...') and await cleanup() at the end to keep behavior
identical while removing duplication.

108-124: Consider asserting the rotated revisionToken is surfaced by requirements_manage_requirement.

The mock now exposes a revisionToken on detail.versions[0], but no test in this file asserts it is propagated through the MCP tool output for the manage flow (only the schema-text check at line 318 covers transition_requirement). Given the whole point of the PR is the rotation/contract, a focused assertion that the manage tool output (or its outputSchema) advertises revisionToken would prevent silent regressions in the contract surfaced to MCP clients.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/mcp-http.test.ts` around lines 108 - 124, Add an assertion in the
manage-flow test that the rotated revisionToken from the manageRequirement mock
is propagated in the MCP response/outputSchema: when using the mocked
manageRequirement (the vi.fn() mock with detail.versions[0].revisionToken),
assert that the API response for requirements_manage_requirement (or the test
helper that validates outputSchema for the manage flow) includes
detail.versions[0].revisionToken (or that the returned outputSchema advertises a
revisionToken field for the manage operation) so the rotated token is explicitly
verified and cannot regress.
docs/database-schema.md (1)

198-198: Add UK marker on revision_token in the erDiagram for consistency.

Other single-column unique constraints in this diagram are annotated with UK (e.g. email UK, prefix UK, unique_id UK). Since revision_token is covered by uq_requirement_versions_revision_token, it should follow the same convention.

📝 Proposed change
-        text revision_token "uniqueidentifier"
+        text revision_token UK "uniqueidentifier"

As per coding guidelines, "When any database schema, migration, or seed change is made, update docs/database-schema.md: Entity-Relationship Diagram (Mermaid erDiagram)…".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/database-schema.md` at line 198, The ER diagram entry for the column
revision_token should be marked as unique to match the constraint
uq_requirement_versions_revision_token; update the mermaid erDiagram line that
currently reads "text revision_token 'uniqueidentifier'" to include the UK
marker (e.g., "text revision_token UK 'uniqueidentifier'") so it follows the
same convention used for other single-column unique constraints like email UK
and prefix UK.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@docs/database-schema.md`:
- Line 198: The ER diagram entry for the column revision_token should be marked
as unique to match the constraint uq_requirement_versions_revision_token; update
the mermaid erDiagram line that currently reads "text revision_token
'uniqueidentifier'" to include the UK marker (e.g., "text revision_token UK
'uniqueidentifier'") so it follows the same convention used for other
single-column unique constraints like email UK and prefix UK.

In `@tests/unit/mcp-http.test.ts`:
- Around line 221-378: The tests repeat createClient → listTools →
tools.find(...) → close boilerplate across the "tool schemas" suite; add a small
async helper (e.g., getTool) that calls createClient(), awaits
client.listTools(), finds the tool by name (using tools.find), and returns {
tool, cleanup } where cleanup closes both client and transport; replace each
test's repeated sequence with a call to getTool('requirements_...') and await
cleanup() at the end to keep behavior identical while removing duplication.
- Around line 108-124: Add an assertion in the manage-flow test that the rotated
revisionToken from the manageRequirement mock is propagated in the MCP
response/outputSchema: when using the mocked manageRequirement (the vi.fn() mock
with detail.versions[0].revisionToken), assert that the API response for
requirements_manage_requirement (or the test helper that validates outputSchema
for the manage flow) includes detail.versions[0].revisionToken (or that the
returned outputSchema advertises a revisionToken field for the manage operation)
so the rotated token is explicitly verified and cannot regress.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bb6d97df-d079-4884-9dfc-d84010c7a26b

📥 Commits

Reviewing files that changed from the base of the PR and between 867448b and 077b6e9.

📒 Files selected for processing (11)
  • components/RequirementForm.tsx
  • docs/database-schema.md
  • docs/version-lifecycle-dates.md
  • lib/dal/requirements.ts
  • tests/quality/QUALITY.md
  • tests/quality/functional.test.ts
  • tests/unit/mcp-http.test.ts
  • tests/unit/requirement-form.test.tsx
  • tests/unit/requirements-dal.test.ts
  • tests/unit/requirements-id-route.test.ts
  • typeorm/migrations/0002_requirement_version_revision_token.mjs
🚧 Files skipped from review as they are similar to previous changes (5)
  • typeorm/migrations/0002_requirement_version_revision_token.mjs
  • tests/unit/requirement-form.test.tsx
  • docs/version-lifecycle-dates.md
  • tests/unit/requirements-id-route.test.ts
  • tests/unit/requirements-dal.test.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
tests/unit/mcp-http.test.ts (1)

199-211: Optional: ensure cleanup on listTools failure.

If client.listTools() throws, the helper returns without calling client.close()/transport.close(), leaking the in-memory transport for the rest of the test run. Wrapping the call in try/catch (or using afterEach registration) would make the helper resilient if a future tool registration regression is ever introduced.

♻️ Proposed fix
 async function getTool(name: string) {
   const { client, transport } = await createClient()
-  const tools = await client.listTools()
-  const tool = tools.tools.find(tool => tool.name === name)
-
-  return {
-    cleanup: async () => {
-      await client.close()
-      await transport.close()
-    },
-    tool,
+  const cleanup = async () => {
+    await client.close()
+    await transport.close()
+  }
+  try {
+    const tools = await client.listTools()
+    const tool = tools.tools.find(tool => tool.name === name)
+    return { cleanup, tool }
+  } catch (error) {
+    await cleanup()
+    throw error
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/mcp-http.test.ts` around lines 199 - 211, The helper getTool
should guarantee cleanup of the in-memory client/transport if client.listTools()
throws; wrap the listTools call in a try/finally (or try/catch that rethrows) so
that client.close() and transport.close() are always awaited in the finally
block; update the getTool function to acquire client/transport, perform
listTools inside try and return tool in the try, but always run the cleanup
calls in finally to avoid leaking the transport on failures.
.github/workflows/quality-checks.yml (1)

30-31: Pin the dotenv-linter installer in CI and consider caching the binary.

Same supply-chain/reproducibility concern as the Dockerfile: this curls install.sh from master into sudo sh on every PR/push run, so a change in the upstream installer or its targeted release flips CI behavior without a code change in this repo. Pinning by tag (and optionally caching the resulting binary keyed on that tag) makes the gate deterministic and faster.

♻️ Pin install.sh by release tag
-      - name: Install dotenv-linter
-        run: curl -sSfL https://raw.githubusercontent.com/dotenv-linter/dotenv-linter/master/install.sh | sudo sh -s -- -b /usr/local/bin
+      - name: Install dotenv-linter
+        env:
+          DOTENV_LINTER_VERSION: v3.3.0
+        run: |
+          curl -sSfL "https://raw.githubusercontent.com/dotenv-linter/dotenv-linter/${DOTENV_LINTER_VERSION}/install.sh" \
+            | sudo sh -s -- -b /usr/local/bin "${DOTENV_LINTER_VERSION}"
+          dotenv-linter --version

Also worth considering: keep the version aligned with the devcontainer Dockerfile so local and CI runs produce identical results.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/quality-checks.yml around lines 30 - 31, The CI step
"Install dotenv-linter" currently curls install.sh from master; change it to
fetch a pinned release (replace the installer URL to reference a specific
tag/release) so the workflow is deterministic, and update the step to verify the
release (e.g., checksum or GPG) if available; optionally add an actions/cache
entry keyed by the pinned tag and runner OS to cache the downloaded
dotenv-linter binary so future runs restore the cached binary instead of
re-downloading; ensure the pinned tag matches the version used in the
devcontainer to keep local and CI versions aligned.
.devcontainer/Dockerfile (1)

24-26: Pin the dotenv-linter installer (supply‑chain hardening, reproducible builds).

The installer is fetched from master and piped into sh with no version pin and no checksum verification, so any future change to install.sh or its referenced release will silently land in the devcontainer image (and in CI on every run). For a tool wired into npm run check, a sudden CLI/flag change can also break the gate.

Recommend pinning to a tagged release (and ideally verifying the resulting binary), e.g.:

♻️ Pin to a specific dotenv-linter release
-# Install dotenv-linter for env file checks.
-RUN curl -sSfL https://raw.githubusercontent.com/dotenv-linter/dotenv-linter/master/install.sh | \
-    sh -s -- -b /usr/local/bin
+# Install dotenv-linter for env file checks (pinned for reproducibility).
+ARG DOTENV_LINTER_VERSION=v3.3.0
+RUN curl -sSfL "https://raw.githubusercontent.com/dotenv-linter/dotenv-linter/${DOTENV_LINTER_VERSION}/install.sh" | \
+    sh -s -- -b /usr/local/bin "${DOTENV_LINTER_VERSION}" && \
+    dotenv-linter --version
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.devcontainer/Dockerfile around lines 24 - 26, The current RUN step piping
install.sh from master is unpinned and unsafe; change the install to fetch a
specific dotenv-linter release and verify it before installing: replace the curl
of https://.../install.sh with a download of the installer or release asset for
a specific tag (e.g., a GitHub release tag), validate the checksum/signature for
that release, and then install the binary into /usr/local/bin; update the RUN
that references install.sh so it uses the tagged URL and checksum verification
logic to ensure reproducible, supply‑chain hardened builds.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In @.devcontainer/Dockerfile:
- Around line 24-26: The current RUN step piping install.sh from master is
unpinned and unsafe; change the install to fetch a specific dotenv-linter
release and verify it before installing: replace the curl of
https://.../install.sh with a download of the installer or release asset for a
specific tag (e.g., a GitHub release tag), validate the checksum/signature for
that release, and then install the binary into /usr/local/bin; update the RUN
that references install.sh so it uses the tagged URL and checksum verification
logic to ensure reproducible, supply‑chain hardened builds.

In @.github/workflows/quality-checks.yml:
- Around line 30-31: The CI step "Install dotenv-linter" currently curls
install.sh from master; change it to fetch a pinned release (replace the
installer URL to reference a specific tag/release) so the workflow is
deterministic, and update the step to verify the release (e.g., checksum or GPG)
if available; optionally add an actions/cache entry keyed by the pinned tag and
runner OS to cache the downloaded dotenv-linter binary so future runs restore
the cached binary instead of re-downloading; ensure the pinned tag matches the
version used in the devcontainer to keep local and CI versions aligned.

In `@tests/unit/mcp-http.test.ts`:
- Around line 199-211: The helper getTool should guarantee cleanup of the
in-memory client/transport if client.listTools() throws; wrap the listTools call
in a try/finally (or try/catch that rethrows) so that client.close() and
transport.close() are always awaited in the finally block; update the getTool
function to acquire client/transport, perform listTools inside try and return
tool in the try, but always run the cleanup calls in finally to avoid leaking
the transport on failures.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7452e8ee-b337-4595-8b5f-cd8c0484d906

📥 Commits

Reviewing files that changed from the base of the PR and between e295fa5 and 32b429e.

📒 Files selected for processing (13)
  • .devcontainer/.env.example
  • .devcontainer/Dockerfile
  • .env.development
  • .env.example
  • .env.sqlserver.ci
  • .env.sqlserver.example
  • .github/copilot-instructions.md
  • .github/workflows/integration-tests.yml
  • .github/workflows/quality-checks.yml
  • CONTRIBUTING.md
  • docs/database-schema.md
  • package.json
  • tests/unit/mcp-http.test.ts
💤 Files with no reviewable changes (1)
  • .github/workflows/integration-tests.yml
✅ Files skipped from review due to trivial changes (6)
  • .github/copilot-instructions.md
  • .env.sqlserver.example
  • .devcontainer/.env.example
  • CONTRIBUTING.md
  • .env.development
  • .env.example
🚧 Files skipped from review as they are similar to previous changes (2)
  • .env.sqlserver.ci
  • docs/database-schema.md

@johlju

johlju commented Apr 25, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 25, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
.github/workflows/quality-checks.yml (1)

49-69: Consider running the npm script directly to keep CI and local in lockstep.

The PR-failure comment tells contributors to reproduce by running npm run dotenv:check (line 111), but CI shells out to the dotenv-linter/action-dotenv-linter@v3 action which ships its own dotenv-linter binary independent of the version pulled in by npm ci. That can cause "passes locally, fails in CI" (or vice versa) when the two versions drift, and it forces every flag/exclude change to be made in two places (here and package.json).

Since the npm script already encodes the exact same flags and exclusions, calling it directly removes the duplication and the version-skew risk.

♻️ Proposed simplification
-      - name: Run dotenv-linter root check
-        uses: dotenv-linter/action-dotenv-linter@v3
-        with:
-          dotenv_linter_flags: >-
-            --exclude .env.sqlserver
-            --exclude .env.local
-            --exclude .env.*.local
-          filter_mode: nofilter
-          fail_level: any
-
-      - name: Run dotenv-linter devcontainer check
-        uses: dotenv-linter/action-dotenv-linter@v3
-        with:
-          dotenv_linter_flags: >-
-            .devcontainer
-            --recursive
-            --exclude .
-            --exclude .devcontainer/.env
-            --exclude .devcontainer/elevated/.env
-          filter_mode: nofilter
-          fail_level: any
+      - name: Run dotenv-linter check
+        run: npm run dotenv:check

If keeping the action is intentional (e.g., for inline PR annotations via filter_mode/fail_level), feel free to dismiss — but please confirm dotenv-linter is also installed/pinned consistently with what npm run dotenv:check expects so contributors aren't chasing version drift.

Also applies to: 111-111

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/quality-checks.yml around lines 49 - 69, The workflow uses
the dotenv-linter GitHub Action steps named "Run dotenv-linter root check" and
"Run dotenv-linter devcontainer check" which call the action's bundled binary
causing potential version drift vs the repo npm script; replace those steps to
run the repository npm scripts (e.g., run npm ci then npm run dotenv:check for
the root check, and the equivalent npm script for the devcontainer check such as
npm run dotenv:check:devcontainer or npm run dotenv:check -- <devcontainer
args>) so CI executes the same pinned version and flags as the local npm script;
ensure Node is setup (actions/setup-node) and workspace is installed before
running the npm scripts.
tests/unit/mcp-http.test.ts (2)

199-208: Optional: cache the tools list across schema sub-tests.

getTool opens/closes a fresh MCP client + transport and re-calls listTools() for every assertion (7 times across the new tool schemas describe). It works, but you can cut it down to a single client per describe with a beforeAll/afterAll and a memoized tools.tools array. Given Chill review, just flagging as optional cleanup.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/mcp-http.test.ts` around lines 199 - 208, Refactor to avoid
recreating the MCP client for every assertion by moving client/transport setup
into a describe-level beforeAll and teardown into afterAll: call createClient()
once, call client.listTools() once and store the resulting tools array in a
memoized variable, then update getTool(name) to return tools.find(t => t.name
=== name) using that cached array; ensure afterAll awaits client.close() and
transport.close(); reference createClient, client.listTools, getTool,
client.close, and transport.close.

233-248: Inconsistent setup and missing finally cleanup in this sub-test.

The other six sub-tests under tool schemas go through getTool, but this one inlines createClient() and closes the client/transport at the end without a try/finally. If any of the expect(...) calls fail, the client and HTTP transport leak into the next test, which can cause cascading failures or hangs. Either route this through getTool (returning the full tool list) or wrap the body in try/finally.

♻️ Possible cleanup
-    it('lists the core MCP tools', async () => {
-      const { client, transport } = await createClient()
-      const tools = await client.listTools()
-
-      expect(tools.tools.map(tool => tool.name)).toEqual(
-        expect.arrayContaining([
-          'requirements_get_requirement',
-          'requirements_manage_requirement',
-          'requirements_query_catalog',
-          'requirements_transition_requirement',
-        ]),
-      )
-
-      await client.close()
-      await transport.close()
-    })
+    it('lists the core MCP tools', async () => {
+      const { client, transport } = await createClient()
+      try {
+        const tools = await client.listTools()
+        expect(tools.tools.map(tool => tool.name)).toEqual(
+          expect.arrayContaining([
+            'requirements_get_requirement',
+            'requirements_manage_requirement',
+            'requirements_query_catalog',
+            'requirements_transition_requirement',
+          ]),
+        )
+      } finally {
+        await client.close()
+        await transport.close()
+      }
+    })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/mcp-http.test.ts` around lines 233 - 248, This test inlines
createClient() and closes client/transport without a try/finally, causing
resource leaks on assertion failure; update the test to either call the existing
getTool helper to obtain the client/transport and tool list, or wrap the
createClient() usage in a try/finally so client.close() and transport.close()
are always called (ensure you reference createClient(), client.close(),
transport.close(), or getTool where appropriate and move the await
client.close() and await transport.close() into the finally block or replace the
body with a call to getTool that returns the full tool list).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/quality-checks.yml:
- Around line 59-69: Remove the stray "--exclude ." token from the
dotenv_linter_flags for the "Run dotenv-linter devcontainer check" step: open
the action block that uses dotenv-linter/action-dotenv-linter@v3 (the step named
"Run dotenv-linter devcontainer check") and edit the dotenv_linter_flags value
to delete the "--exclude ." entry so the flags list matches the package.json
dotenv:check script and only excludes the intended .devcontainer/.env and
.devcontainer/elevated/.env files.

---

Nitpick comments:
In @.github/workflows/quality-checks.yml:
- Around line 49-69: The workflow uses the dotenv-linter GitHub Action steps
named "Run dotenv-linter root check" and "Run dotenv-linter devcontainer check"
which call the action's bundled binary causing potential version drift vs the
repo npm script; replace those steps to run the repository npm scripts (e.g.,
run npm ci then npm run dotenv:check for the root check, and the equivalent npm
script for the devcontainer check such as npm run dotenv:check:devcontainer or
npm run dotenv:check -- <devcontainer args>) so CI executes the same pinned
version and flags as the local npm script; ensure Node is setup
(actions/setup-node) and workspace is installed before running the npm scripts.

In `@tests/unit/mcp-http.test.ts`:
- Around line 199-208: Refactor to avoid recreating the MCP client for every
assertion by moving client/transport setup into a describe-level beforeAll and
teardown into afterAll: call createClient() once, call client.listTools() once
and store the resulting tools array in a memoized variable, then update
getTool(name) to return tools.find(t => t.name === name) using that cached
array; ensure afterAll awaits client.close() and transport.close(); reference
createClient, client.listTools, getTool, client.close, and transport.close.
- Around line 233-248: This test inlines createClient() and closes
client/transport without a try/finally, causing resource leaks on assertion
failure; update the test to either call the existing getTool helper to obtain
the client/transport and tool list, or wrap the createClient() usage in a
try/finally so client.close() and transport.close() are always called (ensure
you reference createClient(), client.close(), transport.close(), or getTool
where appropriate and move the await client.close() and await transport.close()
into the finally block or replace the body with a call to getTool that returns
the full tool list).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: af569697-42b0-45cd-a45b-c249f37426f0

📥 Commits

Reviewing files that changed from the base of the PR and between 32b429e and 7370771.

📒 Files selected for processing (4)
  • .github/workflows/quality-checks.yml
  • docs/arkitekturbeskrivning-kravhantering.md
  • package.json
  • tests/unit/mcp-http.test.ts
✅ Files skipped from review due to trivial changes (1)
  • docs/arkitekturbeskrivning-kravhantering.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • package.json

Comment thread .github/workflows/quality-checks.yml
@johlju

johlju commented Apr 25, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 25, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@johlju

johlju commented Apr 25, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 25, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
tests/unit/mcp-http.test.ts (2)

251-253: getTool is synchronous but awaited at every call site.

getTool returns the array element directly, yet callers use const queryTool = await getTool('…') (lines 267, 280, 297, 316, 328, 339, 354). It works because await on a non-Promise is a no-op, but it misleads readers into thinking schema lookup is async. Either drop the awaits or make getTool async for consistency.

♻️ Proposed fix (drop the awaits)
-      const queryTool = await getTool('requirements_query_catalog')
+      const queryTool = getTool('requirements_query_catalog')

(Apply the same to the other six call sites.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/mcp-http.test.ts` around lines 251 - 253, getTool is synchronous
but is awaited at its call sites; remove the unnecessary awaits (e.g., change
const queryTool = await getTool('…') to const queryTool = getTool('…')) at all
call sites that reference getTool so the code accurately reflects a synchronous
schema lookup (do this for each occurrence where getTool is currently awaited).

116-132: Fixture for manageRequirement.detail.versions[0] omits id.

The MCP edit contract instructs clients to read requirement.versions[0].id (as baseVersionId) and requirement.versions[0].revisionToken from outputs of read/write tools (see lib/mcp/server.ts:685-698 and the description text asserted at lines 309-312). This mock returns only revisionToken and versionNumber, so the fixture diverges from the contract being advertised. The output schema is z.record(z.string(), z.unknown()) so tests still pass, but a future test that exercises the round-trip (baseVersionId taken from a previous edit response) would silently get undefined. Recommend including a representative id so fixtures stay aligned with the documented client flow.

♻️ Proposed fix
       detail: {
         uniqueId: 'INT0001',
         versions: [
           {
+            id: 10,
             revisionToken: '22222222-2222-4222-8222-222222222222',
             versionNumber: 2,
           },
         ],
       },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/mcp-http.test.ts` around lines 116 - 132, The mock for
manageRequirement in the test fixture omits the version id expected by the MCP
edit contract; update the mocked manageRequirement response so
manageRequirement.mockResolvedValue includes an id field on detail.versions[0]
(e.g., add detail.versions[0].id) so that code paths reading
requirement.versions[0].id (used as baseVersionId) receive a representative
value; keep the existing revisionToken and versionNumber fields intact when
updating the fixture.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/quality-checks.yml:
- Around line 52-55: The dotenv_linter_flags entry currently uses a glob-like
pattern `--exclude .env.*.local` which dotenv-linter does not support; replace
that pattern by listing each concrete filename to exclude (e.g., add `--exclude
.env.production.local`, `--exclude .env.development.local`, etc.) under the
dotenv_linter_flags block so the linter skips the intended files; verify
repository-specific `.local` filenames (or .gitignore) and add each explicit
`--exclude <filename>` line instead of the glob.

In `@tests/unit/mcp-http.test.ts`:
- Around line 229-249: The inner beforeAll calls createClient() before the outer
beforeEach has seeded the service mock; move the mock seeding into the inner
beforeAll so serviceState.getService.mockReturnValue(createFakeService()) runs
before createClient() (and thus before handleRequirementsMcpRequest/listTools),
ensuring createKravhanteringMcpServer and createClient use the fake service;
keep or remove the outer beforeEach seeding (avoid double-mocking) and ensure
cleanup remains in afterAll.

---

Nitpick comments:
In `@tests/unit/mcp-http.test.ts`:
- Around line 251-253: getTool is synchronous but is awaited at its call sites;
remove the unnecessary awaits (e.g., change const queryTool = await getTool('…')
to const queryTool = getTool('…')) at all call sites that reference getTool so
the code accurately reflects a synchronous schema lookup (do this for each
occurrence where getTool is currently awaited).
- Around line 116-132: The mock for manageRequirement in the test fixture omits
the version id expected by the MCP edit contract; update the mocked
manageRequirement response so manageRequirement.mockResolvedValue includes an id
field on detail.versions[0] (e.g., add detail.versions[0].id) so that code paths
reading requirement.versions[0].id (used as baseVersionId) receive a
representative value; keep the existing revisionToken and versionNumber fields
intact when updating the fixture.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b7307f7c-45a3-4de8-b977-567c0a15dd34

📥 Commits

Reviewing files that changed from the base of the PR and between 32b429e and 2dca9c8.

📒 Files selected for processing (4)
  • .github/workflows/quality-checks.yml
  • docs/arkitekturbeskrivning-kravhantering.md
  • package.json
  • tests/unit/mcp-http.test.ts
✅ Files skipped from review due to trivial changes (1)
  • docs/arkitekturbeskrivning-kravhantering.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • package.json

Comment thread .github/workflows/quality-checks.yml Outdated
Comment thread tests/unit/mcp-http.test.ts
@johlju
johlju merged commit 30449fa into viscalyx:main Apr 25, 2026
7 checks passed
@johlju
johlju deleted the fix/a4-april branch April 25, 2026 12:44
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.

1 participant