Skip to content

Release v0.7.0: data you can trust - #126

Merged
Exotic209093 merged 28 commits into
mainfrom
release/v0.7.0
Oct 4, 2026
Merged

Exotic209093 merged 28 commits into
mainfrom
release/v0.7.0

Conversation

@Exotic209093

@Exotic209093 Exotic209093 commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

Summary

Ships every fix from the 2026-08-31 audit in one store update, and restores green CI on main.

The eight open v0.7.0 issues

CI on main was red from #107 to #125. Fixing it found real bugs in earlier merged fixes, now corrected:

Also included:

  • Jest 30 and typescript-eslint 8, clearing all 37 audit advisories.
  • Stray BOMs and mis-encoded comments removed.
  • npm run validate:salesforce extended with the v0.7.0 checks.
  • Version bumped to 0.7.0, changelog added, and the new store name and summary from docs/launch-kit.md.
  • Store and privacy permission declarations now match the manifest: unlimitedStorage added, activeTab removed.

Test plan

  • npm run typecheck, npm run lint -- --max-warnings=0
  • npm run audit:prod and npm run audit:dev: 0 vulnerabilities
  • npm run test:coverage -- --runInBand: 62 suites, 613 tests. Every fix has a regression test, each confirmed to fail without its fix.
  • npm run package: wavelink-0.7.0.zip passes the Manifest V3 smoke check.
  • npm run validate:salesforce on the nebula-dev dev org, read-only and write mode: 13/13 checks pass, and cleanup passed. Covers Bulk per-row identity, ragged columns, verbatim values, #N/A clearing, and Bulk vs REST value types.
  • Packaged-extension browser checks on 0.7.0, run by the maintainer.

🤖 Generated with Claude Code

Exotic209093 and others added 27 commits October 4, 2026 21:25
Reference-lookup mappings in externalId/relatedField mode produce keys
such as `Account: { External_Key__c: ... }`. The validator only knew the
reference field name (`AccountId`), so every row failed with
`Unknown field "Account"` and the guided import could never be confirmed.

The validator now resolves relationship keys via the reference field's
relationshipName (falling back to `__c` -> `__r` / `FooId` -> `Foo`, as the
import UI does), applies the reference field's createable/updateable
rules, requires exactly one non-empty scalar match value, and treats a
required reference field as supplied when its relationship key is
present. Callers can optionally pass describe fields of the referenced
object to require an External ID or idLookup match field.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copy to target sent Added and Changed records as a single insert when
the match field was not an External ID, duplicating every Changed
record. The comparison now also selects Id (never compared), inserts
only Added records, and updates Changed records by the target record's
Id with just the differing fields. External ID match fields keep the
single upsert. The typed COPY confirmation now shows the insert/update
(or upsert) counts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…#48, #51)

