Ctrl K
ComunidadGuías…

test/cascade-turn-snapshot-v2.test.js

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// Detach P1 — snapshot v2 (S1–S7).
//
// The v1 snapshot carried the per-step cursors but not the four fields a
// CORRECT resume needs:
//   - generatorOffset: the metadata RPC is offset-scoped. Resuming from 0
//     re-sums every prior turn of the cascade and over-bills it.
//   - sawActive: a resumed ACTIVE turn must not wait the IDLE grace again.
//   - usageByStep: the fallback when the metadata RPC fails.
//   - consumedThrough: proves every counted step was actually consumed.
//     Without it `fromSnapshot({stepOffset:3, cursors:[]})` is accepted and
//     the resumed window re-emits steps 0–2 (a duplicated prefix).
//
// S1–S6 are RED-B against the pre-change module (the fields are undefined and
// the empty-cursor snapshot is accepted); S7 is a post-bump guard.
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { CascadeTurn, CascadeTurnCursorError } from '../src/cascade-turn.js';

function step(text, { thinking = '' } = {}) {
  return { responseText: text, thinking, modifiedText: '', status: 3, toolCalls: [] };
}

function consumedTurn({ generatorOffset = 0, sawActive = false } = {}) {
  const turn = new CascadeTurn({ cascadeId: 'snapshot-v2', generatorOffset, sawActive });
  return turn;
}

async function readAndConsume(turn, steps) {
  const read = await turn.readWindow(start => steps.slice(start));
  for (let i = 0; i < read.steps.length; i++) {
    await turn.consumeStep(read.steps[i], read.windowStart + i);
  }
  return read;
}

describe('CascadeTurn snapshot v2', () => {
  it('S1 carries generatorOffset (the metadata RPC must not restart at 0)', async () => {
    const turn = consumedTurn({ generatorOffset: 7 });
    await readAndConsume(turn, [step('a')]);
    const snap = JSON.parse(JSON.stringify(turn.snapshot()));
    assert.equal(snap.generatorOffset, 7,
      'a resumed turn without generatorOffset asks GetCascadeTrajectoryGeneratorMetadata from offset 0 and sums prior turns');
  });

  it('S2 carries sawActive (no second IDLE grace after a resume)', async () => {
    const turn = consumedTurn();
    await readAndConsume(turn, [step('a')]);
    // The client sets this when GetCascadeTrajectory reports non-IDLE.
    turn.sawActive = true;
    const snap = turn.snapshot();
    assert.equal(snap.sawActive, true);
    const restored = CascadeTurn.fromSnapshot(JSON.parse(JSON.stringify(snap)));
    assert.equal(restored.sawActive, true);
  });

  it('S3 carries usageByStep and survives JSON', async () => {
    const turn = consumedTurn();
    await readAndConsume(turn, [step('a')]);
    turn.usageByStep.set(0, {
      inputTokens: 11, outputTokens: 7, cacheReadTokens: 3, cacheWriteTokens: 2,
    });
    const snap = JSON.parse(JSON.stringify(turn.snapshot()));
    assert.deepEqual(snap.usageByStep, [[0, {
      inputTokens: 11, outputTokens: 7, cacheReadTokens: 3, cacheWriteTokens: 2,
    }]]);
    const restored = CascadeTurn.fromSnapshot(snap);
    assert.deepEqual([...restored.usageByStep.entries()], [[0, {
      inputTokens: 11, outputTokens: 7, cacheReadTokens: 3, cacheWriteTokens: 2,
    }]]);
  });

  it('S4 consumedThrough equals stepOffset after a full window', async () => {
    const turn = consumedTurn();
    await readAndConsume(turn, [step('a'), step('b'), step('c')]);
    assert.equal(turn.stepOffset, 3);
    assert.equal(turn.snapshot().consumedThrough, 3);
  });

  it('S5 refuses a mid-batch snapshot instead of losing the unconsumed steps', async () => {
    const turn = consumedTurn();
    const read = await turn.readWindow(() => [step('a'), step('b')]);
    await turn.consumeStep(read.steps[0], read.windowStart); // step 1 NOT consumed
    assert.equal(turn.stepOffset, 2);
    assert.equal(turn.consumedThrough, 1);
    assert.throws(
      () => turn.snapshot(),
      (err) => {
        assert.ok(err instanceof CascadeTurnCursorError);
        assert.equal(err.code, 'CASCADE_TURN_CURSOR');
        assert.match(err.message, /mid-batch/);
        return true;
      },
    );

    // Positive control in the same file: consuming the rest makes it valid.
    await turn.consumeStep(read.steps[1], read.windowStart + 1);
    assert.equal(turn.snapshot().consumedThrough, 2);
  });

  it('S6 refuses an empty-cursor snapshot (duplicated prefix)', () => {
    assert.throws(
      () => CascadeTurn.fromSnapshot({
        version: 2,
        cascadeId: 'empty',
        stepOffset: 3,
        pollFloor: 0,
        windowStart: 0,
        yieldedByStep: [],
        thinkingByStep: [],
        consumedThrough: 0,
        generatorOffset: 0,
        sawActive: false,
        usageByStep: [],
        chunks: [],
      }),
      CascadeTurnCursorError,
      'consumedThrough=0 with stepOffset=3 must be refused',
    );

    // Positive control: the same shape with every counted step consumed is a
    // valid resume (the window is 0 and the empty cursors are legitimate for
    // steps that produced no text).
    const ok = CascadeTurn.fromSnapshot({
      version: 2,
      cascadeId: 'empty-ok',
      stepOffset: 3,
      pollFloor: 0,
      windowStart: 0,
      yieldedByStep: [],
      thinkingByStep: [],
      consumedThrough: 3,
      generatorOffset: 0,
      sawActive: false,
      usageByStep: [],
      chunks: [],
    });
    assert.equal(ok.stepOffset, 3);
    assert.equal(ok.consumedThrough, 3);
  });

  it('S7 rejects a v1 snapshot after the bump (guard, not red-first)', () => {
    assert.throws(
      () => CascadeTurn.fromSnapshot({
        version: 1,
        stepOffset: 1,
        pollFloor: 0,
        yieldedByStep: [[0, 1]],
        thinkingByStep: [],
        chunks: [],
      }),
      (err) => {
        assert.ok(err instanceof CascadeTurnCursorError);
        assert.match(err.message, /version 1 is not supported/);
        return true;
      },
    );
  });
});