CodeNomad/scripts/test-session-navigation-native.mjs
Pascal André d25dd91e12
fix(ui): preserve semantic icons for historical timeline tools (#738)
## Summary

- retain the representative tool name in bounded session-outline entries
so historical timeline markers keep semantic icons without loading
message bodies
- revalidate legacy persisted outlines once when tool icon metadata is
missing, while preserving immediate timeline geometry
- bound representative names to the RPC contract for non-ASCII tool
names
- reuse shared content icons in Chat settings and shorten the localized
System label
- keep timeline tool visibility in the existing Chat settings and View
menu; no extra checkbox is added beneath the rail

## Validation

- `npm run typecheck --workspace @codenomad/ui`
- `npm run typecheck --workspace @neuralnomads/codenomad`
- `node --import tsx --test
packages/server/src/opencode/session-pruning/navigation.test.ts`
- `node scripts/test-session-pruning-native.mjs
<isolated-opencode-2.0.12> <plugin-source>`
- `node --import tsx --test
packages/ui/src/components/session-outline-projection.test.ts
packages/ui/src/stores/session-outline-persistence.test.ts`
- `node --conditions=browser --import tsx --test --test-force-exit
packages/ui/src/stores/session-outline.test.ts`
- `node --import tsx --test --test-concurrency=1
--test-name-pattern="mixed timeline keeps"
packages/ui/tests/browser/history-navigation.test.ts`
- `node --import tsx --test --test-concurrency=1
--test-name-pattern="popup controls preserve"
packages/ui/tests/browser/system-messages.test.ts`
2026-09-21 23:46:51 +02:00

56 lines
3.9 KiB
JavaScript

import assert from "node:assert/strict"
export async function testSessionNavigationNative({ client, location, locationOptions, template }) {
const seed = await client.session.create({ title: "Isolated long navigation fixture", location }, locationOptions)
const exported = await client.session.export({ sessionID: seed.id })
await client.session.remove({ sessionID: seed.id })
const id = index => `msg_navigation_${String(index).padStart(5, "0")}`
const nativeTool = structuredClone(template.content.find(part => part.type === "tool"))
assert(nativeTool)
const messages = Array.from({ length: 1501 }, (_, index) => ({
...structuredClone(template), id: id(index), content: index === 6
? [{ ...nativeTool, id: "part_navigation_tool" }]
: [{ type: "text", text: `Navigation passage ${index}` }],
}))
const session = await client.session.import({ ...exported, messages, location }, locationOptions)
const rpc = async (method, input) => (await client.rpc.call({ rpcID: "codenomad.session-pruning", method,
input: { sessionID: session.id, ...input }, location }, locationOptions)).output
try {
assert.deepEqual(await rpc("window", { target: { kind: "around", messageID: "msg_removed" } }),
{ status: "blocked", reason: "anchor_missing" }, "missing restore anchor is distinct from ownership conflicts")
const around = await rpc("window", { target: { kind: "around", messageID: id(1200) } })
assert.equal(around.status, "window", JSON.stringify(around))
assert.equal(around.messages.length, 200)
assert.equal(around.messages[80].id, id(1200))
const native = await client.session.export({ sessionID: session.id })
assert.deepEqual(around.messages, native.messages.slice(1120, 1320), "SQL reconstruction matches native export exactly")
assert.deepEqual(around.messages[80], await client.session.message.get({ sessionID: session.id, messageID: id(1200) }))
assert.deepEqual(await rpc("window", { target: around.resume }), around)
const prior = await rpc("window", { target: around.older })
assert(prior.messages.some(message => message.id === around.messages[0].id), "prior window retains the viewport anchor")
const following = await rpc("window", { target: around.newer })
assert(following.messages.some(message => message.id === around.messages.at(-1).id), "next window retains the viewport anchor")
let cursor, count = 0, indexedTool
const checkpoints = []
do {
const outline = await rpc("outline", cursor ? { cursor } : {})
assert.equal(outline.status, "outline")
assert.equal(outline.entries.length, Math.min(16384, native.messages.length - count), "structural index does not wait for excerpt pagination")
assert(outline.entries.every(entry => !("preview" in entry)))
indexedTool ??= outline.entries.find(entry => entry.id === id(6))
checkpoints.push(...outline.checkpoints.map(({ changed, ...checkpoint }) => checkpoint))
count += outline.entries.length
cursor = outline.cursor
} while (cursor)
assert.equal(count, native.messages.length)
assert.equal(indexedTool?.toolName, nativeTool.name, "outline reads the representative name from native tool storage")
const verified = await rpc("outline", { known: checkpoints })
assert.equal(verified.status, "outline", JSON.stringify(verified))
assert.equal(verified.entries.length, native.messages.length % 512, "unchanged saved ranges do not resend their structure")
assert.equal(verified.checkpoints.filter(checkpoint => !checkpoint.changed).length, 2)
const previews = await rpc("outlinePreview", { messageIDs: [id(1200)] })
assert.equal(previews.status, "previews", JSON.stringify(previews))
assert.equal(previews.entries[0].text, "Navigation passage 1200")
console.log("PASS: native 1501-message outline with representative tool metadata, direct distant windows, overlap and exact native payload/restore parity")
} finally { await client.session.remove({ sessionID: session.id }) }
}