From 99862522eed42c44c32b050a1789be91244be6f6 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Wed, 14 Oct 2026 20:29:55 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=AA=20Identify=20intended=20core=20ref?= =?UTF-8?q?usals=20and=20retained=20observations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/tests/answer-identity.test.ts | 11 +-- .../core/tests/evaluate-component.test.ts | 75 ++++++++++++++----- packages/core/tests/execution-init.test.ts | 12 ++- packages/core/tests/expand.test.ts | 8 +- packages/core/tests/syntax-api.test.ts | 11 ++- packages/git/tests/api-entrypoint.test.ts | 5 ++ packages/git/tests/git.test.ts | 6 +- packages/testing/tests/assert-throws.test.ts | 8 +- .../workspace-effect-transaction.test.ts | 38 +++++++--- specs/executable-mdx-spec.md | 5 +- 10 files changed, 129 insertions(+), 50 deletions(-) diff --git a/packages/core/tests/answer-identity.test.ts b/packages/core/tests/answer-identity.test.ts index 457c29f44..38f374b9f 100644 --- a/packages/core/tests/answer-identity.test.ts +++ b/packages/core/tests/answer-identity.test.ts @@ -584,14 +584,11 @@ describe("Tier CIV — an identity belongs to one answer of one import", () => { }); /** - * Tier PA10b — two sibling spawns resolving at the same time. + * Tier PA10b — serial component-answer capture. * - * `` lets two `` children have an import open together, so a - * resolution is no longer the only one in flight. What makes an answer this - * import's is the exact window it was asked in, and windows now belong to the - * engine scope that opened them — so these rows are written with two real - * scopes standing for two spawns, and never with one spawn's bookkeeping - * standing in for the other's. + * Each request retains the window that was open when its handler began. A + * descendant scope preserves that correlation, and closing the window makes + * the retained request unable to record an answer’s identity in a later one. */ describe("Tier PA10b — one serial resolution window", () => { /** diff --git a/packages/core/tests/evaluate-component.test.ts b/packages/core/tests/evaluate-component.test.ts index fd67b1178..0239566d3 100644 --- a/packages/core/tests/evaluate-component.test.ts +++ b/packages/core/tests/evaluate-component.test.ts @@ -279,30 +279,65 @@ describe("protected generated component answers", () => { it("refuses copied, mutated, wrapped, unclaimed, misidentified and wrong-form answers", function* () { const source = `'} />`; const ran: string[] = []; - const transforms: ((answer: FunctionComponentDefinition) => FunctionComponentDefinition)[] = [ - (answer) => ({ ...answer }), - (answer) => ({ - ...answer, - *fn(): Operation { - ran.push("same name"); - return "forged"; + const variants: { + name: string; + options: Parameters[0]; + reason: string; + }[] = [ + { + name: "copied", + options: { transform: (answer) => ({ ...answer }) }, + reason: "carries no identity", + }, + { + name: "replaced body", + options: { + transform: (answer) => ({ + ...answer, + *fn(): Operation { + ran.push("same name"); + return "forged"; + }, + }), }, - }), - independentWrapper, - (answer) => { - answer.props = { type: "object" }; - return answer; + reason: "carries no identity", + }, + { + name: "wrapped", + options: { transform: independentWrapper }, + reason: "carries no identity", + }, + { + name: "mutated", + options: { + transform: (answer) => { + answer.props = { type: "object" }; + return answer; + }, + }, + reason: "carries no identity", + }, + { name: "unclaimed", options: { unclaimed: true }, reason: "carries no identity" }, + { + name: "misidentified", + options: { revision: "2" }, + reason: "chain answered with test://syntax-provider#Syntax@2", }, ]; - for (const transform of transforms) { - const stream = new InMemoryStream(); - expect(yield* refusal(run(source, [syntaxProfile({ transform })], stream))).toBeTruthy(); - expect(admissions(yield* stream.readAll())).toHaveLength(0); - } - for (const options of [{ unclaimed: true }, { revision: "2" }]) { + for (const { name, options, reason } of variants) { const stream = new InMemoryStream(); - expect(yield* refusal(run(source, [syntaxProfile(options)], stream))).toBeTruthy(); - expect(admissions(yield* stream.readAll())).toHaveLength(0); + expect({ + variant: name, + refusal: yield* refusal(run(source, [syntaxProfile(options)], stream)), + }).toEqual({ + variant: name, + refusal: expect.stringContaining(reason), + }); + expect({ variant: name, admissions: admissions(yield* stream.readAll()) }).toEqual({ + variant: name, + admissions: [], + }); + expect({ variant: name, ran }).toEqual({ variant: name, ran: [] }); } expect( yield* refusal(run(`'} />`, [syntaxProfile()])), diff --git a/packages/core/tests/execution-init.test.ts b/packages/core/tests/execution-init.test.ts index 6b0696834..ee88ab28e 100644 --- a/packages/core/tests/execution-init.test.ts +++ b/packages/core/tests/execution-init.test.ts @@ -4,6 +4,8 @@ import { createContext, ensure, scoped, spawn, suspend, withResolvers } from "ef import type { Operation } from "effection"; import { InMemoryStream } from "@executablemd/durable-streams"; import { Execution } from "../api.ts"; +import { parseFilesFatal } from "@executablemd/runtime"; +import { fatalCause } from "../src/errors.ts"; import { collect, execute, Execution as RootExecution, inlineSource } from "../mod.ts"; import { executeInstalled, fileReadEntry, Markdown, sourceDigest } from "../host.ts"; import type { JournalAdmission } from "../host.ts"; @@ -240,7 +242,10 @@ describe("Tier EI — execution-owned initialization", () => { }); it("EI7: a no-op initializer invents no Files provider; explicit initialization supplies one", function* () { - expect(String(yield* failure(run('')))).toContain("Files"); + expect(parseFilesFatal(fatalCause(yield* failure(run(''))))).toEqual({ + type: "executablemd.runtime.files-fatal/v1", + kind: "provider-unavailable", + }); const files = recordedFiles({ "note.md": "INSTALLED BY INIT" }); const output = yield* scoped(function* () { yield* Execution.around({ @@ -253,6 +258,9 @@ describe("Tier EI — execution-owned initialization", () => { }); expect(output).toContain("INSTALLED BY INIT"); expect(files.performed).toEqual(["read note.md"]); - expect(String(yield* failure(run('')))).toContain("Files"); + expect(parseFilesFatal(fatalCause(yield* failure(run(''))))).toEqual({ + type: "executablemd.runtime.files-fatal/v1", + kind: "provider-unavailable", + }); }); }); diff --git a/packages/core/tests/expand.test.ts b/packages/core/tests/expand.test.ts index 32b978543..5c6c4f2e8 100644 --- a/packages/core/tests/expand.test.ts +++ b/packages/core/tests/expand.test.ts @@ -1,6 +1,6 @@ import { describe, it } from "@executablemd/test-support/bdd"; import { expect } from "@executablemd/test-support/expect"; -import { ensure, scoped, sleep, spawn, withResolvers } from "effection"; +import { ensure, scoped, spawn, withResolvers } from "effection"; import type { Subscription } from "effection"; import { createBlockCounter, expandSegments } from "../src/expand.ts"; import { regionStream } from "../src/expansion-region.ts"; @@ -1469,6 +1469,7 @@ describe("Tier SR — expanding installed structural syntax", () => { it("SR7: one demand permits one chunk, and later work waits for later demand", function* () { const order: string[] = []; const released = withResolvers(); + const firstDelivered = withResolvers(); const environment = yield* installedEnvironment( [structuralDeclaration(), regionDeclaration()], function* (request) { @@ -1483,6 +1484,7 @@ describe("Tier SR — expanding installed structural syntax", () => { // element's text asks again — each ask is one more permission to work. const first = yield* readText(subscription); order.push(`first:${first}`); + firstDelivered.resolve(); yield* released.operation; const second = yield* readText(subscription); order.push(`second:${second}`); @@ -1505,9 +1507,7 @@ describe("Tier SR — expanding installed structural syntax", () => { yield* spawn(function* () { // Released only once the first chunk has been delivered, so the second // segment's authored work cannot have run before this point. - while (!order.some((entry) => entry.startsWith("first:"))) { - yield* sleep(0); - } + yield* firstDelivered.operation; expect(order).not.toContain("ran:2"); released.resolve(); }); diff --git a/packages/core/tests/syntax-api.test.ts b/packages/core/tests/syntax-api.test.ts index 0c6e4f982..fbaa54bda 100644 --- a/packages/core/tests/syntax-api.test.ts +++ b/packages/core/tests/syntax-api.test.ts @@ -181,11 +181,16 @@ describe("Tier SA — contextual Syntax", () => { .snapshot() .filter((event) => event.type === "yield" && event.description.type === "syntax_symbols"); expect(records).toHaveLength(2); - for (const record of records) { + for (const [index, record] of records.entries()) { if (record.type !== "yield" || record.result.status !== "ok") { - throw new Error("no retained Syntax answer"); + throw new Error( + `Syntax occurrence ${index + 1} retained an unexpected result: ${JSON.stringify(record)}`, + ); } - expect(JSON.stringify(record.result.value)).toContain("FINAL NAMED"); + expect({ occurrence: index + 1, retained: JSON.stringify(record.result.value) }).toEqual({ + occurrence: index + 1, + retained: expect.stringContaining("FINAL NAMED"), + }); } }); diff --git a/packages/git/tests/api-entrypoint.test.ts b/packages/git/tests/api-entrypoint.test.ts index b7aff8997..638dffdbc 100644 --- a/packages/git/tests/api-entrypoint.test.ts +++ b/packages/git/tests/api-entrypoint.test.ts @@ -22,6 +22,11 @@ * fails under Deno, one missing from `package.json` fails under Node and Bun, * and only running all three tells those apart. * + * Emitted Git API identity composition across physical package copies is + * exercised by "publishes contextual Git APIs that compose across copies and + * loads a named Plugin" in `scripts/tests/cli-npm-bin.test.ts`; this file holds + * the portable source export-map contract. + * * Read through `Reflect.get` rather than a cast: what a module exported is * whatever it exported, and the members below are proven present rather than * assumed. diff --git a/packages/git/tests/git.test.ts b/packages/git/tests/git.test.ts index 5d802ab20..b3d89a543 100644 --- a/packages/git/tests/git.test.ts +++ b/packages/git/tests/git.test.ts @@ -3,9 +3,9 @@ * * `GitQuery.resolve()` verifies and resolves one revision expression in the * contextual working directory. The default invokes the Git CLI; a host or a - * test replaces it lexically. Nothing here shells out: every test either - * replaces `GitQuery` or stubs the process boundary beneath it, so the suite - * runs the same wherever it is checked out. + * test replaces it lexically. GT1–GT5 replace `GitQuery` or stub the process + * boundary. GT6–GT7 exercise the shipped default against temporary directories, + * including a real Git checkout. */ import { describe, it } from "@executablemd/test-support/bdd"; diff --git a/packages/testing/tests/assert-throws.test.ts b/packages/testing/tests/assert-throws.test.ts index a3c3c8a6a..33a66e24c 100644 --- a/packages/testing/tests/assert-throws.test.ts +++ b/packages/testing/tests/assert-throws.test.ts @@ -119,7 +119,13 @@ describe("", () => { ), ); expect(run.results[0]?.status).toBe("fail"); - expect(run.results[0]?.error?.message ?? "").not.toContain("none was raised"); + expect(run.results[0]?.error).toMatchObject({ + kind: "assertion", + actual: "1", + expected: "2", + }); + expect(run.output).toContain("**AssertEquals** failed"); + expect(run.output).not.toContain("**AssertThrows** failed"); }); it("stops expanding body children after the first raised error, rendering nothing", function* () { diff --git a/packages/workflow/tests/workspace-effect-transaction.test.ts b/packages/workflow/tests/workspace-effect-transaction.test.ts index f7c8ee14d..7754d5ec2 100644 --- a/packages/workflow/tests/workspace-effect-transaction.test.ts +++ b/packages/workflow/tests/workspace-effect-transaction.test.ts @@ -43,6 +43,7 @@ import { type WorkflowWorkspaceMutation, type WorkflowWorkspaceTransaction, } from "../src/deno/workspace/effect.ts"; +import { journalableWorkspaceCode } from "../src/deno/workspace/errors.ts"; import { readWorkflowWorkspace } from "../src/deno/workspace/inspect.ts"; import type { WorkflowWorkspaceSnapshot } from "../src/deno/workspace/inspect.ts"; import type { @@ -199,18 +200,24 @@ function retainedRepositoryNames(path: string): string[] { } } +function* workspaceContent(read: Operation): Operation { + try { + return yield* read; + } catch (error) { + if (journalableWorkspaceCode(error) === "ENOENT") { + return undefined; + } + throw error; + } +} + function* inspectWorkspace( database: WorkflowRunDatabase, path: string, ): Operation<{ root: string; content: string | undefined }> { const inspected = yield* database.transact(function* (transaction) { return yield* withPrivateWorkspaceTransaction(database, transaction, function* (workspace) { - let content: string | undefined; - try { - content = yield* workspace.filesystem.readTextFile(path); - } catch { - content = undefined; - } + const content = yield* workspaceContent(workspace.filesystem.readTextFile(path)); return { root: yield* workspace.currentRoot(), content, @@ -316,6 +323,19 @@ function setSqliteAuthorizer( } describe("Tier WAC — atomic provider-level Workspace effects", () => { + it("Workspace observation preserves a read failure that is not genuine absence", function* () { + const failures = [ + Object.assign(new Error("Workspace read denied"), { code: "EACCES" }), + Object.assign(new Error("Workspace absence impostor"), { code: "ENOENT" }), + ]; + for (const failure of failures) { + const read = (function* (): Operation { + throw failure; + })(); + expect(yield* raised(workspaceContent(read))).toBe(failure); + } + }); + it("WAC1: mutation, root, filtered Yield and commit become visible together", function* () { const root = yield* useStorageRoot(); const runId = "atomic-success"; @@ -1653,7 +1673,7 @@ describe("Tier WAC — atomic provider-level Workspace effects", () => { }); }); - it("WAC24: a row written through the storage view commits with the effect's bytes", function* () { + it("WAC27: a row written through the storage view commits with the effect's bytes", function* () { const root = yield* useStorageRoot(); const runId = "atomic-storage-commit"; @@ -1697,7 +1717,7 @@ describe("Tier WAC — atomic provider-level Workspace effects", () => { }); }); - it("WAC25: a refused mutation takes its rows back with its bytes", function* () { + it("WAC28: a refused mutation takes its rows back with its bytes", function* () { const root = yield* useStorageRoot(); const runId = "atomic-storage-rollback"; @@ -1753,7 +1773,7 @@ describe("Tier WAC — atomic provider-level Workspace effects", () => { * its root is still open, which is exactly where "for this callback" has to * be a fact rather than a convention. */ - it("WAC26: every capability a mutation retained answers nothing once it returned", function* () { + it("WAC29: every capability a mutation retained answers nothing once it returned", function* () { const root = yield* useStorageRoot(); const runId = "atomic-storage-retained"; const path = join(root, `${runId}.sqlite`); diff --git a/specs/executable-mdx-spec.md b/specs/executable-mdx-spec.md index 35bbaa378..a7f21436b 100644 --- a/specs/executable-mdx-spec.md +++ b/specs/executable-mdx-spec.md @@ -11777,7 +11777,7 @@ visible warning blocks, gather into a separate error report). | FL27 | Colocated document | `xmd test packages/core/src/components/File.test.md` covers both forms, `as` capture, nested parents, replacement, exact content for both authoring shapes, a leading-dots name, and isolation between temporary directories — with no search path and no JavaScript | | FL34–FL35 | The authored form decides the effect | A self-closing `` reads and never writes, and a paired `` writes and never reads, under a contextual `hasContent` handler installed outside the invocation that lies consistently *and* under one that answers `[false, true]` / `[true, false]`. A sibling reporting the contextual answer proves the handler live; the provider calls and the file bytes prove the element followed how it was written (§5.6) | | FL36 | An honest chain | With no handler interfering, both forms reach the same providers and produce the same bytes | -| FL37 | A look-alike is not an invocation | The definition called with an object implementing `hasContent()` — the whole public shape, reporting the answer that would select the write branch — refuses before reaching a provider, leaving the file unchanged; there is no contextual fallback either | +| FL37 | A replaced canonical answer is refused | The import wrapper preparing a look-alike invocation returns a definition canonical execution did not produce, so verification refuses before invoking it, reaches no provider and leaves the file unchanged ([file-component.test.ts](../packages/core/tests/file-component.test.ts)); FX4 in [fixed-filesystem-components.test.ts](../packages/core/tests/fixed-filesystem-components.test.ts) reaches the fixed dispatchers directly to prove invocation authentication | ### Tier FD — `` @@ -12659,6 +12659,9 @@ workspaces](./workflow-workspace-spec.md) §13. | WAC24 | Real host crash | A `SIGKILL` while the mutation, immutable root, current-root pointer and routed journal row are written and uncommitted leaves a second connection seeing only the baseline, and a fresh process recovers the baseline filesystem, root, retained counts and journal exactly | | WAC25 | Committed restart | A second process reopens a run whose Workspace effects committed in a process that has ended, observes the same filesystem, current root, ordered events, event identities and event-to-root associations, and performs no recorded effect again | | WAC26 | Historical reconstruction | A fresh process selects an older event's root through the adapter-private materializer, invalidates the authoritative negative resolution, rebuilds its exact topology, bytes, modes, hardlinks and symbolic links from that root's retained DOFS content, and resnapshots to the selected identity | +| WAC27 | Storage-view commit | A row written through the mutation's storage view and its filesystem bytes commit together against the root its journal event names ([workspace-effect-transaction.test.ts](../packages/workflow/tests/workspace-effect-transaction.test.ts)) | +| WAC28 | Storage-view rollback | A refused mutation rolls back its storage-view row and filesystem bytes, retains no new root and records its failed Yield against the baseline root ([workspace-effect-transaction.test.ts](../packages/workflow/tests/workspace-effect-transaction.test.ts)) | +| WAC29 | Mutation capability lifetime | Storage, filesystem and savepoint capabilities retained by a completed mutation refuse even while its enclosing transaction remains open before commit ([workspace-effect-transaction.test.ts](../packages/workflow/tests/workspace-effect-transaction.test.ts)) | ### Tier WTX — WorkflowRun savepoints and transaction ownership