Skip to content

RC Release of xlsx support#1551

Merged
TheLastCicada merged 84 commits into
developfrom
v2-rc2
Mar 26, 2026
Merged

RC Release of xlsx support#1551
TheLastCicada merged 84 commits into
developfrom
v2-rc2

Conversation

@TheLastCicada

@TheLastCicada TheLastCicada commented Mar 25, 2026

Copy link
Copy Markdown
Contributor

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 smoke job.

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. Governance isCreated responses now also surface background creation status/error/started-at when not yet created.

Upgrades XLS export for V2 project and unit to a new createV2Xls path, including child associations when exporting and aligning response naming; v2/filestore/get_file now reads fileId from 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.

TheLastCicada and others added 30 commits February 18, 2026 18:42
feat(CI): add v2 remote sync verification test job
…ests

feat(CI): add live API tests for governance body creation
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.
…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
TheLastCicada and others added 18 commits March 24, 2026 16:08
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
Comment thread .github/workflows/tests.yaml
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.
Comment thread src/datalayer/writeService.js
…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.
Comment thread src/controllers/v2/governance-v2.controller.js Outdated
Comment thread src/middleware.js
}
if (READ_ONLY && isReadOnlyMethodBlocked(req.method)) {
return sendReadOnlyError(res);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

'Could not push changelist to datalayer after retrying 60 times',
);
failedCallback();
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

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.
@TheLastCicada

Copy link
Copy Markdown
Contributor Author

Bugbot fixes (round 2)

Fixed

  • CI log truncation (tests.yaml): The test-readonly-live-smoke job was using raw cat for CADT logs without truncation or artifact upload. Aligned with the loop-based head/tail pattern and upload-artifact@v6 step used by all other jobs.
  • Defensive throw after retry loop (writeService.js): createDataLayerStoreWithRetry could silently return undefined if the for-loop completed without an explicit return/throw. Added a safety-net throw after the loop.
  • Unhandled promise rejection (governance.controller.js, governance-v2.controller.js): The async .catch() on createGoveranceBody() awaited _setCreationStatus, which could throw and produce an unhandled rejection (crash on Node 24+). Converted to synchronous .catch() with nested .catch() on the status update.

Assessed as false positive / by design

  • Read-only enforcement before API key auth (middleware.js): Intentional — read-only mode blocks writes at the middleware level before any side effects. Revealing read-only status is not sensitive.
  • createDataLayerStoreWithRetry appears unused: Called in 7+ files (organizations, governance, filestore models). Bugbot failed to detect the usages.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 4 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Comment thread src/datalayer/wallet.js
Comment thread src/datalayer/writeService.js Outdated
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.
@TheLastCicada TheLastCicada merged commit 73c1e3c into develop Mar 26, 2026
27 checks passed
@TheLastCicada TheLastCicada deleted the v2-rc2 branch March 26, 2026 16:33
@TheLastCicada TheLastCicada restored the v2-rc2 branch March 26, 2026 17:32
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