-
Notifications
You must be signed in to change notification settings - Fork 1
Implement reverse test #249
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4c3a0e5
0c5811d
d48e328
6fe5858
b3b059e
40984d8
20fc0e0
0013ece
32eb811
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| node_modules | ||
| .env | ||
| .DS_Store | ||
| ./tmp | ||
| ./tmp | ||
| .cursor |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| const fs = require("fs"); | ||
| const path = require("path"); | ||
| const yaml = require("yaml"); | ||
| const { consola } = require("consola"); | ||
| const fsUtils = require("./fsUtils"); | ||
|
|
||
| /** | ||
| * Transform a YAML custom properties object into the Silverfin API format. | ||
| * Input: flat object with dot-notation keys (e.g. "namespace.key.subkey": value) | ||
| * Output: array of { namespace, key, value } objects | ||
| */ | ||
| function transformCustomToProperties(customData) { | ||
| const namespaceMap = new Map(); | ||
|
|
||
| for (const [fullKey, value] of Object.entries(customData)) { | ||
| const keyParts = fullKey.split("."); | ||
|
|
||
| if (keyParts.length < 2) { | ||
| consola.warn(`Skipping key "${fullKey}" — expected namespace.key format`); | ||
| continue; | ||
| } | ||
|
|
||
| const namespace = keyParts[0]; | ||
| const key = keyParts[1]; | ||
| const namespaceKey = `${namespace}.${key}`; | ||
|
|
||
| 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; | ||
|
Comment on lines
+31
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Build real nested objects for deep dotted keys. Lines 35-36 collapse the remaining path into a single 🐛 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
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 4 nesting levels doesn't happen in practice There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
✏️ Learnings added
|
||
| } | ||
|
Comment on lines
+27
to
+37
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential data inconsistency when mixing simple and nested keys for the same namespace.key. If the input contains both a simple key (e.g.,
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
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't happen in the YAML files in practice There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
✏️ Learnings added
🧠 Learnings used |
||
| } | ||
|
|
||
| return Array.from(namespaceMap.values()); | ||
| } | ||
|
|
||
| /** | ||
| * Find a test by name across YAML files in a template's tests/ folder. | ||
| * If handle is provided, only search that handle's folder. | ||
| * If not, scan all reconciliation_texts folders. | ||
| * Extracts custom data from all 4 levels: company, period, reconciliation, account. | ||
| * Returns { file, handle, company, periods } where periods contains per-period custom, | ||
| * reconciliation custom, and account custom data. | ||
| */ | ||
| function findTestData(testName, handle) { | ||
| const templateType = "reconciliationText"; | ||
| const baseDir = path.join(process.cwd(), fsUtils.FOLDERS[templateType]); | ||
|
|
||
| if (!fs.existsSync(baseDir)) { | ||
| consola.error(`Directory not found: ${fsUtils.FOLDERS[templateType]}`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const handleDirs = handle ? [handle] : fs.readdirSync(baseDir).filter((entry) => { | ||
| return fs.statSync(path.join(baseDir, entry)).isDirectory(); | ||
| }); | ||
|
|
||
| for (const dir of handleDirs) { | ||
| const testsDir = path.join(baseDir, dir, "tests"); | ||
| if (!fs.existsSync(testsDir)) continue; | ||
|
|
||
| const yamlFiles = fs.readdirSync(testsDir).filter((f) => f.endsWith(".yml")); | ||
|
|
||
| for (const file of yamlFiles) { | ||
| const filePath = path.join(testsDir, file); | ||
| const content = fs.readFileSync(filePath, "utf-8"); | ||
| const parsed = yaml.parse(content, { maxAliasCount: 10000, merge: true }); | ||
|
|
||
| if (!parsed || !parsed[testName]) continue; | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const testData = parsed[testName]; | ||
| const result = { file, handle: dir, company: null, periods: {} }; | ||
|
|
||
| // Company-level custom | ||
| if (testData?.data?.company?.custom) { | ||
| result.company = { custom: testData.data.company.custom }; | ||
| } | ||
|
|
||
| // Period-level data | ||
| const periods = testData?.data?.periods; | ||
| if (periods) { | ||
| for (const [periodKey, periodData] of Object.entries(periods)) { | ||
| if (!periodData) continue; | ||
|
|
||
| const periodEntry = { custom: null, reconciliations: {}, accounts: {} }; | ||
|
|
||
| // Period-level custom | ||
| if (periodData.custom) { | ||
| periodEntry.custom = periodData.custom; | ||
| } | ||
|
|
||
| // Reconciliation-level custom | ||
| if (periodData.reconciliations) { | ||
| for (const [reconHandle, reconData] of Object.entries(periodData.reconciliations)) { | ||
| if (reconData?.custom) { | ||
| periodEntry.reconciliations[reconHandle] = reconData.custom; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Account-level custom | ||
| if (periodData.accounts) { | ||
| for (const [accountNumber, accountData] of Object.entries(periodData.accounts)) { | ||
| if (accountData?.custom) { | ||
| periodEntry.accounts[accountNumber] = accountData.custom; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| result.periods[periodKey] = periodEntry; | ||
| } | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
| } | ||
|
|
||
| consola.error(`Test "${testName}" not found in any YAML file`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| module.exports = { transformCustomToProperties, findTestData }; | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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_dateand 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 buildingupdates.Also applies to: 561-613
🤖 Prompt for AI Agents
There was a problem hiding this comment.
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.