From 39317e4e97281fb867eaf320583302b9b5801a4d Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Wed, 14 Oct 2026 20:10:17 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=AA=20Observe=20cleanup,=20input=20ref?= =?UTF-8?q?usal,=20and=20complete=20test=20outcomes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/cli/tests/evaluate-workflow.test.ts | 73 +++++++------ packages/cli/tests/repl-execution.test.ts | 7 +- packages/core/tests/expand.test.ts | 103 ++++++++++++++++--- scripts/runtime-test-exclusions.ts | 2 +- scripts/tests/release-targets.test.ts | 33 +++++- scripts/tests/runtime-tests.test.ts | 3 +- scripts/tests/staged-write.test.ts | 7 +- scripts/tests/test-weights.test.ts | 25 ++++- specs/executable-mdx-spec.md | 6 +- 9 files changed, 199 insertions(+), 60 deletions(-) diff --git a/packages/cli/tests/evaluate-workflow.test.ts b/packages/cli/tests/evaluate-workflow.test.ts index 5b24f0b0e..061d2747d 100644 --- a/packages/cli/tests/evaluate-workflow.test.ts +++ b/packages/cli/tests/evaluate-workflow.test.ts @@ -9,14 +9,11 @@ * What only this tier can show is that the two hosts differ in their *profile* * and in nothing else. The component, the evaluator and the durable protocol * are the same ones the ordinary run uses; what the workflow adds is a - * Workspace-bound ceiling, a basis that advances with the run, and the released - * `source` spelling — which the ordinary profile refuses and this one accepts - * without comment. + * Workspace-bound ceiling and a basis that advances with the run. Every host + * accepts the canonical `text` input and refuses `source` and `program`. * * It runs against the real attachment rather than the `xmd workflow` command - * line: a run started from the command boundary needs a git repository with a - * committed definition, and a row that silently degraded when that was - * unavailable would assert nothing. The command boundary itself is Tier WAL's. + * line, whose start/resume behavior is exercised separately by Tier WAL. */ import { describe, it } from "@executablemd/test-support/bdd"; @@ -126,23 +123,30 @@ describe("Tier FE — the workflow host's profile", () => { expect(Reflect.get(entry, "capability")).toBe("directory:ensure"); }); - it("FE19: `source` and `text` behave identically here, and neither warns", function* () { + it("FE19: canonical `text` repeats across fresh runs without warnings", function* () { + let reads = 0; + yield* API.Files.around({ + *readTextFile(args, next) { + reads++; + return yield* next(...args); + }, + }); const root = yield* useStorageRoot(); yield* withStorage(root, function* () { - const aliased = yield* createRun(); - const alias = yield* runDocument( - aliased, + const first = yield* createRun(); + const initial = yield* runDocument( + first, `${NOTE}\n\n` + `\\n'} as="answer" />\n\n` + `\n`, ); - expect(alias.failure).toBe(undefined); - const aliasText = String(alias.output); - expect(aliasText).toContain(NOTE); + expect(initial.failure).toBe(undefined); + const initialText = String(initial.output); + expect(initialText).toContain(NOTE); // Silently: no deprecation sentence anywhere in the run's output. - expect(aliasText).not.toContain("earlier spelling"); - expect(aliasText).not.toContain("deprecated"); + expect(initialText).not.toContain("earlier spelling"); + expect(initialText).not.toContain("deprecated"); const canonical = yield* createRun({ runId: "canonical" }); const direct = yield* runDocument( @@ -153,24 +157,35 @@ describe("Tier FE — the workflow host's profile", () => { ); expect(direct.failure).toBe(undefined); - // Identical behavior: the two spellings state the same argument. - expect(String(direct.output)).toBe(aliasText); + expect(String(direct.output)).toBe(initialText); + expect(reads).toBe(2); }); }); - it("FE19: `program` is a spelling no profile has", function* () { - const root = yield* useStorageRoot(); - yield* withStorage(root, function* () { - const database = yield* createRun(); - const attempt = yield* runDocument( - database, - `\\n'} />\n`, - ); - - expect(attempt.failure).not.toBe(undefined); - expect(String(attempt.failure)).toMatch(/program|additional/i); + for (const prop of ["source", "program"]) { + it(`FE19: obsolete \`${prop}\` refuses before fragment effects`, function* () { + let reads = 0; + yield* API.Files.around({ + *readTextFile(args, next) { + reads++; + return yield* next(...args); + }, + }); + const root = yield* useStorageRoot(); + yield* withStorage(root, function* () { + const database = yield* createRun(); + const attempt = yield* runDocument( + database, + `${NOTE}\n\n` + + `\\n'} />\n`, + ); + + expect(attempt.failure).not.toBe(undefined); + expect(String(attempt.failure)).toMatch(new RegExp(`${prop}|additional`, "i")); + expect(reads).toBe(0); + }); }); - }); + } it("FE21: the workflow fragment reaches the run's Workspace, not the caller's disk", function* () { const root = yield* useStorageRoot(); diff --git a/packages/cli/tests/repl-execution.test.ts b/packages/cli/tests/repl-execution.test.ts index fe9a1369e..af072270f 100644 --- a/packages/cli/tests/repl-execution.test.ts +++ b/packages/cli/tests/repl-execution.test.ts @@ -717,21 +717,20 @@ describe("REPL execution: pausing expansion", () => { it("P1: tearing the owner down releases nothing that was held", function* () { const holder = execution(); const source = yield* referenceSource(); - let released = -1; let held = -1; - yield* scoped(function* () { + const controller = yield* scoped(function* () { const session = opened(yield* submitReplEntry({ ...options(holder), source })); controlling(session).pause(); yield* pausedOrDone(session); expect(session.expansion.state).toBe("paused"); - released = controlling(session).released; held = (yield* readAll(holder)).length; + return controlling(session); }); // The scope is gone: every hold unwound through its own discard, so nothing // was resumed on the way out and the history stopped where it was held. - expect(released).toBe(0); + expect(controller.released).toBe(0); expect((yield* readAll(holder)).length).toBe(held); }); diff --git a/packages/core/tests/expand.test.ts b/packages/core/tests/expand.test.ts index 3262f09e3..32b978543 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 { scoped, sleep, spawn, withResolvers } from "effection"; +import { ensure, scoped, sleep, spawn, withResolvers } from "effection"; import type { Subscription } from "effection"; import { createBlockCounter, expandSegments } from "../src/expand.ts"; import { regionStream } from "../src/expansion-region.ts"; @@ -23,7 +23,7 @@ import { interpolate } from "../src/interpolate.ts"; import { validateProps, PropValidationError } from "../src/validate.ts"; import { renderSegments } from "../src/render.ts"; import type { Operation } from "effection"; -import { DocumentationError } from "../src/errors.ts"; +import { DocumentationError, useSegmentCauses } from "../src/errors.ts"; import type { ComponentElement, Segment, @@ -1525,6 +1525,8 @@ describe("Tier SR — expanding installed structural syntax", () => { it("SR8: a handler's return halts and joins a blocked producer", function* () { const order: string[] = []; + const cleanupStarted = withResolvers(); + const cleanupAllowed = withResolvers(); const environment = yield* installedEnvironment( [structuralDeclaration(), regionDeclaration()], function* (request) { @@ -1533,12 +1535,15 @@ describe("Tier SR — expanding installed structural syntax", () => { throw new Error("expected a region"); } const subscription = yield* yield* region.expand(); - yield* subscription.next(); + expect(yield* readText(subscription)).toBe("step 1"); order.push("handler returning"); }, ); yield* scoped(function* () { + // A real execution already owns this table. Keeping it outside the + // occurrence prevents direct expansion's fallback scope masking a leak. + yield* useSegmentCauses(); yield* useTestComponents({ Step: { kind: "function", @@ -1551,17 +1556,48 @@ describe("Tier SR — expanding installed structural syntax", () => { }, }, }); - yield* expandInstalled( - '\n \n\n\n\n', - environment, + yield* Component.around( + { + *applyModifiers() { + // A modifier runs in the producer's scope, so this cleanup survives + // the first chunk and is joined when the handler returns. + yield* ensure(function* () { + order.push("cleanup started"); + cleanupStarted.resolve(); + yield* cleanupAllowed.operation; + order.push("cleanup finished"); + }); + order.push("ran:1"); + return { output: "step 1", exitCode: 0, stderr: "" }; + }, + }, + { at: "min" }, ); - order.push("occurrence settled"); + try { + const occurrence = yield* spawn(function* () { + yield* expandInstalled( + '\n \n```text exec\nfirst\n```\n\n' + + "\n\n", + environment, + ); + order.push("occurrence settled"); + }); + yield* cleanupStarted.operation; + expect(order).toEqual(["ran:1", "handler returning", "cleanup started"]); + cleanupAllowed.resolve(); + yield* occurrence; + } finally { + cleanupAllowed.resolve(); + } }); - // The producer was blocked on demand when the handler returned. It is - // halted with the handler's scope, so the second segment never ran and the - // occurrence settled after the handler rather than racing it. - expect(order).toEqual(["ran:1", "handler returning", "occurrence settled"]); + expect(order).toEqual([ + "ran:1", + "handler returning", + "cleanup started", + "cleanup finished", + "occurrence settled", + ]); }); it("SR9: a failing body delivers its prefix, then raises the original failure", function* () { @@ -1615,7 +1651,7 @@ describe("Tier SR — expanding installed structural syntax", () => { expect(raised).toBe(failure); }); - it("SR10: each expansion and each subscription is its own", function* () { + it("SR10: each expansion call owns its producer and output", function* () { const runs: string[] = []; let first: string[] = []; let second: string[] = []; @@ -1656,6 +1692,49 @@ describe("Tier SR — expanding installed structural syntax", () => { expect(second.join("")).toContain("run 2"); }); + it("SR10: two subscriptions to one returned stream own their producers and output", function* () { + const runs: string[] = []; + let first = ""; + let second = ""; + const environment = yield* installedEnvironment( + [structuralDeclaration(), regionDeclaration()], + function* (request) { + const [region] = request.regions; + if (region === undefined) { + throw new Error("expected a region"); + } + const stream = yield* region.expand(); + const firstSubscription = yield* stream; + const secondSubscription = yield* stream; + first = yield* readText(firstSubscription); + second = yield* readText(secondSubscription); + }, + ); + + yield* scoped(function* () { + yield* useTestComponents({ + Count: { + kind: "function", + name: "Count", + props: { type: "object", properties: {}, additionalProperties: false }, + // deno-lint-ignore require-yield + *fn() { + runs.push("ran"); + return `run ${String(runs.length)}`; + }, + }, + }); + yield* expandInstalled( + '\n \n\n', + environment, + ); + }); + + expect(runs).toHaveLength(2); + expect(first).toBe("run 1"); + expect(second).toBe("run 2"); + }); + it("SR11: without an environment, and for the engine's own syntax, nothing changed", function* () { // No environment at all: the name is an ordinary component lookup. const rendered = yield* scoped(function* () { diff --git a/scripts/runtime-test-exclusions.ts b/scripts/runtime-test-exclusions.ts index 13057e7aa..fd0a8c93e 100644 --- a/scripts/runtime-test-exclusions.ts +++ b/scripts/runtime-test-exclusions.ts @@ -476,7 +476,7 @@ const DENO_ONLY_TOOLING: RuntimeExclusion[] = [ { path: "packages/cli/tests/evaluate-workflow.test.ts", reason: - "proves the workflow evaluation profile — its Workspace-bound ceiling, its advancing basis and its released `source` spelling — against a real node:sqlite WorkflowRun database with the Deno DOFS Workspace attached. The claim is that this host's profile differs from the ordinary one and nothing else does, so a substituted store would be a substitute for the subject; Bun has no node:sqlite and Node 22 keeps it behind --experimental-sqlite", + "proves the workflow evaluation profile — its Workspace-bound ceiling, its advancing basis and its canonical `text` input with obsolete-prop refusal — against a real node:sqlite WorkflowRun database with the Deno DOFS Workspace attached. The claim is that this host's profile differs from the ordinary one and nothing else does, so a substituted store would be a substitute for the subject; Bun has no node:sqlite and Node 22 keeps it behind --experimental-sqlite", issue: "https://github.com/taras/executable.md/issues/713", }, { diff --git a/scripts/tests/release-targets.test.ts b/scripts/tests/release-targets.test.ts index 171ac3be9..15616cc14 100644 --- a/scripts/tests/release-targets.test.ts +++ b/scripts/tests/release-targets.test.ts @@ -28,14 +28,12 @@ const ATTEST_ACTION = "actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6"; * is either a job that cannot prepare or a mapping nothing uses. */ function* matrixTargets(): Operation { - const workflow = yield* readTextFile(RELEASE_WORKFLOW); - return [...workflow.matchAll(/^\s+- target:\s*(\S+)/gm)].map((match) => match[1] ?? ""); + return (yield* releaseWorkflow()).matrix.map((row) => row.target); } /** Each matrix member's `artifact:`, as `release.yml` names the file it uploads. */ function* matrixArtifacts(): Operation { - const workflow = yield* readTextFile(RELEASE_WORKFLOW); - return [...workflow.matchAll(/^\s+artifact:\s*(\S+)/gm)].map((match) => match[1] ?? ""); + return (yield* releaseWorkflow()).matrix.map((row) => row.artifact); } /** Executable lines only: a comment naming a command runs nothing. */ @@ -58,7 +56,13 @@ interface Job { steps: Step[]; } +interface ReleasePair { + target: string; + artifact: string; +} + interface Workflow { + matrix: ReleasePair[]; permissions: Record; jobs: Record; } @@ -129,7 +133,21 @@ function* releaseWorkflow(): Operation { const source = yield* readTextFile(RELEASE_WORKFLOW); const document = object(matter(`---\n${source}\n---`).data, "release.yml"); const jobs = object(document.jobs, "release.yml.jobs"); + const build = object(jobs.build, "release.yml.jobs.build"); + const strategy = object(build.strategy, "release.yml.jobs.build.strategy"); + const matrix = object(strategy.matrix, "release.yml.jobs.build.strategy.matrix"); + const label = "release.yml.jobs.build.strategy.matrix.include"; + if (!Array.isArray(matrix.include)) { + throw new Error(`${label} is not an array`); + } return { + matrix: matrix.include.map((entry, index) => { + const row = object(entry, `${label}[${index}]`); + return { + target: string(row.target, `${label}[${index}].target`), + artifact: string(row.artifact, `${label}[${index}].artifact`), + }; + }), permissions: stringRecord(document.permissions, "release.yml.permissions"), jobs: Object.fromEntries( Object.entries(jobs).map(([id, value]) => [id, job(value, `release.yml.jobs.${id}`)]), @@ -184,6 +202,13 @@ describe("the shipped release-target table", () => { ]); }); + it("pairs every target with its own uploaded artifact", function* () { + const byTarget = (left: ReleasePair, right: ReleasePair) => + left.target.localeCompare(right.target); + const published = PUBLISHED_TARGETS.map(({ target, artifact }) => ({ target, artifact })); + expect((yield* releaseWorkflow()).matrix.sort(byTarget)).toEqual(published.sort(byTarget)); + }); + it("names exactly the artifacts release.yml uploads, Windows suffix included", function* () { const published = PUBLISHED_TARGETS.map((row) => row.artifact).sort(); expect(published).toEqual([...(yield* matrixArtifacts())].sort()); diff --git a/scripts/tests/runtime-tests.test.ts b/scripts/tests/runtime-tests.test.ts index 7bbe8be75..f5a04f5ac 100644 --- a/scripts/tests/runtime-tests.test.ts +++ b/scripts/tests/runtime-tests.test.ts @@ -137,13 +137,14 @@ describe("running a shard", () => { const runs = recorder(); const failing: RunLaunch = function* (launch) { yield* runs.run(launch); - return 3; + return runs.files.length + 2; }; const outcome = yield* TestRuns.with(failing, () => runShardFiles("deno", [ONE, TWO, THREE], ROOT), ); expect(runs.files).toEqual([ONE, TWO, THREE]); + expect(outcome.ran).toEqual([ONE, TWO, THREE]); expect(outcome.failure).toEqual({ file: ONE, status: 3 }); }); diff --git a/scripts/tests/staged-write.test.ts b/scripts/tests/staged-write.test.ts index 7c19e5b4b..8551a531e 100644 --- a/scripts/tests/staged-write.test.ts +++ b/scripts/tests/staged-write.test.ts @@ -55,11 +55,10 @@ describe("replaceThroughStaging", () => { it("survives many concurrent replacements of the same file", function* () { const file = yield* target("before"); - yield* all( - Array.from({ length: 32 }, (_, index) => replaceThroughStaging(file, `after ${index}`)), - ); + const candidates = Array.from({ length: 32 }, (_, index) => `after ${index}`); + yield* all(candidates.map((contents) => replaceThroughStaging(file, contents))); - expect(yield* readTextFile(file)).toContain("after "); + expect(candidates).toContain(yield* readTextFile(file)); expect(yield* directoryOf(file)).toEqual(["package.json"]); }); diff --git a/scripts/tests/test-weights.test.ts b/scripts/tests/test-weights.test.ts index e69282b64..a10763911 100644 --- a/scripts/tests/test-weights.test.ts +++ b/scripts/tests/test-weights.test.ts @@ -9,7 +9,7 @@ import { describe, it } from "@executablemd/test-support/bdd"; import { expect } from "@executablemd/test-support/expect"; import { sleep } from "effection"; import type { Operation } from "effection"; -import { ensureDir, exists, readdir, writeTextFile } from "@effectionx/fs"; +import { ensureDir, exists, readdir, readTextFile, writeTextFile } from "@effectionx/fs"; import { useTempDirectory } from "@executablemd/test-support/temp"; import { pathToFileURL } from "node:url"; @@ -283,7 +283,7 @@ describe("measuring the corpus", () => { expect(runs.launches.length).toEqual(2); }); - it("replaces the file only after every measurement passed", function* () { + it("publishes a new file after every measurement passed", function* () { const root = yield* workspace(); const target = weightsFile(root); @@ -294,4 +294,25 @@ describe("measuring the corpus", () => { expect(yield* exists(target)).toBe(true); expect(yield* stagedNames(root)).toEqual([]); }); + + it("replaces the older file with the exact completed measurements", function* () { + const root = yield* workspace(); + const target = weightsFile(root); + const older = formatTestWeights(VALID); + yield* writeTextFile(target, older); + const runs = recorder(); + const weights = yield* TestRuns.with( + function* (launch) { + expect(yield* readTextFile(target)).toBe(older); + return yield* runs.run(launch); + }, + () => measureTestWeights(root, SOURCE), + ); + + yield* writeTestWeights(target, weights); + + expect(runs.launches).toHaveLength(6); + expect(yield* readTextFile(target)).toBe(formatTestWeights(weights)); + expect(yield* stagedNames(root)).toEqual([]); + }); }); diff --git a/specs/executable-mdx-spec.md b/specs/executable-mdx-spec.md index ddc0aad29..35bbaa378 100644 --- a/specs/executable-mdx-spec.md +++ b/specs/executable-mdx-spec.md @@ -12290,7 +12290,7 @@ through the captured capability because there is no other way to reach it. | FE16 | Repeated Evaluate occurrences cannot consume one another's records. | | FE17 | Actual cancellation stops active work, waits for cleanup, and records no false success. | | FE18 | Existing untagged #369 records remain readable; new records use the closed version-2 shape. | -| FE19 | Workflow `source` and canonical `text` have identical behavior without a warning; the ordinary profile rejects `source`, and every profile rejects `program`. | +| FE19 | Canonical `text` produces identical output across fresh workflow executions without warnings; `source` and `program` refuse before fragment effects under every profile. | | FE20 | Exact trusted Fetch limits are retained and narrowed; missing or changed limits refuse before requests. | | FE21 | Ordinary and workflow hosts exercise the same fragment evaluator rather than separate semantic implementations. | | FE22 | `xmd plan` output remains ordinary text usable at the command boundary and acquires no special complete-root type. | @@ -13804,9 +13804,9 @@ ordering and teardown are proved with latches rather than elapsed time. | SR5 | Demand runs the body | A read expands the region's authored body, including a nested component; a region created, expanded and subscribed to but never read runs nothing at all | | SR6 | Region output is not document output | Chunks carry the region's rendered text while the construct renders nothing, and no chunk reaches `DocumentOutput` middleware | | SR7 | One demand, one chunk | Subscribing runs nothing; the first read delivers the first chunk while the second element's work has not begun; the next element runs only once a later read asks for it, so a delivered chunk buys no work beyond itself | -| SR8 | The handler's scope owns the producer | A handler returning while a producer is blocked on demand halts and joins it before the occurrence settles, so the unreached segment never runs | +| SR8 | The handler's scope owns the producer | A handler returning while a producer is blocked on demand starts its cleanup; the occurrence stays unsettled behind a controlled cleanup gate and settles only after cleanup finishes, while the unreached segment never runs | | SR9 | Prefix then failure | A body failing after output delivers its prefix first — including output one authored segment appended before failing partway through — then the next read raises the original failure object rather than a rebuilt one | -| SR10 | Fresh each time | Repeated `expand()` calls are independent expansions with their own producers, and nothing refuses a second one | +| SR10 | Fresh each time | Repeated `expand()` calls and two subscriptions to one returned stream each run independent producers and return their own output | | SR11 | Nothing else moved | Expansion without an execution environment resolves the name as an ordinary component; with declarations present, the engine's own constructs and their refusals are unchanged and no handler is reached | ### Tier ED — One declaration catalog, holding both arms (§5.3)