Files
gbrain/test/cli-finish-teardown.test.ts
T
4ee530f3c5 v0.42.42.0 fix(cli): bounded teardown + explicit exit — kill the 10s force-exit tax on txn-mode poolers (#2084) (#2141)
* feat(core): finishCliTeardown + flushThenExit — bounded teardown, owned exit verdict (#2084)

cli-force-exit.ts becomes the single owner of one-shot CLI exit + teardown:
- finishCliTeardown: bounded sink drain -> bounded disconnect under a backstop
  whose deadline is COMPUTED from the bounds it guards (floor 10s;
  GBRAIN_TEARDOWN_DEADLINE_MS env override). Arms at teardown start, never
  before the op handler.
- flushThenExit: stdio write-fence (unref'd guard, EPIPE-safe) + REF'D
  aliveness grace for non-TTY stdio — Bun only delivers queued pipe writes
  while the process is alive (no flush API reaches the native queue).
- setCliExitVerdict/currentExitCode: the exit verdict lives in a gbrain-owned
  channel, never read back from process.exitCode (PGLite's Emscripten runtime
  scribbles its own status there mid-run).
- background-work.ts exports backgroundWorkSinkCount() for the deadline formula.

Unit tests + a spawned-Bun harness proving byte-complete piped output.

* fix(cli): route all nine disconnect sites through finishCliTeardown; one exit seam (#2084)

Deletes the pre-handler 10s force-exit timer (it measured handler + teardown
combined: PgBouncer txn-mode deployments paid a flat 10s banner tax on every
query, and any >10s op was killed mid-run with exit 0 and truncated output).
Sweeps op-dispatch, CLI_ONLY fall-through, search dashboard, read-only timeout
path, dream, doctor x3 (fixing a pre-existing pool leak when DB checks throw),
and ze-switch. The ONE process exit lives in main().then/catch via
flushThenExit(currentExitCode()), gated by shouldForceExitAfterMain().
Exit-code writers (op-dispatch catch, reindex, transcripts, brainstorm,
autopilot, frontmatter) now set the verdict through setCliExitVerdict.

* fix(pglite): contain Emscripten's process.exitCode writes at PGlite.create (#2084)

PGLite's WASM runtime writes its own status into process.exitCode (99 at
create; in-memory brains run initdb whose status lands on a later tick; the
exit status at close) — on PGLite every error exit was silently clobbered.
preservingProcessExitCode wraps create() to keep the global tidy; db.close()
stays unwrapped (its 0-write is baseline behavior test runners depend on).
The CLI verdict itself is immune: it lives in the owned channel.

* test: e2e + structural pins for the #2084 teardown contract

E2E: failed op exits 1; every swept command spawned (brain-copy isolation for
mutators, no-network); slow-handler regression via the deadline env knob;
piped --json parses complete; teardown banner absent on every happy path;
daemon survival untouched. Structural: no bare awaited engine disconnects in
cli.ts; DISCONNECT_HARD_DEADLINE_MS gone; >=9 helper call sites; verdict
channel + create-wrap pins.

* test: fix R1 env-isolation violations in retrieval-reflex tests

Pre-existing on master: both files mutated GBRAIN_RETRIEVAL_REFLEX directly,
failing scripts/check-test-isolation.sh (bun run verify). Converted to the
canonical withEnv() pattern; the reflex describe's beforeEach also never
restored the flag, leaking it across the shard.

* docs: KEY_FILES entries for the teardown contract; close + file TODOS (#2084)

KEY_FILES.md: current-state entry for cli-force-exit.ts (helper + central exit
seam pair, verdict channel, cli.ts-scoped claim); background-work.ts and
pglite-engine.ts entries updated. TODOS.md: the drain-before-owner-disconnect
P3 (filed from #1972) is done by this wave; files the trigger-gated
GBRAIN_COMMAND_DEADLINE_MS follow-up (eng-review D2/D14).

* fix: pre-landing review fixes (#2084)

Review army (testing/maintainability/security/performance, 0 critical):
- drain defense-in-depth: a throwing drain warns and still disconnects
  (cannot escape a caller's finally or skip the engine teardown)
- behavioral tests for preservingProcessExitCode (connect pins 0; create-throw
  restores the pre-call verdict)
- D9 widening test (live-registry sink count feeds the deadline formula),
  env 0/negative boundary cases, verdict mirror-write assertion
- stale comments: header diagram backstop line, structural-test 'both
  lifecycle calls' contradiction, KEY_FILES 10s-force-exit clauses, e2e D11
  falsification story corrected
- named the formula's pool-end literals

* fix: adversarial-review hardening — daemon-safe command resolution, flush knob, ref'd backstop (#2084)

Cross-model adversarial review (Claude subagent + Codex, both P1'd it):
- shouldForceExitAfterMain now resolves the command through parseGlobalFlags —
  the old first-non-dash heuristic read `gbrain --timeout 30s serve` as
  command "30s" and the new exit seam would have killed the daemon ~250ms
  after boot with exit 0 (unit-pinned)
- GBRAIN_FLUSH_GRACE_MS env override for the non-TTY aliveness grace (batch
  consumers piping large payloads to slow readers can raise it; agent loops
  can lower it)
- backstop timer is now REF'D: a hung teardown on an otherwise-empty event
  loop previously exited naturally — skipping the flush and surfacing
  PGLite's scribbled process.exitCode
- flushThenExit: real process.exit latched once per process
- doctor-site comment corrected; in-command process.exit teardown-bypass
  class (pre-existing) filed as a P2 TODO

* chore: bump version and changelog (v0.42.42.0)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: move #2084 exitCode-containment lifecycle tests to the serial quarantine (R3)

* docs: update project documentation for v0.42.42.0

- docs/TESTING.md: replace the stale 4-file serial-quarantine enumeration
  with a current-state description (the quarantine is glob-discovered, now
  several dozen files incl. the #2084 exitCode-containment suite); add unit
  inventory entries for test/cli-finish-teardown.test.ts and
  test/flush-then-exit-harness.test.ts.
- docs/architecture/KEY_FILES.md: rephrase the pglite-engine exitCode
  containment note to current-state wording (clears the
  check-key-files-current-state prose-history warning).

llms bundles regenerated (byte-identical: both docs are link-only).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: apply cross-model doc-review findings for v0.42.42.0

Codex review of docs-vs-shipped-code found 9 gaps; all verified against
the code before fixing:

- CHANGELOG.md (0.42.42.0 entry, precision narrowing only — no entries
  touched): "every CLI exit path" -> "every cli.ts disconnect site";
  "on every path" -> "on every routed exit path"; dream/doctor/ze-switch
  claim scoped to dispatcher teardown (command-internal process.exit
  sites are tracked in TODOS as the open P2).
- docs/architecture/KEY_FILES.md: the teardown backstop is REF'D, not
  unref'd (matches the F3 adversarial-review decision in the code).
- src/core/cli-force-exit.ts: header diagram comment had the same stale
  unref'd claim + `process.exitCode ?? 0`; now matches the implementation
  (ref'd timer, `currentExitCode()`). Comment-only change.
- docs/TESTING.md: verify is the 30-check parallel battery via
  run-verify-parallel.sh (was described as 4 checks); CI is 10 weighted
  LPT shards + dedicated verify/serial/slow jobs (was "4-way FNV on
  shard 1"); test:serial runs one bun process per file (not
  --max-concurrency=1); dead "cap: 10" line rewritten as debt guidance;
  inventory entries added for test/cli-should-force-exit.test.ts and
  test/e2e/pglite-cli-exit.serial.test.ts.

bun run verify green (30/30); #2084 test files green; llms bundles
regenerated (byte-identical — reference docs are link-only).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: route v0.42.41.0's raw exitCode writers through the verdict channel; reconcile merged structural pins (#2084)

CI fallout from merging the v0.42.41.0 triage wave into the #2084 exit-seam
design — both waves fixed the same timer-placement bug independently:

- doctor.ts + extract.ts set failure exit codes via raw `process.exitCode =`
  writes (v0.42.41.0's process.exit -> exitCode conversion); the #2084 exit
  seam reads only the gbrain-owned verdict channel, so doctor FAILs exited 0
  (Tier 1 RLS e2e + half-migrated-Minions tests). Converted to
  setCliExitVerdict, same as the wallclock-124 site in the merge commit.
- cli-force-exit-teardown-arming.test.ts pinned v0.42.41.0's inline
  finally-armed timer, which the merge replaced with finishCliTeardown;
  rewritten to pin the merged invariant (no pre-try arming in cli.ts; the
  backstop arms inside the helper before the drain).
- eval-capture drain timing bound 1s -> 2s: flaked at 1023ms under CI shard
  load after the new test files shifted LPT shard packing (13x budget slack
  still proves bounded-not-hung).

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 07:28:13 -07:00

511 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* #2084 — unit tests for the one-shot CLI teardown + exit contract in
* src/core/cli-force-exit.ts: finishCliTeardown (teardown-only, computed
* backstop deadline), flushThenExit (write-fence + guard + EPIPE + once-latch),
* and computeTeardownDeadlineMs (formula / floor / env override).
*
* Real short timers, no fake clocks. Every test that touches process.exitCode
* or GBRAIN_TEARDOWN_DEADLINE_MS restores it in a finally so the suite stays
* order-independent.
*/
import { describe, test, expect } from 'bun:test';
import {
finishCliTeardown,
flushThenExit,
computeTeardownDeadlineMs,
TEARDOWN_DEADLINE_FLOOR_MS,
setCliExitVerdict,
currentExitCode,
_resetCliExitVerdictForTests,
type MinimalWritable,
} from '../src/core/cli-force-exit.ts';
import { POOL_END_TIMEOUT_SECONDS } from '../src/core/db.ts';
import {
backgroundWorkSinkCount,
__registerDrainerForTest,
} from '../src/core/background-work.ts';
import { withEnv } from './helpers/with-env.ts';
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
function fakeStream(): MinimalWritable & { writes: string[] } {
const writes: string[] = [];
return {
writes,
write(chunk: string, cb?: (err?: Error | null) => void) {
writes.push(chunk);
if (cb) queueMicrotask(() => cb());
return true;
},
once() {
return this;
},
};
}
describe('computeTeardownDeadlineMs', () => {
test('formula: sinks × drain + facts grace + 2 × pool bound + slack', () => {
const poolEndBoundMs = POOL_END_TIMEOUT_SECONDS * 1000 + 500;
// 4 sinks × 2000 + 2000 + 2×poolEnd + 2000 — the Site B worst case that
// falsified the old static 10s (eng-review D9).
const got = computeTeardownDeadlineMs({ sinkCount: 4, drainTimeoutMs: 2000 });
expect(got).toBe(4 * 2000 + 2000 + 2 * poolEndBoundMs + 2000);
expect(got).toBeGreaterThan(10_000); // the codex-found arithmetic bug, pinned
});
test('floors at TEARDOWN_DEADLINE_FLOOR_MS for small budgets', () => {
const got = computeTeardownDeadlineMs({ sinkCount: 1, drainTimeoutMs: 100 });
expect(got).toBe(TEARDOWN_DEADLINE_FLOOR_MS);
});
test('GBRAIN_TEARDOWN_DEADLINE_MS env override wins over the formula', async () => {
await withEnv({ GBRAIN_TEARDOWN_DEADLINE_MS: '1234' }, async () => {
expect(computeTeardownDeadlineMs({ sinkCount: 4, drainTimeoutMs: 2000 })).toBe(1234);
});
});
test('garbage env values fall back to the formula', async () => {
await withEnv({ GBRAIN_TEARDOWN_DEADLINE_MS: 'banana' }, async () => {
expect(
computeTeardownDeadlineMs({ sinkCount: 1, drainTimeoutMs: 100 }),
).toBe(TEARDOWN_DEADLINE_FLOOR_MS);
});
});
test('zero and negative env values fall back to the formula (not "fire immediately")', async () => {
await withEnv({ GBRAIN_TEARDOWN_DEADLINE_MS: '0' }, async () => {
expect(computeTeardownDeadlineMs({ sinkCount: 1, drainTimeoutMs: 100 })).toBe(
TEARDOWN_DEADLINE_FLOOR_MS,
);
});
await withEnv({ GBRAIN_TEARDOWN_DEADLINE_MS: '-5' }, async () => {
expect(computeTeardownDeadlineMs({ sinkCount: 1, drainTimeoutMs: 100 })).toBe(
TEARDOWN_DEADLINE_FLOOR_MS,
);
});
});
test('a newly registered sink widens the computed deadline (D9: formula reads the live registry)', () => {
// Register two sinks and compare between them: in a bare unit-test process
// no production sinks are loaded, so the zero-sink baseline sits below the
// 10s floor and would mask the first sink's delta.
const mkSink = (name: string) =>
__registerDrainerForTest({ name, order: 99, drain: async () => ({ unfinished: 0 }) });
const un1 = mkSink('test-2084-sink-a');
try {
const withOne = computeTeardownDeadlineMs({
sinkCount: backgroundWorkSinkCount(),
drainTimeoutMs: 5000,
});
const un2 = mkSink('test-2084-sink-b');
try {
const withTwo = computeTeardownDeadlineMs({
sinkCount: backgroundWorkSinkCount(),
drainTimeoutMs: 5000,
});
expect(withOne).toBeGreaterThan(TEARDOWN_DEADLINE_FLOOR_MS); // above the floor — delta is visible
expect(withTwo).toBe(withOne + 5000);
} finally {
un2();
}
} finally {
un1();
}
});
});
describe('finishCliTeardown — clean path', () => {
test('drains with the injected budget, disconnects, returns; no exit, no warn', async () => {
const calls: string[] = [];
let drainBudget = -1;
const exits: number[] = [];
const warns: string[] = [];
await finishCliTeardown({
engine: { disconnect: async () => void calls.push('disconnect') },
drainTimeoutMs: 777,
deadlineMs: 250,
drain: async ({ timeoutMs }) => {
drainBudget = timeoutMs;
calls.push('drain');
},
exit: (c) => void exits.push(c),
warn: (m) => void warns.push(m),
stdout: fakeStream(),
stderr: fakeStream(),
});
// Past the 250ms deadline: a leaked backstop would fire here.
await sleep(400);
expect(calls).toEqual(['drain', 'disconnect']);
expect(drainBudget).toBe(777);
expect(exits).toEqual([]);
expect(warns).toEqual([]);
});
test('drain runs BEFORE disconnect (live-engine window for sinks)', async () => {
const order: string[] = [];
await finishCliTeardown({
engine: { disconnect: async () => void order.push('disconnect') },
deadlineMs: 1000,
drain: async () => {
await sleep(20);
order.push('drain');
},
exit: () => {},
warn: () => {},
});
expect(order).toEqual(['drain', 'disconnect']);
});
});
describe('finishCliTeardown — backstop on hung teardown', () => {
test('hung disconnect fires the banner and exits with current exitCode', async () => {
const prevCode = process.exitCode;
try {
_resetCliExitVerdictForTests(); // no verdict set ⇒ currentExitCode() === 0
const exits: number[] = [];
const warns: string[] = [];
let resolveHang!: () => void;
const teardown = finishCliTeardown({
engine: { disconnect: () => new Promise<void>((r) => (resolveHang = r)) },
deadlineMs: 100,
drain: async () => {},
exit: (c) => void exits.push(c),
warn: (m) => void warns.push(m),
stdout: fakeStream(),
stderr: fakeStream(),
graceMs: 0,
});
await sleep(300);
expect(warns.length).toBe(1);
expect(warns[0]).toContain('did not return within');
expect(warns[0]).toContain('100ms');
expect(exits).toEqual([0]);
resolveHang(); // unhang so the promise settles
await teardown;
} finally {
_resetCliExitVerdictForTests();
process.exitCode = prevCode;
}
});
test('backstop honors an exit code the errored op already set', async () => {
const prevCode = process.exitCode;
try {
setCliExitVerdict(1); // what the op-dispatch catch does
const exits: number[] = [];
let resolveHang!: () => void;
const teardown = finishCliTeardown({
engine: { disconnect: () => new Promise<void>((r) => (resolveHang = r)) },
deadlineMs: 100,
drain: async () => {},
exit: (c) => void exits.push(c),
warn: () => {},
stdout: fakeStream(),
stderr: fakeStream(),
graceMs: 0,
});
await sleep(300);
expect(exits).toEqual([1]);
resolveHang();
await teardown;
} finally {
_resetCliExitVerdictForTests();
process.exitCode = prevCode;
}
});
test('hung DRAIN (not just disconnect) also trips the backstop', async () => {
const prevCode = process.exitCode;
try {
_resetCliExitVerdictForTests();
const exits: number[] = [];
const warns: string[] = [];
let resolveHang!: () => void;
const teardown = finishCliTeardown({
engine: { disconnect: async () => {} },
deadlineMs: 100,
drain: () => new Promise<void>((r) => (resolveHang = r)),
exit: (c) => void exits.push(c),
warn: (m) => void warns.push(m),
stdout: fakeStream(),
stderr: fakeStream(),
graceMs: 0,
});
await sleep(300);
expect(warns.length).toBe(1);
expect(exits).toEqual([0]);
resolveHang();
await teardown;
} finally {
process.exitCode = prevCode;
}
});
});
describe('verdict channel — immune to PGLite WASM process.exitCode writes', () => {
test('engine teardown that rewrites process.exitCode does not change the verdict', async () => {
// PGLite's Emscripten runtime writes its own status into process.exitCode
// at arbitrary points (99 at create, initdb status on a later tick for
// in-memory brains, 0 at close) — pre-#2084 this clobbered an errored
// op's exit 1 back to 0 on every PGLite error path. The verdict lives in
// the gbrain-owned channel and never reads the global back.
const prevCode = process.exitCode;
try {
setCliExitVerdict(1); // the op errored
await finishCliTeardown({
engine: {
disconnect: async () => {
process.exitCode = 0; // what PGLite's WASM shutdown does
},
},
deadlineMs: 1000,
drain: async () => {},
exit: () => {},
warn: () => {},
});
expect(currentExitCode()).toBe(1);
} finally {
_resetCliExitVerdictForTests();
process.exitCode = prevCode;
}
});
test('mid-run WASM write (in-memory initdb status) cannot fake a verdict', () => {
_resetCliExitVerdictForTests();
try {
process.exitCode = 100; // what in-memory PGLite's initdb does mid-run
expect(currentExitCode()).toBe(0); // no gbrain verdict was ever set
setCliExitVerdict(2);
expect(currentExitCode()).toBe(2);
// The mirror write exists for EXTERNAL readers of the global.
expect(process.exitCode).toBe(2);
} finally {
_resetCliExitVerdictForTests();
process.exitCode = 0;
}
});
});
describe('finishCliTeardown — disconnect failure (D3: exit code reports the op)', () => {
test('a throwing drain is warned, disconnect still runs, helper resolves', async () => {
// The registry is contractually non-throwing; this pins the defense-in-depth
// guard — a drain rejection must not skip disconnect or escape the caller's
// finally (it would replace a successful op's completion).
const calls: string[] = [];
const warns: string[] = [];
await finishCliTeardown({
engine: { disconnect: async () => void calls.push('disconnect') },
deadlineMs: 1000,
drain: async () => {
throw new Error('sink registry blew up');
},
exit: () => {},
warn: (m) => void warns.push(m),
});
expect(calls).toEqual(['disconnect']);
expect(warns.length).toBe(1);
expect(warns[0]).toContain('sink registry blew up');
});
test('disconnect throw is warned and swallowed; helper resolves', async () => {
const warns: string[] = [];
const exits: number[] = [];
await finishCliTeardown({
engine: {
disconnect: async () => {
throw new Error('pool already dead');
},
},
deadlineMs: 1000,
drain: async () => {},
exit: (c) => void exits.push(c),
warn: (m) => void warns.push(m),
});
expect(warns.length).toBe(1);
expect(warns[0]).toContain('pool already dead');
expect(exits).toEqual([]); // helper never exits on the non-backstop path
});
});
describe('flushThenExit', () => {
test('exits after BOTH stream callbacks fire, exactly once, with the code', async () => {
const prevCode = process.exitCode;
try {
const events: string[] = [];
const exits: number[] = [];
const slowStream = (name: string): MinimalWritable => ({
write(_c: string, cb?: (err?: Error | null) => void) {
setTimeout(() => {
events.push(`${name}-flushed`);
cb?.();
}, 50);
return true;
},
once() {
return this;
},
});
flushThenExit(3, {
exit: (c) => {
events.push('exit');
exits.push(c);
},
stdout: slowStream('stdout'),
stderr: slowStream('stderr'),
guardMs: 2000,
graceMs: 0,
});
await sleep(200);
expect(events).toEqual(['stdout-flushed', 'stderr-flushed', 'exit']);
expect(exits).toEqual([3]);
expect(process.exitCode).toBe(3); // belt-and-braces for natural exit
} finally {
process.exitCode = prevCode;
}
});
test('non-TTY default: exit waits the aliveness grace AFTER the fence', async () => {
const prevCode = process.exitCode;
try {
const exits: number[] = [];
const t0 = Date.now();
let fencedAt = -1;
const stream: MinimalWritable = {
write(_c: string, cb?: (err?: Error | null) => void) {
fencedAt = Date.now() - t0;
if (cb) queueMicrotask(() => cb());
return true;
},
once() {
return this;
},
};
flushThenExit(0, {
exit: (c) => void exits.push(c),
stdout: stream,
stderr: stream,
guardMs: 2000,
graceMs: 120, // fakes are non-TTY; explicit grace keeps the test tight
});
await sleep(60);
expect(exits).toEqual([]); // fence done, still inside the grace window
await sleep(150);
expect(exits).toEqual([0]);
expect(fencedAt).toBeGreaterThanOrEqual(0);
} finally {
process.exitCode = prevCode;
}
});
test('guard fires when a callback never arrives (blocked pipe)', async () => {
const prevCode = process.exitCode;
try {
const exits: number[] = [];
const blockedStream: MinimalWritable = {
write() {
return false; // never calls cb — reader stopped consuming
},
once() {
return this;
},
};
const t0 = Date.now();
flushThenExit(0, {
exit: (c) => void exits.push(c),
stdout: blockedStream,
stderr: blockedStream,
guardMs: 100,
graceMs: 0,
});
await sleep(300);
expect(exits).toEqual([0]);
expect(Date.now() - t0).toBeGreaterThanOrEqual(100);
} finally {
process.exitCode = prevCode;
}
});
test('sync write throw (EPIPE) still exits', async () => {
const prevCode = process.exitCode;
try {
const exits: number[] = [];
const epipeStream: MinimalWritable = {
write() {
throw Object.assign(new Error('write EPIPE'), { code: 'EPIPE' });
},
once() {
return this;
},
};
flushThenExit(0, {
exit: (c) => void exits.push(c),
stdout: epipeStream,
stderr: epipeStream,
guardMs: 2000,
graceMs: 0,
});
await sleep(50);
expect(exits).toEqual([0]);
} finally {
process.exitCode = prevCode;
}
});
test('GBRAIN_FLUSH_GRACE_MS env override is honored (batch/incident knob)', async () => {
const prevCode = process.exitCode;
try {
await withEnv({ GBRAIN_FLUSH_GRACE_MS: '0' }, async () => {
const exits: number[] = [];
flushThenExit(0, {
exit: (c) => void exits.push(c),
stdout: fakeStream(),
stderr: fakeStream(),
guardMs: 2000,
// no graceMs → resolves through the env override (fakes are non-TTY)
});
await sleep(60);
expect(exits).toEqual([0]); // grace 0: exit right after the fence
});
} finally {
process.exitCode = prevCode;
}
});
test('once-latch: guard + late callbacks cannot double-exit', async () => {
const prevCode = process.exitCode;
try {
const exits: number[] = [];
// stdout flushes late (after the guard), stderr never — both race finish().
const lateStream: MinimalWritable = {
write(_c: string, cb?: (err?: Error | null) => void) {
setTimeout(() => cb?.(), 150);
return true;
},
once() {
return this;
},
};
const neverStream: MinimalWritable = {
write() {
return false;
},
once() {
return this;
},
};
flushThenExit(0, {
exit: (c) => void exits.push(c),
stdout: lateStream,
stderr: neverStream,
guardMs: 80,
graceMs: 0,
});
await sleep(400);
expect(exits).toEqual([0]); // exactly one exit despite guard + late cb
} finally {
process.exitCode = prevCode;
}
});
});