RC Release of xlsx support#1551
Conversation
feat(CI): add v2 remote sync verification test job
…ests feat(CI): add live API tests for governance body creation
update v2-rc2 from develop
update v2-rc2 from develop
The V2 deleteAllOrganizationData function used a hardcoded meta key 'userDeletedOrgUid' instead of the 'userDeletedOrgs' constant used by MetaV2.getUserDeletedOrgUids(). This mismatch meant that after deleting an org, sync-default-organizations-v2 would not find it in the deleted list and would re-import it with is_home: false. Replace the inline meta logic with MetaV2.addUserDeletedOrgUid() to match V1's pattern. The call runs after transaction commit but before mutex release so sync-default-organizations-v2 cannot race in and re-import the org before the meta write completes. Also fix importOrganization's cleanup call to use removeUserDeletedOrgUid() instead of a raw destroy with the wrong key. Additionally, add is_home/isHome to the omit list in syncOrganizationMeta for both V1 and V2 to prevent datalayer store data from overwriting the home org flag.
update v2-rc2 from develop
…key-mismatch fix: use correct meta key for user-deleted org tracking in V2
…tation V2 XLSX import was non-functional — the V1 utility functions (tableDataFromXlsx, collapseTablesData) could not match V2 model names or foreign keys. V2 export omitted child records because associations were missing. This commit: - Add missing hasMany associations to ProjectV2 (validations, verifications, projectMethodologies, stakeholderProjects) - Create src/utils/v2-xls.js with metadata-driven helpers that derive schema from Sequelize associations at runtime - Rewrite ProjectV2/UnitV2 updateFromXLS to use new V2 utilities - Fix export controllers to include all child associations when xls=true - Add integration tests that verify actual staging (INSERT/UPDATE), singular/plural sheet name support, and Excel export
- Deduplicate Sequelize includes in project controller when xls=true and columns also request child associations (avoids duplicate alias error) - Replace naive replace(/s$/, '') singularization with naiveSingular() that handles -ies → -y (e.g. projectMethodologies → projectMethodology) - Restore unitSerialId derivation from block range via prepareXlsRow hook on UnitV2, called generically from stageV2XlsRecords - Fix org creation test to skip PENDING record when polling for the finalized V1 organization - Rewrite audit tests to use baseline-delta counts and future timestamps so background sync activity cannot cause false failures
- Restore child record JSON parsing (locations, estimations, ratings, coBenefits) and cadTrustProjectId backfilling in updateProjectPropertiesV2, which is used by CSV batch upload - Guard parseV2Xlsx against ragged rows where trailing cells are missing, preventing undefined values from overwriting existing fields during update staging
Blank trailing rows in XLSX files were parsed as objects with all-null values, causing stageV2XlsRecords to generate random UUIDs and stage INSERT records with empty payloads. Add an isEmptyRow guard at the top of both parent and child staging loops.
feat(V2): rewrite XLSX import/export with metadata-driven V2 implementation
Add wallet-level transaction health classification (rejected, in_mempool, pending) with safe auto-clearing of rejected transactions when all unconfirmed txs are rejected. Includes retry-aware store creation, governance creation status tracking, read-only-aware /health/wallet endpoints with truncated tx IDs, and middleware bypass for health routes. Hardens all major write paths (writeService, organizations, filestore, governance) against stuck/rejected transactions. - wallet.js: getTransactionHealth, clearRejectedTransactions, getDLWalletId - writeService.js: createDataLayerStoreWithRetry with tx_id correlation - persistance.js: verbose=true for create_data_store tx correlation - governance controllers: creation status tracking via Meta/MetaV2 - organizations model: _checkAndClearRejectedTxs in push paths - filestore models: migrate to createDataLayerStoreWithRetry - middleware: HEALTH_ENDPOINTS set for robust bypass matching - wallet-health.js: read-only guard suppresses details on observer nodes - 28 unit tests covering health classification, safety guards, and read-only/non-read-only endpoint response contracts
…tion
- writeService: createDataLayerStore now returns { storeId, txIds } so
createDataLayerStoreWithRetry can collect tx IDs across attempts. On
confirmation failure, txIds are attached to the error object for
capture in the catch path.
- wallet-health: replace local wallet_formatDuration duplicate with
import from wallet.js to eliminate maintenance drift risk.
- governance-v2: set creationStatus to 'completed' after
addV2ToExistingGovernanceBody returns, preventing status stuck at
'creating_stores' when V1 governance pre-exists
- writeService: wrap exported createDataLayerStore to return plain
storeId string for backward compat (internal function returns
{storeId, txIds} for retry loop correlation)
- writeService: increment retryAttempts on recursive call after
clearing rejected txs in pushChangesWhenStoreIsAvailable to prevent
unbounded recursion when wallet state is slow to converge
- wallet: remove unused totalUnconfirmed variable in
clearRejectedTransactions
- writeService: add maxRetries >= 1 validation at top of createDataLayerStoreWithRetry to prevent silent undefined return - governance v1: store _creationStartedAt on Governance class instead of Meta to match v2 pattern and avoid fragile cross-class state
Integrate the new /health/wallet endpoint into all polling wait functions to provide real-time wallet status in CI logs. Adds a shared recovery-stuck detector that fast-fails tests only when rejected transactions persist for 2+ minutes (indicating server recovery mechanism failure), not on transient rejections. Also enhances waitForGovernanceCreated with creationStatus fast-fail.
- Health endpoints: use config.READ_ONLY === true instead of || false for the read-only guard, ensuring only an explicit true value activates the security restriction (both V1 and V2) - wallet.js: recompute elapsed after the 15s sleep in waitForAllTransactionsToConfirm so diagnostic logging triggers at the correct 5-minute mark instead of ~15s late
Remove the !hasSuccess guard that is already guaranteed by the early return above, and inline the allFailed check into a single conditional.
- waitForSpendableCoins: throw on timeout instead of returning
{ success: false }, which was silently ignored by .then() chains
in filestore models causing cascading store-creation failures
- governance controllers: replace manual Meta upserts in .catch()
with _setCreationStatus, which also sets governanceCreationStartedAt
and cleans up error metadata on non-error status
Move formatDuration access from a static module-level import to the wallet parameter already passed to getWalletHealthResponse, eliminating the redundant dependency. Update mock wallet in tests to include formatDuration.
- Add _checkAndClearRejectedTxs to OrganizationsV2 matching V1 pattern, called in _pushDataInParallel catch blocks for ORG_UID and DATA_MODEL_VERSION stores - Mark creation state as FAILED in top-level catch of createV2Organization and createOrganization so the 409 guard doesn't permanently block new creation attempts within the same session
…nitoring feat: add transaction health monitoring and recovery
Add raw API response summaries, entity-level JSON dumps on failure, and failure field tracking to all verification steps in both V1 and V2 remote sync test jobs. When a field assertion fails, the full entity JSON is dumped for debugging and a summary of all failed field names is shown at the end.
…ync tests U1 and U2 had their unitItmosReferenceId expected values swapped.
…ation-tests ci(TESTS): expand live API verification assertions in workflow
…e push Governance update endpoints (POST /meta/orgList) silently failed when the DataLayer store was not owned by the wallet. The API returned 200 and updated the local DB, but the background push retried 60 times then gave up without rolling back, leaving stale confirmed:false rows. - Add assertStoreIsOwned check before any DB writes in both V1 and V2 updateGoveranceBodyData, causing immediate HTTP 400 on failure - Wire rollbackChangesIfFailed as failedCallback to pushDataLayerChangeList so push exhaustion restores prior governance state - Detect "not owned by DL Wallet" in persistance.js and throw with permanent flag to skip futile retries - Handle permanent errors in writeService.js by invoking failedCallback immediately instead of scheduling 60 retries - Add integration tests for ownership rejection, callback wiring, and permanent error detection
pushChangesWhenStoreIsAvailable used a fire-and-forget pattern: when the store wasn't ready, it scheduled a detached setTimeout retry via retryPushToStore and immediately threw. This broke the promise chain for callers that awaited the function, most critically in governance body creation where the thrown error prevented the onConfirm callback from ever being registered — leaving CADT unaware of successfully created on-chain stores. - Remove retryPushToStore (fire-and-forget scheduler) - Convert pushChangesWhenStoreIsAvailable to a for-loop with await delay(30000) so retries stay in the same promise chain - Callers like syncDataLayer now properly block until success or exhaustion, fixing governance creation, V2 addV2ToExisting, and org upsert flows - On retry exhaustion, await failedCallback before throwing - Keep transaction health diagnostics every 5 retries with immediate re-check after clearing rejected txs
The V2 list endpoints require page and limit query parameters. The pre-flight check and fetchFirstId helper were sending bare GET requests without pagination, causing deterministic 400 validation errors.
Run read-only live smoke in the shared tests pipeline so it follows the same trigger and execution pattern as other live-api jobs, while keeping its lightweight setup by skipping faucet funding and governance/mirror bootstrap dependencies.
Consolidate 4 separate commit cycles into 2 by batching the project and unit XLSX imports into a single commit, and similarly batching the round-trip re-imports. Halves the number of expensive blockchain commit-and-sync waits in live API tests.
docs: add V2 pagination requirements to cursor rule
Delete the unused V1 governance subscribe validation schema left behind after removing the deprecated subscribe route so the validation surface matches active endpoints.
The --grep 'Step 1[1-6]:' pattern skipped Step 11a, 12a, and 15a describe blocks which verify imported data and round-trip fidelity. Add optional 'a' suffix to the character class.
Map READ_ONLY assertion errors to the shared 403 payload in V1 and V2 GET offer generation handlers so read-only mode is enforced consistently for GET endpoints that can trigger write-side behavior.
Broaden the live read-only smoke suite with representative write and write-like GET endpoint canaries across v1 and v2.
stageV2XlsRecords stored parsed XLSX data using camelCase attribute names (e.g. cadTrustProjectId) but the V2 commit pipeline expects snake_case DB field names (e.g. cad_trust_project_id). This caused "primary key value is null or undefined" errors when committing XLSX imports because transformFullXslsToChangeList could not find the PK column in the header row. Add toDbFieldNames() helper that uses Sequelize rawAttributes to convert camelCase keys to their snake_case field counterparts before staging, matching the format used by normal V2 API paths.
Align the successful unsubscribe response text with the 200 status to avoid client confusion when parsing operation results.
The FK validation in v2-xls.spec.js still used camelCase cadTrustProjectId after the staging data was converted to snake_case, causing the if-condition to always be false and the expect assertion to never execute.
The faucet curl was fire-and-forget with no response capture. When the faucet returned an empty response (0 bytes), the step silently proceeded to balance polling which timed out after 5 minutes. Now each faucet request captures the HTTP status and body, retries up to 5 times with 15-second intervals, and logs diagnostics for each attempt.
Enforce READ_ONLY write blocking and add smoke coverage
The faucet returns HTTP 202 with an empty body on success, but the retry validation required a non-empty body, causing 4 spurious retries that all fail with "pending transaction exists". Accept any 2xx status. CADT logs were silently truncated by GitHub Actions output limits — a 15-minute run with verbose debug logging exceeded the step buffer, losing the critical post-commit window. Replace full cat with smart head+tail output and upload the complete logs as artifacts (7-day retention) for untruncated access during investigations. Bump upload-artifact from v4 to v7.
v7 tag does not exist yet; v6 is the latest available and matches the version already used in build.yaml.
The normal V2 API controllers (POST /v2/project, POST /v2/unit) set org_uid from the home organization on every staged record. The XLSX import path (stageV2XlsRecords) did not, so the datalayer changelist lacked org_uid. When sync-registries-v2 read the data back and tried to upsert, it hit SequelizeUniqueConstraintError because both ProjectV2.orgUid and UnitV2.orgUid have allowNull: false. This caused the organization to get permanently stuck at isSynced=false, blocking all downstream XLSX tests (verify imports, export, round-trip).
…xtures test: add sample XLSX files for v2 import/export testing
The test-readonly-live-smoke job used raw cat for CADT logs and had no artifact upload step, unlike every other live test job which was updated to use loop-based head/tail truncation and upload-artifact.
…Retry The for-loop always exits via return or throw in current logic, but if the catch block is later modified the function could silently return undefined. Add an explicit throw after the loop as a safety net.
| } | ||
| if (READ_ONLY && isReadOnlyMethodBlocked(req.method)) { | ||
| return sendReadOnlyError(res); | ||
| } |
There was a problem hiding this comment.
Read-only enforcement bypasses API key authentication check
Low Severity
The new READ_ONLY middleware enforcement at line 169 runs before the API key authentication middleware (line ~200). When an API key is configured, unauthenticated POST/PUT/DELETE requests to a read-only instance receive a 403 "read-only mode" response instead of a 403 "API key not found", revealing the instance's read-only configuration to unauthenticated callers. Previously this state was not disclosed without valid credentials.
| 'Could not push changelist to datalayer after retrying 60 times', | ||
| ); | ||
| failedCallback(); | ||
| return; |
There was a problem hiding this comment.
Exported createDataLayerStoreWithRetry appears unused in codebase
Low Severity
createDataLayerStoreWithRetry is a new ~70-line function exported from writeService.js but doesn't appear to be called anywhere in the codebase. The default export at line 393 includes it, yet no other file imports or invokes it. This adds significant dead code with complex retry/recovery logic that will need to be maintained.
The async .catch() handlers on createGoveranceBody() awaited _setCreationStatus, which could throw and produce an unhandled promise rejection (process crash on Node 24+). Convert to synchronous .catch() with nested .catch() on the status update.
Bugbot fixes (round 2)Fixed
Assessed as false positive / by design
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
There are 4 total unresolved issues (including 2 from previous reviews).
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
waitForSpendableCoins now throws on timeout, making the if (!coinCheck.success) branches unreachable dead code. Remove them from both V1 and V2 organization models. Non-permanent errors thrown by pushChangeListToDataLayer were re-thrown immediately, bypassing the 60-attempt retry loop. Log the error and let the loop continue instead, matching the behavior when the function returns false.