- AppRoot: route every dataset swap through replaceDataset, which also clears
  rows cleaned from the previous file, so a file uploaded on Import (or a
  retry/rollback/snapshot dataset) is pushed as itself rather than as the
  last cleansed file. The Cleanser -> Import hand-off is unchanged (#44).
- AppRoot: pass the org context to the Advanced -> Data Push screen so the
  production typed confirmation and warnings apply there too (#48).
- DataPushScreen: invalidate mapped records, validation, dry run and the
  reachable stages whenever the dataset or its cleaned rows change, and let
  Retry / Prepare Delete Push flag their restored state so the automap effect
  no longer overwrites restored manual mappings (#51).
- Regression tests drive the real AppRoot and DataPushScreen flows.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
StorageService.setLocal now checks getBytesInUse/getManifest before every
write and the background module registers chrome.commands/chrome.alarms
listeners at import time. The shared chrome mock lacked these, so every
local-storage write and every background-module import threw in tests
(offscreen-bulk-push, push-checkpoints, migrationStorage suites failed on main).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Row identity is now carried end to end instead of being reconstructed by
positional zips:

- #47 Bulk: after a job completes, both successfulResults and failedResults
  are fetched and mapped back to input indices by fingerprinting the cells we
  uploaded (Bulk 2.0 rejects synthetic key columns and does not promise result
  order). Completion payloads, push results, and history now carry per-row
  errors plus ids/idRecordIndexes, so Retry Failed Rows and the error file work
  for Bulk pushes. Unidentifiable rows report index -1 rather than a guess.
- #47 REST: each returned ID is recorded with its input index, so parallel
  batches and HTTP 204 upsert updates (no ID) no longer shift IDs onto the
  wrong rows. buildPushOutcomeDatasets attaches IDs only via idRecordIndexes.
- #46: DataMapper returns sourceIndexes (mapped -> source position); retry and
  outcome builders translate push indices through it, and rows dropped at
  mapping are no longer reported as successes.
- #45: the ingest CSV header is the union of keys across all records, explicit
  null is sent as #N/A so blank-means-clear works on Bulk, and values with \r
  are quoted. Bulk result getters now reject non-OK responses.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ring

main did not typecheck: OFFSCREEN_TOKEN_REFRESH was a MessageType without a
PayloadMap entry, and MigrationWorkspaceScreen read failedRecords/ids off the
string[] its awaitPushResult returned. awaitPushResult now returns the stored
result and target IDs are paired with source rows through idRecordIndexes
(#47) instead of a positional zip.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PR #107 (issue #93) intentionally changed buildSoql to emit `= null` /
`!= null`, the documented SOQL form, but did not update the two builder
tests that still asserted the old `= NULL` output. The expectation is
obsolete; the assertions are kept and pointed at the new output.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- background: drop SalesforceApiClient import left unused by #116 and
  make y/mo const in computeNextRunAt (#112).
- storage: drop SCHEMA_CACHE_TTL import added unused by #114.
- ExportModal: remove the redundant `\[` escape in the sheet-name regex
  (#119); the character class is unchanged.
- FieldRecommendations: remove redundant role="list" on <ul> (#123).
- SoqlPreview: remove role="textbox"/aria-readonly from the read-only
  <pre> (#123). It announced a text field that cannot be focused or
  edited; the aria-label is kept.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ocks

The handler suites added in #125 never passed:

- background-storage-handlers / background-auth-handlers invoked only the
  last registered chrome.runtime.onMessage listener, assuming it was the
  MessageBus. The background also registers a raw scheduler-control
  listener after the bus, so every MessageBus message came back
  NOT_HANDLED. Add tests/mocks/runtimeMessages.ts, which offers the
  message to each listener like Chrome does, and use it in both suites.
- AUTH_LOGOUT expected getOrg() to return undefined for a removed org;
  StorageService.getOrg is typed and implemented to return null. Assert
  null, and also assert the active org is cleared.
- SalesforceAuth.login test mocked chrome.tabs.query callback-style, but
  auth.ts uses the MV3 promise form, so the mock threw "callback is not a
  function". Mock a resolved promise instead.

No assertions were weakened; handler behaviour is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PR #118 (issue #65) intentionally made rawCall reject absolute URLs whose
origin differs from the org instanceUrl, so the Bearer token cannot be
sent to another host. The old "uses an absolute URL verbatim" test still
targeted https://other.example.com and so asserted the token-leaking
behaviour the fix removed. That expectation is obsolete; replace it with
a same-origin verbatim test plus a test that the cross-origin call is
rejected with URL_ORIGIN_MISMATCH and no fetch is made.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
#50)

DataPushScreen now describes the objects referenced by relationship
lookups (cached per tab) and passes them to validation and the dry run,
so a lookup matching on a non-External ID field is caught before push.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Two fix PRs made the Bulk API path corrupt field values:

- PR #118 (issue #66) applied the spreadsheet formula-injection guard to
  the Bulk *ingest* CSV. Any value starting with - + = @ was uploaded with
  a leading apostrophe: -5 failed on number fields and '+44 ... phones or
  '@Handles were stored with the apostrophe. The REST path never did
  this, so the same import behaved differently above the Bulk threshold.
  #66 is about files opened in Excel; exports keep their guard.
- PR #119 (issue #80) enabled Papa dynamicTyping on Bulk query results,
  which coerces Text fields: "02134" -> 2134, 18-digit numbers lose
  precision, "true" -> true. This broke the existing bulk-query test
  (Id "001" -> 1). Reverted; correct typing needs field describe data.

Adds tests/unit/bulk-csv-fidelity.test.ts for both directions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Follow-up to 9f6f7b3, which fixed the PR #110 merge bug (the poller
returned string[] while the consumer read result.ids/failedRecords, so
every migrated object threw, no ID map was saved and child lookups were
never remapped - Copy between orgs, #43) but kept two defects and had no
test:

- awaitPushResult still waited for ids.length > 0. A push where every
  record failed stores a result with ids: [], so the run polled for the
  full 5 minutes, then marked the object "done" with nothing reported.
  It now returns the first stored result (results are only written on
  completion), and a real timeout fails the object.
- Per-record push failures are surfaced in the object's errors.

The poller and the idRecordIndexes pairing move to ui/utils/migrationPush
with unit tests. Also restores the em dashes PR #110 turned into mojibake
and drops the BOM it added.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PR #114 (#56/#63/#94/#99) added a pre-write quota check to setLocal with
two defects:

- Infinite recursion: when usage is above 85% of quota and an old
  terminal push exists, setLocal -> evictForQuota -> pruneTerminalPushes
  -> setLocal -> evictForQuota ... re-reads the not-yet-pruned map each
  time and never returns, hanging every local write (push checkpoints,
  history, settings). Eviction now prunes via a shared helper and writes
  directly to chrome.storage.local, never back through setLocal.
- Any failure of the usage check or eviction (e.g. getBytesInUse
  rejecting) turned the caller's write into a StorageError. The check is
  now best-effort; the write and its quota-error retry still run.

The chrome mock returned stored object references from get(), so
callers could mutate "storage" without set(). That masked the recursion
above. get() now returns a copy, as chrome.storage does. The new
tests/unit/storage-quota.test.ts hangs on the old code and passes now.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Lockfile-only refresh from `npm audit fix` (brace-expansion, browserslist,
fast-uri, serialize-javascript and friends). package-lock.json is
unchanged since the last green CI run; these advisories were published
since then. Production audit stays clean.

`npm run audit:dev` still fails: braces <=3.0.3 (GHSA-vfj7-8cjw-p6xm,
pulled in by jest 29 via micromatch) has no patched release. Clearing it
needs the jest 30 major upgrade, which is not part of this change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…udit advisories

Clears 37 high-severity dev advisories (braces <=3.0.3 via jest 29's
micromatch, GHSA-vfj7-8cjw-p6xm) that kept the CI audit:dev step red.
typescript-eslint 8 flagged one unused catch binding.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The clone wizard still zipped returned IDs onto source rows positionally,
skipping only rows listed in failedRecords. Bulk and multi-threaded REST
results arrive out of order, and Bulk failures with an unidentifiable row
(-1) are absent from failedRecords, so child records were re-parented to
the wrong targets.

pairClonedIds now maps each ID through idRecordIndexes. IDs whose index is
unknown, out of range, shared by another ID, or marked failed are left
unmapped and reported as a warning in the execution log instead of being
guessed. The screen also reuses migrationPush.awaitPushResult, so a push in
which every record failed no longer polls until the five-minute timeout.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Rows excluded by mapRecords were never pushed and appeared in neither the
success nor the error download. buildPushOutcomeDatasets now takes the
source-indexed mapping errors and writes every unpushed source row to the
error file ("Not pushed (mapping error): ...") with its WaveLinkSourceRow,
in source order alongside push failures. The error-file button also shows
when only mapping drops occurred.

Retry Failed Rows still re-sends only push failures: mapping-dropped rows
would fail mapping again with the same mappings, so the retry toast now
says how many were left out and points to the error file.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The CSV exporter stringified every value before applying the #66
formula-injection guard, so genuine negative numbers exported as the
text '-5. Guard string cells only, matching the Excel exporter; numbers
and booleans pass through unchanged while string cells starting with
= + - @ tab or CR are still neutralised. The Bulk upload CSV remains
unguarded (183a36f).

Refs #66

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Bulk API 2.0 query results are CSV, so every value reached exports as a
string while REST returned typed JSON (#80). PR #119 guessed types from
the CSV text and corrupted Text fields; 183a36f reverted it.

Type each page in the background from the queried object's describe
(via the schema cache): boolean -> boolean; int/double/currency/percent
-> number only when exactly representable (otherwise kept as string);
empty cell -> null like REST; dates, datetimes, Ids and text stay
strings. Relationship columns such as Account.Owner.Name resolve via
each lookup's referenceTo; polymorphic or unresolvable columns stay
strings. The queried object comes from the job info `object` field,
memoised per job. Describe or status failures degrade to strings.

Also extracts describeSObjectCached, reused by SF_DESCRIBE_SOBJECT.

Refs #80

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Bump to 0.7.0, add the changelog, apply the new store name and summary,
align store and privacy permission declarations with the manifest
(unlimitedStorage added, activeTab removed), record the nebula-dev
validation run, and extend validate:salesforce with v0.7.0 checks.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-04T21:05:51.987736Z 35c1bb1 PR opened
🔒 Security Review ✅ Completed 2026-10-04T21:06:03.867191Z 35c1bb1 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 35c1bb1677

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +97 to +98
const queue = unclaimed.get(fingerprint(columns.map(c => (c === undefined ? '' : row[c] ?? ''))));
return queue && queue.length > 0 ? queue.shift()! : -1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Treat duplicate Bulk fingerprints as unidentified

When multiple input rows serialize to identical cells, they share this queue, but Salesforce does not preserve result order and this function claims every success before processing failures. The returned IDs can therefore be assigned to the wrong source indices; clone and migration workflows then use those indices to build ID maps and may re-parent child records to the wrong cloned parent. Ambiguous fingerprints should produce index -1 rather than being claimed in input order.

Useful? React with 👍 / 👎.

Comment thread src/ui/utils/pushRetry.ts
Comment on lines +67 to +69
const sourceIndex = toSourceRecordIndex(error.recordIndex, sourceIndexes);
if (!inSource(sourceIndex)) continue;
messages.set(sourceIndex, [...(messages.get(sourceIndex) ?? []), error.message]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep unidentified failures out of success downloads

When a Bulk failure has recordIndex: -1—an explicitly supported outcome when a result row cannot be fingerprinted—this branch silently discards it. The later loop consequently places every pushed row without another mapped error into the success dataset, so a failed row can appear in the success download while the error download is empty; unresolved failures must remain visible without classifying all unmatched rows as successful.

Useful? React with 👍 / 👎.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Exotic209093
Exotic209093 merged commit 4cadcb8 into main Oct 4, 2026
1 check passed
@Exotic209093
Exotic209093 deleted the release/v0.7.0 branch October 4, 2026 21:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment