DEV Community

Arnold Holm
Arnold Holm

Posted on

Wrong Target, Clean Success: A Node Test for Misrouted Saves

A dated defect worth learning from

On 23 August 2026, issue 475 in tradingview-mcp reported that pine_new and pine_open changed editor content without changing the saved script bound to it. A later save could therefore write to the wrong script. The report cites TradingView Desktop 3.3.0.7992 and Node 24.15.0. Its pine_new observation stopped before a successful save, and the destructive pine_open sequence was inferred from source rather than run to completion. This is dated motivation for a synthetic test, not a claim about today's software.

The pattern

The pattern is simple. A caller intends one record. The store has a different record active. A save follows the active binding instead of the intent. The response reports success. The intended record never changes. The active record loses its original text. Nobody gets a signal, because every message on the happy path looks correct until someone opens the wrong file.

Two identities sit behind every save like this. The intended identity is the record the caller means to write. The active identity is the record the store currently has open. When the two differ, the write should stop before it starts. That is the whole idea, and the rest of this article is one small way to test it.

Why the success message makes it worse

The report raises another useful point: opening a script sounds like inspection. A caller may not expect it to alter an existing buffer. Yet a successful response can hide that distinction. The report also suggested a cheap remedy in the same spirit: have the save return the name and id of the script it actually wrote. A caller that sees the wrong name come back notices immediately. The check in this article moves that idea to the caller side, before the write instead of after it.

The toy model

The demonstration is synthetic and narrow on purpose. It models two saved scripts, A and B, as full records in a Map. Each record carries an id, a name, and source text. A naive save writes new source into whatever record is active, which is how the defect lives. A guarded save takes an intended id and an active id, validates both, and writes only when they match.

I keep the checks strict in a specific way. Each case snapshots the full records with structuredClone before the operation, then compares the complete Map with assert.deepEqual after each rejected write (structuredClone is available in Node 17 and later). Checking one field would leave the rest of a record unverified, and a wrong claim could pass. After every rejected operation the test compares the complete Map against its snapshot, one rejection at a time. This also catches an unexpected added or deleted entry. The correct-target case checks the whole untouched B record the same way, so the untouched side is verified as a record, not as one field.

synthetic A intended B active mismatch rejects and leaves both unchanged

Standalone test program

The program below is one file and depends only on node:assert/strict. Save it as check.mjs and run it with Node.

import assert from 'node:assert/strict';

function makeStore() {
  return new Map([
    ['A', { id: 'A', name: 'Script A', source: 'source A v1' }],
    ['B', { id: 'B', name: 'Script B', source: 'source B v1' }],
  ]);
}

// Full-record snapshot, so comparisons cover every field.
function snapshot(store) {
  const out = new Map();
  for (const [key, record] of store) out.set(key, structuredClone(record));
  return out;
}

function naiveSave(store, activeId, newSource) {
  const record = store.get(activeId);
  if (!record) throw new Error('missing id');
  record.source = newSource;
  return activeId;
}

function guardedSave(store, intendedId, activeId, newSource) {
  if (typeof intendedId !== 'string' || intendedId.length === 0) {
    throw new Error('invalid intended id');
  }
  if (typeof activeId !== 'string' || activeId.length === 0) {
    throw new Error('invalid active id');
  }
  if (intendedId !== activeId) throw new Error('wrong target');
  const record = store.get(intendedId);
  if (!record) throw new Error('missing id');
  record.source = newSource;
  return intendedId;
}

// Case 1: naive misroute, the defect demonstrated.
{
  const store = makeStore();
  const before = snapshot(store);
  assert.notEqual('A', 'B');
  const out = naiveSave(store, 'B', 'source A v2');
  assert.equal(out, 'B');
  assert.deepEqual(store.get('A'), before.get('A'));
  assert.deepEqual(store.get('B'), { id: 'B', name: 'Script B', source: 'source A v2' });
  console.log('case 1 naive misroute: B holds A v2, A record unchanged');
}

// Case 2: mismatch rejected, both full records unchanged.
{
  const store = makeStore();
  const before = snapshot(store);
  assert.throws(() => guardedSave(store, 'A', 'B', 'source A v2'), /wrong target/);
  assert.deepEqual(store, before);
  console.log('case 2 guarded mismatch: rejected, both records unchanged');
}

