Skip to content

Commit a322dc7

Browse files
universe-hcyclaude
andcommitted
fix: 修复 push/pop 巡检报出的三处逻辑问题
- parsePopArgs 用正则严格校验整数,拒绝 2junk/1.5 等被 parseInt 静默截断的非法序号 - 非交互会话 partial 压缩失败回退 full 时补发通知,避免 push 标记被静默丢弃 - resume 时 push 点失效通知统一为英文,与兄弟通知一致 - 补充 2junk/1.5 回归测试 Co-Authored-By: claude-opus-4-8[1m] <noreply@anthropic.com>
1 parent 5b1a67c commit a322dc7

4 files changed

Lines changed: 41 additions & 13 deletions

File tree

src/commands/__tests__/pushpop.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,18 @@ describe('parsePopArgs', () => {
8181
expect(result as string).toMatch(/Invalid marker/)
8282
})
8383

84+
test('--to 2junk rejects trailing garbage (no partial parseInt)', () => {
85+
const result = parsePopArgs('--to 2junk')
86+
expect(typeof result).toBe('string')
87+
expect(result as string).toMatch(/Invalid marker/)
88+
})
89+
90+
test('--to=1.5 rejects non-integer', () => {
91+
const result = parsePopArgs('--to=1.5')
92+
expect(typeof result).toBe('string')
93+
expect(result as string).toMatch(/Invalid marker/)
94+
})
95+
8496
test('unknown flag returns error string', () => {
8597
const result = parsePopArgs('--unknown')
8698
expect(typeof result).toBe('string')

src/commands/pop/pop.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@ import type { LocalCommandResult } from '../../commands.js'
22
import type { ToolUseContext } from '../../Tool.js'
33
import type { PopOptions } from '../../services/pushStack/state.js'
44

5+
/**
6+
* Parses a marker argument (`#N` or `N`) into a positive ordinal. Rejects any
7+
* input that is not a pure integer — `Number.parseInt` would otherwise silently
8+
* accept `2junk` (→2) or `1.5` (→1) and pop the wrong branch. Returns null on
9+
* any malformed value.
10+
*/
11+
function parseMarkerOrdinal(raw: string): number | null {
12+
const digits = raw.replace(/^#/, '')
13+
if (!/^\d+$/.test(digits)) return null
14+
const n = Number.parseInt(digits, 10)
15+
return Number.isInteger(n) && n >= 1 ? n : null
16+
}
17+
518
/**
619
* Parses `/pop` flags into PopOptions. Recognized:
720
* --to #N | --to N cross-layer pop back to marker ordinal N (§4.7)
@@ -22,12 +35,12 @@ export function parsePopArgs(args: string): PopOptions | string {
2235
const next = tokens[++i]
2336
if (next === undefined)
2437
return 'Missing marker number after --to (e.g. /pop --to #1).'
25-
const n = Number.parseInt(next.replace(/^#/, ''), 10)
26-
if (!Number.isInteger(n) || n < 1) return `Invalid marker number: ${next}`
38+
const n = parseMarkerOrdinal(next)
39+
if (n === null) return `Invalid marker number: ${next}`
2740
opts.to = n
2841
} else if (tok.startsWith('--to=')) {
29-
const n = Number.parseInt(tok.slice('--to='.length).replace(/^#/, ''), 10)
30-
if (!Number.isInteger(n) || n < 1) return `Invalid marker number: ${tok}`
42+
const n = parseMarkerOrdinal(tok.slice('--to='.length))
43+
if (n === null) return `Invalid marker number: ${tok}`
3144
opts.to = n
3245
} else {
3346
return `Unknown option: ${tok}`

src/screens/REPL.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2375,7 +2375,7 @@ export function REPL({
23752375
if (droppedCount > 0) {
23762376
addNotification({
23772377
key: 'push-stack-resume-dropped',
2378-
text: `${droppedCount} push 点在恢复后已失效(被压缩/snip 卷走),已移除`,
2378+
text: `${droppedCount} push point(s) became invalid after resume (carried off by compaction/snip) and were removed.`,
23792379
priority: 'medium',
23802380
timeoutMs: 6000,
23812381
});

src/services/compact/autoCompact.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -446,19 +446,22 @@ export async function autoCompactIfNeeded(
446446
}
447447
}
448448
}
449-
// goFull or partial failed — warn before running full compaction.
450-
if (isInteractive && !goFull) {
451-
// Partial failed silently; full compaction will clear all markers.
449+
// goFull or partial failed — warn before running full compaction. This
450+
// must fire in non-interactive sessions too: otherwise a partial
451+
// failure there would drop every push marker silently, violating the
452+
// "never silently lose a stack marker" invariant.
453+
if (goFull) {
452454
toolUseContext.addNotification?.({
453-
key: 'autocompact-push-full-fallback',
454-
text: `Auto-compacted (partial failed); all ${stack.length} push point(s) are now invalid. Use /rewind if needed.`,
455+
key: 'autocompact-push-full',
456+
text: `Full auto-compact: all ${stack.length} push point(s) have been removed.`,
455457
priority: 'medium',
456458
timeoutMs: 10000,
457459
})
458-
} else if (goFull) {
460+
} else {
461+
// Partial failed/unavailable; full compaction will clear all markers.
459462
toolUseContext.addNotification?.({
460-
key: 'autocompact-push-full',
461-
text: `Full auto-compact: all ${stack.length} push point(s) have been removed.`,
463+
key: 'autocompact-push-full-fallback',
464+
text: `Auto-compacted (partial failed); all ${stack.length} push point(s) are now invalid. Use /rewind if needed.`,
462465
priority: 'medium',
463466
timeoutMs: 10000,
464467
})

0 commit comments

Comments
 (0)