Skip to content

Implement reverse test#249

Open
Benjvandam wants to merge 9 commits intomainfrom
implement-reverse-test
Open

Implement reverse test#249
Benjvandam wants to merge 9 commits intomainfrom
implement-reverse-test

Conversation

@Benjvandam
Copy link
Copy Markdown

@Benjvandam Benjvandam commented Mar 16, 2026

Fixes # #242

Description

New update-text-properties command that uploads custom text properties from a Liquid Test YAML file to a reconciliation in a company file. It extracts custom data from all 4 levels: company, period, reconciliation, and account.

Supports --handle for faster YAML file lookup and --dry-run to preview the payload without uploading.

Testing Instructions

Steps:

  1. Navigate to a market repo (e.g. be_market) that has reconciliation templates with liquid test YAML files
  2. Open a reconciliation in a company file on Silverfin and copy the full URL
  3. Run with dry-run to verify the payload:
    silverfin update-text-properties -u "<url>" -t <test_name> -h <handle> --dry-run
    
  4. Verify the output shows the correct properties extracted from the YAML test
  5. Run without dry-run to push the properties:
    silverfin update-text-properties -u "<url>" -t <test_name> -h <handle>
    
  6. Verify in the Silverfin UI that the custom properties are updated on the reconciliation
  7. Test with a YAML file that has company-level custom data (under data.company.custom) and verify those are also pushed
  8. Test without --handle to verify it scans all template folders and finds the test automatically

Author Checklist

  • Skip bumping the CLI version

Reviewer Checklist

  • PR has a clear title and description
  • Manually tested the changes that the PR introduces
  • Changes introduced are covered by tests of acceptable quality

@Benjvandam Benjvandam self-assigned this Mar 16, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 16, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6534be67-79e6-42b2-8a05-27f47a27c0d8

📥 Commits

Reviewing files that changed from the base of the PR and between 0013ece and 32eb811.

📒 Files selected for processing (1)
  • bin/cli.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • bin/cli.js

Walkthrough

Adds an update-text-properties CLI command plus supporting utilities and API functions to parse Liquid Test YAML, transform "custom" text properties, and upload them to company, period, reconciliation, and account endpoints. Also updates changelog, package version, and .gitignore.

Changes

Cohort / File(s) Summary
Configuration & Changelog
\.gitignore, CHANGELOG.md, package.json
\.gitignore now ignores .cursor. CHANGELOG.md adds v1.55.0 entry documenting the new update-text-properties command. package.json version bumped to 1.55.0.
CLI command
bin/cli.js
Adds update-text-properties command with -u/--url, -t/--test, optional -h/--handle, and --dry-run. Locates test YAML, transforms custom blocks into properties, queues company/period/reconciliation/account updates, supports dry-run, resolves IDs dynamically, applies updates sequentially, and sets process exit code on failures.
API additions
lib/api/sfApi.js
Adds exports: updateReconciliationCustom, updateCompanyCustom, updatePeriodCustom, updateAccountCustom — functions that POST { properties } to Silverfin endpoints and return per-request results with per-item error handling.
Text property utilities
lib/utils/textPropertyUtils.js
New module exporting transformCustomToProperties(customData) (converts dot-notated keys into structured property entries) and findTestData(testName, handle) (scans reconciliationText templates, parses YAML with merge/alias support, and extracts company/period/reconciliation/account custom blocks).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Implement reverse test' is vague and does not clearly describe the main change, which is adding an 'update-text-properties' CLI command to upload custom text properties from YAML files. Revise the title to be more specific and descriptive, such as 'Add update-text-properties command for uploading custom text properties from YAML'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description is comprehensive, includes required sections (description, testing instructions, checklists), and clearly explains the new command's functionality, options, and multi-level custom data extraction.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch implement-reverse-test

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

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 (1)
bin/cli.js (1)

540-567: Consider adding URL validation and more detailed error output.

Two minor observations:

  1. If liquidTestUtils.extractURL fails to parse an invalid URL, the error may not be user-friendly. Consider validating the parsed urlData before proceeding.

  2. On upload failure (line 564), consider logging the response details to help with debugging:

