mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-10-20 02:39:12 +00:00
fix(skills): persist draft skill attachments across window restore
Extend the bounded restorable attachment codec with native skill IDs and display names. Skills now survive capture, serialization and hydration without blocking window-state writes; removed skill attachments stay removed. Addresses gatekeeper P1 on PR #791. Add round-trip, post-restore send, removal and malformed/oversized source coverage. UI typecheck and 56 focused codec/state/skill regressions pass.
This commit is contained in:
parent
0014bbc962
commit
ea3b4ef6df
2 changed files with 23 additions and 1 deletions
|
|
@ -3,6 +3,7 @@ import { test } from "node:test"
|
|||
import { createSkillAttachment } from "../types/attachment"
|
||||
import { promptSkills } from "./prompt-skills"
|
||||
import { normalizeSessionMessage } from "../stores/message-v2/normalizers"
|
||||
import { hydrateRestorableAttachment, normalizeRestorableAttachmentRecord, serializeDraftAttachments } from "../stores/client-state-attachments-codec"
|
||||
|
||||
test("explicit skill ids are deduplicated without injecting skill content", () => {
|
||||
const skill = createSkillAttachment("opaque/id", "Review")
|
||||
|
|
@ -24,3 +25,18 @@ test("historical skill labels survive normalization without retaining skill body
|
|||
assert.deepEqual(result.message.parts[1], { id: "message-skill-0", type: "skill", skillId: "skill", name: "Review", sessionID: "session", messageID: "message" })
|
||||
assert.equal(JSON.stringify(result.message).includes("large native skill content"), false)
|
||||
})
|
||||
|
||||
test("skill drafts survive persistent window-state capture and removal", () => {
|
||||
const attachment = createSkillAttachment("review", "Review")
|
||||
const saved = serializeDraftAttachments({ session: "review this" }, { session: [attachment] })
|
||||
const reloaded = normalizeRestorableAttachmentRecord(JSON.parse(JSON.stringify(saved.attachments)), saved.drafts)!
|
||||
const hydrated = hydrateRestorableAttachment(reloaded.attachments.session[0])!
|
||||
assert.deepEqual(hydrated, attachment)
|
||||
assert.deepEqual(promptSkills([hydrated], saved.drafts.session), [{ id: "review" }])
|
||||
const removed = serializeDraftAttachments(saved.drafts, { session: [] })
|
||||
assert.equal(removed.attachments.session, undefined)
|
||||
assert.equal(removed.drafts.session, "review this")
|
||||
for (const source of [{ type: "skill", id: "x".repeat(513), name: "Review" }, { type: "skill", id: "review", name: "x".repeat(1025) }, { type: "skill", id: "", name: "Review" }]) {
|
||||
assert.equal(normalizeRestorableAttachmentRecord({ session: [{ ...attachment, source }] }, saved.drafts)!.attachments.session, undefined)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ export type RestorableAttachmentSource =
|
|||
| { type: "file"; path: string; mime: string; data?: string }
|
||||
| { type: "text"; value: string }
|
||||
| { type: "agent"; name: string }
|
||||
| { type: "skill"; id: string; name: string }
|
||||
| { type: "symbol"; path: string; name: string; kind: number; range: { start: Position; end: Position } }
|
||||
|
||||
interface Position {
|
||||
|
|
@ -84,6 +85,11 @@ function normalizeSource(
|
|||
const name = takeString(value.name, MAX_DISPLAY)
|
||||
return name === undefined ? undefined : { type: "agent", name }
|
||||
}
|
||||
if (value.type === "skill") {
|
||||
const id = takeString(value.id, MAX_ID)
|
||||
const name = takeString(value.name, MAX_DISPLAY)
|
||||
return id === undefined || name === undefined ? undefined : { type: "skill", id, name }
|
||||
}
|
||||
|
||||
const path = takeString(value.path, MAX_PATH)
|
||||
if (value.type === "file") {
|
||||
|
|
@ -104,7 +110,7 @@ function normalizeSource(
|
|||
|
||||
function normalizeAttachment(value: unknown): RestorableAttachment | undefined {
|
||||
if (!isRecord(value)) return
|
||||
if (!["file", "text", "symbol", "agent"].includes(String(value.type))) return
|
||||
if (!["file", "text", "symbol", "agent", "skill"].includes(String(value.type))) return
|
||||
|
||||
const id = takeString(value.id, MAX_ID)
|
||||
const display = takeString(value.display, MAX_DISPLAY)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue