This commit is contained in:
abhinav7x94 2026-08-26 04:01:27 +05:30 committed by GitHub
commit ac24229f52
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 152 additions and 13 deletions

View file

@ -0,0 +1,101 @@
import { createElement, type ReactElement } from "react"
import { renderToStaticMarkup } from "react-dom/server"
import { describe, expect, it } from "vitest"
import { McpAppPreviewProvider } from "../McpAppProvider"
import { Save } from "../views/Save"
import { Upload } from "../views/Upload"
import {
isWritableTag,
preferredWritableTag,
retainedWritableTag,
} from "./writableTag"
describe("isWritableTag", () => {
it("accepts only tags in the writable set", () => {
expect(isWritableTag("team", ["personal", "team"])).toBe(true)
expect(isWritableTag("read-only", ["personal", "team"])).toBe(false)
expect(isWritableTag(null, ["personal", "team"])).toBe(false)
})
})
function renderWithApp(view: ReactElement): string {
return renderToStaticMarkup(createElement(McpAppPreviewProvider, null, view))
}
describe("preferredWritableTag", () => {
it("keeps the candidate when it is writable", () => {
expect(preferredWritableTag("team", ["personal", "team"])).toBe("team")
})
it("falls back when the candidate is not writable", () => {
expect(preferredWritableTag("read-only", ["personal", "team"])).toBe(
"personal",
)
})
it("uses the first writable tag when there is no candidate", () => {
expect(preferredWritableTag(null, ["personal", "team"])).toBe("personal")
})
it("returns null when no writable tags are available", () => {
expect(preferredWritableTag("read-only", [])).toBeNull()
})
})
describe("retainedWritableTag", () => {
it("keeps a selection while it remains writable", () => {
expect(retainedWritableTag("team", ["personal", "team"])).toBe("team")
})
it("clears a selection instead of silently retargeting it", () => {
expect(retainedWritableTag("team", ["personal"])).toBeNull()
expect(retainedWritableTag(null, ["personal"])).toBeNull()
})
})
describe("writable space views", () => {
const props = {
activeTag: "read-only",
writableTags: ["sm_project_personal"],
onAdvance: () => {},
onError: () => {},
}
it("selects a writable fallback in the save form", () => {
const markup = renderWithApp(
createElement(Save, { ...props, prefill: "Remember this" }),
)
expect(markup).toContain("Personal")
expect(markup).not.toContain("Select space")
})
it("selects a writable fallback in the upload form", () => {
const markup = renderWithApp(createElement(Upload, props))
expect(markup).toContain("Personal")
expect(markup).not.toContain("Select space")
})
it("disables saving when no writable spaces are available", () => {
const markup = renderWithApp(
createElement(Save, {
...props,
prefill: "Remember this",
writableTags: [],
}),
)
expect(markup).not.toContain("Select space")
expect(markup).toContain('disabled=""')
})
it("disables uploading when no writable spaces are available", () => {
const markup = renderWithApp(
createElement(Upload, { ...props, writableTags: [] }),
)
expect(markup).not.toContain("Select space")
expect(markup).toContain('disabled=""')
})
})

View file

@ -0,0 +1,21 @@
export function isWritableTag(
candidate: string | null | undefined,
writableTags: readonly string[],
): candidate is string {
return !!candidate && writableTags.includes(candidate)
}
export function preferredWritableTag(
candidate: string | null | undefined,
writableTags: readonly string[],
): string | null {
if (isWritableTag(candidate, writableTags)) return candidate
return writableTags[0] ?? null
}
export function retainedWritableTag(
candidate: string | null,
writableTags: readonly string[],
): string | null {
return isWritableTag(candidate, writableTags) ? candidate : null
}

View file

@ -11,6 +11,11 @@ import {
} from "../design/ui"
import { useApp } from "../hooks/useApp"
import { formatTagLabel } from "../lib/formatTag"
import {
isWritableTag,
preferredWritableTag,
retainedWritableTag,
} from "../lib/writableTag"
interface Props {
activeTag?: string | null
@ -31,16 +36,14 @@ export function Save({
}: Props) {
const { callTool, handoffToModel } = useApp()
const [content, setContent] = useState(prefill ?? "")
const [selectedTag, setSelectedTag] = useState<string | null>(
activeTag ?? writableTags[0] ?? null,
const [selectedTag, setSelectedTag] = useState<string | null>(() =>
preferredWritableTag(activeTag, writableTags),
)
const [saving, setSaving] = useState(false)
useEffect(() => {
if (!selectedTag && writableTags.length > 0) {
setSelectedTag(writableTags[0])
}
}, [selectedTag, writableTags])
setSelectedTag((current) => retainedWritableTag(current, writableTags))
}, [writableTags])
const options = useMemo(
() =>
@ -53,10 +56,11 @@ export function Save({
)
const trimmed = content.trim()
const canSave = trimmed.length > 0 && !!selectedTag && !saving
const canSave =
trimmed.length > 0 && isWritableTag(selectedTag, writableTags) && !saving
const handleSave = async () => {
if (!canSave || !selectedTag) return
if (!canSave || !isWritableTag(selectedTag, writableTags)) return
setSaving(true)
const result = await callTool(
"save-memory",

View file

@ -1,4 +1,4 @@
import { useMemo, useState } from "react"
import { useEffect, useMemo, useState } from "react"
import {
uploadPreparationSchema,
uploadResponseSchema,
@ -16,6 +16,11 @@ import {
import { useApp } from "../hooks/useApp"
import { formatTagLabel } from "../lib/formatTag"
import { FileText, X } from "../lib/icons"
import {
isWritableTag,
preferredWritableTag,
retainedWritableTag,
} from "../lib/writableTag"
interface Props {
activeTag?: string | null
@ -43,11 +48,15 @@ export function Upload({
}: Props) {
const { callTool, handoffToModel } = useApp()
const [file, setFile] = useState<File | null>(null)
const [selectedTag, setSelectedTag] = useState<string | null>(
activeTag ?? writableTags[0] ?? null,
const [selectedTag, setSelectedTag] = useState<string | null>(() =>
preferredWritableTag(activeTag, writableTags),
)
const [uploading, setUploading] = useState(false)
useEffect(() => {
setSelectedTag((current) => retainedWritableTag(current, writableTags))
}, [writableTags])
const options = useMemo(
() =>
writableTags.map((tag) => ({
@ -58,10 +67,11 @@ export function Upload({
[writableTags],
)
const canUpload = !!file && !!selectedTag && !uploading
const canUpload =
!!file && isWritableTag(selectedTag, writableTags) && !uploading
const handleUpload = async () => {
if (!file || !selectedTag) return
if (!file || !isWritableTag(selectedTag, writableTags)) return
setUploading(true)
try {
const preparation = await callTool(

View file

@ -1,6 +1,9 @@
import { defineConfig } from "vitest/config"
export default defineConfig({
esbuild: {
jsx: "automatic",
},
test: {
include: ["e2e/**/*.test.ts", "src/**/*.test.ts"],
fileParallelism: false,