🔧 Optional: Improve error messaging
     // Upload to Silverfin
     const response = await SF.updateReconciliationCustom(firmId, companyId, periodId, reconciliationId, properties);
     if (response && response.status >= 200 && response.status < 300) {
       consola.success("Text properties updated successfully");
     } else {
-      consola.error("Failed to update text properties");
+      consola.error("Failed to update text properties", response?.data || response?.statusText || "");
       process.exit(1);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/cli.js` around lines 540 - 567, Wrap the call to
liquidTestUtils.extractURL in a try/catch and validate the returned urlData
(check that firmId, companyId, periodId and reconciliationId are present) and
log a clear user-facing error and exit if parsing fails or required IDs are
missing; after calling SF.updateReconciliationCustom, enhance the failure branch
to log detailed response information (e.g., response.status, response.data or
response.error) before calling process.exit(1) so failures are easier to debug —
reference extractURL, urlData (firmId, companyId, periodId, reconciliationId),
findTestData, transformCustomToProperties, SF.updateReconciliationCustom and
response when making these changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/utils/textPropertyUtils.js`:
- Around line 27-37: The code currently has inconsistent behavior when mixing
simple keys ("ns.key") and nested keys ("ns.key.sub") because namespaceMap
entries are set once and later keys either get ignored or overwrite the value;
update the logic in the block that handles keyParts and namespaceMap so
conflicts are merged deterministically: whenever adding a nested key
(keyParts.length > 2) and namespaceMap already has a primitive value for
namespaceKey, convert that entry's value into an object and store the original
primitive under a reserved property (e.g., "" or "_value"); conversely, when
adding a simple key and an entry exists whose value is an object, store the
simple value under that reserved property instead of replacing the object; use
the existing symbols namespaceMap, namespaceKey, keyParts, subKey and ensure you
consistently create/merge objects rather than overwriting or ignoring entries.

---

Nitpick comments:
In `@bin/cli.js`:
- Around line 540-567: Wrap the call to liquidTestUtils.extractURL in a
try/catch and validate the returned urlData (check that firmId, companyId,
periodId and reconciliationId are present) and log a clear user-facing error and
exit if parsing fails or required IDs are missing; after calling
SF.updateReconciliationCustom, enhance the failure branch to log detailed
response information (e.g., response.status, response.data or response.error)
before calling process.exit(1) so failures are easier to debug — reference
extractURL, urlData (firmId, companyId, periodId, reconciliationId),
findTestData, transformCustomToProperties, SF.updateReconciliationCustom and
response when making these changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6eb889f5-0776-47ba-8d67-f95f23f16839

📥 Commits

Reviewing files that changed from the base of the PR and between 840b02d and 40984d8.

📒 Files selected for processing (5)
  • .gitignore
  • CHANGELOG.md
  • bin/cli.js
  • lib/api/sfApi.js
  • lib/utils/textPropertyUtils.js

Comment on lines +27 to +37
if (keyParts.length === 2) {
if (!namespaceMap.has(namespaceKey)) {
namespaceMap.set(namespaceKey, { namespace, key, value });
}
} else {
if (!namespaceMap.has(namespaceKey)) {
namespaceMap.set(namespaceKey, { namespace, key, value: {} });
}
const subKey = keyParts.slice(2).join(".");
namespaceMap.get(namespaceKey).value[subKey] = value;
}
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Mar 16, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential data inconsistency when mixing simple and nested keys for the same namespace.key.

If the input contains both a simple key (e.g., "ns.key": "value") and nested keys (e.g., "ns.key.subkey": "nested"), the order of iteration determines the result:

  • Simple key first → the nested key will overwrite .value with an object containing subkey
  • Nested key first → the simple key is ignored (line 28 check !namespaceMap.has)

This could lead to unexpected behavior depending on object property iteration order.

🛡️ Proposed fix to handle mixed keys consistently
     if (keyParts.length === 2) {
-      if (!namespaceMap.has(namespaceKey)) {
-        namespaceMap.set(namespaceKey, { namespace, key, value });
-      }
+      // Simple key always sets the value directly (overwrite if exists)
+      const existing = namespaceMap.get(namespaceKey);
+      if (existing && typeof existing.value === "object") {
+        consola.warn(`Key "${fullKey}" conflicts with nested keys under same namespace.key — skipping`);
+        continue;
+      }
+      namespaceMap.set(namespaceKey, { namespace, key, value });
     } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/utils/textPropertyUtils.js` around lines 27 - 37, The code currently has
inconsistent behavior when mixing simple keys ("ns.key") and nested keys
("ns.key.sub") because namespaceMap entries are set once and later keys either
get ignored or overwrite the value; update the logic in the block that handles
keyParts and namespaceMap so conflicts are merged deterministically: whenever
adding a nested key (keyParts.length > 2) and namespaceMap already has a
primitive value for namespaceKey, convert that entry's value into an object and
store the original primitive under a reserved property (e.g., "" or "_value");
conversely, when adding a simple key and an entry exists whose value is an
object, store the simple value under that reserved property instead of replacing
the object; use the existing symbols namespaceMap, namespaceKey, keyParts,
subKey and ensure you consistently create/merge objects rather than overwriting
or ignoring entries.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Shouldn't happen in the YAML files in practice

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@Benjvandam, understood! Since the YAML files in practice won't mix simple keys (e.g., "ns.key": "value") and nested keys (e.g., "ns.key.subkey": "nested") for the same namespace.key, the current behavior is safe. I'll note this for future reviews.


✏️ Learnings added
Learnt from: Benjvandam
Repo: silverfin/silverfin-cli PR: 249
File: lib/utils/textPropertyUtils.js:27-37
Timestamp: 2026-03-23T15:05:12.258Z
Learning: In `lib/utils/textPropertyUtils.js` (silverfin-cli), custom property keys in Liquid Test YAML files never mix simple keys (`namespace.key`) and nested keys (`namespace.key.subkey`) for the same `namespace.key`. Therefore, the existing logic in `transformCustomToProperties` that ignores duplicate `namespaceKey` entries is intentional and safe — no conflict-resolution handling is needed.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: Benjvandam
Repo: silverfin/silverfin-cli PR: 249
File: lib/utils/textPropertyUtils.js:31-36
Timestamp: 2026-03-23T15:03:32.071Z
Learning: In `lib/utils/textPropertyUtils.js` (silverfin-cli), custom property keys in the Liquid Test YAML files follow at most a 3-segment dot-notation format (`namespace.key.subkey`). Keys with 4 or more segments (e.g., `namespace.key.a.b`) do not occur in practice, so collapsing deeper segments with `keyParts.slice(2).join(".")` is intentional and sufficient.

bensilverfin and others added 2 commits March 23, 2026 15:46
Extract and push custom data from all 4 levels in liquid test YAML:
company, period, reconciliation, and account. Company/period/account
endpoints send one property per request. Reconciliation and account
IDs are resolved via the API by handle/number.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
lib/api/sfApi.js (1)

587-633: Extract the repeated per-property POST helper.

updateCompanyCustom, updatePeriodCustom, and updateAccountCustom all duplicate the same request/collect/error-handling loop. Pulling that into one helper will keep response handling consistent when one of these endpoints inevitably changes.

♻️ Possible refactor
+async function postCustomProperties(instance, url, properties) {
+  const results = [];
+  for (const prop of properties) {
+    try {
+      const response = await instance.post(url, prop);
+      apiUtils.responseSuccessHandler(response);
+      results.push(response);
+    } catch (error) {
+      const response = await apiUtils.responseErrorHandler(error);
+      results.push(response);
+    }
+  }
+  return results;
+}
+
 async function updateCompanyCustom(firmId, companyId, properties) {
   const instance = AxiosFactory.createInstance("firm", firmId);
-  const results = [];
-  for (const prop of properties) {
-    try {
-      const response = await instance.post(`/companies/${companyId}/custom`, prop);
-      apiUtils.responseSuccessHandler(response);
-      results.push(response);
-    } catch (error) {
-      const response = await apiUtils.responseErrorHandler(error);
-      results.push(response);
-    }
-  }
-  return results;
+  return postCustomProperties(instance, `/companies/${companyId}/custom`, properties);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/api/sfApi.js` around lines 587 - 633, The three functions
updateCompanyCustom, updatePeriodCustom, and updateAccountCustom duplicate the
same per-property POST loop and error/success handling; extract that loop into a
shared helper (e.g., postPropertiesWithCollect or postPerPropertyCustom) that
accepts an Axios instance, a URL (or URL builder), and the properties array,
iterates over properties, calls instance.post(url, prop), invokes
apiUtils.responseSuccessHandler on success and apiUtils.responseErrorHandler on
catch, collects each response into an array and returns it; then replace the
bodies of updateCompanyCustom, updatePeriodCustom, and updateAccountCustom to
call this helper with the appropriate URL for company, period, and account.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@bin/cli.js`:
- Around line 541-543: The code parses the target URL into urlData via
liquidTestUtils.extractURL (producing firmId, companyId and the target
period/reconciliation/account identity), but the update-building logic (the
loops that assemble the updates array and the fiscal_year.end_date matching
logic) currently selects records by fiscal_year.end_date and enqueues every
block found; change that logic to filter using the identity fields extracted
from options.url/urlData (e.g., periodId, reconciliationId, accountId or
equivalent) so only records whose period/reconciliation/account IDs match
urlData are included when constructing updates, and ensure subsequent
update/enqueue code uses firmId/companyId from urlData for scoping rather than
broadly matching fiscal_year.end_date.
- Around line 603-609: The current guard checks the truthiness of the value
returned by SF.findAccountByNumber, but that function can return an error object
(from apiUtils.responseErrorHandler) which is truthy and will cause accessing
account.account.id to throw; update the conditional after calling
SF.findAccountByNumber to explicitly verify the resolved shape and existence of
account.account.id (e.g., ensure account && account.account &&
account.account.id) before calling SF.updateAccountCustom, and if missing log a
descriptive error (using consola.error) and return null so lookup failures and
error objects are handled cleanly.
- Around line 631-643: The loop that applies uploads in bin/cli.js (iterating
over updates and calling update.apply()) currently logs failures but never sets
a non-zero exit status; modify the end of the loop (or after the loop completes)
to set process.exitCode = 1 when any response in responses is non-2xx (i.e.,
when failed.length > 0) so CI sees failure; alternatively throw an Error after
detecting failures — update the logic around the variables update, responses,
and failed to ensure the process exits non-zero on any upload failure.

In `@lib/utils/textPropertyUtils.js`:
- Around line 60-75: The current loop (using handleDirs, testsDir, yamlFiles and
checking parsed[testName]) returns the first YAML that contains
parsed[testName], which causes nondeterministic selection when handle is
omitted; instead, collect all matching filePath/parsed pairs across handleDirs
and only return when there is exactly one unique match, otherwise disambiguate
by preferring matches in the directory that equals the provided handle (variable
handle) or throw a clear error asking the caller to specify a handle; apply the
same change to the similar logic around lines 120-125 so both code paths gather
all matches, enforce uniqueness, and only resolve when a single unambiguous
match remains.
- Around line 31-36: The code currently collapses deep dotted keys into a single
string key by using subKey = keyParts.slice(2).join("."), causing e.g.
ns.key.a.b to become { "a.b": value } instead of a nested object; update the
logic where namespaceMap is populated (look for namespaceMap, namespaceKey,
keyParts, subKey) to build real nested objects for keyParts.slice(2) by
iterating the remaining segments and creating nested plain objects until the
final segment where you assign value (instead of joining with "."); ensure
namespaceMap.get(namespaceKey).value is mutated to contain the proper nested
structure rather than a single dotted-key entry.

---

Nitpick comments:
In `@lib/api/sfApi.js`:
- Around line 587-633: The three functions updateCompanyCustom,
updatePeriodCustom, and updateAccountCustom duplicate the same per-property POST
loop and error/success handling; extract that loop into a shared helper (e.g.,
postPropertiesWithCollect or postPerPropertyCustom) that accepts an Axios
instance, a URL (or URL builder), and the properties array, iterates over
properties, calls instance.post(url, prop), invokes
apiUtils.responseSuccessHandler on success and apiUtils.responseErrorHandler on
catch, collects each response into an array and returns it; then replace the
bodies of updateCompanyCustom, updatePeriodCustom, and updateAccountCustom to
call this helper with the appropriate URL for company, period, and account.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fa47adc3-f595-435c-8c10-00b968bb4ef5

📥 Commits

Reviewing files that changed from the base of the PR and between 40984d8 and 20fc0e0.

📒 Files selected for processing (3)
  • bin/cli.js
  • lib/api/sfApi.js
  • lib/utils/textPropertyUtils.js

Comment on lines +541 to +543
// Parse URL to extract IDs
const urlData = liquidTestUtils.extractURL(options.url);
const { firmId, companyId } = urlData;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Scope period/reconciliation/account updates to the URL target.

Lines 542-543 parse a specific reconciliation URL, but Line 567 switches to matching by fiscal_year.end_date and the loops below enqueue every period/reconciliation/account block found in the YAML. That turns a targeted command into a multi-record writer and can overwrite unrelated remote custom data from the same test file. Use the URL’s period/reconciliation identity to filter the update set before building updates.

Also applies to: 561-613

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

In `@bin/cli.js` around lines 541 - 543, The code parses the target URL into
urlData via liquidTestUtils.extractURL (producing firmId, companyId and the
target period/reconciliation/account identity), but the update-building logic
(the loops that assemble the updates array and the fiscal_year.end_date matching
logic) currently selects records by fiscal_year.end_date and enqueues every
block found; change that logic to filter using the identity fields extracted
from options.url/urlData (e.g., periodId, reconciliationId, accountId or
equivalent) so only records whose period/reconciliation/account IDs match
urlData are included when constructing updates, and ensure subsequent
update/enqueue code uses firmId/companyId from urlData for scoping rather than
broadly matching fiscal_year.end_date.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The purpose of the command is to set up a full test scenario in a company file — that requires pushing custom data to all levels and templates referenced in the test, not just the single reconciliation from the URL. The URL is used to identify the target company/firm/period, but the test YAML defines the complete data set needed for that scenario to work.

Comment on lines +31 to +36
} else {
if (!namespaceMap.has(namespaceKey)) {
namespaceMap.set(namespaceKey, { namespace, key, value: {} });
}
const subKey = keyParts.slice(2).join(".");
namespaceMap.get(namespaceKey).value[subKey] = value;
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Mar 23, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Build real nested objects for deep dotted keys.

Lines 35-36 collapse the remaining path into a single "a.b" key, so ns.key.a.b is serialized differently from ns.key.a. Any property deeper than three segments will be uploaded in the wrong shape.

🐛 Possible fix
     } else {
       if (!namespaceMap.has(namespaceKey)) {
         namespaceMap.set(namespaceKey, { namespace, key, value: {} });
       }
-      const subKey = keyParts.slice(2).join(".");
-      namespaceMap.get(namespaceKey).value[subKey] = value;
+      let current = namespaceMap.get(namespaceKey).value;
+      for (const part of keyParts.slice(2, -1)) {
+        if (typeof current[part] !== "object" || current[part] === null) {
+          current[part] = {};
+        }
+        current = current[part];
+      }
+      current[keyParts[keyParts.length - 1]] = value;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/utils/textPropertyUtils.js` around lines 31 - 36, The code currently
collapses deep dotted keys into a single string key by using subKey =
keyParts.slice(2).join("."), causing e.g. ns.key.a.b to become { "a.b": value }
instead of a nested object; update the logic where namespaceMap is populated
(look for namespaceMap, namespaceKey, keyParts, subKey) to build real nested
objects for keyParts.slice(2) by iterating the remaining segments and creating
nested plain objects until the final segment where you assign value (instead of
joining with "."); ensure namespaceMap.get(namespaceKey).value is mutated to
contain the proper nested structure rather than a single dotted-key entry.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

4 nesting levels doesn't happen in practice

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@Benjvandam, understood! Thanks for the context. I'll note that for future reviews.


✏️ Learnings added
Learnt from: Benjvandam
Repo: silverfin/silverfin-cli PR: 249
File: lib/utils/textPropertyUtils.js:31-36
Timestamp: 2026-03-23T15:03:32.071Z
Learning: In `lib/utils/textPropertyUtils.js` (silverfin-cli), custom property keys in the Liquid Test YAML files follow at most a 3-segment dot-notation format (`namespace.key.subkey`). Keys with 4 or more segments (e.g., `namespace.key.a.b`) do not occur in practice, so collapsing deeper segments with `keyParts.slice(2).join(".")` is intentional and sufficient.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

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.

2 participants