mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 00:49:41 +02:00
fix: date a working row by its turn, not by the session age
`TuiSessionRow` declared `lastSubmitAt`/`inputTokens`/`outputTokens`, `stateSince()` ordered the WORKING group by the first of them and `renderRowLines()` painted the other two, but nothing ever filled any of them in: the unified list carries none, and the `session:updated` payload that does was discarded (an event only schedules a refetch). So a running turn was dated by its SESSION's creation instead. Measured against the live server before the fix: w65 (created 21h ago, turn started one minute earlier) outranked w67 (created 15 minutes ago, turn started five minutes earlier), the reverse of the rule docs/tui.md states, and the elapsed column read `21h` for a turn a minute old. The token column was unreachable code for the same reason. `fetchLiveSessionMetrics()` reads the three fields from `GET /api/sessions` and `applyLiveMetrics()` folds them onto the rows. That route answers from the server's cached LIGHT state (no terminal buffers): 10-20ms measured, against the ~550ms the unified list in the same `Promise.all` already costs, so it is cheap enough to ride every refresh. It is best-effort like the approvals and tmux reads beside it, because losing the anchor is better than losing the list. A ZERO is treated as unknown rather than merged: `stateSince()` reads `lastSubmitAt ?? createdAt` and 0 is not nullish, so a merged 0 would date every never-submitted session to the epoch. The snapshot path gets the same merge, or `codeman tui --list` would number the WORKING group differently from the dashboard that `codeman tui <n>` indexes into. Verified live: working rows now show 28m/8m (turn age, tokens 280.5k/65.2k) where they showed 21h/34m and no tokens. The e2e assertion fails on master's wiring with `[*] 10m` against a session that pressed Enter one minute ago. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
applyLiveMetrics,
|
||||
applyMuxNames,
|
||||
buildListLines,
|
||||
confirmAccepts,
|
||||
@@ -27,9 +28,9 @@ import {
|
||||
tmuxRowsToSessions,
|
||||
tmuxSocketFromEnv,
|
||||
} from '../../src/tui/tui-app.js';
|
||||
import { createTuiModel } from '../../src/tui/tui-model.js';
|
||||
import { createTuiModel, stateSince } from '../../src/tui/tui-model.js';
|
||||
import { glyphsFor } from '../../src/tui/tui-render.js';
|
||||
import type { TuiTmuxSession } from '../../src/tui/tui-client.js';
|
||||
import type { TuiLiveSessionMetrics, TuiTmuxSession } from '../../src/tui/tui-client.js';
|
||||
import type { TuiConfirmState, TuiRow, TuiSessionRow } from '../../src/tui/tui-types.js';
|
||||
|
||||
const GLYPHS = glyphsFor('unicode');
|
||||
@@ -339,6 +340,66 @@ describe('applyMuxNames', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyLiveMetrics', () => {
|
||||
const metrics: TuiLiveSessionMetrics[] = [
|
||||
{ sessionId: 'aaaa1111', lastSubmitAt: 5_000, inputTokens: 900, outputTokens: 100 },
|
||||
{ sessionId: 'bbbb2222', lastSubmitAt: 0, inputTokens: 0, outputTokens: 0 },
|
||||
];
|
||||
|
||||
it('folds the turn stamp and the token totals onto the matching row', () => {
|
||||
const rows = applyLiveMetrics(
|
||||
[
|
||||
{ sessionId: 'aaaa1111', sources: ['live'] },
|
||||
{ sessionId: 'cccc3333', sources: ['live'] },
|
||||
],
|
||||
metrics
|
||||
);
|
||||
expect(rows[0]).toMatchObject({ lastSubmitAt: 5_000, inputTokens: 900, outputTokens: 100 });
|
||||
// No live counterpart (a history row): nothing to fold, nothing invented.
|
||||
expect(rows[1].lastSubmitAt).toBeUndefined();
|
||||
expect(rows[1].inputTokens).toBeUndefined();
|
||||
});
|
||||
|
||||
it('treats a zero as unknown, so a never-submitted session is not dated to the epoch', () => {
|
||||
const [merged] = applyLiveMetrics([{ sessionId: 'bbbb2222', sources: ['live'], createdAt: 1_000 }], metrics);
|
||||
expect(merged.lastSubmitAt).toBeUndefined();
|
||||
expect(merged.inputTokens).toBeUndefined();
|
||||
expect(stateSince('working', merged)).toBe(1_000);
|
||||
});
|
||||
|
||||
it('copies rather than mutating its input, and survives an empty metrics list', () => {
|
||||
const input: TuiSessionRow[] = [{ sessionId: 'aaaa1111', sources: ['live'] }];
|
||||
const rows = applyLiveMetrics(input, []);
|
||||
expect(rows[0]).not.toBe(input[0]);
|
||||
expect(rows[0].lastSubmitAt).toBeUndefined();
|
||||
expect(input[0].lastSubmitAt).toBeUndefined();
|
||||
});
|
||||
|
||||
it('orders the WORKING group by when the turn started, not by session age', () => {
|
||||
// The regression this merge exists for: `old` was created a day before
|
||||
// `fresh` but started its turn a minute AFTER it, so `fresh` has been
|
||||
// working longer and has to lead. Without the merge both fall back to
|
||||
// createdAt and `old` wins.
|
||||
const unified: TuiSessionRow[] = [
|
||||
{ sessionId: 'old00000', name: 'old', sources: ['live'], isWorking: true, createdAt: 1_000 },
|
||||
{ sessionId: 'fresh000', name: 'fresh', sources: ['live'], isWorking: true, createdAt: 500_000 },
|
||||
];
|
||||
const turns: TuiLiveSessionMetrics[] = [
|
||||
{ sessionId: 'old00000', lastSubmitAt: 900_000 },
|
||||
{ sessionId: 'fresh000', lastSubmitAt: 600_000 },
|
||||
];
|
||||
|
||||
const before = createTuiModel();
|
||||
before.replaceSessions(unified);
|
||||
expect(before.rows().map((r) => r.session.name)).toEqual(['old', 'fresh']);
|
||||
|
||||
const after = createTuiModel();
|
||||
after.replaceSessions(applyLiveMetrics(unified, turns));
|
||||
expect(after.rows().map((r) => r.session.name)).toEqual(['fresh', 'old']);
|
||||
expect(after.rows()[0].since).toBe(600_000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildListLines', () => {
|
||||
it('numbers rows in the dashboard order, so `tui <n>` and `tui --list` agree', () => {
|
||||
const model = createTuiModel();
|
||||
|
||||
Reference in New Issue
Block a user