Note
Medium Risk
Touches core request gating and datalayer write/retry logic, so failures could block writes or cause confusing operational behavior; changes are mitigated by being mostly additive and focused on error handling/observability.
Overview
Improves reliability and observability of live CI runs by adding faucet retries, truncating/attaching CADT logs as GitHub artifacts, updating the governance store ID used in tests, and introducing a new
READ_ONLY live smokejob.Tightens and standardizes read-only behavior: middleware now blocks non-GET methods early with a canonical 403 response, and multiple V1/V2 controllers (
filestore,offer,organization,governance) now explicitly assert read-only and return the standard read-only error. GovernanceisCreatedresponses now also surface background creation status/error/started-at when not yet created.Upgrades XLS export for V2
projectandunitto a newcreateV2Xlspath, including child associations when exporting and aligning response naming;v2/filestore/get_filenow readsfileIdfrom query params.Hardens datalayer store creation and pushing: store creation returns tx IDs (via verbose RPC), adds rejection-aware confirmation polling, introduces
createDataLayerStoreWithRetry, detects permanent “store not owned” push failures, improves wallet transaction diagnostics/health checks, and refactors push retries into an awaited loop with optional auto-clear of fully rejected transactions.Written by Cursor Bugbot for commit e618417. This will update automatically on new commits. Configure here.