feat(core): replace instruction checkpoints with value-delta sync (#36254)
This commit is contained in:
parent
768a69bbbd
commit
96a9731947
57 changed files with 2053 additions and 1278 deletions
|
|
@ -6,10 +6,10 @@ import { Location } from "@opencode-ai/core/location"
|
|||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { Instructions } from "@opencode-ai/core/instructions"
|
||||
import { InstructionBuiltIns } from "@opencode-ai/core/instructions/builtins"
|
||||
import { location } from "../fixture/location"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { readInitial, readUpdate } from "../lib/instructions"
|
||||
|
||||
const directory = AbsolutePath.make(FSUtil.resolve("/repo/packages/core"))
|
||||
const projectDirectory = AbsolutePath.make(FSUtil.resolve("/repo"))
|
||||
|
|
@ -36,7 +36,7 @@ describe("InstructionBuiltIns", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* TestClock.setTime(timestamp)
|
||||
const context = yield* InstructionBuiltIns.Service
|
||||
const initialized = yield* Instructions.initialize(yield* context.load())
|
||||
const initialized = yield* readInitial(yield* context.load())
|
||||
|
||||
expect(initialized.text).toBe(
|
||||
[
|
||||
|
|
@ -54,19 +54,16 @@ describe("InstructionBuiltIns", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.effect("reconciles the date without repeating unchanged environment instructions", () =>
|
||||
it.effect("updates the date without repeating unchanged environment instructions", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* TestClock.setTime(timestamp)
|
||||
const context = yield* InstructionBuiltIns.Service
|
||||
const initialized = yield* Instructions.initialize(yield* context.load())
|
||||
const initialized = yield* readInitial(yield* context.load())
|
||||
|
||||
yield* TestClock.setTime(timestamp + 24 * 60 * 60 * 1000)
|
||||
const refreshed = yield* Instructions.reconcile(yield* context.load(), initialized.applied)
|
||||
const refreshed = yield* readUpdate(yield* context.load(), initialized)
|
||||
|
||||
expect(refreshed).toMatchObject({
|
||||
_tag: "Updated",
|
||||
text: `Today's date is now: ${localDate(timestamp + 24 * 60 * 60 * 1000)}`,
|
||||
})
|
||||
expect(refreshed.text).toBe(`Today's date is now: ${localDate(timestamp + 24 * 60 * 60 * 1000)}`)
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -74,10 +71,10 @@ describe("InstructionBuiltIns", () => {
|
|||
Effect.gen(function* () {
|
||||
yield* TestClock.setTime(timestamp)
|
||||
const context = yield* InstructionBuiltIns.Service
|
||||
const initialized = yield* Instructions.initialize(yield* context.load())
|
||||
const initialized = yield* readInitial(yield* context.load())
|
||||
|
||||
yield* TestClock.setTime(timestamp + 60 * 60 * 1000)
|
||||
expect(yield* Instructions.reconcile(yield* context.load(), initialized.applied)).toEqual({ _tag: "Unchanged" })
|
||||
expect((yield* readUpdate(yield* context.load(), initialized)).changed).toBe(false)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,137 +1,142 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import { Cause, Effect, Exit, Schema } from "effect"
|
||||
import { Cause, Effect, Exit, Option, Schema } from "effect"
|
||||
import { Instructions } from "@opencode-ai/core/instructions"
|
||||
import { it } from "../lib/effect"
|
||||
|
||||
const key = Instructions.Key.make
|
||||
const stringContext = (input: {
|
||||
const key = (value: string) => Instructions.Key.make(value)
|
||||
const source = (input: {
|
||||
key: string
|
||||
value: string | Instructions.Unavailable
|
||||
baseline?: (value: string) => string
|
||||
update?: (previous: string, current: string) => string
|
||||
value: string | Instructions.Unavailable | Instructions.Removed
|
||||
initial?: (value: string) => string
|
||||
changed?: (previous: string, current: string) => string
|
||||
removed?: (value: string) => string
|
||||
}) =>
|
||||
Instructions.make({
|
||||
key: key(input.key),
|
||||
codec: Schema.toCodecJson(Schema.String),
|
||||
load: Effect.succeed(input.value),
|
||||
baseline: input.baseline ?? String,
|
||||
update: input.update ?? ((_previous, current) => current),
|
||||
removed: input.removed,
|
||||
read: Effect.succeed(input.value),
|
||||
render: {
|
||||
initial: input.initial ?? String,
|
||||
changed: input.changed ?? ((_previous, current) => current),
|
||||
removed: input.removed,
|
||||
},
|
||||
})
|
||||
|
||||
describe("Instructions", () => {
|
||||
it.effect("stores the canonical JSON encoding of the loaded value", () =>
|
||||
it.effect("reads each source once and derives the initial delta and text", () =>
|
||||
Effect.gen(function* () {
|
||||
const context = Instructions.make({
|
||||
let reads = 0
|
||||
const instructions = Instructions.make({
|
||||
key: key("core/date"),
|
||||
codec: Schema.toCodecJson(Schema.DateFromString),
|
||||
load: Effect.succeed(new Date("2026-06-03T12:00:00.000Z")),
|
||||
baseline: (date) => date.toISOString(),
|
||||
update: (_previous, date) => date.toISOString(),
|
||||
removed: () => "Date removed",
|
||||
})
|
||||
|
||||
expect((yield* Instructions.initialize(context)).applied["core/date"].value).toBe("2026-06-03T12:00:00.000Z")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("loads once and initializes a baseline with the applied values", () =>
|
||||
Effect.gen(function* () {
|
||||
let loads = 0
|
||||
const context = Instructions.combine([
|
||||
Instructions.make({
|
||||
key: key("core/date"),
|
||||
codec: Schema.toCodecJson(Schema.String),
|
||||
load: Effect.sync(() => {
|
||||
loads++
|
||||
return "2026-06-03"
|
||||
}),
|
||||
baseline: (date) => `Today's date is ${date}.`,
|
||||
update: (previous, current) => `The date changed from ${previous} to ${current}.`,
|
||||
removed: () => "The date was removed.",
|
||||
codec: Schema.toCodecJson(Schema.String),
|
||||
read: Effect.sync(() => {
|
||||
reads++
|
||||
return "2026-07-09"
|
||||
}),
|
||||
stringContext({ key: "core/location", value: "/repo", baseline: (value) => `Directory: ${value}` }),
|
||||
])
|
||||
|
||||
expect(yield* Instructions.initialize(context)).toEqual({
|
||||
text: "Today's date is 2026-06-03.\n\nDirectory: /repo",
|
||||
applied: {
|
||||
"core/date": { value: "2026-06-03", removed: "The date was removed." },
|
||||
"core/location": { value: "/repo" },
|
||||
},
|
||||
})
|
||||
expect(loads).toBe(1)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("renders updates only after a structured value changes", () =>
|
||||
Effect.gen(function* () {
|
||||
const previous = {
|
||||
"core/date": { value: "2026-06-03", removed: "The date was removed." },
|
||||
"core/location": { value: "/repo", removed: "Removed: /repo" },
|
||||
}
|
||||
const changed = Instructions.combine([
|
||||
stringContext({
|
||||
key: "core/date",
|
||||
value: "2026-06-04",
|
||||
update: (before, current) => `The date changed from ${before} to ${current}.`,
|
||||
removed: () => "The date was removed.",
|
||||
}),
|
||||
stringContext({ key: "core/location", value: "/repo" }),
|
||||
])
|
||||
|
||||
expect(yield* Instructions.reconcile(changed, previous)).toEqual({
|
||||
_tag: "Updated",
|
||||
text: "The date changed from 2026-06-03 to 2026-06-04.",
|
||||
applied: {
|
||||
"core/date": { value: "2026-06-04", removed: "The date was removed." },
|
||||
"core/location": { value: "/repo", removed: "Removed: /repo" },
|
||||
render: {
|
||||
initial: (date) => `Today's date: ${date}`,
|
||||
changed: (previous, current) => `The date changed from ${previous} to ${current}`,
|
||||
},
|
||||
})
|
||||
|
||||
const admitted = yield* Instructions.read(instructions).pipe(Effect.flatMap(Instructions.diff))
|
||||
const hash = Instructions.hash("2026-07-09")
|
||||
|
||||
expect(reads).toBe(1)
|
||||
expect(admitted).toEqual({
|
||||
delta: { "core/date": hash },
|
||||
blobs: { [hash]: "2026-07-09" },
|
||||
})
|
||||
expect(Instructions.renderInitial(instructions, { "core/date": "2026-07-09" })).toBe("Today's date: 2026-07-09")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("derives no delta when the encoded value is unchanged", () =>
|
||||
Effect.gen(function* () {
|
||||
const instructions = source({ key: "core/date", value: "2026-07-09" })
|
||||
const admitted = yield* Instructions.read(instructions).pipe(
|
||||
Effect.flatMap((observed) => Instructions.diff(observed, { "core/date": Instructions.hash("2026-07-09") })),
|
||||
)
|
||||
|
||||
expect(admitted).toEqual({ delta: {}, blobs: {} })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("renders a changed value from stored values", () =>
|
||||
Effect.gen(function* () {
|
||||
const instructions = source({
|
||||
key: "core/date",
|
||||
value: "2026-07-10",
|
||||
changed: (previous, current) => `The date changed from ${previous} to ${current}`,
|
||||
})
|
||||
const admitted = yield* Instructions.read(instructions).pipe(
|
||||
Effect.flatMap((observed) => Instructions.diff(observed, { "core/date": Instructions.hash("2026-07-09") })),
|
||||
)
|
||||
|
||||
expect(admitted.delta).toEqual({ "core/date": Instructions.hash("2026-07-10") })
|
||||
expect(
|
||||
yield* Instructions.reconcile(
|
||||
Instructions.combine([
|
||||
stringContext({ key: "core/date", value: "2026-06-03", removed: () => "The date was removed." }),
|
||||
stringContext({ key: "core/location", value: "/repo" }),
|
||||
]),
|
||||
previous,
|
||||
Instructions.renderUpdate(
|
||||
instructions,
|
||||
{ "core/date": "2026-07-09" },
|
||||
{ "core/date": Option.some("2026-07-10") },
|
||||
),
|
||||
).toEqual({ _tag: "Unchanged" })
|
||||
).toBe("The date changed from 2026-07-09 to 2026-07-10")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("uses the baseline for a newly added source", () =>
|
||||
it.effect("admits and renders an observed removal", () =>
|
||||
Effect.gen(function* () {
|
||||
const context = stringContext({
|
||||
key: "core/skills",
|
||||
value: "effect",
|
||||
baseline: (skill) => `Available skill: ${skill}`,
|
||||
const instructions = source({
|
||||
key: "core/remote",
|
||||
value: Instructions.removed,
|
||||
removed: (previous) => `Stop applying ${previous}`,
|
||||
})
|
||||
const admitted = yield* Instructions.read(instructions).pipe(
|
||||
Effect.flatMap((observed) => Instructions.diff(observed, { "core/remote": Instructions.hash("instructions") })),
|
||||
)
|
||||
|
||||
expect(yield* Instructions.reconcile(context, {})).toEqual({
|
||||
_tag: "Updated",
|
||||
text: "Available skill: effect",
|
||||
applied: { "core/skills": { value: "effect" } },
|
||||
expect(admitted).toEqual({ delta: { "core/remote": "removed" }, blobs: {} })
|
||||
expect(
|
||||
Instructions.renderUpdate(instructions, { "core/remote": "instructions" }, { "core/remote": Option.none() }),
|
||||
).toBe("Stop applying instructions")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("treats JSON null as a value rather than a removal", () =>
|
||||
Effect.gen(function* () {
|
||||
const instructions = Instructions.make<Schema.Json>({
|
||||
key: key("api/value"),
|
||||
codec: Schema.toCodecJson(Schema.Json),
|
||||
read: Effect.succeed(null),
|
||||
render: {
|
||||
initial: String,
|
||||
changed: (_previous, current) => String(current),
|
||||
removed: () => "removed",
|
||||
},
|
||||
})
|
||||
const admitted = yield* Instructions.read(instructions).pipe(
|
||||
Effect.flatMap((observed) => Instructions.diff(observed, { "api/value": Instructions.hash("previous") })),
|
||||
)
|
||||
|
||||
expect(admitted).toEqual({
|
||||
delta: { "api/value": Instructions.hash(null) },
|
||||
blobs: { [Instructions.hash(null)]: null },
|
||||
})
|
||||
expect(
|
||||
Instructions.renderUpdate(instructions, { "api/value": "previous" }, { "api/value": Option.some(null) }),
|
||||
).toBe("null")
|
||||
expect(Instructions.applyDelta({ "api/value": "previous" }, { "api/value": Option.some(null) })).toEqual({
|
||||
"api/value": null,
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("retains the belief while a source is temporarily unavailable", () =>
|
||||
it.effect("blocks the initial delta while any source is unavailable", () =>
|
||||
Effect.gen(function* () {
|
||||
const previous = { "core/remote": { value: "instructions", removed: "Instructions removed" } }
|
||||
const context = stringContext({ key: "core/remote", value: Instructions.unavailable })
|
||||
|
||||
expect(yield* Instructions.reconcile(context, previous)).toEqual({ _tag: "Unchanged" })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("blocks initialization while a source is unavailable", () =>
|
||||
Effect.gen(function* () {
|
||||
const exit = yield* Instructions.initialize(
|
||||
stringContext({ key: "core/remote", value: Instructions.unavailable }),
|
||||
).pipe(Effect.exit)
|
||||
const exit = yield* Instructions.read(source({ key: "core/remote", value: Instructions.unavailable })).pipe(
|
||||
Effect.flatMap(Instructions.diff),
|
||||
Effect.exit,
|
||||
)
|
||||
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isFailure(exit))
|
||||
|
|
@ -139,176 +144,89 @@ describe("Instructions", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.effect("emits the previously stored removal message", () =>
|
||||
it.effect("keeps the stored value while a source is unavailable mid-session", () =>
|
||||
Effect.gen(function* () {
|
||||
expect(
|
||||
yield* Instructions.reconcile(Instructions.empty, {
|
||||
"core/instructions": { value: "contents", removed: "Instructions removed; stop applying them." },
|
||||
}),
|
||||
).toEqual({
|
||||
_tag: "Updated",
|
||||
text: "Instructions removed; stop applying them.",
|
||||
applied: {},
|
||||
})
|
||||
const admitted = yield* Instructions.read(source({ key: "core/remote", value: Instructions.unavailable })).pipe(
|
||||
Effect.flatMap((observed) => Instructions.diff(observed, { "core/remote": Instructions.hash("instructions") })),
|
||||
)
|
||||
|
||||
expect(admitted).toEqual({ delta: {}, blobs: {} })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("retains an unannounced removal silently", () =>
|
||||
it.effect("does not infer removal when a source is absent from the current version", () =>
|
||||
Effect.gen(function* () {
|
||||
expect(yield* Instructions.reconcile(Instructions.empty, { "core/date": { value: "2026-06-04" } })).toEqual({
|
||||
_tag: "Unchanged",
|
||||
})
|
||||
const admitted = yield* Instructions.read(Instructions.empty).pipe(
|
||||
Effect.flatMap((observed) =>
|
||||
Instructions.diff(observed, { "core/retired": Instructions.hash("old instructions") }),
|
||||
),
|
||||
)
|
||||
|
||||
// The retained belief survives alongside other updates.
|
||||
expect(
|
||||
yield* Instructions.reconcile(stringContext({ key: "core/skills", value: "effect" }), {
|
||||
"core/date": { value: "2026-06-04" },
|
||||
}),
|
||||
).toEqual({
|
||||
_tag: "Updated",
|
||||
text: "effect",
|
||||
applied: {
|
||||
"core/skills": { value: "effect" },
|
||||
"core/date": { value: "2026-06-04" },
|
||||
},
|
||||
})
|
||||
expect(admitted).toEqual({ delta: {}, blobs: {} })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("renders multiple removals in stable key order", () =>
|
||||
it.effect("renders a newly added source with its initial renderer", () =>
|
||||
Effect.gen(function* () {
|
||||
expect(
|
||||
yield* Instructions.reconcile(Instructions.empty, {
|
||||
"core/z": { value: "z", removed: "Removed z" },
|
||||
"core/a": { value: "a", removed: "Removed a" },
|
||||
}),
|
||||
).toMatchObject({ _tag: "Updated", text: "Removed a\n\nRemoved z" })
|
||||
const instructions = source({
|
||||
key: "core/skills",
|
||||
value: "effect",
|
||||
initial: (skill) => `Available skill: ${skill}`,
|
||||
})
|
||||
|
||||
expect(Instructions.renderUpdate(instructions, {}, { "core/skills": Option.some("effect") })).toBe(
|
||||
"Available skill: effect",
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("hashes objects independently of key order", () =>
|
||||
Effect.sync(() => {
|
||||
expect(Instructions.hash({ a: 1, b: { x: true, y: false } })).toBe(
|
||||
Instructions.hash({ b: { y: false, x: true }, a: 1 }),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("renders sources in composition order", () =>
|
||||
Effect.sync(() => {
|
||||
const instructions = Instructions.combine([
|
||||
source({ key: "core/date", value: "date" }),
|
||||
source({ key: "core/location", value: "location" }),
|
||||
])
|
||||
|
||||
expect(Instructions.renderInitial(instructions, { "core/date": "date", "core/location": "location" })).toBe(
|
||||
"date\n\nlocation",
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects duplicate source keys", () =>
|
||||
Effect.sync(() => {
|
||||
expect(() =>
|
||||
Instructions.combine([source({ key: "core/date", value: "one" }), source({ key: "core/date", value: "two" })]),
|
||||
).toThrow(new Instructions.DuplicateKeyError({ key: key("core/date") }))
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects empty model-visible renderings", () =>
|
||||
Effect.gen(function* () {
|
||||
const exit = yield* Instructions.initialize(
|
||||
stringContext({ key: "core/empty", value: "value", baseline: () => "" }),
|
||||
).pipe(Effect.exit)
|
||||
Effect.sync(() => {
|
||||
const instructions = source({ key: "core/empty", value: "value", initial: () => "" })
|
||||
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isFailure(exit)) expect(Cause.pretty(exit.cause)).toContain("rendered an empty baseline")
|
||||
expect(() => Instructions.renderInitial(instructions, { "core/empty": "value" })).toThrow(
|
||||
"Instruction source core/empty rendered an empty initial",
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("re-announces the baseline when a stored value no longer decodes", () =>
|
||||
Effect.gen(function* () {
|
||||
expect(
|
||||
yield* Instructions.reconcile(stringContext({ key: "core/date", value: "2026-06-04" }), {
|
||||
"core/date": { value: 42, removed: "Date removed" },
|
||||
}),
|
||||
).toEqual({
|
||||
_tag: "Updated",
|
||||
text: "2026-06-04",
|
||||
applied: { "core/date": { value: "2026-06-04" } },
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("renders undecodable re-announcements alongside other updates", () =>
|
||||
Effect.gen(function* () {
|
||||
const context = Instructions.combine([
|
||||
stringContext({
|
||||
key: "core/date",
|
||||
value: "2026-06-04",
|
||||
update: (before, current) => `${before} -> ${current}`,
|
||||
}),
|
||||
stringContext({ key: "core/location", value: "/repo" }),
|
||||
])
|
||||
|
||||
expect(
|
||||
yield* Instructions.reconcile(context, {
|
||||
"core/date": { value: "2026-06-03" },
|
||||
"core/location": { value: 42 },
|
||||
}),
|
||||
).toEqual({
|
||||
_tag: "Updated",
|
||||
text: "2026-06-03 -> 2026-06-04\n\n/repo",
|
||||
applied: {
|
||||
"core/date": { value: "2026-06-04" },
|
||||
"core/location": { value: "/repo" },
|
||||
},
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rebaselines from one coherent source observation", () =>
|
||||
Effect.gen(function* () {
|
||||
let loads = 0
|
||||
const context = Instructions.make({
|
||||
key: key("core/date"),
|
||||
codec: Schema.toCodecJson(Schema.String),
|
||||
load: Effect.sync(() => {
|
||||
loads++
|
||||
return "2026-06-04"
|
||||
}),
|
||||
baseline: String,
|
||||
update: (_previous, current) => current,
|
||||
})
|
||||
|
||||
expect(yield* Instructions.rebaseline(context, { "core/date": { value: "2026-06-03" } })).toEqual({
|
||||
text: "2026-06-04",
|
||||
applied: { "core/date": { value: "2026-06-04" } },
|
||||
})
|
||||
expect(loads).toBe(1)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rebaselines an unavailable source from the last-applied belief", () =>
|
||||
Effect.gen(function* () {
|
||||
const context = Instructions.combine([
|
||||
stringContext({ key: "core/date", value: "2026-06-04" }),
|
||||
stringContext({
|
||||
key: "core/remote",
|
||||
value: Instructions.unavailable,
|
||||
baseline: (value) => `Instructions: ${value}`,
|
||||
}),
|
||||
])
|
||||
|
||||
expect(
|
||||
yield* Instructions.rebaseline(context, {
|
||||
"core/remote": { value: "contents", removed: "Instructions removed" },
|
||||
}),
|
||||
).toEqual({
|
||||
text: "2026-06-04\n\nInstructions: contents",
|
||||
applied: {
|
||||
"core/date": { value: "2026-06-04" },
|
||||
"core/remote": { value: "contents", removed: "Instructions removed" },
|
||||
},
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("drops undecodable beliefs and removed sources at rebaseline", () =>
|
||||
Effect.gen(function* () {
|
||||
const context = stringContext({ key: "core/remote", value: Instructions.unavailable })
|
||||
|
||||
// Undecodable belief cannot be restated; removed source entries self-clean.
|
||||
expect(
|
||||
yield* Instructions.rebaseline(context, {
|
||||
"core/remote": { value: 42 },
|
||||
"core/gone": { value: "gone" },
|
||||
}),
|
||||
).toEqual({ text: "", applied: {} })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("diffs list values by key with a changed comparator", () =>
|
||||
it.effect("diffs list values by key", () =>
|
||||
Effect.sync(() => {
|
||||
const previous = [
|
||||
{ name: "effect", description: "Build with Effect" },
|
||||
{ name: "debugging", description: "Diagnose bugs" },
|
||||
{ name: "retired", description: "Old" },
|
||||
]
|
||||
const current = [
|
||||
{ name: "effect", description: "Build with Effect v4" },
|
||||
{ name: "debugging", description: "Diagnose bugs" },
|
||||
{ name: "writing", description: "Write prose" },
|
||||
]
|
||||
|
||||
|
|
@ -331,47 +249,4 @@ describe("Instructions", () => {
|
|||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects duplicate source keys", () =>
|
||||
Effect.sync(() => {
|
||||
expect(() =>
|
||||
Instructions.combine([
|
||||
stringContext({ key: "core/date", value: "one" }),
|
||||
stringContext({ key: "core/date", value: "two" }),
|
||||
]),
|
||||
).toThrow(new Instructions.DuplicateKeyError({ key: key("core/date") }))
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("combines instructions in order", () =>
|
||||
Effect.gen(function* () {
|
||||
expect(
|
||||
(yield* Instructions.initialize(
|
||||
Instructions.combine([
|
||||
stringContext({ key: "core/date", value: "date" }),
|
||||
stringContext({ key: "core/location", value: "location" }),
|
||||
]),
|
||||
)).text,
|
||||
).toBe("date\n\nlocation")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("requires namespaced source keys", () =>
|
||||
Effect.sync(() => {
|
||||
const decodeKey = Schema.decodeUnknownSync(Instructions.Key)
|
||||
|
||||
expect(decodeKey("core/date")).toBe(key("core/date"))
|
||||
expect(() => decodeKey("date")).toThrow()
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("requires namespaced applied keys", () =>
|
||||
Effect.sync(() => {
|
||||
const decodeApplied = Schema.decodeUnknownSync(Instructions.Applied)
|
||||
|
||||
expect(Object.keys(decodeApplied({ "core/date": { value: "date" } }))).toEqual(["core/date"])
|
||||
expect(() => decodeApplied({ date: { value: "date" } })).toThrow()
|
||||
expect(() => decodeApplied({ "core/date": { value: "date", removed: "" } })).toThrow()
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue