fix: coerce stringified numeric anchor in timeline() MCP tool (#2176)
* fix: coerce stringified numeric anchor in timeline() to repair MCP anchor routing HTTP query params arrive as strings, so the typeof anchor === 'number' dispatch always missed the observation-ID branch, falling through to ISO-timestamp parsing and silently returning a wrong-epoch window with the correct anchor echoed in the header. Closes the timeline regression reported on cut-guardian. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: extract parseNumericAnchor helper and expand timeline tests Address CodeRabbit review nitpicks on PR #2176: - Extract anchor coercion into private parseNumericAnchor helper - Add whitespace-padded numeric-string anchor test case - Add explicit numeric-anchor-not-found regression test Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: assert exact ordering and rendered anchor header in timeline tests Address CodeRabbit nitpick on PR #2176: drop sort to verify chronological ordering, and assert that the rendered anchor/header text echoes the requested numeric ID and marks the anchor row. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: extract anchor-render helper and tighten garbage-anchor assertion Address CodeRabbit nitpicks: DRY-up the three repeated anchor header/row assertions into expectAnchorRendered(), and assert the exact "Invalid timestamp: 123abc" error in the garbage-anchor branch instead of a generic non-empty-string check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -471,6 +471,14 @@ export class SearchManager {
|
||||
};
|
||||
}
|
||||
|
||||
private parseNumericAnchor(anchor: unknown): number | null {
|
||||
if (typeof anchor === 'number') return anchor;
|
||||
if (typeof anchor === 'string' && /^\d+$/.test(anchor.trim())) {
|
||||
return Number(anchor.trim());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool handler: timeline
|
||||
*/
|
||||
@@ -478,6 +486,7 @@ export class SearchManager {
|
||||
const { anchor, query, depth_before, depth_after, project } = args;
|
||||
const depthBefore = depth_before != null ? Number(depth_before) : 10;
|
||||
const depthAfter = depth_after != null ? Number(depth_after) : 10;
|
||||
const anchorAsNumber = this.parseNumericAnchor(anchor);
|
||||
const cwd = process.cwd();
|
||||
|
||||
// Validate: must provide either anchor or query, not both
|
||||
@@ -550,21 +559,21 @@ export class SearchManager {
|
||||
timelineData = this.sessionStore.getTimelineAroundObservation(topResult.id, topResult.created_at_epoch, depthBefore, depthAfter, project);
|
||||
}
|
||||
// MODE 2: Anchor-based timeline
|
||||
else if (typeof anchor === 'number') {
|
||||
else if (anchorAsNumber !== null) {
|
||||
// Observation ID
|
||||
const obs = this.sessionStore.getObservationById(anchor);
|
||||
const obs = this.sessionStore.getObservationById(anchorAsNumber);
|
||||
if (!obs) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: `Observation #${anchor} not found`
|
||||
text: `Observation #${anchorAsNumber} not found`
|
||||
}],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
anchorId = anchor;
|
||||
anchorId = anchorAsNumber;
|
||||
anchorEpoch = obs.created_at_epoch;
|
||||
timelineData = this.sessionStore.getTimelineAroundObservation(anchor, anchorEpoch, depthBefore, depthAfter, project);
|
||||
timelineData = this.sessionStore.getTimelineAroundObservation(anchorAsNumber, anchorEpoch, depthBefore, depthAfter, project);
|
||||
} else if (typeof anchor === 'string') {
|
||||
// Session ID or ISO timestamp
|
||||
if (anchor.startsWith('S') || anchor.startsWith('#S')) {
|
||||
|
||||
Reference in New Issue
Block a user