chore(quark): sync tests with standalone
This commit is contained in:
parent
2a8ad383ea
commit
f36acdc6b9
3 changed files with 564 additions and 69 deletions
|
|
@ -13,8 +13,8 @@ const Job = Layout.struct({
|
|||
})
|
||||
|
||||
const Jobs = Layout.collection(Job, ({ members, first }) => ({
|
||||
labels: members((job) => job.labels),
|
||||
nextRetry: first((job) => job.status === "retrying"),
|
||||
labels: members(["labels"], (job) => job.labels),
|
||||
nextRetry: first(["status"], (job) => job.status === "retrying"),
|
||||
}))
|
||||
|
||||
describe("Layout", () => {
|
||||
|
|
@ -37,12 +37,17 @@ describe("Layout", () => {
|
|||
"Keyed layout must declare exactly one key field",
|
||||
)
|
||||
expect(() =>
|
||||
Layout.compile(Layout.struct({ left: Layout.key(Layout.number), right: Layout.key(Layout.number) })),
|
||||
Layout.compile(
|
||||
Layout.struct({
|
||||
left: Layout.key(Layout.number),
|
||||
right: Layout.key(Layout.number),
|
||||
}),
|
||||
),
|
||||
).toThrow("Keyed layout must declare exactly one key field")
|
||||
})
|
||||
|
||||
it("generates the same trusted equivalence as the closure backend", () => {
|
||||
const closure = Layout.compile(Item)
|
||||
const closure = Layout.compile(Item, { backend: "closure" })
|
||||
const generated = Layout.compile(Item, { backend: "generated" })
|
||||
const values = [
|
||||
{ id: 1, label: "one" },
|
||||
|
|
@ -53,12 +58,70 @@ describe("Layout", () => {
|
|||
values.forEach((left) => {
|
||||
values.forEach((right) => {
|
||||
expect(generated.equivalent(left, right)).toBe(closure.equivalent(left, right))
|
||||
expect(generated.diff(left, right) === 0).toBe(generated.equivalent(left, right))
|
||||
expect(closure.diff(left, right) === 0).toBe(closure.equivalent(left, right))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("honors custom primitive comparators in generated plans", () => {
|
||||
const CaseInsensitive = {
|
||||
...Layout.string,
|
||||
foldCase: true,
|
||||
equivalent(left: string, right: string) {
|
||||
return this.foldCase ? left.toLowerCase() === right.toLowerCase() : left === right
|
||||
},
|
||||
}
|
||||
const Value = Layout.struct({ id: Layout.key(Layout.number), value: CaseInsensitive })
|
||||
const closure = Layout.compile(Value, { backend: "closure" })
|
||||
const generated = Layout.compile(Value, { backend: "generated" })
|
||||
const left = { id: 1, value: "same" }
|
||||
const right = { id: 1, value: "SAME" }
|
||||
|
||||
expect(closure.equivalent(left, right)).toBe(true)
|
||||
expect(generated.equivalent(left, right)).toBe(true)
|
||||
expect(generated.diff(left, right)).toBe(0)
|
||||
})
|
||||
|
||||
it("keeps generated nested keyed unions independent when they share variants", () => {
|
||||
const variants = {
|
||||
yes: Layout.struct({ value: Layout.string }),
|
||||
no: Layout.struct({ value: Layout.string }),
|
||||
}
|
||||
const Left = Layout.keyedUnion({
|
||||
key: Layout.key("leftID", Layout.number),
|
||||
tag: "leftType",
|
||||
variants,
|
||||
})
|
||||
const Right = Layout.keyedUnion({
|
||||
key: Layout.key("rightID", Layout.number),
|
||||
tag: "rightType",
|
||||
variants,
|
||||
})
|
||||
const Root = Layout.struct({
|
||||
id: Layout.key(Layout.number),
|
||||
left: Left,
|
||||
right: Right,
|
||||
})
|
||||
const value: Layout.Type<typeof Root> = {
|
||||
id: 1,
|
||||
left: { leftID: 1, leftType: "yes", value: "same" },
|
||||
right: { rightID: 1, rightType: "yes", value: "same" },
|
||||
}
|
||||
const copy = structuredClone(value)
|
||||
const closure = Layout.compile(Root, { backend: "closure" })
|
||||
const generated = Layout.compile(Root, { backend: "generated" })
|
||||
|
||||
expect(closure.equivalent(value, copy)).toBe(true)
|
||||
expect(generated.equivalent(value, copy)).toBe(true)
|
||||
expect(generated.diff(value, copy)).toBe(0)
|
||||
})
|
||||
|
||||
it("compiles nested discriminated unions and skips immutable fields", () => {
|
||||
const Ref = Layout.struct({ messageID: Layout.string, partID: Layout.string })
|
||||
const Ref = Layout.struct({
|
||||
messageID: Layout.string,
|
||||
partID: Layout.string,
|
||||
})
|
||||
const Row = Layout.keyedUnion({
|
||||
key: Layout.key("id", Layout.string),
|
||||
tag: "type",
|
||||
|
|
@ -82,7 +145,7 @@ describe("Layout", () => {
|
|||
}),
|
||||
},
|
||||
})
|
||||
const plan = Layout.compile(Row)
|
||||
const plan = Layout.compile(Row, { backend: "closure" })
|
||||
const generated = Layout.compile(Row, { backend: "generated" })
|
||||
const group = {
|
||||
id: "group-1",
|
||||
|
|
@ -94,9 +157,19 @@ describe("Layout", () => {
|
|||
completed: false,
|
||||
}
|
||||
|
||||
expect(plan.equivalent(group, { ...group, origin: { messageID: "ignored", partID: "ignored" } })).toBe(true)
|
||||
expect(
|
||||
plan.equivalent(group, {
|
||||
...group,
|
||||
origin: { messageID: "ignored", partID: "ignored" },
|
||||
}),
|
||||
).toBe(true)
|
||||
expect(plan.equivalent(group, { ...group, completed: true })).toBe(false)
|
||||
expect(plan.equivalent(group, { ...group, pending: [{ messageID: "assistant-1", partID: "read-1" }] })).toBe(false)
|
||||
expect(
|
||||
plan.equivalent(group, {
|
||||
...group,
|
||||
pending: [{ messageID: "assistant-1", partID: "read-1" }],
|
||||
}),
|
||||
).toBe(false)
|
||||
expect(
|
||||
plan.equivalent(group, {
|
||||
id: "group-1",
|
||||
|
|
@ -127,8 +200,14 @@ describe("Layout", () => {
|
|||
})
|
||||
|
||||
it("includes nested keys in structural equivalence", () => {
|
||||
const Child = Layout.struct({ id: Layout.key(Layout.number), value: Layout.string })
|
||||
const Parent = Layout.struct({ id: Layout.key(Layout.number), child: Child })
|
||||
const Child = Layout.struct({
|
||||
id: Layout.key(Layout.number),
|
||||
value: Layout.string,
|
||||
})
|
||||
const Parent = Layout.struct({
|
||||
id: Layout.key(Layout.number),
|
||||
child: Child,
|
||||
})
|
||||
const parent = Layout.compile(Parent)
|
||||
|
||||
expect(
|
||||
|
|
@ -184,6 +263,158 @@ describe("Layout", () => {
|
|||
expect(jobs.first("nextRetry")?.().id).toBe("four")
|
||||
})
|
||||
|
||||
it("skips index projection when the change is disjoint from its declared fields", () => {
|
||||
let extractions = 0
|
||||
let matchChecks = 0
|
||||
const IndexedJobs = Layout.collection(Job, ({ members, first }) => ({
|
||||
labels: members(["labels"], (job) => {
|
||||
extractions++
|
||||
return job.labels
|
||||
}),
|
||||
nextRetry: first(["status"], (job) => {
|
||||
matchChecks++
|
||||
return job.status === "retrying"
|
||||
}),
|
||||
}))
|
||||
const jobs = IndexedJobs.make([{ id: "one", labels: ["billing"], status: "running" }])
|
||||
const labels = jobs.get("one")!().labels
|
||||
const baseline = { extractions, matchChecks }
|
||||
|
||||
// Status-only change: labels extraction reads only `labels`, so it skips.
|
||||
jobs.update({ id: "one", labels, status: "retrying" })
|
||||
expect(extractions).toBe(baseline.extractions)
|
||||
expect(matchChecks).toBe(baseline.matchChecks + 1)
|
||||
expect(jobs.first("nextRetry")?.().id).toBe("one")
|
||||
|
||||
// Labels-only change: the first-match check reads only `status`, so it skips.
|
||||
jobs.update({ id: "one", labels: ["urgent"], status: "retrying" })
|
||||
expect(extractions).toBe(baseline.extractions + 1)
|
||||
expect(matchChecks).toBe(baseline.matchChecks + 1)
|
||||
expect(jobs.hasMember("labels", "urgent")).toBe(true)
|
||||
expect(jobs.hasMember("labels", "billing")).toBe(false)
|
||||
|
||||
// Equivalent update: nothing runs.
|
||||
jobs.update({ id: "one", labels: ["urgent"], status: "retrying" })
|
||||
expect(extractions).toBe(baseline.extractions + 1)
|
||||
expect(matchChecks).toBe(baseline.matchChecks + 1)
|
||||
})
|
||||
|
||||
it("normalizes numeric dependency names", () => {
|
||||
let extractions = 0
|
||||
const Numeric = Layout.collection(
|
||||
Layout.struct({ id: Layout.key(Layout.string), 0: Layout.string, other: Layout.string }),
|
||||
({ members }) => ({
|
||||
zero: members([0], (value) => {
|
||||
extractions++
|
||||
return [value[0]]
|
||||
}),
|
||||
}),
|
||||
)
|
||||
const values = Numeric.make([{ id: "one", 0: "zero", other: "before" }])
|
||||
const baseline = extractions
|
||||
|
||||
values.update({ id: "one", 0: "zero", other: "after" })
|
||||
|
||||
expect(extractions).toBe(baseline)
|
||||
expect(values.hasMember("zero", "zero")).toBe(true)
|
||||
})
|
||||
|
||||
it("reuses precomputed field diffs during direct collection mutations", () => {
|
||||
let comparisons = 0
|
||||
const counted: Layout.Field<string> = {
|
||||
equivalent(left, right) {
|
||||
comparisons++
|
||||
return left === right
|
||||
},
|
||||
}
|
||||
const CountedJob = Layout.struct({
|
||||
id: Layout.key(Layout.string),
|
||||
value: counted,
|
||||
status: Layout.string,
|
||||
})
|
||||
const CountedJobs = Layout.collection(CountedJob, ({ first }) => ({
|
||||
changed: first(["value"], (job) => job.value === "after"),
|
||||
}))
|
||||
const one = { id: "one", value: "before", status: "idle" }
|
||||
const two = { id: "two", value: "before", status: "idle" }
|
||||
const jobs = CountedJobs.make([one, two])
|
||||
comparisons = 0
|
||||
|
||||
jobs.set([
|
||||
{ ...one },
|
||||
{ ...two, value: "after" },
|
||||
])
|
||||
|
||||
expect(comparisons).toBe(4)
|
||||
expect(jobs.get("one")?.()).toBe(one)
|
||||
expect(jobs.first("changed")?.().id).toBe("two")
|
||||
|
||||
comparisons = 0
|
||||
jobs.update({ ...jobs.get("one")!(), status: "busy" })
|
||||
expect(comparisons).toBe(1)
|
||||
|
||||
comparisons = 0
|
||||
jobs.modify("two", (job) => ({ ...job, value: "done" }))
|
||||
expect(comparisons).toBe(1)
|
||||
expect(jobs.first("changed")).toBeUndefined()
|
||||
})
|
||||
|
||||
it("refreshes indexes that read a changed union discriminant", () => {
|
||||
const Row = Layout.keyedUnion({
|
||||
key: Layout.key("id", Layout.number),
|
||||
tag: "type",
|
||||
variants: {
|
||||
waiting: Layout.struct({ value: Layout.string }),
|
||||
ready: Layout.struct({ value: Layout.string }),
|
||||
},
|
||||
})
|
||||
const Rows = Layout.collection(Row, ({ members, first }) => ({
|
||||
types: members(["type"], (row) => [row.type]),
|
||||
firstReady: first(["type"], (row) => row.type === "ready"),
|
||||
}))
|
||||
const rows = Rows.make([{ id: 1, type: "waiting", value: "same" }])
|
||||
|
||||
rows.update({ id: 1, type: "ready", value: "same" })
|
||||
|
||||
expect(rows.get(1)?.().type).toBe("ready")
|
||||
expect(rows.hasMember("types", "waiting")).toBe(false)
|
||||
expect(rows.hasMember("types", "ready")).toBe(true)
|
||||
expect(rows.first("firstReady")?.().type).toBe("ready")
|
||||
})
|
||||
|
||||
it("tracks lazy member iterables while they are consumed", () => {
|
||||
const LazyJobs = Layout.collection(Job, ({ members }) => ({
|
||||
statuses: members(function* (job) {
|
||||
yield job.status
|
||||
}),
|
||||
}))
|
||||
const jobs = LazyJobs.make([{ id: "one", labels: [], status: "running" }])
|
||||
|
||||
expect(jobs.hasMember("statuses", "running")).toBe(true)
|
||||
jobs.update({ id: "one", labels: [], status: "retrying" })
|
||||
expect(jobs.hasMember("statuses", "running")).toBe(false)
|
||||
expect(jobs.hasMember("statuses", "retrying")).toBe(true)
|
||||
})
|
||||
|
||||
it("passes actual frozen values to identity and reflection projections", () => {
|
||||
const actual = Object.freeze({ id: "one", labels: [] as readonly string[], status: "running" })
|
||||
const accepted = new WeakSet<object>([actual])
|
||||
const EnumeratedJobs = Layout.collection(Job, ({ members, first }) => ({
|
||||
properties: members((job) => Object.keys(job)),
|
||||
selves: members((job) => [job]),
|
||||
frozen: members((job) => [Object.isFrozen(job)]),
|
||||
accepted: first((job) => accepted.has(job)),
|
||||
}))
|
||||
const jobs = EnumeratedJobs.make([actual])
|
||||
|
||||
expect(jobs.hasMember("properties", "id")).toBe(true)
|
||||
expect(jobs.hasMember("properties", "labels")).toBe(true)
|
||||
expect(jobs.hasMember("properties", "status")).toBe(true)
|
||||
expect(jobs.hasMember("selves", actual)).toBe(true)
|
||||
expect(jobs.hasMember("frozen", true)).toBe(true)
|
||||
expect(jobs.first("accepted")?.()).toBe(actual)
|
||||
})
|
||||
|
||||
it("applies explicit member deltas without re-extracting unchanged membership", () => {
|
||||
let extractions = 0
|
||||
const IndexedJobs = Layout.collection(Job, ({ members }) => ({
|
||||
|
|
@ -206,12 +437,20 @@ describe("Layout", () => {
|
|||
expect(jobs.hasMember("labels", "billing")).toBe(true)
|
||||
expect(jobs.hasMember("labels", "urgent")).toBe(true)
|
||||
|
||||
jobs.modify("two", (job) => ({ ...job, labels: [] }), { members: { labels: { remove: ["billing"] } } })
|
||||
jobs.modify("two", (job) => ({ ...job, labels: [] }), {
|
||||
members: { labels: { remove: ["billing"] } },
|
||||
})
|
||||
expect(jobs.hasMember("labels", "billing")).toBe(false)
|
||||
|
||||
if (false) {
|
||||
// @ts-expect-error Member deltas require arrays so string members are not split into characters.
|
||||
jobs.modify("one", (job) => job, { members: { labels: { add: "urgent" } } })
|
||||
jobs.modify("one", (job) => job, {
|
||||
members: {
|
||||
labels: {
|
||||
// @ts-expect-error Member deltas require arrays so strings are not split into characters.
|
||||
add: "urgent",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -238,16 +477,67 @@ describe("Layout", () => {
|
|||
)
|
||||
})
|
||||
|
||||
it("restores pending comparison state when publication throws", () => {
|
||||
const LabeledJob = Layout.struct({
|
||||
id: Layout.key(Layout.string),
|
||||
label: Layout.string,
|
||||
status: Layout.string,
|
||||
})
|
||||
const IndexedJobs = Layout.collection(LabeledJob, ({ first }) => ({
|
||||
retry: first(["status"], (job) => job.status === "retrying"),
|
||||
}))
|
||||
const jobs = IndexedJobs.make([{ id: "one", label: "before", status: "running" }])
|
||||
const slot = jobs.get("one")!
|
||||
const dispose = slot.subscribe(() => {
|
||||
throw new Error("listener failed")
|
||||
})
|
||||
|
||||
expect(() => jobs.update({ id: "one", label: "after", status: "running" })).toThrow("listener failed")
|
||||
dispose()
|
||||
const equivalent = { ...slot() }
|
||||
|
||||
expect(jobs.set([equivalent])).toBe(false)
|
||||
expect(slot()).not.toBe(equivalent)
|
||||
})
|
||||
|
||||
it("does not leak a pending comparison into reentrant publication", () => {
|
||||
const LabeledJob = Layout.struct({
|
||||
id: Layout.key(Layout.string),
|
||||
label: Layout.string,
|
||||
status: Layout.string,
|
||||
})
|
||||
const IndexedJobs = Layout.collection(LabeledJob, ({ first }) => ({
|
||||
retry: first(["status"], (job) => job.status === "retrying"),
|
||||
}))
|
||||
const jobs = IndexedJobs.make([{ id: "one", label: "before", status: "running" }])
|
||||
const slot = jobs.get("one")!
|
||||
let reentered = false
|
||||
let changed: boolean | undefined
|
||||
const dispose = slot.subscribe(() => {
|
||||
if (reentered) return
|
||||
reentered = true
|
||||
changed = jobs.set([{ ...slot() }])
|
||||
})
|
||||
|
||||
jobs.update({ id: "one", label: "after", status: "running" })
|
||||
|
||||
expect(changed).toBe(false)
|
||||
dispose()
|
||||
})
|
||||
|
||||
it("tracks first matches whose key is undefined", () => {
|
||||
const undefinedField: Layout.Field<undefined> = { equivalent: Object.is }
|
||||
const OptionalJobs = Layout.collection(
|
||||
Layout.struct({ id: Layout.key(undefinedField), status: Layout.string }),
|
||||
({ first }) => ({ retry: first((job) => job.status === "retrying") }),
|
||||
({ first }) => ({ retry: first(["status"], (job) => job.status === "retrying") }),
|
||||
)
|
||||
const jobs = OptionalJobs.make([{ id: undefined, status: "running" }])
|
||||
|
||||
jobs.update({ id: undefined, status: "retrying" })
|
||||
|
||||
expect(jobs.first("retry")?.()).toEqual({ id: undefined, status: "retrying" })
|
||||
expect(jobs.first("retry")?.()).toEqual({
|
||||
id: undefined,
|
||||
status: "retrying",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue