mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-20 07:37:35 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
faa72aea3a |
@@ -197,6 +197,22 @@ export function record<T, E>(codec: Of<T, E>): Of<Record<string, T>, Record<stri
|
||||
)
|
||||
}
|
||||
|
||||
/** A record that drops entries whose values are invalid, the replacement for `catchDecoding` to none. */
|
||||
export function sparseRecord<T, E>(codec: Of<T, E>): Of<Record<string, T>, Record<string, E>> {
|
||||
return make(
|
||||
(input) => {
|
||||
if (typeof input !== "object" || input === null || Array.isArray(input)) return INVALID
|
||||
const out: Record<string, T> = {}
|
||||
for (const [key, item] of Object.entries(input)) {
|
||||
const value = codec.decode(item)
|
||||
if (value !== INVALID) out[key] = value
|
||||
}
|
||||
return out
|
||||
},
|
||||
(value) => Object.fromEntries(Object.entries(value).map(([key, item]) => [key, codec.encode(item)])),
|
||||
)
|
||||
}
|
||||
|
||||
/** An invalid record becomes empty rather than failing the whole store, like `Persistence.record`. */
|
||||
export function lenientRecord<T, E>(codec: Of<T, E>): Of<Record<string, T>, Record<string, E>> {
|
||||
const strict = record(codec)
|
||||
@@ -240,6 +256,21 @@ export function transform<T, E, T2>(
|
||||
)
|
||||
}
|
||||
|
||||
/** Decodes with `source`, maps, then validates with `target`: Effect's `decodeTo` with a transform. */
|
||||
export function decodeTo<T, E, T2, E2>(
|
||||
source: Of<T, E>,
|
||||
target: Of<T2, E2>,
|
||||
options: { decode: (value: T) => E2; encode: (value: T2) => T },
|
||||
): Of<T2, E> {
|
||||
return make(
|
||||
(input) => {
|
||||
const value = source.decode(input)
|
||||
return value === INVALID ? INVALID : target.decode(options.decode(value))
|
||||
},
|
||||
(value) => source.encode(options.encode(value)),
|
||||
)
|
||||
}
|
||||
|
||||
/** Invalid and missing values become `value()`, like `Persistence.fallback`. */
|
||||
export function fallback<T, E>(codec: Of<T, E>, value: () => NoInfer<T>): Of<T, E> {
|
||||
return make(
|
||||
@@ -284,11 +315,13 @@ function isMigrated<C extends Any>(definition: C | Migrated<C>): definition is M
|
||||
export function withInitial<C extends Any>(definition: C | Migrated<C>, initial: Type<C>): Of<Type<C>, unknown> {
|
||||
const codec = isMigrated(definition) ? definition.current : definition
|
||||
const read = isMigrated(definition) ? definition.read : unknown
|
||||
return make(
|
||||
(input) => {
|
||||
const stored = read.decode(input)
|
||||
if (stored === INVALID) return INVALID
|
||||
return merge(initial, recover(codec, stored, initial))
|
||||
return make(
|
||||
(input) => {
|
||||
const migrated = read.decode(input)
|
||||
if (migrated === INVALID) return INVALID
|
||||
// A migration only describes the fields it rewrites; everything else stored stays as it was.
|
||||
const stored = isObject(input) && isObject(migrated) ? { ...input, ...migrated } : migrated
|
||||
return merge(initial, recover(codec, stored, initial))
|
||||
},
|
||||
(value) => codec.encode(value),
|
||||
)
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Schema } from "effect"
|
||||
import { IconState, ModelState, ProjectState, VcsState, serverState } from "./persistence"
|
||||
import { createRoot } from "solid-js"
|
||||
import { isServer } from "solid-js/web"
|
||||
import { Persist, persisted } from "@/runtime/persistence/storage"
|
||||
import { Persistence } from "@/runtime/persistence/schema"
|
||||
import { Codec } from "@/runtime/persistence/codec"
|
||||
|
||||
const initial = { list: [], hidden: {}, projects: {}, lastProject: {}, recentlyClosed: {} }
|
||||
|
||||
function serverSchema(canonical?: () => string | undefined) {
|
||||
return Persistence.withInitial(serverState(canonical), initial)
|
||||
return Codec.withInitial(serverState(canonical), initial)
|
||||
}
|
||||
|
||||
describe("server persistence schema", () => {
|
||||
@@ -29,7 +28,7 @@ describe("server persistence schema", () => {
|
||||
],
|
||||
projects: { local: [{ worktree: "/project", expanded: true }] },
|
||||
}
|
||||
const state = Schema.decodeUnknownSync(schema)(input)
|
||||
const state = Codec.decodeOrThrow(schema, input)
|
||||
expect(state).toEqual({
|
||||
list: [
|
||||
{ type: "http", http: { url: "http://localhost:4096" } },
|
||||
@@ -48,13 +47,13 @@ describe("server persistence schema", () => {
|
||||
recentlyClosed: {},
|
||||
})
|
||||
expect(input.list[1]).toHaveProperty("username", "legacy")
|
||||
const encoded = Schema.encodeSync(schema)(state)
|
||||
const encoded = schema.encode(state)
|
||||
expect(encoded).toEqual(state)
|
||||
expect(Schema.decodeUnknownSync(schema)(encoded)).toEqual(state)
|
||||
expect(Codec.decodeOrThrow(schema, encoded)).toEqual(state)
|
||||
})
|
||||
|
||||
test("defaults missing or malformed fields and drops invalid entries independently", () => {
|
||||
const decode = Schema.decodeUnknownSync(serverSchema())
|
||||
const decode = ((input: unknown) => Codec.decodeOrThrow(serverSchema(), input))
|
||||
const empty = { list: [], hidden: {}, projects: {}, lastProject: {}, recentlyClosed: {} }
|
||||
expect(decode({})).toEqual(empty)
|
||||
expect(decode({ list: null, hidden: [], projects: false, lastProject: 1, recentlyClosed: "bad" })).toEqual(empty)
|
||||
@@ -74,7 +73,7 @@ describe("server persistence schema", () => {
|
||||
|
||||
test("moves canonical project buckets without changing server keys or unrelated scopes", () => {
|
||||
const schema = serverSchema(() => "https://opencode.example.com")
|
||||
const state = Schema.decodeUnknownSync(schema)({
|
||||
const state = Codec.decodeOrThrow(schema, {
|
||||
list: ["https://opencode.example.com"],
|
||||
hidden: { "https://opencode.example.com": true },
|
||||
projects: {
|
||||
@@ -100,14 +99,14 @@ describe("server persistence schema", () => {
|
||||
expect(state.list[0]?.http.url).toBe("https://opencode.example.com")
|
||||
expect(state.hidden).toEqual({ "https://opencode.example.com": true })
|
||||
expect(state.recentlyClosed).toEqual({ local: ["/closed"], "https://opencode.example.com": ["/old-closed"] })
|
||||
expect(Schema.encodeSync(schema)(state)).toEqual(state)
|
||||
expect(Schema.decodeUnknownSync(schema)(state)).toEqual(state)
|
||||
expect(schema.encode(state)).toEqual(state)
|
||||
expect(Codec.decodeOrThrow(schema, state)).toEqual(state)
|
||||
})
|
||||
|
||||
test("reads the latest canonical local prop on each decode", () => {
|
||||
const props: { canonicalLocalServer?: string } = {}
|
||||
const schema = serverSchema(() => props.canonicalLocalServer)
|
||||
const decode = Schema.decodeUnknownSync(schema)
|
||||
const decode = ((input: unknown) => Codec.decodeOrThrow(schema, input))
|
||||
const input = {
|
||||
projects: { remote: [{ worktree: "/project", expanded: true }] },
|
||||
lastProject: { remote: "/project" },
|
||||
@@ -122,7 +121,7 @@ describe("server persistence schema", () => {
|
||||
})
|
||||
|
||||
test("migrates a last project without a project list", () => {
|
||||
expect(Schema.decodeUnknownSync(serverSchema(() => "remote"))({ lastProject: { remote: "/project" } })).toEqual({
|
||||
expect(Codec.decodeOrThrow(serverSchema(() => "remote"), { lastProject: { remote: "/project" } })).toEqual({
|
||||
list: [],
|
||||
hidden: {},
|
||||
projects: {},
|
||||
@@ -134,7 +133,7 @@ describe("server persistence schema", () => {
|
||||
|
||||
describe("model persistence schema", () => {
|
||||
test("defaults missing state and keeps valid entries beside malformed entries", () => {
|
||||
const decode = Schema.decodeUnknownSync(Persistence.withInitial(ModelState, { user: [], recent: [], variant: {} }))
|
||||
const decode = ((input: unknown) => Codec.decodeOrThrow(Codec.withInitial(ModelState, { user: [], recent: [], variant: {} }), input))
|
||||
expect(decode({})).toEqual({ user: [], recent: [], variant: {} })
|
||||
expect(decode({ user: null, recent: 1, variant: [] })).toEqual({ user: [], recent: [], variant: {} })
|
||||
const state = decode({
|
||||
@@ -155,24 +154,24 @@ describe("model persistence schema", () => {
|
||||
recent: [{ providerID: "provider", modelID: "model" }],
|
||||
variant: { model: "high" },
|
||||
})
|
||||
expect(Schema.encodeSync(ModelState)(state)).toEqual(state)
|
||||
expect(ModelState.encode(state)).toEqual(state)
|
||||
})
|
||||
})
|
||||
|
||||
describe("directory cache schemas", () => {
|
||||
test("defaults missing and malformed VCS caches but retains optional branch metadata", () => {
|
||||
const decode = Schema.decodeUnknownSync(Persistence.withInitial(VcsState, { value: undefined }))
|
||||
const decode = ((input: unknown) => Codec.decodeOrThrow(Codec.withInitial(VcsState, { value: undefined }), input))
|
||||
expect(decode({})).toEqual({ value: undefined })
|
||||
expect(decode({ value: null })).toEqual({ value: undefined })
|
||||
expect(decode({ value: { branch: 1 } })).toEqual({ value: undefined })
|
||||
expect(decode({ value: { default_branch: "main" } })).toEqual({ value: { default_branch: "main" } })
|
||||
const state = decode({ value: { branch: "feature", default_branch: "main", obsolete: true } })
|
||||
expect(state).toEqual({ value: { branch: "feature", default_branch: "main" } })
|
||||
expect(Schema.encodeSync(VcsState)(state)).toEqual(state)
|
||||
expect(VcsState.encode(state)).toEqual(state)
|
||||
})
|
||||
|
||||
test("validates project name, icon overrides and startup commands", () => {
|
||||
const decode = Schema.decodeUnknownSync(Persistence.withInitial(ProjectState, { value: undefined }))
|
||||
const decode = ((input: unknown) => Codec.decodeOrThrow(Codec.withInitial(ProjectState, { value: undefined }), input))
|
||||
expect(decode({})).toEqual({ value: undefined })
|
||||
expect(decode({ value: [] })).toEqual({ value: undefined })
|
||||
expect(decode({ value: { icon: { override: 1 } } })).toEqual({ value: undefined })
|
||||
@@ -185,7 +184,7 @@ describe("directory cache schemas", () => {
|
||||
commands: { start: "bun dev" },
|
||||
},
|
||||
})
|
||||
expect(Schema.encodeSync(ProjectState)(state)).toEqual(state)
|
||||
expect(ProjectState.encode(state)).toEqual(state)
|
||||
expect(state.value).toEqual({
|
||||
name: "Project",
|
||||
icon: { override: "data:image/png;base64,abc", color: "blue" },
|
||||
@@ -194,12 +193,12 @@ describe("directory cache schemas", () => {
|
||||
})
|
||||
|
||||
test("validates optional icon strings", () => {
|
||||
const decode = Schema.decodeUnknownSync(Persistence.withInitial(IconState, { value: undefined }))
|
||||
const decode = ((input: unknown) => Codec.decodeOrThrow(Codec.withInitial(IconState, { value: undefined }), input))
|
||||
expect(decode({})).toEqual({ value: undefined })
|
||||
expect(decode({ value: 42 })).toEqual({ value: undefined })
|
||||
expect(decode({ value: null })).toEqual({ value: undefined })
|
||||
expect(decode({ value: "" })).toEqual({ value: "" })
|
||||
expect(Schema.encodeSync(IconState)(decode({ value: "data:image/png;base64,abc" }))).toEqual({
|
||||
expect(IconState.encode(decode({ value: "data:image/png;base64,abc" }))).toEqual({
|
||||
value: "data:image/png;base64,abc",
|
||||
})
|
||||
})
|
||||
@@ -255,7 +254,7 @@ test.skipIf(isServer)(
|
||||
const stored = values.get("opencode.global.dat:server")
|
||||
expect(stored).toBeDefined()
|
||||
if (!stored) throw new Error("server state was not written")
|
||||
const decoded = Schema.decodeUnknownSync(Schema.fromJsonString(serverSchema()))(stored)
|
||||
const decoded = Codec.decodeOrThrow(Codec.fromJsonString(serverSchema()), stored)
|
||||
expect(decoded.projects.local).toEqual([{ worktree: "/project", expanded: true }])
|
||||
expect(stored).not.toContain("username")
|
||||
expect(decoded.list).toEqual(root.state[0].list)
|
||||
@@ -264,3 +263,4 @@ test.skipIf(isServer)(
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -1,136 +1,126 @@
|
||||
import { Effect, Option, Schema, SchemaGetter } from "effect"
|
||||
import { Persistence } from "@/runtime/persistence/schema"
|
||||
import { Codec } from "@/runtime/persistence/codec"
|
||||
import { ServerKey } from "./key"
|
||||
|
||||
export const ServerKey = Schema.String.pipe(Schema.brand("ServerConnection.Key"))
|
||||
export { ServerKey }
|
||||
|
||||
export const ServerHttpBase = Persistence.struct({
|
||||
url: Schema.String,
|
||||
password: Schema.optional(Schema.String),
|
||||
export const ServerHttpBase = Codec.struct({
|
||||
url: Codec.string,
|
||||
password: Codec.optional(Codec.string),
|
||||
})
|
||||
|
||||
export const ServerHttp = Persistence.struct({
|
||||
type: Schema.Literal("http"),
|
||||
export const ServerHttp = Codec.struct({
|
||||
type: Codec.literal("http"),
|
||||
http: ServerHttpBase,
|
||||
authToken: Schema.optional(Schema.Boolean),
|
||||
displayName: Schema.optional(Schema.String),
|
||||
label: Schema.optional(Schema.String),
|
||||
authToken: Codec.optional(Codec.boolean),
|
||||
displayName: Codec.optional(Codec.string),
|
||||
label: Codec.optional(Codec.string),
|
||||
})
|
||||
|
||||
const StoredServer = Schema.Union([ServerHttp, ServerHttpBase, Schema.String]).pipe(
|
||||
Schema.decodeTo(ServerHttp, {
|
||||
decode: SchemaGetter.transform((value) => {
|
||||
if (typeof value === "string") return { type: "http", http: { url: value } }
|
||||
if ("http" in value) return value
|
||||
return { type: "http", http: value }
|
||||
}),
|
||||
encode: SchemaGetter.transform((value) => value),
|
||||
}),
|
||||
)
|
||||
|
||||
const ProjectList = Persistence.array(
|
||||
Persistence.struct({
|
||||
worktree: Schema.String,
|
||||
expanded: Persistence.fallback(Schema.Boolean, () => true),
|
||||
}),
|
||||
)
|
||||
const Projects = Persistence.record(ProjectList)
|
||||
const LastProject = Persistence.record(Schema.String.pipe(Schema.catchDecoding(() => Effect.succeed(Option.none()))))
|
||||
|
||||
const State = Persistence.struct({
|
||||
list: Persistence.array(StoredServer),
|
||||
hidden: Schema.Record(
|
||||
Schema.String,
|
||||
Schema.mutableKey(Schema.Boolean.pipe(Schema.catchDecoding(() => Effect.succeed(Option.none())))),
|
||||
),
|
||||
projects: Schema.Record(Schema.String, Schema.mutableKey(ProjectList)),
|
||||
lastProject: Schema.Record(
|
||||
Schema.String,
|
||||
Schema.mutableKey(Schema.String.pipe(Schema.catchDecoding(() => Effect.succeed(Option.none())))),
|
||||
),
|
||||
recentlyClosed: Schema.Record(Schema.String, Schema.mutableKey(Persistence.array(Schema.String))),
|
||||
// Servers were stored as a URL string, then as the HTTP block alone, before the current shape.
|
||||
const StoredServer = Codec.decodeTo(Codec.union([ServerHttp, ServerHttpBase, Codec.string]), ServerHttp, {
|
||||
decode: (value) => {
|
||||
if (typeof value === "string") return { type: "http" as const, http: { url: value } }
|
||||
if ("http" in value) return value
|
||||
return { type: "http" as const, http: value }
|
||||
},
|
||||
encode: (value) => value,
|
||||
})
|
||||
|
||||
const ProjectList = Codec.lenientArray(
|
||||
Codec.struct({
|
||||
worktree: Codec.string,
|
||||
expanded: Codec.fallback(Codec.boolean, () => true),
|
||||
}),
|
||||
)
|
||||
const Projects = Codec.lenientRecord(ProjectList)
|
||||
const LastProject = Codec.fallback(Codec.sparseRecord(Codec.string), () => ({}))
|
||||
|
||||
const State = Codec.struct({
|
||||
list: Codec.lenientArray(StoredServer),
|
||||
hidden: Codec.sparseRecord(Codec.boolean),
|
||||
projects: Codec.record(ProjectList),
|
||||
lastProject: Codec.sparseRecord(Codec.string),
|
||||
recentlyClosed: Codec.record(Codec.lenientArray(Codec.string)),
|
||||
})
|
||||
|
||||
const StoredState = Codec.struct({ projects: Projects, lastProject: LastProject })
|
||||
|
||||
// Projects and last-opened entries recorded under the canonical local server's URL move under
|
||||
// "local" when that URL is known, so they survive the server changing address.
|
||||
export function serverState(canonicalLocalServer: () => string | undefined = () => undefined) {
|
||||
return Persistence.migrate(
|
||||
return Codec.migrate(
|
||||
State,
|
||||
Schema.Struct({ projects: Projects, lastProject: LastProject }).pipe(
|
||||
Schema.decode({
|
||||
decode: SchemaGetter.transform((value) => {
|
||||
const canonical = canonicalLocalServer()
|
||||
if (!canonical || canonical === "local") return value
|
||||
const previous = value.projects[canonical]
|
||||
const last = value.lastProject[canonical]
|
||||
if (!previous && last === undefined) return value
|
||||
Codec.transform(StoredState, {
|
||||
decode: (value) => {
|
||||
const canonical = canonicalLocalServer()
|
||||
if (!canonical || canonical === "local") return value
|
||||
const previous = value.projects[canonical]
|
||||
const last = value.lastProject[canonical]
|
||||
if (!previous && last === undefined) return value
|
||||
|
||||
const projects = { ...value.projects }
|
||||
if (previous) {
|
||||
const local = projects.local ?? []
|
||||
const worktrees = new Set(local.map((project) => project.worktree))
|
||||
projects.local = [
|
||||
...local,
|
||||
...previous.filter((project) => {
|
||||
if (worktrees.has(project.worktree)) return false
|
||||
worktrees.add(project.worktree)
|
||||
return true
|
||||
}),
|
||||
]
|
||||
delete projects[canonical]
|
||||
}
|
||||
const lastProject = { ...value.lastProject }
|
||||
if (last !== undefined) {
|
||||
lastProject.local ??= last
|
||||
delete lastProject[canonical]
|
||||
}
|
||||
return { ...value, projects, lastProject }
|
||||
}),
|
||||
encode: SchemaGetter.transform((value) => value),
|
||||
}),
|
||||
),
|
||||
const projects = { ...value.projects }
|
||||
if (previous) {
|
||||
const local = projects.local ?? []
|
||||
const worktrees = new Set(local.map((project) => project.worktree))
|
||||
projects.local = [
|
||||
...local,
|
||||
...previous.filter((project) => {
|
||||
if (worktrees.has(project.worktree)) return false
|
||||
worktrees.add(project.worktree)
|
||||
return true
|
||||
}),
|
||||
]
|
||||
delete projects[canonical]
|
||||
}
|
||||
const lastProject = { ...value.lastProject }
|
||||
if (last !== undefined) {
|
||||
lastProject.local ??= last
|
||||
delete lastProject[canonical]
|
||||
}
|
||||
return { ...value, projects, lastProject }
|
||||
},
|
||||
encode: (value) => value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export const ModelState = Persistence.struct({
|
||||
user: Persistence.array(
|
||||
Persistence.struct({
|
||||
providerID: Schema.String,
|
||||
modelID: Schema.String,
|
||||
visibility: Schema.Literals(["show", "hide"]),
|
||||
favorite: Schema.optional(Schema.Boolean),
|
||||
export const ModelState = Codec.struct({
|
||||
user: Codec.lenientArray(
|
||||
Codec.struct({
|
||||
providerID: Codec.string,
|
||||
modelID: Codec.string,
|
||||
visibility: Codec.literals(["show", "hide"]),
|
||||
favorite: Codec.optional(Codec.boolean),
|
||||
}),
|
||||
),
|
||||
recent: Persistence.array(Persistence.struct({ providerID: Schema.String, modelID: Schema.String })),
|
||||
variant: Schema.Record(
|
||||
Schema.String,
|
||||
Schema.mutableKey(
|
||||
Schema.UndefinedOr(Schema.String).pipe(Schema.catchDecoding(() => Effect.succeed(Option.none()))),
|
||||
),
|
||||
),
|
||||
recent: Codec.lenientArray(Codec.struct({ providerID: Codec.string, modelID: Codec.string })),
|
||||
variant: Codec.sparseRecord(Codec.undefinedOr(Codec.string)),
|
||||
})
|
||||
|
||||
export const VcsState = Persistence.struct({
|
||||
value: Schema.optional(
|
||||
Persistence.struct({
|
||||
branch: Schema.optional(Schema.String),
|
||||
default_branch: Schema.optional(Schema.String),
|
||||
export const VcsState = Codec.struct({
|
||||
value: Codec.optional(
|
||||
Codec.struct({
|
||||
branch: Codec.optional(Codec.string),
|
||||
default_branch: Codec.optional(Codec.string),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
const ProjectMeta = Persistence.struct({
|
||||
name: Schema.optional(Schema.String),
|
||||
icon: Schema.optional(
|
||||
Persistence.struct({
|
||||
override: Schema.optional(Schema.String),
|
||||
color: Schema.optional(Schema.String),
|
||||
const ProjectMeta = Codec.struct({
|
||||
name: Codec.optional(Codec.string),
|
||||
icon: Codec.optional(
|
||||
Codec.struct({
|
||||
override: Codec.optional(Codec.string),
|
||||
color: Codec.optional(Codec.string),
|
||||
}),
|
||||
),
|
||||
commands: Schema.optional(Persistence.struct({ start: Schema.optional(Schema.String) })),
|
||||
commands: Codec.optional(Codec.struct({ start: Codec.optional(Codec.string) })),
|
||||
})
|
||||
|
||||
export const ProjectState = Persistence.struct({
|
||||
value: Schema.optional(ProjectMeta),
|
||||
export const ProjectState = Codec.struct({
|
||||
value: Codec.optional(ProjectMeta),
|
||||
})
|
||||
|
||||
export const IconState = Persistence.struct({
|
||||
value: Schema.optional(Schema.String),
|
||||
export const IconState = Codec.struct({
|
||||
value: Codec.optional(Codec.string),
|
||||
})
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { canRemoveServer, createServerProjects, resolveServerList, ServerConnection } from "./registry"
|
||||
import { Schema } from "effect"
|
||||
import { serverState } from "./persistence"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { ServerScope } from "./scope"
|
||||
import { Persistence } from "@/runtime/persistence/schema"
|
||||
import { Codec } from "@/runtime/persistence/codec"
|
||||
|
||||
function serverSchema() {
|
||||
return Persistence.withInitial(serverState(), {
|
||||
return Codec.withInitial(serverState(), {
|
||||
list: [],
|
||||
hidden: {},
|
||||
projects: {},
|
||||
@@ -19,7 +18,7 @@ function serverSchema() {
|
||||
describe("resolveServerList", () => {
|
||||
test("lets startup auth_token credentials override a persisted same-url server", () => {
|
||||
const list = resolveServerList({
|
||||
stored: Schema.decodeUnknownSync(serverSchema())({ list: [{ url: "https://server.example.test" }] }).list,
|
||||
stored: Codec.decodeOrThrow(serverSchema(), { list: [{ url: "https://server.example.test" }] }).list,
|
||||
props: [
|
||||
{
|
||||
type: "http",
|
||||
@@ -44,7 +43,7 @@ describe("resolveServerList", () => {
|
||||
|
||||
test("keeps persisted credentials when startup has no auth_token", () => {
|
||||
const list = resolveServerList({
|
||||
stored: Schema.decodeUnknownSync(serverSchema())({
|
||||
stored: Codec.decodeOrThrow(serverSchema(), {
|
||||
list: [{ url: "https://server.example.test", password: "saved" }],
|
||||
}).list,
|
||||
props: [{ type: "http", http: { url: "https://server.example.test" } }],
|
||||
@@ -77,7 +76,7 @@ test("treats WSL sidecars as remote server connections", () => {
|
||||
})
|
||||
|
||||
test("keeps exact persisted server identities and prevents removing provided servers", () => {
|
||||
const stored = Schema.decodeUnknownSync(serverSchema())({
|
||||
const stored = Codec.decodeOrThrow(serverSchema(), {
|
||||
list: ["http://localhost:4096", "http://localhost:4096/", "http://127.0.0.1:4096"],
|
||||
}).list
|
||||
expect(resolveServerList({ stored }).map((server) => String(ServerConnection.key(server)))).toEqual([
|
||||
@@ -91,7 +90,7 @@ test("keeps exact persisted server identities and prevents removing provided ser
|
||||
})
|
||||
|
||||
test("project actions update schema-derived state and follow dynamic server scopes", () => {
|
||||
const [store, setStore] = createStore(Schema.decodeUnknownSync(serverSchema())({}))
|
||||
const [store, setStore] = createStore(Codec.decodeOrThrow(serverSchema(), {}))
|
||||
const props: { server: ServerConnection.Key; canonicalLocalServer?: ServerConnection.Key } = {
|
||||
server: ServerConnection.Key.make("https://remote.example"),
|
||||
}
|
||||
@@ -115,3 +114,4 @@ test("project actions update schema-derived state and follow dynamic server scop
|
||||
expect(store.projects.local).toEqual([{ worktree: "/local", expanded: true }])
|
||||
expect(store.projects[props.server]).toEqual([{ worktree: "/remote", expanded: false }])
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user