// Case 3: correct target, A updated, whole B record untouched.
{
  const store = makeStore();
  const before = snapshot(store);
  const out = guardedSave(store, 'A', 'A', 'source A v2');
  assert.equal(out, 'A');
  assert.deepEqual(store.get('A'), { id: 'A', name: 'Script A', source: 'source A v2' });
  assert.deepEqual(store.get('B'), before.get('B'));
  console.log('case 3 correct target: A updated, B record unchanged');
}

// Case 4: invalid and missing identities, checked after every rejection.
{
  const store = makeStore();
  const before = snapshot(store);
  assert.throws(() => guardedSave(store, '', 'A', 'x'), /invalid intended id/);
  assert.deepEqual(store, before);
  assert.throws(() => guardedSave(store, 'A', '', 'x'), /invalid active id/);
  assert.deepEqual(store, before);
  assert.throws(() => guardedSave(store, 'Z', 'Z', 'x'), /missing id/);
  assert.deepEqual(store, before);
  console.log('case 4 invalid ids: three rejections, no writes');
}
Enter fullscreen mode Exit fullscreen mode

What each case checks

Case 1 keeps the naive misroute on display. The intended record is A, the active record is B, and the naive save writes A's new text into B. The test asserts B now holds A's text while A keeps its original full record, which is the defect stated as a fact.

Case 2 sends the same mismatch through the guarded save. The guard throws wrong target, and the test compares both full records against their snapshots. Nothing was written.

Case 3 matches intended and active on A. The guard writes, A's full record shows the new source, and the whole B record stays equal to its snapshot.

Case 4 feeds three bad identities: an empty intended id, an empty active id, and a missing id Z. Every rejection is followed by a full-record comparison, so a partial write cannot hide between attempts.

Each case group prints one line after its assertions pass. An assertion failure stops the program before that line. The run record below contains the observed output.

Test table

The table states the contract the asserts enforce. Expected mutation means what the store should look like after the operation.

Scenario Input Expected mutation
Naive misroute intended A, active B, naive save with A v2 source B record rewritten to hold A v2; A record unchanged; B v1 text gone
Guarded mismatch intended A, active B, guarded save no mutation; both full records equal their snapshots; wrong target error
Correct target intended A, active A, guarded save A record updated to v2 source; whole B record unchanged
Invalid intended id intended empty string, active A no mutation; both full records equal snapshots; invalid id error
Invalid active id intended A, active empty string no mutation; both full records equal snapshots; invalid id error
Missing record intended Z, active Z no mutation; both full records equal snapshots; missing id error

Run status

I ran the exact listing with node check.mjs on Node v23.6.1 on 9 September 2026. The process exited with code 0 and printed:

case 1 naive misroute: B holds A v2, A record unchanged
case 2 guarded mismatch: rejected, both records unchanged
case 3 correct target: A updated, B record unchanged
case 4 invalid ids: three rejections, no writes
Enter fullscreen mode Exit fullscreen mode

Where this check stops

The model is synchronous, single-threaded, and in memory. It shows the guard catching a mismatch inside one toy store, and nothing beyond that. It is not evidence about async flows, races between operations, atomicity of a check plus a write, persistence, or concurrent writers. It is not a real editor test. No TradingView component, MCP server, or Monaco editor is involved anywhere in the listing.

The gap matters. An explicit intended-id guard in a synchronous model is not sufficient for async persistence. Between the check and the write, the active binding can change, or a second writer can commit first. The comparison has to happen at the single point where the store persists, as an atomic conditional write or a version check that accepts or rejects the whole operation together. That is where a real fix belongs. This article names that place and does not claim any implementation of it.

Reuse it

The useful part is the shape of the test. Snapshot whole records before the operation. Reject on identity mismatch before any write. Compare full records after every rejection, one rejection at a time. Keep the naive path in case 1 so the failure stays visible next to the fix. Then change names, fields, and store to match your own system. A toy that runs in seconds and states its own limits is easier to trust than a promise that never shows its checks.

Top comments (0)