chore: initialize qiming workspace repository
This commit is contained in:
14
qimingcode/packages/opencode/test/util/data-url.test.ts
Normal file
14
qimingcode/packages/opencode/test/util/data-url.test.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { decodeDataUrl } from "../../src/util/data-url"
|
||||
|
||||
describe("decodeDataUrl", () => {
|
||||
test("decodes base64 data URLs", () => {
|
||||
const body = '{\n "ok": true\n}\n'
|
||||
const url = `data:text/plain;base64,${Buffer.from(body).toString("base64")}`
|
||||
expect(decodeDataUrl(url)).toBe(body)
|
||||
})
|
||||
|
||||
test("decodes plain data URLs", () => {
|
||||
expect(decodeDataUrl("data:text/plain,hello%20world")).toBe("hello world")
|
||||
})
|
||||
})
|
||||
754
qimingcode/packages/opencode/test/util/effect-zod.test.ts
Normal file
754
qimingcode/packages/opencode/test/util/effect-zod.test.ts
Normal file
@@ -0,0 +1,754 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect, Schema, SchemaGetter } from "effect"
|
||||
import z from "zod"
|
||||
|
||||
import { zod, ZodOverride } from "../../src/util/effect-zod"
|
||||
|
||||
function json(schema: z.ZodTypeAny) {
|
||||
const { $schema: _, ...rest } = z.toJSONSchema(schema)
|
||||
return rest
|
||||
}
|
||||
|
||||
describe("util.effect-zod", () => {
|
||||
test("converts class schemas for route dto shapes", () => {
|
||||
class Method extends Schema.Class<Method>("ProviderAuthMethod")({
|
||||
type: Schema.Union([Schema.Literal("oauth"), Schema.Literal("api")]),
|
||||
label: Schema.String,
|
||||
}) {}
|
||||
|
||||
const out = zod(Method)
|
||||
|
||||
expect(out.meta()?.ref).toBe("ProviderAuthMethod")
|
||||
expect(
|
||||
out.parse({
|
||||
type: "oauth",
|
||||
label: "OAuth",
|
||||
}),
|
||||
).toEqual({
|
||||
type: "oauth",
|
||||
label: "OAuth",
|
||||
})
|
||||
})
|
||||
|
||||
test("converts structs with optional fields, arrays, and records", () => {
|
||||
const out = zod(
|
||||
Schema.Struct({
|
||||
foo: Schema.optional(Schema.String),
|
||||
bar: Schema.Array(Schema.Number),
|
||||
baz: Schema.Record(Schema.String, Schema.Boolean),
|
||||
}),
|
||||
)
|
||||
|
||||
expect(
|
||||
out.parse({
|
||||
bar: [1, 2],
|
||||
baz: { ok: true },
|
||||
}),
|
||||
).toEqual({
|
||||
bar: [1, 2],
|
||||
baz: { ok: true },
|
||||
})
|
||||
expect(
|
||||
out.parse({
|
||||
foo: "hi",
|
||||
bar: [1],
|
||||
baz: { ok: false },
|
||||
}),
|
||||
).toEqual({
|
||||
foo: "hi",
|
||||
bar: [1],
|
||||
baz: { ok: false },
|
||||
})
|
||||
})
|
||||
|
||||
describe("Tuples", () => {
|
||||
test("fixed-length tuple parses matching array", () => {
|
||||
const out = zod(Schema.Tuple([Schema.String, Schema.Number]))
|
||||
expect(out.parse(["a", 1])).toEqual(["a", 1])
|
||||
expect(out.safeParse(["a"]).success).toBe(false)
|
||||
expect(out.safeParse(["a", "b"]).success).toBe(false)
|
||||
})
|
||||
|
||||
test("single-element tuple parses a one-element array", () => {
|
||||
const out = zod(Schema.Tuple([Schema.Boolean]))
|
||||
expect(out.parse([true])).toEqual([true])
|
||||
expect(out.safeParse([true, false]).success).toBe(false)
|
||||
})
|
||||
|
||||
test("tuple inside a union picks the right branch", () => {
|
||||
const out = zod(Schema.Union([Schema.String, Schema.Tuple([Schema.String, Schema.Number])]))
|
||||
expect(out.parse("hello")).toBe("hello")
|
||||
expect(out.parse(["foo", 42])).toEqual(["foo", 42])
|
||||
expect(out.safeParse(["foo"]).success).toBe(false)
|
||||
})
|
||||
|
||||
test("plain arrays still work (no element positions)", () => {
|
||||
const out = zod(Schema.Array(Schema.String))
|
||||
expect(out.parse(["a", "b", "c"])).toEqual(["a", "b", "c"])
|
||||
expect(out.parse([])).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
test("string literal unions produce z.enum with enum in JSON Schema", () => {
|
||||
const Action = Schema.Literals(["allow", "deny", "ask"])
|
||||
const out = zod(Action)
|
||||
|
||||
expect(out.parse("allow")).toBe("allow")
|
||||
expect(out.parse("deny")).toBe("deny")
|
||||
expect(() => out.parse("nope")).toThrow()
|
||||
|
||||
// Matches native z.enum JSON Schema output
|
||||
const bridged = json(out)
|
||||
const native = json(z.enum(["allow", "deny", "ask"]))
|
||||
expect(bridged).toEqual(native)
|
||||
expect(bridged.enum).toEqual(["allow", "deny", "ask"])
|
||||
})
|
||||
|
||||
test("ZodOverride annotation provides the Zod schema for branded IDs", () => {
|
||||
const override = z.string().startsWith("per")
|
||||
const ID = Schema.String.annotate({ [ZodOverride]: override }).pipe(Schema.brand("TestID"))
|
||||
|
||||
const Parent = Schema.Struct({ id: ID, name: Schema.String })
|
||||
const out = zod(Parent)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
expect((out as any).parse({ id: "per_abc", name: "test" })).toEqual({ id: "per_abc", name: "test" })
|
||||
|
||||
const schema = json(out) as any
|
||||
expect(schema.properties.id).toEqual({ type: "string", pattern: "^per.*" })
|
||||
})
|
||||
|
||||
test("Schema.Class nested in a parent preserves ref via identifier", () => {
|
||||
class Inner extends Schema.Class<Inner>("MyInner")({
|
||||
value: Schema.String,
|
||||
}) {}
|
||||
|
||||
class Outer extends Schema.Class<Outer>("MyOuter")({
|
||||
inner: Inner,
|
||||
}) {}
|
||||
|
||||
const out = zod(Outer)
|
||||
expect(out.meta()?.ref).toBe("MyOuter")
|
||||
|
||||
const shape = (out as any).shape ?? (out as any)._def?.shape?.()
|
||||
expect(shape.inner.meta()?.ref).toBe("MyInner")
|
||||
})
|
||||
|
||||
test("Schema.Class preserves identifier and uses enum format", () => {
|
||||
class Rule extends Schema.Class<Rule>("PermissionRule")({
|
||||
permission: Schema.String,
|
||||
pattern: Schema.String,
|
||||
action: Schema.Literals(["allow", "deny", "ask"]),
|
||||
}) {}
|
||||
|
||||
const out = zod(Rule)
|
||||
expect(out.meta()?.ref).toBe("PermissionRule")
|
||||
|
||||
const schema = json(out) as any
|
||||
expect(schema.properties.action).toEqual({
|
||||
type: "string",
|
||||
enum: ["allow", "deny", "ask"],
|
||||
})
|
||||
})
|
||||
|
||||
test("ZodOverride on ID carries pattern through Schema.Class", () => {
|
||||
const ID = Schema.String.annotate({
|
||||
[ZodOverride]: z.string().startsWith("per"),
|
||||
})
|
||||
|
||||
class Request extends Schema.Class<Request>("TestRequest")({
|
||||
id: ID,
|
||||
name: Schema.String,
|
||||
}) {}
|
||||
|
||||
const schema = json(zod(Request)) as any
|
||||
expect(schema.properties.id).toEqual({ type: "string", pattern: "^per.*" })
|
||||
expect(schema.properties.name).toEqual({ type: "string" })
|
||||
})
|
||||
|
||||
test("Permission schemas match original Zod equivalents", () => {
|
||||
const MsgID = Schema.String.annotate({ [ZodOverride]: z.string().startsWith("msg") })
|
||||
const PerID = Schema.String.annotate({ [ZodOverride]: z.string().startsWith("per") })
|
||||
const SesID = Schema.String.annotate({ [ZodOverride]: z.string().startsWith("ses") })
|
||||
|
||||
class Tool extends Schema.Class<Tool>("PermissionTool")({
|
||||
messageID: MsgID,
|
||||
callID: Schema.String,
|
||||
}) {}
|
||||
|
||||
class Request extends Schema.Class<Request>("PermissionRequest")({
|
||||
id: PerID,
|
||||
sessionID: SesID,
|
||||
permission: Schema.String,
|
||||
patterns: Schema.Array(Schema.String),
|
||||
metadata: Schema.Record(Schema.String, Schema.Unknown),
|
||||
always: Schema.Array(Schema.String),
|
||||
tool: Schema.optional(Tool),
|
||||
}) {}
|
||||
|
||||
const bridged = json(zod(Request)) as any
|
||||
expect(bridged.properties.id).toEqual({ type: "string", pattern: "^per.*" })
|
||||
expect(bridged.properties.sessionID).toEqual({ type: "string", pattern: "^ses.*" })
|
||||
expect(bridged.properties.permission).toEqual({ type: "string" })
|
||||
expect(bridged.required?.sort()).toEqual(["id", "sessionID", "permission", "patterns", "metadata", "always"].sort())
|
||||
|
||||
// Tool field is present with the ref from Schema.Class identifier
|
||||
const toolSchema = json(zod(Tool)) as any
|
||||
expect(toolSchema.properties.messageID).toEqual({ type: "string", pattern: "^msg.*" })
|
||||
expect(toolSchema.properties.callID).toEqual({ type: "string" })
|
||||
})
|
||||
|
||||
test("ZodOverride survives Schema.brand", () => {
|
||||
const override = z.string().startsWith("ses")
|
||||
const ID = Schema.String.annotate({ [ZodOverride]: override }).pipe(Schema.brand("SessionID"))
|
||||
|
||||
// The branded schema's AST still has the override
|
||||
class Parent extends Schema.Class<Parent>("Parent")({
|
||||
sessionID: ID,
|
||||
}) {}
|
||||
|
||||
const schema = json(zod(Parent)) as any
|
||||
expect(schema.properties.sessionID).toEqual({ type: "string", pattern: "^ses.*" })
|
||||
})
|
||||
|
||||
describe("Schema.check translation", () => {
|
||||
test("filter returning string triggers refinement with that message", () => {
|
||||
const isEven = Schema.makeFilter((n: number) => (n % 2 === 0 ? undefined : "expected an even number"))
|
||||
const schema = zod(Schema.Number.check(isEven))
|
||||
|
||||
expect(schema.parse(4)).toBe(4)
|
||||
const result = schema.safeParse(3)
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error!.issues[0].message).toBe("expected an even number")
|
||||
})
|
||||
|
||||
test("filter returning false triggers refinement with fallback message", () => {
|
||||
const nonEmpty = Schema.makeFilter((s: string) => s.length > 0)
|
||||
const schema = zod(Schema.String.check(nonEmpty))
|
||||
|
||||
expect(schema.parse("hi")).toBe("hi")
|
||||
const result = schema.safeParse("")
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error!.issues[0].message).toMatch(/./)
|
||||
})
|
||||
|
||||
test("filter returning undefined passes validation", () => {
|
||||
const alwaysOk = Schema.makeFilter(() => undefined)
|
||||
const schema = zod(Schema.Number.check(alwaysOk))
|
||||
|
||||
expect(schema.parse(42)).toBe(42)
|
||||
})
|
||||
|
||||
test("annotations.message on the filter is used when filter returns false", () => {
|
||||
const positive = Schema.makeFilter((n: number) => n > 0, { message: "must be positive" })
|
||||
const schema = zod(Schema.Number.check(positive))
|
||||
|
||||
const result = schema.safeParse(-1)
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error!.issues[0].message).toBe("must be positive")
|
||||
})
|
||||
|
||||
test("cross-field check on a record flags missing key", () => {
|
||||
const hasKey = Schema.makeFilter((data: Record<string, { enabled: boolean }>) =>
|
||||
"required" in data ? undefined : "missing 'required' key",
|
||||
)
|
||||
const schema = zod(Schema.Record(Schema.String, Schema.Struct({ enabled: Schema.Boolean })).check(hasKey))
|
||||
|
||||
expect(schema.parse({ required: { enabled: true } })).toEqual({
|
||||
required: { enabled: true },
|
||||
})
|
||||
|
||||
const result = schema.safeParse({ other: { enabled: true } })
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error!.issues[0].message).toBe("missing 'required' key")
|
||||
})
|
||||
})
|
||||
|
||||
describe("StructWithRest / catchall", () => {
|
||||
test("struct with a string-keyed record rest parses known AND extra keys", () => {
|
||||
const schema = zod(
|
||||
Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
apiKey: Schema.optional(Schema.String),
|
||||
baseURL: Schema.optional(Schema.String),
|
||||
}),
|
||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||
),
|
||||
)
|
||||
|
||||
// Known fields come through as declared
|
||||
expect(schema.parse({ apiKey: "sk-x" })).toEqual({ apiKey: "sk-x" })
|
||||
|
||||
// Extra keys are preserved (catchall)
|
||||
expect(
|
||||
schema.parse({
|
||||
apiKey: "sk-x",
|
||||
baseURL: "https://api.example.com",
|
||||
customField: "anything",
|
||||
nested: { foo: 1 },
|
||||
}),
|
||||
).toEqual({
|
||||
apiKey: "sk-x",
|
||||
baseURL: "https://api.example.com",
|
||||
customField: "anything",
|
||||
nested: { foo: 1 },
|
||||
})
|
||||
})
|
||||
|
||||
test("catchall value type constrains the extras", () => {
|
||||
const schema = zod(
|
||||
Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
count: Schema.Number,
|
||||
}),
|
||||
[Schema.Record(Schema.String, Schema.Number)],
|
||||
),
|
||||
)
|
||||
|
||||
// Known field + numeric extras
|
||||
expect(schema.parse({ count: 10, a: 1, b: 2 })).toEqual({ count: 10, a: 1, b: 2 })
|
||||
|
||||
// Non-numeric extra is rejected
|
||||
expect(schema.safeParse({ count: 10, bad: "not a number" }).success).toBe(false)
|
||||
})
|
||||
|
||||
test("JSON schema output marks additionalProperties appropriately", () => {
|
||||
const schema = zod(
|
||||
Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
id: Schema.String,
|
||||
}),
|
||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||
),
|
||||
)
|
||||
const shape = json(schema) as { additionalProperties?: unknown }
|
||||
// Presence of `additionalProperties` (truthy or a schema) signals catchall.
|
||||
expect(shape.additionalProperties).not.toBe(false)
|
||||
expect(shape.additionalProperties).toBeDefined()
|
||||
})
|
||||
|
||||
test("plain struct without rest still emits additionalProperties unchanged (regression)", () => {
|
||||
const schema = zod(Schema.Struct({ id: Schema.String }))
|
||||
expect(schema.parse({ id: "x" })).toEqual({ id: "x" })
|
||||
})
|
||||
})
|
||||
|
||||
describe("transforms (Schema.decodeTo)", () => {
|
||||
test("Number -> pseudo-Duration (seconds) applies the decode function", () => {
|
||||
// Models the account/account.ts DurationFromSeconds pattern.
|
||||
const SecondsToMs = Schema.Number.pipe(
|
||||
Schema.decodeTo(Schema.Number, {
|
||||
decode: SchemaGetter.transform((n: number) => n * 1000),
|
||||
encode: SchemaGetter.transform((ms: number) => ms / 1000),
|
||||
}),
|
||||
)
|
||||
|
||||
const schema = zod(SecondsToMs)
|
||||
expect(schema.parse(3)).toBe(3000)
|
||||
expect(schema.parse(0)).toBe(0)
|
||||
})
|
||||
|
||||
test("String -> Number via parseInt decode", () => {
|
||||
const ParsedInt = Schema.String.pipe(
|
||||
Schema.decodeTo(Schema.Number, {
|
||||
decode: SchemaGetter.transform((s: string) => Number.parseInt(s, 10)),
|
||||
encode: SchemaGetter.transform((n: number) => String(n)),
|
||||
}),
|
||||
)
|
||||
|
||||
const schema = zod(ParsedInt)
|
||||
expect(schema.parse("42")).toBe(42)
|
||||
expect(schema.parse("0")).toBe(0)
|
||||
})
|
||||
|
||||
test("transform inside a struct field applies per-field", () => {
|
||||
const Field = Schema.Number.pipe(
|
||||
Schema.decodeTo(Schema.Number, {
|
||||
decode: SchemaGetter.transform((n: number) => n + 1),
|
||||
encode: SchemaGetter.transform((n: number) => n - 1),
|
||||
}),
|
||||
)
|
||||
|
||||
const schema = zod(
|
||||
Schema.Struct({
|
||||
plain: Schema.Number,
|
||||
bumped: Field,
|
||||
}),
|
||||
)
|
||||
|
||||
expect(schema.parse({ plain: 5, bumped: 10 })).toEqual({ plain: 5, bumped: 11 })
|
||||
})
|
||||
|
||||
test("chained decodeTo composes transforms in order", () => {
|
||||
// String -> Number (parseInt) -> Number (doubled).
|
||||
// Exercises the encoded() reduce, not just a single link.
|
||||
const Chained = Schema.String.pipe(
|
||||
Schema.decodeTo(Schema.Number, {
|
||||
decode: SchemaGetter.transform((s: string) => Number.parseInt(s, 10)),
|
||||
encode: SchemaGetter.transform((n: number) => String(n)),
|
||||
}),
|
||||
Schema.decodeTo(Schema.Number, {
|
||||
decode: SchemaGetter.transform((n: number) => n * 2),
|
||||
encode: SchemaGetter.transform((n: number) => n / 2),
|
||||
}),
|
||||
)
|
||||
|
||||
const schema = zod(Chained)
|
||||
expect(schema.parse("21")).toBe(42)
|
||||
expect(schema.parse("0")).toBe(0)
|
||||
})
|
||||
|
||||
test("Schema.Class is unaffected by transform walker (returns plain object, not instance)", () => {
|
||||
// Schema.Class uses Declaration + encoding under the hood to construct
|
||||
// class instances. The walker must NOT apply that transform, or zod
|
||||
// parsing would return class instances instead of plain objects.
|
||||
class Method extends Schema.Class<Method>("TxTestMethod")({
|
||||
type: Schema.String,
|
||||
value: Schema.Number,
|
||||
}) {}
|
||||
|
||||
const schema = zod(Method)
|
||||
const parsed = schema.parse({ type: "oauth", value: 1 })
|
||||
expect(parsed).toEqual({ type: "oauth", value: 1 })
|
||||
// Guardrail: ensure we didn't get back a Method instance.
|
||||
expect(parsed).not.toBeInstanceOf(Method)
|
||||
})
|
||||
})
|
||||
|
||||
describe("optimizations", () => {
|
||||
test("walk() memoizes by AST identity — same AST node returns same Zod", () => {
|
||||
const shared = Schema.Struct({ id: Schema.String, name: Schema.String })
|
||||
const left = zod(shared)
|
||||
const right = zod(shared)
|
||||
expect(left).toBe(right)
|
||||
})
|
||||
|
||||
test("nested reuse of the same AST reuses the cached Zod child", () => {
|
||||
// Two different parents embed the same inner schema. The inner zod
|
||||
// child should be identical by reference inside both parents.
|
||||
class Inner extends Schema.Class<Inner>("MemoTestInner")({
|
||||
value: Schema.String,
|
||||
}) {}
|
||||
|
||||
class OuterA extends Schema.Class<OuterA>("MemoTestOuterA")({
|
||||
inner: Inner,
|
||||
}) {}
|
||||
|
||||
class OuterB extends Schema.Class<OuterB>("MemoTestOuterB")({
|
||||
inner: Inner,
|
||||
}) {}
|
||||
|
||||
const shapeA = (zod(OuterA) as any).shape ?? (zod(OuterA) as any)._def?.shape?.()
|
||||
const shapeB = (zod(OuterB) as any).shape ?? (zod(OuterB) as any)._def?.shape?.()
|
||||
expect(shapeA.inner).toBe(shapeB.inner)
|
||||
})
|
||||
|
||||
test("multiple checks run in a single refinement layer (all fire on one value)", () => {
|
||||
// Three checks attached to the same schema. All three must run and
|
||||
// report — asserting that no check silently got dropped when we
|
||||
// flattened into one superRefine.
|
||||
const positive = Schema.makeFilter((n: number) => (n > 0 ? undefined : "not positive"))
|
||||
const even = Schema.makeFilter((n: number) => (n % 2 === 0 ? undefined : "not even"))
|
||||
const under100 = Schema.makeFilter((n: number) => (n < 100 ? undefined : "too big"))
|
||||
|
||||
const schema = zod(Schema.Number.check(positive).check(even).check(under100))
|
||||
|
||||
const neg = schema.safeParse(-3)
|
||||
expect(neg.success).toBe(false)
|
||||
expect(neg.error!.issues.map((i) => i.message)).toEqual(expect.arrayContaining(["not positive", "not even"]))
|
||||
|
||||
const big = schema.safeParse(101)
|
||||
expect(big.success).toBe(false)
|
||||
expect(big.error!.issues.map((i) => i.message)).toContain("too big")
|
||||
|
||||
// Passing value satisfies all three
|
||||
expect(schema.parse(42)).toBe(42)
|
||||
})
|
||||
|
||||
test("FilterGroup flattens into the single refinement layer alongside its siblings", () => {
|
||||
const positive = Schema.makeFilter((n: number) => (n > 0 ? undefined : "not positive"))
|
||||
const even = Schema.makeFilter((n: number) => (n % 2 === 0 ? undefined : "not even"))
|
||||
const group = Schema.makeFilterGroup([positive, even])
|
||||
const under100 = Schema.makeFilter((n: number) => (n < 100 ? undefined : "too big"))
|
||||
|
||||
const schema = zod(Schema.Number.check(group).check(under100))
|
||||
|
||||
const bad = schema.safeParse(-3)
|
||||
expect(bad.success).toBe(false)
|
||||
expect(bad.error!.issues.map((i) => i.message)).toEqual(expect.arrayContaining(["not positive", "not even"]))
|
||||
})
|
||||
})
|
||||
|
||||
describe("well-known refinement translation", () => {
|
||||
test("Schema.isInt emits type: integer in JSON Schema", () => {
|
||||
const schema = zod(Schema.Number.check(Schema.isInt()))
|
||||
const native = json(z.number().int())
|
||||
expect(json(schema)).toEqual(native)
|
||||
expect(schema.parse(3)).toBe(3)
|
||||
expect(schema.safeParse(1.5).success).toBe(false)
|
||||
})
|
||||
|
||||
test("Schema.isGreaterThan(0) emits exclusiveMinimum: 0", () => {
|
||||
const schema = zod(Schema.Number.check(Schema.isGreaterThan(0)))
|
||||
expect((json(schema) as any).exclusiveMinimum).toBe(0)
|
||||
expect(schema.parse(1)).toBe(1)
|
||||
expect(schema.safeParse(0).success).toBe(false)
|
||||
expect(schema.safeParse(-1).success).toBe(false)
|
||||
})
|
||||
|
||||
test("Schema.isGreaterThanOrEqualTo(0) emits minimum: 0", () => {
|
||||
const schema = zod(Schema.Number.check(Schema.isGreaterThanOrEqualTo(0)))
|
||||
expect((json(schema) as any).minimum).toBe(0)
|
||||
expect(schema.parse(0)).toBe(0)
|
||||
expect(schema.safeParse(-1).success).toBe(false)
|
||||
})
|
||||
|
||||
test("Schema.isLessThan(10) emits exclusiveMaximum: 10", () => {
|
||||
const schema = zod(Schema.Number.check(Schema.isLessThan(10)))
|
||||
expect((json(schema) as any).exclusiveMaximum).toBe(10)
|
||||
expect(schema.parse(9)).toBe(9)
|
||||
expect(schema.safeParse(10).success).toBe(false)
|
||||
})
|
||||
|
||||
test("Schema.isLessThanOrEqualTo(10) emits maximum: 10", () => {
|
||||
const schema = zod(Schema.Number.check(Schema.isLessThanOrEqualTo(10)))
|
||||
expect((json(schema) as any).maximum).toBe(10)
|
||||
expect(schema.parse(10)).toBe(10)
|
||||
expect(schema.safeParse(11).success).toBe(false)
|
||||
})
|
||||
|
||||
test("Schema.isMultipleOf(5) emits multipleOf: 5", () => {
|
||||
const schema = zod(Schema.Number.check(Schema.isMultipleOf(5)))
|
||||
expect((json(schema) as any).multipleOf).toBe(5)
|
||||
expect(schema.parse(10)).toBe(10)
|
||||
expect(schema.safeParse(7).success).toBe(false)
|
||||
})
|
||||
|
||||
test("Schema.isFinite validates at runtime", () => {
|
||||
const schema = zod(Schema.Number.check(Schema.isFinite()))
|
||||
expect(schema.parse(1)).toBe(1)
|
||||
expect(schema.safeParse(Infinity).success).toBe(false)
|
||||
expect(schema.safeParse(NaN).success).toBe(false)
|
||||
})
|
||||
|
||||
test("chained isInt + isGreaterThan(0) matches z.number().int().positive()", () => {
|
||||
const schema = zod(Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0)))
|
||||
const native = json(z.number().int().positive())
|
||||
expect(json(schema)).toEqual(native)
|
||||
expect(schema.parse(3)).toBe(3)
|
||||
expect(schema.safeParse(0).success).toBe(false)
|
||||
expect(schema.safeParse(1.5).success).toBe(false)
|
||||
})
|
||||
|
||||
test("chained isInt + isGreaterThanOrEqualTo(0) matches z.number().int().min(0)", () => {
|
||||
const schema = zod(Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)))
|
||||
const native = json(z.number().int().min(0))
|
||||
expect(json(schema)).toEqual(native)
|
||||
expect(schema.parse(0)).toBe(0)
|
||||
expect(schema.safeParse(-1).success).toBe(false)
|
||||
})
|
||||
|
||||
test("Schema.isBetween emits both bounds", () => {
|
||||
const schema = zod(Schema.Number.check(Schema.isBetween({ minimum: 1, maximum: 10 })))
|
||||
const shape = json(schema) as any
|
||||
expect(shape.minimum).toBe(1)
|
||||
expect(shape.maximum).toBe(10)
|
||||
expect(schema.parse(5)).toBe(5)
|
||||
expect(schema.safeParse(11).success).toBe(false)
|
||||
expect(schema.safeParse(0).success).toBe(false)
|
||||
})
|
||||
|
||||
test("Schema.isBetween with exclusive bounds emits exclusiveMinimum/Maximum", () => {
|
||||
const schema = zod(
|
||||
Schema.Number.check(
|
||||
Schema.isBetween({ minimum: 1, maximum: 10, exclusiveMinimum: true, exclusiveMaximum: true }),
|
||||
),
|
||||
)
|
||||
const shape = json(schema) as any
|
||||
expect(shape.exclusiveMinimum).toBe(1)
|
||||
expect(shape.exclusiveMaximum).toBe(10)
|
||||
expect(schema.parse(5)).toBe(5)
|
||||
expect(schema.safeParse(1).success).toBe(false)
|
||||
expect(schema.safeParse(10).success).toBe(false)
|
||||
})
|
||||
|
||||
test("Schema.isInt32 (FilterGroup) produces integer bounds", () => {
|
||||
const schema = zod(Schema.Number.check(Schema.isInt32()))
|
||||
const shape = json(schema) as any
|
||||
expect(shape.type).toBe("integer")
|
||||
expect(shape.minimum).toBe(-2147483648)
|
||||
expect(shape.maximum).toBe(2147483647)
|
||||
expect(schema.parse(42)).toBe(42)
|
||||
expect(schema.safeParse(1.5).success).toBe(false)
|
||||
expect(schema.safeParse(2147483648).success).toBe(false)
|
||||
})
|
||||
|
||||
test("Schema.isMinLength on string emits minLength", () => {
|
||||
const schema = zod(Schema.String.check(Schema.isMinLength(3)))
|
||||
expect((json(schema) as any).minLength).toBe(3)
|
||||
expect(schema.parse("abc")).toBe("abc")
|
||||
expect(schema.safeParse("ab").success).toBe(false)
|
||||
})
|
||||
|
||||
test("Schema.isMaxLength on string emits maxLength", () => {
|
||||
const schema = zod(Schema.String.check(Schema.isMaxLength(5)))
|
||||
expect((json(schema) as any).maxLength).toBe(5)
|
||||
expect(schema.parse("abcde")).toBe("abcde")
|
||||
expect(schema.safeParse("abcdef").success).toBe(false)
|
||||
})
|
||||
|
||||
test("Schema.isLengthBetween on string emits both bounds", () => {
|
||||
const schema = zod(Schema.String.check(Schema.isLengthBetween(2, 4)))
|
||||
const shape = json(schema) as any
|
||||
expect(shape.minLength).toBe(2)
|
||||
expect(shape.maxLength).toBe(4)
|
||||
expect(schema.parse("abc")).toBe("abc")
|
||||
expect(schema.safeParse("a").success).toBe(false)
|
||||
expect(schema.safeParse("abcde").success).toBe(false)
|
||||
})
|
||||
|
||||
test("Schema.isMinLength on array emits minItems", () => {
|
||||
const schema = zod(Schema.Array(Schema.String).check(Schema.isMinLength(1)))
|
||||
expect((json(schema) as any).minItems).toBe(1)
|
||||
expect(schema.parse(["x"])).toEqual(["x"])
|
||||
expect(schema.safeParse([]).success).toBe(false)
|
||||
})
|
||||
|
||||
test("Schema.isPattern emits pattern", () => {
|
||||
const schema = zod(Schema.String.check(Schema.isPattern(/^per/)))
|
||||
expect((json(schema) as any).pattern).toBe("^per")
|
||||
expect(schema.parse("per_abc")).toBe("per_abc")
|
||||
expect(schema.safeParse("abc").success).toBe(false)
|
||||
})
|
||||
|
||||
test("Schema.isStartsWith matches native zod .startsWith() JSON Schema", () => {
|
||||
const schema = zod(Schema.String.check(Schema.isStartsWith("per")))
|
||||
const native = json(z.string().startsWith("per"))
|
||||
expect(json(schema)).toEqual(native)
|
||||
expect(schema.parse("per_abc")).toBe("per_abc")
|
||||
expect(schema.safeParse("abc").success).toBe(false)
|
||||
})
|
||||
|
||||
test("Schema.isEndsWith matches native zod .endsWith() JSON Schema", () => {
|
||||
const schema = zod(Schema.String.check(Schema.isEndsWith(".json")))
|
||||
const native = json(z.string().endsWith(".json"))
|
||||
expect(json(schema)).toEqual(native)
|
||||
expect(schema.parse("a.json")).toBe("a.json")
|
||||
expect(schema.safeParse("a.txt").success).toBe(false)
|
||||
})
|
||||
|
||||
test("Schema.isUUID emits format: uuid", () => {
|
||||
const schema = zod(Schema.String.check(Schema.isUUID()))
|
||||
expect((json(schema) as any).format).toBe("uuid")
|
||||
})
|
||||
|
||||
test("mix of well-known and anonymous filters translates known and reroutes unknown to superRefine", () => {
|
||||
// isInt is well-known (translates to .int()); the anonymous filter falls
|
||||
// back to superRefine.
|
||||
const notSeven = Schema.makeFilter((n: number) => (n !== 7 ? undefined : "no sevens allowed"))
|
||||
const schema = zod(Schema.Number.check(Schema.isInt()).check(notSeven))
|
||||
|
||||
const shape = json(schema) as any
|
||||
// Well-known translation is preserved — type is integer, not plain number
|
||||
expect(shape.type).toBe("integer")
|
||||
|
||||
// Runtime: both constraints fire
|
||||
expect(schema.parse(3)).toBe(3)
|
||||
expect(schema.safeParse(1.5).success).toBe(false)
|
||||
const seven = schema.safeParse(7)
|
||||
expect(seven.success).toBe(false)
|
||||
expect(seven.error!.issues[0].message).toBe("no sevens allowed")
|
||||
})
|
||||
|
||||
test("inside a struct field, well-known refinements propagate through", () => {
|
||||
// Mirrors config.ts port: z.number().int().positive().optional()
|
||||
const Port = Schema.optional(Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0)))
|
||||
const schema = zod(Schema.Struct({ port: Port }))
|
||||
const shape = json(schema) as any
|
||||
expect(shape.properties.port.type).toBe("integer")
|
||||
expect(shape.properties.port.exclusiveMinimum).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Schema.optionalWith defaults", () => {
|
||||
test("parsing undefined returns the default value", () => {
|
||||
const schema = zod(
|
||||
Schema.Struct({
|
||||
mode: Schema.String.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed("ctrl-x"))),
|
||||
}),
|
||||
)
|
||||
expect(schema.parse({})).toEqual({ mode: "ctrl-x" })
|
||||
expect(schema.parse({ mode: undefined })).toEqual({ mode: "ctrl-x" })
|
||||
})
|
||||
|
||||
test("parsing a real value returns that value (default does not fire)", () => {
|
||||
const schema = zod(
|
||||
Schema.Struct({
|
||||
mode: Schema.String.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed("ctrl-x"))),
|
||||
}),
|
||||
)
|
||||
expect(schema.parse({ mode: "ctrl-y" })).toEqual({ mode: "ctrl-y" })
|
||||
})
|
||||
|
||||
test("default on a number field", () => {
|
||||
const schema = zod(
|
||||
Schema.Struct({
|
||||
count: Schema.Number.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed(42))),
|
||||
}),
|
||||
)
|
||||
expect(schema.parse({})).toEqual({ count: 42 })
|
||||
expect(schema.parse({ count: 7 })).toEqual({ count: 7 })
|
||||
})
|
||||
|
||||
test("multiple defaulted fields inside a struct", () => {
|
||||
const schema = zod(
|
||||
Schema.Struct({
|
||||
leader: Schema.String.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed("ctrl-x"))),
|
||||
quit: Schema.String.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed("ctrl-c"))),
|
||||
inner: Schema.String,
|
||||
}),
|
||||
)
|
||||
expect(schema.parse({ inner: "hi" })).toEqual({
|
||||
leader: "ctrl-x",
|
||||
quit: "ctrl-c",
|
||||
inner: "hi",
|
||||
})
|
||||
expect(schema.parse({ leader: "a", quit: "b", inner: "c" })).toEqual({
|
||||
leader: "a",
|
||||
quit: "b",
|
||||
inner: "c",
|
||||
})
|
||||
})
|
||||
|
||||
test("JSON Schema output includes the default key", () => {
|
||||
const schema = zod(
|
||||
Schema.Struct({
|
||||
mode: Schema.String.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed("ctrl-x"))),
|
||||
}),
|
||||
)
|
||||
const shape = json(schema) as any
|
||||
expect(shape.properties.mode.default).toBe("ctrl-x")
|
||||
})
|
||||
|
||||
test("default referencing a computed value resolves when evaluated", () => {
|
||||
// Simulates `keybinds.ts` style of per-platform defaults: the default is
|
||||
// produced by an Effect that computes a value at decode time.
|
||||
const platform = "darwin"
|
||||
const fallback = platform === "darwin" ? "cmd-k" : "ctrl-k"
|
||||
const schema = zod(
|
||||
Schema.Struct({
|
||||
command_palette: Schema.String.pipe(Schema.optional, Schema.withDecodingDefault(Effect.sync(() => fallback))),
|
||||
}),
|
||||
)
|
||||
expect(schema.parse({})).toEqual({ command_palette: "cmd-k" })
|
||||
const shape = json(schema) as any
|
||||
expect(shape.properties.command_palette.default).toBe("cmd-k")
|
||||
})
|
||||
|
||||
test("plain Schema.optional (no default) still emits .optional() (regression)", () => {
|
||||
const schema = zod(Schema.Struct({ foo: Schema.optional(Schema.String) }))
|
||||
expect(schema.parse({})).toEqual({})
|
||||
expect(schema.parse({ foo: "hi" })).toEqual({ foo: "hi" })
|
||||
})
|
||||
})
|
||||
})
|
||||
38
qimingcode/packages/opencode/test/util/error.test.ts
Normal file
38
qimingcode/packages/opencode/test/util/error.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { errorData, errorFormat, errorMessage } from "../../src/util/error"
|
||||
|
||||
describe("util.error", () => {
|
||||
test("formats native Error instances", () => {
|
||||
const err = new Error("boom")
|
||||
expect(errorMessage(err)).toBe("boom")
|
||||
expect(errorFormat(err)).toContain("boom")
|
||||
|
||||
const data = errorData(err)
|
||||
expect(data.type).toBe("Error")
|
||||
expect(data.message).toBe("boom")
|
||||
expect(String(data.formatted)).toContain("boom")
|
||||
})
|
||||
|
||||
test("extracts message from record-like values", () => {
|
||||
const err = { message: "bad input", code: "E_BAD" }
|
||||
expect(errorMessage(err)).toBe("bad input")
|
||||
|
||||
const data = errorData(err)
|
||||
expect(data.message).toBe("bad input")
|
||||
expect(data.code).toBe("E_BAD")
|
||||
})
|
||||
|
||||
test("handles opaque throwables with custom toString", () => {
|
||||
const err = {
|
||||
toString() {
|
||||
return "ResolveMessage: Cannot resolve module"
|
||||
},
|
||||
}
|
||||
|
||||
expect(errorMessage(err)).toBe("ResolveMessage: Cannot resolve module")
|
||||
|
||||
const data = errorData(err)
|
||||
expect(data.message).toBe("ResolveMessage: Cannot resolve module")
|
||||
expect(String(data.formatted)).toContain("ResolveMessage")
|
||||
})
|
||||
})
|
||||
656
qimingcode/packages/opencode/test/util/filesystem.test.ts
Normal file
656
qimingcode/packages/opencode/test/util/filesystem.test.ts
Normal file
@@ -0,0 +1,656 @@
|
||||
import { describe, test, expect } from "bun:test"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { Filesystem } from "../../src/util"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
describe("filesystem", () => {
|
||||
describe("exists()", () => {
|
||||
test("returns true for existing file", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "test.txt")
|
||||
await fs.writeFile(filepath, "content", "utf-8")
|
||||
|
||||
expect(await Filesystem.exists(filepath)).toBe(true)
|
||||
})
|
||||
|
||||
test("returns false for non-existent file", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "does-not-exist.txt")
|
||||
|
||||
expect(await Filesystem.exists(filepath)).toBe(false)
|
||||
})
|
||||
|
||||
test("returns true for existing directory", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const dirpath = path.join(tmp.path, "subdir")
|
||||
await fs.mkdir(dirpath)
|
||||
|
||||
expect(await Filesystem.exists(dirpath)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isDir()", () => {
|
||||
test("returns true for directory", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const dirpath = path.join(tmp.path, "testdir")
|
||||
await fs.mkdir(dirpath)
|
||||
|
||||
expect(await Filesystem.isDir(dirpath)).toBe(true)
|
||||
})
|
||||
|
||||
test("returns false for file", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "test.txt")
|
||||
await fs.writeFile(filepath, "content", "utf-8")
|
||||
|
||||
expect(await Filesystem.isDir(filepath)).toBe(false)
|
||||
})
|
||||
|
||||
test("returns false for non-existent path", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "does-not-exist")
|
||||
|
||||
expect(await Filesystem.isDir(filepath)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("size()", () => {
|
||||
test("returns file size", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "test.txt")
|
||||
const content = "Hello, World!"
|
||||
await fs.writeFile(filepath, content, "utf-8")
|
||||
|
||||
expect(await Filesystem.size(filepath)).toBe(content.length)
|
||||
})
|
||||
|
||||
test("returns 0 for non-existent file", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "does-not-exist.txt")
|
||||
|
||||
expect(await Filesystem.size(filepath)).toBe(0)
|
||||
})
|
||||
|
||||
test("returns directory size", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const dirpath = path.join(tmp.path, "testdir")
|
||||
await fs.mkdir(dirpath)
|
||||
|
||||
// Directories have size on some systems
|
||||
const size = await Filesystem.size(dirpath)
|
||||
expect(typeof size).toBe("number")
|
||||
})
|
||||
})
|
||||
|
||||
describe("findUp()", () => {
|
||||
test("keeps previous nearest-first behavior for single target", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const parent = path.join(tmp.path, "parent")
|
||||
const child = path.join(parent, "child")
|
||||
await fs.mkdir(child, { recursive: true })
|
||||
await fs.writeFile(path.join(tmp.path, "marker"), "root", "utf-8")
|
||||
await fs.writeFile(path.join(parent, "marker"), "parent", "utf-8")
|
||||
|
||||
const result = await Filesystem.findUp("marker", child, tmp.path)
|
||||
|
||||
expect(result).toEqual([path.join(parent, "marker"), path.join(tmp.path, "marker")])
|
||||
})
|
||||
|
||||
test("respects stop boundary", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const parent = path.join(tmp.path, "parent")
|
||||
const child = path.join(parent, "child")
|
||||
await fs.mkdir(child, { recursive: true })
|
||||
await fs.writeFile(path.join(tmp.path, "marker"), "root", "utf-8")
|
||||
await fs.writeFile(path.join(parent, "marker"), "parent", "utf-8")
|
||||
|
||||
const result = await Filesystem.findUp("marker", child, parent)
|
||||
|
||||
expect(result).toEqual([path.join(parent, "marker")])
|
||||
})
|
||||
|
||||
test("supports multiple targets with nearest-first default ordering", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const parent = path.join(tmp.path, "parent")
|
||||
const child = path.join(parent, "child")
|
||||
await fs.mkdir(child, { recursive: true })
|
||||
|
||||
await fs.writeFile(path.join(parent, "cfg.jsonc"), "{}", "utf-8")
|
||||
await fs.writeFile(path.join(tmp.path, "cfg.json"), "{}", "utf-8")
|
||||
await fs.writeFile(path.join(tmp.path, "cfg.jsonc"), "{}", "utf-8")
|
||||
|
||||
const result = await Filesystem.findUp(["cfg.json", "cfg.jsonc"], child, tmp.path)
|
||||
|
||||
expect(result).toEqual([
|
||||
path.join(parent, "cfg.jsonc"),
|
||||
path.join(tmp.path, "cfg.json"),
|
||||
path.join(tmp.path, "cfg.jsonc"),
|
||||
])
|
||||
})
|
||||
|
||||
test("supports rootFirst ordering for multiple targets", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const parent = path.join(tmp.path, "parent")
|
||||
const child = path.join(parent, "child")
|
||||
await fs.mkdir(child, { recursive: true })
|
||||
|
||||
await fs.writeFile(path.join(parent, "cfg.jsonc"), "{}", "utf-8")
|
||||
await fs.writeFile(path.join(tmp.path, "cfg.json"), "{}", "utf-8")
|
||||
await fs.writeFile(path.join(tmp.path, "cfg.jsonc"), "{}", "utf-8")
|
||||
|
||||
const result = await Filesystem.findUp(["cfg.json", "cfg.jsonc"], child, tmp.path, { rootFirst: true })
|
||||
|
||||
expect(result).toEqual([
|
||||
path.join(tmp.path, "cfg.json"),
|
||||
path.join(tmp.path, "cfg.jsonc"),
|
||||
path.join(parent, "cfg.jsonc"),
|
||||
])
|
||||
})
|
||||
|
||||
test("rootFirst preserves json then jsonc order per directory", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const project = path.join(tmp.path, "project")
|
||||
const nested = path.join(project, "nested")
|
||||
await fs.mkdir(nested, { recursive: true })
|
||||
|
||||
await fs.writeFile(path.join(tmp.path, "opencode.json"), "{}", "utf-8")
|
||||
await fs.writeFile(path.join(tmp.path, "opencode.jsonc"), "{}", "utf-8")
|
||||
await fs.writeFile(path.join(project, "opencode.json"), "{}", "utf-8")
|
||||
await fs.writeFile(path.join(project, "opencode.jsonc"), "{}", "utf-8")
|
||||
|
||||
const result = await Filesystem.findUp(["opencode.json", "opencode.jsonc"], nested, tmp.path, {
|
||||
rootFirst: true,
|
||||
})
|
||||
|
||||
expect(result).toEqual([
|
||||
path.join(tmp.path, "opencode.json"),
|
||||
path.join(tmp.path, "opencode.jsonc"),
|
||||
path.join(project, "opencode.json"),
|
||||
path.join(project, "opencode.jsonc"),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("readText()", () => {
|
||||
test("reads file content", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "test.txt")
|
||||
const content = "Hello, World!"
|
||||
await fs.writeFile(filepath, content, "utf-8")
|
||||
|
||||
expect(await Filesystem.readText(filepath)).toBe(content)
|
||||
})
|
||||
|
||||
test("throws for non-existent file", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "does-not-exist.txt")
|
||||
|
||||
await expect(Filesystem.readText(filepath)).rejects.toThrow()
|
||||
})
|
||||
|
||||
test("reads UTF-8 content correctly", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "unicode.txt")
|
||||
const content = "Hello 世界 🌍"
|
||||
await fs.writeFile(filepath, content, "utf-8")
|
||||
|
||||
expect(await Filesystem.readText(filepath)).toBe(content)
|
||||
})
|
||||
})
|
||||
|
||||
describe("readJson()", () => {
|
||||
test("reads and parses JSON", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "test.json")
|
||||
const data = { key: "value", nested: { array: [1, 2, 3] } }
|
||||
await fs.writeFile(filepath, JSON.stringify(data), "utf-8")
|
||||
|
||||
const result: typeof data = await Filesystem.readJson(filepath)
|
||||
expect(result).toEqual(data)
|
||||
})
|
||||
|
||||
test("throws for invalid JSON", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "invalid.json")
|
||||
await fs.writeFile(filepath, "{ invalid json", "utf-8")
|
||||
|
||||
await expect(Filesystem.readJson(filepath)).rejects.toThrow()
|
||||
})
|
||||
|
||||
test("throws for non-existent file", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "does-not-exist.json")
|
||||
|
||||
await expect(Filesystem.readJson(filepath)).rejects.toThrow()
|
||||
})
|
||||
|
||||
test("returns typed data", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "typed.json")
|
||||
interface Config {
|
||||
name: string
|
||||
version: number
|
||||
}
|
||||
const data: Config = { name: "test", version: 1 }
|
||||
await fs.writeFile(filepath, JSON.stringify(data), "utf-8")
|
||||
|
||||
const result = await Filesystem.readJson<Config>(filepath)
|
||||
expect(result.name).toBe("test")
|
||||
expect(result.version).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("readBytes()", () => {
|
||||
test("reads file as buffer", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "test.txt")
|
||||
const content = "Hello, World!"
|
||||
await fs.writeFile(filepath, content, "utf-8")
|
||||
|
||||
const buffer = await Filesystem.readBytes(filepath)
|
||||
expect(buffer).toBeInstanceOf(Buffer)
|
||||
expect(buffer.toString("utf-8")).toBe(content)
|
||||
})
|
||||
|
||||
test("throws for non-existent file", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "does-not-exist.bin")
|
||||
|
||||
await expect(Filesystem.readBytes(filepath)).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("write()", () => {
|
||||
test("writes text content", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "test.txt")
|
||||
const content = "Hello, World!"
|
||||
|
||||
await Filesystem.write(filepath, content)
|
||||
|
||||
expect(await fs.readFile(filepath, "utf-8")).toBe(content)
|
||||
})
|
||||
|
||||
test("writes buffer content", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "test.bin")
|
||||
const content = Buffer.from([0x00, 0x01, 0x02, 0x03])
|
||||
|
||||
await Filesystem.write(filepath, content)
|
||||
|
||||
const read = await fs.readFile(filepath)
|
||||
expect(read).toEqual(content)
|
||||
})
|
||||
|
||||
test("writes with permissions", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "protected.txt")
|
||||
const content = "secret"
|
||||
|
||||
await Filesystem.write(filepath, content, 0o600)
|
||||
|
||||
const stats = await fs.stat(filepath)
|
||||
// Check permissions on Unix
|
||||
if (process.platform !== "win32") {
|
||||
expect(stats.mode & 0o777).toBe(0o600)
|
||||
}
|
||||
})
|
||||
|
||||
test("creates parent directories", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "nested", "deep", "file.txt")
|
||||
const content = "nested content"
|
||||
|
||||
await Filesystem.write(filepath, content)
|
||||
|
||||
expect(await fs.readFile(filepath, "utf-8")).toBe(content)
|
||||
})
|
||||
})
|
||||
|
||||
describe("writeJson()", () => {
|
||||
test("writes JSON data", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "data.json")
|
||||
const data = { key: "value", number: 42 }
|
||||
|
||||
await Filesystem.writeJson(filepath, data)
|
||||
|
||||
const content = await fs.readFile(filepath, "utf-8")
|
||||
expect(JSON.parse(content)).toEqual(data)
|
||||
})
|
||||
|
||||
test("writes formatted JSON", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "pretty.json")
|
||||
const data = { key: "value" }
|
||||
|
||||
await Filesystem.writeJson(filepath, data)
|
||||
|
||||
const content = await fs.readFile(filepath, "utf-8")
|
||||
expect(content).toContain("\n")
|
||||
expect(content).toContain(" ")
|
||||
})
|
||||
|
||||
test("writes with permissions", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "config.json")
|
||||
const data = { secret: "data" }
|
||||
|
||||
await Filesystem.writeJson(filepath, data, 0o600)
|
||||
|
||||
const stats = await fs.stat(filepath)
|
||||
if (process.platform !== "win32") {
|
||||
expect(stats.mode & 0o777).toBe(0o600)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("mimeType()", () => {
|
||||
test("returns correct MIME type for JSON", async () => {
|
||||
expect(await Filesystem.mimeType("test.json")).toContain("application/json")
|
||||
})
|
||||
|
||||
test("returns correct MIME type for JavaScript", async () => {
|
||||
expect(await Filesystem.mimeType("test.js")).toContain("javascript")
|
||||
})
|
||||
|
||||
test("returns MIME type for TypeScript (or video/mp2t due to extension conflict)", async () => {
|
||||
const mime = await Filesystem.mimeType("test.ts")
|
||||
// .ts is ambiguous: TypeScript vs MPEG-2 TS video
|
||||
expect(mime === "video/mp2t" || mime === "application/typescript" || mime === "text/typescript").toBe(true)
|
||||
})
|
||||
|
||||
test("returns correct MIME type for images", async () => {
|
||||
expect(await Filesystem.mimeType("test.png")).toContain("image/png")
|
||||
expect(await Filesystem.mimeType("test.jpg")).toContain("image/jpeg")
|
||||
})
|
||||
|
||||
test("returns default for unknown extension", async () => {
|
||||
expect(await Filesystem.mimeType("test.unknown")).toBe("application/octet-stream")
|
||||
})
|
||||
|
||||
test("handles files without extension", async () => {
|
||||
expect(await Filesystem.mimeType("Makefile")).toBe("application/octet-stream")
|
||||
})
|
||||
})
|
||||
|
||||
describe("windowsPath()", () => {
|
||||
test("converts Git Bash paths", () => {
|
||||
if (process.platform === "win32") {
|
||||
expect(Filesystem.windowsPath("/c/Users/test")).toBe("C:/Users/test")
|
||||
expect(Filesystem.windowsPath("/d/dev/project")).toBe("D:/dev/project")
|
||||
} else {
|
||||
expect(Filesystem.windowsPath("/c/Users/test")).toBe("/c/Users/test")
|
||||
}
|
||||
})
|
||||
|
||||
test("converts Cygwin paths", () => {
|
||||
if (process.platform === "win32") {
|
||||
expect(Filesystem.windowsPath("/cygdrive/c/Users/test")).toBe("C:/Users/test")
|
||||
expect(Filesystem.windowsPath("/cygdrive/x/dev/project")).toBe("X:/dev/project")
|
||||
} else {
|
||||
expect(Filesystem.windowsPath("/cygdrive/c/Users/test")).toBe("/cygdrive/c/Users/test")
|
||||
}
|
||||
})
|
||||
|
||||
test("converts WSL paths", () => {
|
||||
if (process.platform === "win32") {
|
||||
expect(Filesystem.windowsPath("/mnt/c/Users/test")).toBe("C:/Users/test")
|
||||
expect(Filesystem.windowsPath("/mnt/z/dev/project")).toBe("Z:/dev/project")
|
||||
} else {
|
||||
expect(Filesystem.windowsPath("/mnt/c/Users/test")).toBe("/mnt/c/Users/test")
|
||||
}
|
||||
})
|
||||
|
||||
test("ignores normal Windows paths", () => {
|
||||
expect(Filesystem.windowsPath("C:/Users/test")).toBe("C:/Users/test")
|
||||
expect(Filesystem.windowsPath("D:\\dev\\project")).toBe("D:\\dev\\project")
|
||||
})
|
||||
})
|
||||
|
||||
describe("writeStream()", () => {
|
||||
test("writes from Web ReadableStream", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "streamed.txt")
|
||||
const content = "Hello from stream!"
|
||||
const encoder = new TextEncoder()
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(encoder.encode(content))
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
|
||||
await Filesystem.writeStream(filepath, stream)
|
||||
|
||||
expect(await fs.readFile(filepath, "utf-8")).toBe(content)
|
||||
})
|
||||
|
||||
test("writes from Node.js Readable stream", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "node-streamed.txt")
|
||||
const content = "Hello from Node stream!"
|
||||
const { Readable } = await import("stream")
|
||||
const stream = Readable.from([content])
|
||||
|
||||
await Filesystem.writeStream(filepath, stream)
|
||||
|
||||
expect(await fs.readFile(filepath, "utf-8")).toBe(content)
|
||||
})
|
||||
|
||||
test("writes binary data from Web ReadableStream", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "binary.dat")
|
||||
const binaryData = new Uint8Array([0x00, 0x01, 0x02, 0x03, 0xff])
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(binaryData)
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
|
||||
await Filesystem.writeStream(filepath, stream)
|
||||
|
||||
const read = await fs.readFile(filepath)
|
||||
expect(Buffer.from(read)).toEqual(Buffer.from(binaryData))
|
||||
})
|
||||
|
||||
test("writes large content in chunks", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "large.txt")
|
||||
const chunks = ["chunk1", "chunk2", "chunk3", "chunk4", "chunk5"]
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
for (const chunk of chunks) {
|
||||
controller.enqueue(new TextEncoder().encode(chunk))
|
||||
}
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
|
||||
await Filesystem.writeStream(filepath, stream)
|
||||
|
||||
expect(await fs.readFile(filepath, "utf-8")).toBe(chunks.join(""))
|
||||
})
|
||||
|
||||
test("creates parent directories", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "nested", "deep", "streamed.txt")
|
||||
const content = "nested stream content"
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode(content))
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
|
||||
await Filesystem.writeStream(filepath, stream)
|
||||
|
||||
expect(await fs.readFile(filepath, "utf-8")).toBe(content)
|
||||
})
|
||||
|
||||
test("writes with permissions", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "protected-stream.txt")
|
||||
const content = "secret stream content"
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode(content))
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
|
||||
await Filesystem.writeStream(filepath, stream, 0o600)
|
||||
|
||||
const stats = await fs.stat(filepath)
|
||||
if (process.platform !== "win32") {
|
||||
expect(stats.mode & 0o777).toBe(0o600)
|
||||
}
|
||||
})
|
||||
|
||||
test("writes executable with permissions", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filepath = path.join(tmp.path, "script.sh")
|
||||
const content = "#!/bin/bash\necho hello"
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode(content))
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
|
||||
await Filesystem.writeStream(filepath, stream, 0o755)
|
||||
|
||||
const stats = await fs.stat(filepath)
|
||||
if (process.platform !== "win32") {
|
||||
expect(stats.mode & 0o777).toBe(0o755)
|
||||
}
|
||||
expect(await fs.readFile(filepath, "utf-8")).toBe(content)
|
||||
})
|
||||
})
|
||||
|
||||
describe("resolve()", () => {
|
||||
test("resolves slash-prefixed drive paths on Windows", async () => {
|
||||
if (process.platform !== "win32") return
|
||||
await using tmp = await tmpdir()
|
||||
const forward = tmp.path.replaceAll("\\", "/")
|
||||
expect(Filesystem.resolve(`/${forward}`)).toBe(Filesystem.normalizePath(tmp.path))
|
||||
})
|
||||
|
||||
test("resolves slash-prefixed drive roots on Windows", async () => {
|
||||
if (process.platform !== "win32") return
|
||||
await using tmp = await tmpdir()
|
||||
const drive = tmp.path[0].toUpperCase()
|
||||
expect(Filesystem.resolve(`/${drive}:`)).toBe(Filesystem.resolve(`${drive}:/`))
|
||||
})
|
||||
|
||||
test("resolves Git Bash and MSYS2 paths on Windows", async () => {
|
||||
// Git Bash and MSYS2 both use /<drive>/... paths on Windows.
|
||||
if (process.platform !== "win32") return
|
||||
await using tmp = await tmpdir()
|
||||
const drive = tmp.path[0].toLowerCase()
|
||||
const rest = tmp.path.slice(2).replaceAll("\\", "/")
|
||||
expect(Filesystem.resolve(`/${drive}${rest}`)).toBe(Filesystem.normalizePath(tmp.path))
|
||||
})
|
||||
|
||||
test("resolves Git Bash and MSYS2 drive roots on Windows", async () => {
|
||||
// Git Bash and MSYS2 both use /<drive> paths on Windows.
|
||||
if (process.platform !== "win32") return
|
||||
await using tmp = await tmpdir()
|
||||
const drive = tmp.path[0].toLowerCase()
|
||||
expect(Filesystem.resolve(`/${drive}`)).toBe(Filesystem.resolve(`${drive.toUpperCase()}:/`))
|
||||
})
|
||||
|
||||
test("resolves Cygwin paths on Windows", async () => {
|
||||
if (process.platform !== "win32") return
|
||||
await using tmp = await tmpdir()
|
||||
const drive = tmp.path[0].toLowerCase()
|
||||
const rest = tmp.path.slice(2).replaceAll("\\", "/")
|
||||
expect(Filesystem.resolve(`/cygdrive/${drive}${rest}`)).toBe(Filesystem.normalizePath(tmp.path))
|
||||
})
|
||||
|
||||
test("resolves Cygwin drive roots on Windows", async () => {
|
||||
if (process.platform !== "win32") return
|
||||
await using tmp = await tmpdir()
|
||||
const drive = tmp.path[0].toLowerCase()
|
||||
expect(Filesystem.resolve(`/cygdrive/${drive}`)).toBe(Filesystem.resolve(`${drive.toUpperCase()}:/`))
|
||||
})
|
||||
|
||||
test("resolves WSL mount paths on Windows", async () => {
|
||||
if (process.platform !== "win32") return
|
||||
await using tmp = await tmpdir()
|
||||
const drive = tmp.path[0].toLowerCase()
|
||||
const rest = tmp.path.slice(2).replaceAll("\\", "/")
|
||||
expect(Filesystem.resolve(`/mnt/${drive}${rest}`)).toBe(Filesystem.normalizePath(tmp.path))
|
||||
})
|
||||
|
||||
test("resolves WSL mount roots on Windows", async () => {
|
||||
if (process.platform !== "win32") return
|
||||
await using tmp = await tmpdir()
|
||||
const drive = tmp.path[0].toLowerCase()
|
||||
expect(Filesystem.resolve(`/mnt/${drive}`)).toBe(Filesystem.resolve(`${drive.toUpperCase()}:/`))
|
||||
})
|
||||
|
||||
test("resolves symlinked directory to canonical path", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const target = path.join(tmp.path, "real")
|
||||
await fs.mkdir(target)
|
||||
const link = path.join(tmp.path, "link")
|
||||
await fs.symlink(target, link)
|
||||
expect(Filesystem.resolve(link)).toBe(Filesystem.resolve(target))
|
||||
})
|
||||
|
||||
test("returns unresolved path when target does not exist", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const missing = path.join(tmp.path, "does-not-exist-" + Date.now())
|
||||
const result = Filesystem.resolve(missing)
|
||||
expect(result).toBe(Filesystem.normalizePath(path.resolve(missing)))
|
||||
})
|
||||
|
||||
test("throws ELOOP on symlink cycle", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const a = path.join(tmp.path, "a")
|
||||
const b = path.join(tmp.path, "b")
|
||||
await fs.symlink(b, a)
|
||||
await fs.symlink(a, b)
|
||||
expect(() => Filesystem.resolve(a)).toThrow()
|
||||
})
|
||||
|
||||
// Windows: chmod(0o000) is a no-op, so EACCES cannot be triggered
|
||||
test("throws EACCES on permission-denied symlink target", async () => {
|
||||
if (process.platform === "win32") return
|
||||
if (process.getuid?.() === 0) return // skip when running as root
|
||||
await using tmp = await tmpdir()
|
||||
const dir = path.join(tmp.path, "restricted")
|
||||
await fs.mkdir(dir)
|
||||
const link = path.join(tmp.path, "link")
|
||||
await fs.symlink(dir, link)
|
||||
await fs.chmod(dir, 0o000)
|
||||
try {
|
||||
expect(() => Filesystem.resolve(path.join(link, "child"))).toThrow()
|
||||
} finally {
|
||||
await fs.chmod(dir, 0o755)
|
||||
}
|
||||
})
|
||||
|
||||
// Windows: traversing through a file throws ENOENT (not ENOTDIR),
|
||||
// which resolve() catches as a fallback instead of rethrowing
|
||||
test("rethrows non-ENOENT errors", async () => {
|
||||
if (process.platform === "win32") return
|
||||
await using tmp = await tmpdir()
|
||||
const file = path.join(tmp.path, "not-a-directory")
|
||||
await fs.writeFile(file, "x")
|
||||
expect(() => Filesystem.resolve(path.join(file, "child"))).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("normalizePathPattern()", () => {
|
||||
test("preserves drive root globs on Windows", async () => {
|
||||
if (process.platform !== "win32") return
|
||||
await using tmp = await tmpdir()
|
||||
const root = path.parse(tmp.path).root
|
||||
expect(Filesystem.normalizePathPattern(path.join(root, "*"))).toBe(path.join(root, "*"))
|
||||
})
|
||||
})
|
||||
})
|
||||
59
qimingcode/packages/opencode/test/util/format.test.ts
Normal file
59
qimingcode/packages/opencode/test/util/format.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { formatDuration } from "../../src/util/format"
|
||||
|
||||
describe("util.format", () => {
|
||||
describe("formatDuration", () => {
|
||||
test("returns empty string for zero or negative values", () => {
|
||||
expect(formatDuration(0)).toBe("")
|
||||
expect(formatDuration(-1)).toBe("")
|
||||
expect(formatDuration(-100)).toBe("")
|
||||
})
|
||||
|
||||
test("formats seconds under a minute", () => {
|
||||
expect(formatDuration(1)).toBe("1s")
|
||||
expect(formatDuration(30)).toBe("30s")
|
||||
expect(formatDuration(59)).toBe("59s")
|
||||
})
|
||||
|
||||
test("formats minutes under an hour", () => {
|
||||
expect(formatDuration(60)).toBe("1m")
|
||||
expect(formatDuration(61)).toBe("1m 1s")
|
||||
expect(formatDuration(90)).toBe("1m 30s")
|
||||
expect(formatDuration(120)).toBe("2m")
|
||||
expect(formatDuration(330)).toBe("5m 30s")
|
||||
expect(formatDuration(3599)).toBe("59m 59s")
|
||||
})
|
||||
|
||||
test("formats hours under a day", () => {
|
||||
expect(formatDuration(3600)).toBe("1h")
|
||||
expect(formatDuration(3660)).toBe("1h 1m")
|
||||
expect(formatDuration(7200)).toBe("2h")
|
||||
expect(formatDuration(8100)).toBe("2h 15m")
|
||||
expect(formatDuration(86399)).toBe("23h 59m")
|
||||
})
|
||||
|
||||
test("formats days under a week", () => {
|
||||
expect(formatDuration(86400)).toBe("~1 day")
|
||||
expect(formatDuration(172800)).toBe("~2 days")
|
||||
expect(formatDuration(259200)).toBe("~3 days")
|
||||
expect(formatDuration(604799)).toBe("~6 days")
|
||||
})
|
||||
|
||||
test("formats weeks", () => {
|
||||
expect(formatDuration(604800)).toBe("~1 week")
|
||||
expect(formatDuration(1209600)).toBe("~2 weeks")
|
||||
expect(formatDuration(1609200)).toBe("~2 weeks")
|
||||
})
|
||||
|
||||
test("handles boundary values correctly", () => {
|
||||
expect(formatDuration(59)).toBe("59s")
|
||||
expect(formatDuration(60)).toBe("1m")
|
||||
expect(formatDuration(3599)).toBe("59m 59s")
|
||||
expect(formatDuration(3600)).toBe("1h")
|
||||
expect(formatDuration(86399)).toBe("23h 59m")
|
||||
expect(formatDuration(86400)).toBe("~1 day")
|
||||
expect(formatDuration(604799)).toBe("~6 days")
|
||||
expect(formatDuration(604800)).toBe("~1 week")
|
||||
})
|
||||
})
|
||||
})
|
||||
164
qimingcode/packages/opencode/test/util/glob.test.ts
Normal file
164
qimingcode/packages/opencode/test/util/glob.test.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { describe, test, expect } from "bun:test"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { Glob } from "@opencode-ai/core/util/glob"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
describe("Glob", () => {
|
||||
describe("scan()", () => {
|
||||
test("finds files matching pattern", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.writeFile(path.join(tmp.path, "a.txt"), "", "utf-8")
|
||||
await fs.writeFile(path.join(tmp.path, "b.txt"), "", "utf-8")
|
||||
await fs.writeFile(path.join(tmp.path, "c.md"), "", "utf-8")
|
||||
|
||||
const results = await Glob.scan("*.txt", { cwd: tmp.path })
|
||||
|
||||
expect(results.sort()).toEqual(["a.txt", "b.txt"])
|
||||
})
|
||||
|
||||
test("returns absolute paths when absolute option is true", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.writeFile(path.join(tmp.path, "file.txt"), "", "utf-8")
|
||||
|
||||
const results = await Glob.scan("*.txt", { cwd: tmp.path, absolute: true })
|
||||
|
||||
expect(results[0]).toBe(path.join(tmp.path, "file.txt"))
|
||||
})
|
||||
|
||||
test("excludes directories by default", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.mkdir(path.join(tmp.path, "subdir"))
|
||||
await fs.writeFile(path.join(tmp.path, "file.txt"), "", "utf-8")
|
||||
|
||||
const results = await Glob.scan("*", { cwd: tmp.path })
|
||||
|
||||
expect(results).toEqual(["file.txt"])
|
||||
})
|
||||
|
||||
test("excludes directories when include is 'file'", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.mkdir(path.join(tmp.path, "subdir"))
|
||||
await fs.writeFile(path.join(tmp.path, "file.txt"), "", "utf-8")
|
||||
|
||||
const results = await Glob.scan("*", { cwd: tmp.path, include: "file" })
|
||||
|
||||
expect(results).toEqual(["file.txt"])
|
||||
})
|
||||
|
||||
test("includes directories when include is 'all'", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.mkdir(path.join(tmp.path, "subdir"))
|
||||
await fs.writeFile(path.join(tmp.path, "file.txt"), "", "utf-8")
|
||||
|
||||
const results = await Glob.scan("*", { cwd: tmp.path, include: "all" })
|
||||
|
||||
expect(results.sort()).toEqual(["file.txt", "subdir"])
|
||||
})
|
||||
|
||||
test("handles nested patterns", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.mkdir(path.join(tmp.path, "nested"), { recursive: true })
|
||||
await fs.writeFile(path.join(tmp.path, "nested", "deep.txt"), "", "utf-8")
|
||||
|
||||
const results = await Glob.scan("**/*.txt", { cwd: tmp.path })
|
||||
|
||||
expect(results).toEqual([path.join("nested", "deep.txt")])
|
||||
})
|
||||
|
||||
test("returns empty array for no matches", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
const results = await Glob.scan("*.nonexistent", { cwd: tmp.path })
|
||||
|
||||
expect(results).toEqual([])
|
||||
})
|
||||
|
||||
test("does not follow symlinks by default", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.mkdir(path.join(tmp.path, "realdir"))
|
||||
await fs.writeFile(path.join(tmp.path, "realdir", "file.txt"), "", "utf-8")
|
||||
await fs.symlink(path.join(tmp.path, "realdir"), path.join(tmp.path, "linkdir"))
|
||||
|
||||
const results = await Glob.scan("**/*.txt", { cwd: tmp.path })
|
||||
|
||||
expect(results).toEqual([path.join("realdir", "file.txt")])
|
||||
})
|
||||
|
||||
test("follows symlinks when symlink option is true", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.mkdir(path.join(tmp.path, "realdir"))
|
||||
await fs.writeFile(path.join(tmp.path, "realdir", "file.txt"), "", "utf-8")
|
||||
await fs.symlink(path.join(tmp.path, "realdir"), path.join(tmp.path, "linkdir"))
|
||||
|
||||
const results = await Glob.scan("**/*.txt", { cwd: tmp.path, symlink: true })
|
||||
|
||||
expect(results.sort()).toEqual([path.join("linkdir", "file.txt"), path.join("realdir", "file.txt")])
|
||||
})
|
||||
|
||||
test("includes dotfiles when dot option is true", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.writeFile(path.join(tmp.path, ".hidden"), "", "utf-8")
|
||||
await fs.writeFile(path.join(tmp.path, "visible"), "", "utf-8")
|
||||
|
||||
const results = await Glob.scan("*", { cwd: tmp.path, dot: true })
|
||||
|
||||
expect(results.sort()).toEqual([".hidden", "visible"])
|
||||
})
|
||||
|
||||
test("excludes dotfiles when dot option is false", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.writeFile(path.join(tmp.path, ".hidden"), "", "utf-8")
|
||||
await fs.writeFile(path.join(tmp.path, "visible"), "", "utf-8")
|
||||
|
||||
const results = await Glob.scan("*", { cwd: tmp.path, dot: false })
|
||||
|
||||
expect(results).toEqual(["visible"])
|
||||
})
|
||||
})
|
||||
|
||||
describe("scanSync()", () => {
|
||||
test("finds files matching pattern synchronously", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.writeFile(path.join(tmp.path, "a.txt"), "", "utf-8")
|
||||
await fs.writeFile(path.join(tmp.path, "b.txt"), "", "utf-8")
|
||||
|
||||
const results = Glob.scanSync("*.txt", { cwd: tmp.path })
|
||||
|
||||
expect(results.sort()).toEqual(["a.txt", "b.txt"])
|
||||
})
|
||||
|
||||
test("respects options", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.mkdir(path.join(tmp.path, "subdir"))
|
||||
await fs.writeFile(path.join(tmp.path, "file.txt"), "", "utf-8")
|
||||
|
||||
const results = Glob.scanSync("*", { cwd: tmp.path, include: "all" })
|
||||
|
||||
expect(results.sort()).toEqual(["file.txt", "subdir"])
|
||||
})
|
||||
})
|
||||
|
||||
describe("match()", () => {
|
||||
test("matches simple patterns", () => {
|
||||
expect(Glob.match("*.txt", "file.txt")).toBe(true)
|
||||
expect(Glob.match("*.txt", "file.js")).toBe(false)
|
||||
})
|
||||
|
||||
test("matches directory patterns", () => {
|
||||
expect(Glob.match("**/*.js", "src/index.js")).toBe(true)
|
||||
expect(Glob.match("**/*.js", "src/index.ts")).toBe(false)
|
||||
})
|
||||
|
||||
test("matches dot files", () => {
|
||||
expect(Glob.match(".*", ".gitignore")).toBe(true)
|
||||
expect(Glob.match("**/*.md", ".github/README.md")).toBe(true)
|
||||
})
|
||||
|
||||
test("matches brace expansion", () => {
|
||||
expect(Glob.match("*.{js,ts}", "file.js")).toBe(true)
|
||||
expect(Glob.match("*.{js,ts}", "file.ts")).toBe(true)
|
||||
expect(Glob.match("*.{js,ts}", "file.py")).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
36
qimingcode/packages/opencode/test/util/iife.test.ts
Normal file
36
qimingcode/packages/opencode/test/util/iife.test.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { iife } from "../../src/util/iife"
|
||||
|
||||
describe("util.iife", () => {
|
||||
test("should execute function immediately and return result", () => {
|
||||
let called = false
|
||||
const result = iife(() => {
|
||||
called = true
|
||||
return 42
|
||||
})
|
||||
|
||||
expect(called).toBe(true)
|
||||
expect(result).toBe(42)
|
||||
})
|
||||
|
||||
test("should work with async functions", async () => {
|
||||
let called = false
|
||||
const result = await iife(async () => {
|
||||
called = true
|
||||
return "async result"
|
||||
})
|
||||
|
||||
expect(called).toBe(true)
|
||||
expect(result).toBe("async result")
|
||||
})
|
||||
|
||||
test("should handle functions with no return value", () => {
|
||||
let called = false
|
||||
const result = iife(() => {
|
||||
called = true
|
||||
})
|
||||
|
||||
expect(called).toBe(true)
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
50
qimingcode/packages/opencode/test/util/lazy.test.ts
Normal file
50
qimingcode/packages/opencode/test/util/lazy.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { lazy } from "../../src/util/lazy"
|
||||
|
||||
describe("util.lazy", () => {
|
||||
test("should call function only once", () => {
|
||||
let callCount = 0
|
||||
const getValue = () => {
|
||||
callCount++
|
||||
return "expensive value"
|
||||
}
|
||||
|
||||
const lazyValue = lazy(getValue)
|
||||
|
||||
expect(callCount).toBe(0)
|
||||
|
||||
const result1 = lazyValue()
|
||||
expect(result1).toBe("expensive value")
|
||||
expect(callCount).toBe(1)
|
||||
|
||||
const result2 = lazyValue()
|
||||
expect(result2).toBe("expensive value")
|
||||
expect(callCount).toBe(1)
|
||||
})
|
||||
|
||||
test("should preserve the same reference", () => {
|
||||
const obj = { value: 42 }
|
||||
const lazyObj = lazy(() => obj)
|
||||
|
||||
const result1 = lazyObj()
|
||||
const result2 = lazyObj()
|
||||
|
||||
expect(result1).toBe(obj)
|
||||
expect(result2).toBe(obj)
|
||||
expect(result1).toBe(result2)
|
||||
})
|
||||
|
||||
test("should work with different return types", () => {
|
||||
const lazyString = lazy(() => "string")
|
||||
const lazyNumber = lazy(() => 123)
|
||||
const lazyBoolean = lazy(() => true)
|
||||
const lazyNull = lazy(() => null)
|
||||
const lazyUndefined = lazy(() => undefined)
|
||||
|
||||
expect(lazyString()).toBe("string")
|
||||
expect(lazyNumber()).toBe(123)
|
||||
expect(lazyBoolean()).toBe(true)
|
||||
expect(lazyNull()).toBe(null)
|
||||
expect(lazyUndefined()).toBe(undefined)
|
||||
})
|
||||
})
|
||||
72
qimingcode/packages/opencode/test/util/lock.test.ts
Normal file
72
qimingcode/packages/opencode/test/util/lock.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Lock } from "../../src/util"
|
||||
|
||||
function tick() {
|
||||
return new Promise<void>((r) => queueMicrotask(r))
|
||||
}
|
||||
|
||||
async function flush(n = 5) {
|
||||
for (let i = 0; i < n; i++) await tick()
|
||||
}
|
||||
|
||||
describe("util.lock", () => {
|
||||
test("writer exclusivity: blocks reads and other writes while held", async () => {
|
||||
const key = "lock:" + Math.random().toString(36).slice(2)
|
||||
|
||||
const state = {
|
||||
writer2: false,
|
||||
reader: false,
|
||||
writers: 0,
|
||||
}
|
||||
|
||||
// Acquire writer1
|
||||
using writer1 = await Lock.write(key)
|
||||
state.writers++
|
||||
expect(state.writers).toBe(1)
|
||||
|
||||
// Start writer2 candidate (should block)
|
||||
const writer2Task = (async () => {
|
||||
const w = await Lock.write(key)
|
||||
state.writers++
|
||||
expect(state.writers).toBe(1)
|
||||
state.writer2 = true
|
||||
// Hold for a tick so reader cannot slip in
|
||||
await tick()
|
||||
return w
|
||||
})()
|
||||
|
||||
// Start reader candidate (should block)
|
||||
const readerTask = (async () => {
|
||||
const r = await Lock.read(key)
|
||||
state.reader = true
|
||||
return r
|
||||
})()
|
||||
|
||||
// Flush microtasks and assert neither acquired
|
||||
await flush()
|
||||
expect(state.writer2).toBe(false)
|
||||
expect(state.reader).toBe(false)
|
||||
|
||||
// Release writer1
|
||||
writer1[Symbol.dispose]()
|
||||
state.writers--
|
||||
|
||||
// writer2 should acquire next
|
||||
const writer2 = await writer2Task
|
||||
expect(state.writer2).toBe(true)
|
||||
|
||||
// Reader still blocked while writer2 held
|
||||
await flush()
|
||||
expect(state.reader).toBe(false)
|
||||
|
||||
// Release writer2
|
||||
writer2[Symbol.dispose]()
|
||||
state.writers--
|
||||
|
||||
// Reader should now acquire
|
||||
const reader = await readerTask
|
||||
expect(state.reader).toBe(true)
|
||||
|
||||
reader[Symbol.dispose]()
|
||||
})
|
||||
})
|
||||
44
qimingcode/packages/opencode/test/util/log.test.ts
Normal file
44
qimingcode/packages/opencode/test/util/log.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { afterEach, expect, test } from "bun:test"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { Log } from "../../src/util"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
const log = Global.Path.log
|
||||
|
||||
afterEach(() => {
|
||||
Global.Path.log = log
|
||||
})
|
||||
|
||||
async function files(dir: string) {
|
||||
let last = ""
|
||||
let same = 0
|
||||
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const list = (await fs.readdir(dir)).sort()
|
||||
const next = JSON.stringify(list)
|
||||
same = next === last ? same + 1 : 0
|
||||
if (same >= 2 && list.length === 11) return list
|
||||
last = next
|
||||
await Bun.sleep(10)
|
||||
}
|
||||
|
||||
return (await fs.readdir(dir)).sort()
|
||||
}
|
||||
|
||||
test("init cleanup keeps the newest timestamped logs", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
Global.Path.log = tmp.path
|
||||
|
||||
const list = Array.from({ length: 12 }, (_, i) => `2000-01-${String(i + 1).padStart(2, "0")}T000000.log`)
|
||||
|
||||
await Promise.all(list.map((file) => fs.writeFile(path.join(tmp.path, file), file)))
|
||||
|
||||
await Log.init({ print: false, dev: false })
|
||||
|
||||
const next = await files(tmp.path)
|
||||
|
||||
expect(next).not.toContain(list[0]!)
|
||||
expect(next).toContain(list.at(-1)!)
|
||||
})
|
||||
59
qimingcode/packages/opencode/test/util/module.test.ts
Normal file
59
qimingcode/packages/opencode/test/util/module.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import path from "path"
|
||||
import { Module } from "@opencode-ai/core/util/module"
|
||||
import { Filesystem } from "../../src/util"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
describe("util.module", () => {
|
||||
test("resolves package subpaths from the provided dir", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const root = path.join(tmp.path, "proj")
|
||||
const file = path.join(root, "node_modules/typescript/lib/tsserver.js")
|
||||
await Filesystem.write(file, "export {}\n")
|
||||
await Filesystem.writeJson(path.join(root, "node_modules/typescript/package.json"), { name: "typescript" })
|
||||
|
||||
expect(Module.resolve("typescript/lib/tsserver.js", root)).toBe(file)
|
||||
})
|
||||
|
||||
test("resolves packages through ancestor node_modules", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const root = path.join(tmp.path, "proj")
|
||||
const cwd = path.join(root, "apps/web")
|
||||
const file = path.join(root, "node_modules/eslint/lib/api.js")
|
||||
await Filesystem.write(file, "export {}\n")
|
||||
await Filesystem.writeJson(path.join(root, "node_modules/eslint/package.json"), {
|
||||
name: "eslint",
|
||||
main: "lib/api.js",
|
||||
})
|
||||
await Filesystem.write(path.join(cwd, ".keep"), "")
|
||||
|
||||
expect(Module.resolve("eslint", cwd)).toBe(file)
|
||||
})
|
||||
|
||||
test("resolves relative to the provided dir", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const a = path.join(tmp.path, "a")
|
||||
const b = path.join(tmp.path, "b")
|
||||
const left = path.join(a, "node_modules/biome/index.js")
|
||||
const right = path.join(b, "node_modules/biome/index.js")
|
||||
await Filesystem.write(left, "export {}\n")
|
||||
await Filesystem.write(right, "export {}\n")
|
||||
await Filesystem.writeJson(path.join(a, "node_modules/biome/package.json"), {
|
||||
name: "biome",
|
||||
main: "index.js",
|
||||
})
|
||||
await Filesystem.writeJson(path.join(b, "node_modules/biome/package.json"), {
|
||||
name: "biome",
|
||||
main: "index.js",
|
||||
})
|
||||
|
||||
expect(Module.resolve("biome", a)).toBe(left)
|
||||
expect(Module.resolve("biome", b)).toBe(right)
|
||||
expect(Module.resolve("biome", a)).not.toBe(Module.resolve("biome", b))
|
||||
})
|
||||
|
||||
test("returns undefined when resolution fails", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
expect(Module.resolve("missing-package", tmp.path)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
128
qimingcode/packages/opencode/test/util/process.test.ts
Normal file
128
qimingcode/packages/opencode/test/util/process.test.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Process } from "../../src/util"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
function node(script: string) {
|
||||
return [process.execPath, "-e", script]
|
||||
}
|
||||
|
||||
describe("util.process", () => {
|
||||
test("captures stdout and stderr", async () => {
|
||||
const out = await Process.run(node('process.stdout.write("out");process.stderr.write("err")'))
|
||||
expect(out.code).toBe(0)
|
||||
expect(out.stdout.toString()).toBe("out")
|
||||
expect(out.stderr.toString()).toBe("err")
|
||||
})
|
||||
|
||||
test("returns code when nothrow is enabled", async () => {
|
||||
const out = await Process.run(node("process.exit(7)"), { nothrow: true })
|
||||
expect(out.code).toBe(7)
|
||||
})
|
||||
|
||||
test("throws RunFailedError on non-zero exit", async () => {
|
||||
const err = await Process.run(node('process.stderr.write("bad");process.exit(3)')).catch((error) => error)
|
||||
expect(err).toBeInstanceOf(Process.RunFailedError)
|
||||
if (!(err instanceof Process.RunFailedError)) throw err
|
||||
expect(err.code).toBe(3)
|
||||
expect(err.stderr.toString()).toBe("bad")
|
||||
})
|
||||
|
||||
test("aborts a running process", async () => {
|
||||
const abort = new AbortController()
|
||||
const started = Date.now()
|
||||
setTimeout(() => abort.abort(), 25)
|
||||
|
||||
const out = await Process.run(node("setInterval(() => {}, 1000)"), {
|
||||
abort: abort.signal,
|
||||
nothrow: true,
|
||||
})
|
||||
|
||||
expect(out.code).not.toBe(0)
|
||||
expect(Date.now() - started).toBeLessThan(1000)
|
||||
}, 3000)
|
||||
|
||||
test("kills after timeout when process ignores terminate signal", async () => {
|
||||
if (process.platform === "win32") return
|
||||
|
||||
const abort = new AbortController()
|
||||
const started = Date.now()
|
||||
setTimeout(() => abort.abort(), 25)
|
||||
|
||||
const out = await Process.run(node('process.on("SIGTERM", () => {}); setInterval(() => {}, 1000)'), {
|
||||
abort: abort.signal,
|
||||
nothrow: true,
|
||||
timeout: 25,
|
||||
})
|
||||
|
||||
expect(out.code).not.toBe(0)
|
||||
expect(Date.now() - started).toBeLessThan(1000)
|
||||
}, 3000)
|
||||
|
||||
test("uses cwd when spawning commands", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const out = await Process.run(node("process.stdout.write(process.cwd())"), {
|
||||
cwd: tmp.path,
|
||||
})
|
||||
expect(out.stdout.toString()).toBe(tmp.path)
|
||||
})
|
||||
|
||||
test("merges environment overrides", async () => {
|
||||
const out = await Process.run(node('process.stdout.write(process.env.OPENCODE_TEST ?? "")'), {
|
||||
env: {
|
||||
OPENCODE_TEST: "set",
|
||||
},
|
||||
})
|
||||
expect(out.stdout.toString()).toBe("set")
|
||||
})
|
||||
|
||||
test("uses shell in run on Windows", async () => {
|
||||
if (process.platform !== "win32") return
|
||||
|
||||
const out = await Process.run(["set", "OPENCODE_TEST_SHELL"], {
|
||||
shell: true,
|
||||
env: {
|
||||
OPENCODE_TEST_SHELL: "ok",
|
||||
},
|
||||
})
|
||||
|
||||
expect(out.code).toBe(0)
|
||||
expect(out.stdout.toString()).toContain("OPENCODE_TEST_SHELL=ok")
|
||||
})
|
||||
|
||||
test("runs cmd scripts with spaces on Windows without shell", async () => {
|
||||
if (process.platform !== "win32") return
|
||||
|
||||
await using tmp = await tmpdir()
|
||||
const dir = path.join(tmp.path, "with space")
|
||||
const file = path.join(dir, "echo cmd.cmd")
|
||||
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
await Bun.write(file, "@echo off\r\nif %~1==--stdio exit /b 0\r\nexit /b 7\r\n")
|
||||
|
||||
const proc = Process.spawn([file, "--stdio"], {
|
||||
stdin: "pipe",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
|
||||
expect(await proc.exited).toBe(0)
|
||||
})
|
||||
|
||||
test("rejects missing commands without leaking unhandled errors", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const cmd = path.join(tmp.path, "missing" + (process.platform === "win32" ? ".cmd" : ""))
|
||||
const err = await Process.spawn([cmd], {
|
||||
stdin: "pipe",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
}).exited.catch((err) => err)
|
||||
|
||||
expect(err).toBeInstanceOf(Error)
|
||||
if (!(err instanceof Error)) throw err
|
||||
expect(err).toMatchObject({
|
||||
code: "ENOENT",
|
||||
})
|
||||
})
|
||||
})
|
||||
21
qimingcode/packages/opencode/test/util/timeout.test.ts
Normal file
21
qimingcode/packages/opencode/test/util/timeout.test.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { withTimeout } from "../../src/util/timeout"
|
||||
|
||||
describe("util.timeout", () => {
|
||||
test("should resolve when promise completes before timeout", async () => {
|
||||
const fastPromise = new Promise<string>((resolve) => {
|
||||
setTimeout(() => resolve("fast"), 10)
|
||||
})
|
||||
|
||||
const result = await withTimeout(fastPromise, 100)
|
||||
expect(result).toBe("fast")
|
||||
})
|
||||
|
||||
test("should reject when promise exceeds timeout", async () => {
|
||||
const slowPromise = new Promise<string>((resolve) => {
|
||||
setTimeout(() => resolve("slow"), 200)
|
||||
})
|
||||
|
||||
await expect(withTimeout(slowPromise, 50)).rejects.toThrow("Operation timed out after 50ms")
|
||||
})
|
||||
})
|
||||
100
qimingcode/packages/opencode/test/util/which.test.ts
Normal file
100
qimingcode/packages/opencode/test/util/which.test.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { which } from "../../src/util/which"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
async function cmd(dir: string, name: string, exec = true) {
|
||||
const ext = process.platform === "win32" ? ".cmd" : ""
|
||||
const file = path.join(dir, name + ext)
|
||||
const body = process.platform === "win32" ? "@echo off\r\n" : "#!/bin/sh\n"
|
||||
await fs.writeFile(file, body)
|
||||
if (process.platform !== "win32") {
|
||||
await fs.chmod(file, exec ? 0o755 : 0o644)
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
function env(PATH: string): NodeJS.ProcessEnv {
|
||||
return {
|
||||
PATH,
|
||||
PATHEXT: process.env["PATHEXT"],
|
||||
}
|
||||
}
|
||||
|
||||
function envPath(Path: string): NodeJS.ProcessEnv {
|
||||
return {
|
||||
Path,
|
||||
PathExt: process.env["PathExt"] ?? process.env["PATHEXT"],
|
||||
}
|
||||
}
|
||||
|
||||
function same(a: string | null, b: string) {
|
||||
if (process.platform === "win32") {
|
||||
expect(a?.toLowerCase()).toBe(b.toLowerCase())
|
||||
return
|
||||
}
|
||||
|
||||
expect(a).toBe(b)
|
||||
}
|
||||
|
||||
describe("util.which", () => {
|
||||
test("returns null when command is missing", () => {
|
||||
expect(which("opencode-missing-command-for-test")).toBeNull()
|
||||
})
|
||||
|
||||
test("finds a command from PATH override", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const bin = path.join(tmp.path, "bin")
|
||||
await fs.mkdir(bin)
|
||||
const file = await cmd(bin, "tool")
|
||||
|
||||
same(which("tool", env(bin)), file)
|
||||
})
|
||||
|
||||
test("uses first PATH match", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const a = path.join(tmp.path, "a")
|
||||
const b = path.join(tmp.path, "b")
|
||||
await fs.mkdir(a)
|
||||
await fs.mkdir(b)
|
||||
const first = await cmd(a, "dupe")
|
||||
await cmd(b, "dupe")
|
||||
|
||||
same(which("dupe", env([a, b].join(path.delimiter))), first)
|
||||
})
|
||||
|
||||
test("returns null for non-executable file on unix", async () => {
|
||||
if (process.platform === "win32") return
|
||||
|
||||
await using tmp = await tmpdir()
|
||||
const bin = path.join(tmp.path, "bin")
|
||||
await fs.mkdir(bin)
|
||||
await cmd(bin, "noexec", false)
|
||||
|
||||
expect(which("noexec", env(bin))).toBeNull()
|
||||
})
|
||||
|
||||
test("uses PATHEXT on windows", async () => {
|
||||
if (process.platform !== "win32") return
|
||||
|
||||
await using tmp = await tmpdir()
|
||||
const bin = path.join(tmp.path, "bin")
|
||||
await fs.mkdir(bin)
|
||||
const file = path.join(bin, "pathext.CMD")
|
||||
await fs.writeFile(file, "@echo off\r\n")
|
||||
|
||||
expect(which("pathext", { PATH: bin, PATHEXT: ".CMD" })).toBe(file)
|
||||
})
|
||||
|
||||
test("uses Windows Path casing fallback", async () => {
|
||||
if (process.platform !== "win32") return
|
||||
|
||||
await using tmp = await tmpdir()
|
||||
const bin = path.join(tmp.path, "bin")
|
||||
await fs.mkdir(bin)
|
||||
const file = await cmd(bin, "mixed")
|
||||
|
||||
same(which("mixed", envPath(bin)), file)
|
||||
})
|
||||
})
|
||||
90
qimingcode/packages/opencode/test/util/wildcard.test.ts
Normal file
90
qimingcode/packages/opencode/test/util/wildcard.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { test, expect } from "bun:test"
|
||||
import { Wildcard } from "../../src/util"
|
||||
|
||||
test("match handles glob tokens", () => {
|
||||
expect(Wildcard.match("file1.txt", "file?.txt")).toBe(true)
|
||||
expect(Wildcard.match("file12.txt", "file?.txt")).toBe(false)
|
||||
expect(Wildcard.match("foo+bar", "foo+bar")).toBe(true)
|
||||
})
|
||||
|
||||
test("match with trailing space+wildcard matches command with or without args", () => {
|
||||
// "ls *" should match "ls" (no args) and "ls -la" (with args)
|
||||
expect(Wildcard.match("ls", "ls *")).toBe(true)
|
||||
expect(Wildcard.match("ls -la", "ls *")).toBe(true)
|
||||
expect(Wildcard.match("ls foo bar", "ls *")).toBe(true)
|
||||
|
||||
// "ls*" (no space) should NOT match "ls" alone — wait, it should because .* matches empty
|
||||
// but it WILL match "lstmeval" which is the dangerous case users should avoid
|
||||
expect(Wildcard.match("ls", "ls*")).toBe(true)
|
||||
expect(Wildcard.match("lstmeval", "ls*")).toBe(true)
|
||||
|
||||
// "ls *" (with space) should NOT match "lstmeval"
|
||||
expect(Wildcard.match("lstmeval", "ls *")).toBe(false)
|
||||
|
||||
// multi-word commands
|
||||
expect(Wildcard.match("git status", "git *")).toBe(true)
|
||||
expect(Wildcard.match("git", "git *")).toBe(true)
|
||||
expect(Wildcard.match("git commit -m foo", "git *")).toBe(true)
|
||||
})
|
||||
|
||||
test("all picks the most specific pattern", () => {
|
||||
const rules = {
|
||||
"*": "deny",
|
||||
"git *": "ask",
|
||||
"git status": "allow",
|
||||
}
|
||||
expect(Wildcard.all("git status", rules)).toBe("allow")
|
||||
expect(Wildcard.all("git log", rules)).toBe("ask")
|
||||
expect(Wildcard.all("echo hi", rules)).toBe("deny")
|
||||
})
|
||||
|
||||
test("allStructured matches command sequences", () => {
|
||||
const rules = {
|
||||
"git *": "ask",
|
||||
"git status*": "allow",
|
||||
}
|
||||
expect(Wildcard.allStructured({ head: "git", tail: ["status", "--short"] }, rules)).toBe("allow")
|
||||
expect(Wildcard.allStructured({ head: "npm", tail: ["run", "build", "--watch"] }, { "npm run *": "allow" })).toBe(
|
||||
"allow",
|
||||
)
|
||||
expect(Wildcard.allStructured({ head: "ls", tail: ["-la"] }, rules)).toBeUndefined()
|
||||
})
|
||||
|
||||
test("allStructured prioritizes flag-specific patterns", () => {
|
||||
const rules = {
|
||||
"find *": "allow",
|
||||
"find * -delete*": "ask",
|
||||
"sort*": "allow",
|
||||
"sort -o *": "ask",
|
||||
}
|
||||
expect(Wildcard.allStructured({ head: "find", tail: ["src", "-delete"] }, rules)).toBe("ask")
|
||||
expect(Wildcard.allStructured({ head: "find", tail: ["src", "-print"] }, rules)).toBe("allow")
|
||||
expect(Wildcard.allStructured({ head: "sort", tail: ["-o", "out.txt"] }, rules)).toBe("ask")
|
||||
expect(Wildcard.allStructured({ head: "sort", tail: ["--reverse"] }, rules)).toBe("allow")
|
||||
})
|
||||
|
||||
test("allStructured handles sed flags", () => {
|
||||
const rules = {
|
||||
"sed * -i*": "ask",
|
||||
"sed -n*": "allow",
|
||||
}
|
||||
expect(Wildcard.allStructured({ head: "sed", tail: ["-i", "file"] }, rules)).toBe("ask")
|
||||
expect(Wildcard.allStructured({ head: "sed", tail: ["-i.bak", "file"] }, rules)).toBe("ask")
|
||||
expect(Wildcard.allStructured({ head: "sed", tail: ["-n", "1p", "file"] }, rules)).toBe("allow")
|
||||
expect(Wildcard.allStructured({ head: "sed", tail: ["-i", "-n", "/./p", "myfile.txt"] }, rules)).toBe("ask")
|
||||
})
|
||||
|
||||
test("match normalizes slashes for cross-platform globbing", () => {
|
||||
expect(Wildcard.match("C:\\Windows\\System32\\*", "C:/Windows/System32/*")).toBe(true)
|
||||
expect(Wildcard.match("C:/Windows/System32/drivers", "C:\\Windows\\System32\\*")).toBe(true)
|
||||
})
|
||||
|
||||
test("match handles case-insensitivity on Windows", () => {
|
||||
if (process.platform === "win32") {
|
||||
expect(Wildcard.match("C:\\windows\\system32\\hosts", "C:/Windows/System32/*")).toBe(true)
|
||||
expect(Wildcard.match("c:/windows/system32/hosts", "C:\\Windows\\System32\\*")).toBe(true)
|
||||
} else {
|
||||
// Unix paths are case-sensitive
|
||||
expect(Wildcard.match("/users/test/file", "/Users/test/*")).toBe(false)
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user