Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions packages/core/tests/answer-identity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* `<All>` lets two `<Spawn>` 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", () => {
/**
Expand Down
75 changes: 55 additions & 20 deletions packages/core/tests/evaluate-component.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,30 +279,65 @@ describe("protected generated component answers", () => {
it("refuses copied, mutated, wrapped, unclaimed, misidentified and wrong-form answers", function* () {
const source = `<Evaluate text={'<Syntax />'} />`;
const ran: string[] = [];
const transforms: ((answer: FunctionComponentDefinition) => FunctionComponentDefinition)[] = [
(answer) => ({ ...answer }),
(answer) => ({
...answer,
*fn(): Operation<string> {
ran.push("same name");
return "forged";
const variants: {
name: string;
options: Parameters<typeof syntaxProfile>[0];
reason: string;
}[] = [
{
name: "copied",
options: { transform: (answer) => ({ ...answer }) },
reason: "carries no identity",
},
{
name: "replaced body",
options: {
transform: (answer) => ({
...answer,
*fn(): Operation<string> {
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(`<Evaluate text={'<Syntax></Syntax>'} />`, [syntaxProfile()])),
Expand Down
12 changes: 10 additions & 2 deletions packages/core/tests/execution-init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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('<File path="note.md" />')))).toContain("Files");
expect(parseFilesFatal(fatalCause(yield* failure(run('<File path="note.md" />'))))).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({
Expand All @@ -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('<File path="note.md" />')))).toContain("Files");
expect(parseFilesFatal(fatalCause(yield* failure(run('<File path="note.md" />'))))).toEqual({
type: "executablemd.runtime.files-fatal/v1",
kind: "provider-unavailable",
});
});
});
8 changes: 4 additions & 4 deletions packages/core/tests/expand.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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<void>();
const firstDelivered = withResolvers<void>();
const environment = yield* installedEnvironment(
[structuralDeclaration(), regionDeclaration()],
function* (request) {
Expand All @@ -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}`);
Expand All @@ -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();
});
Expand Down
11 changes: 8 additions & 3 deletions packages/core/tests/syntax-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
});
}
});

Expand Down
5 changes: 5 additions & 0 deletions packages/git/tests/api-entrypoint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -189,7 +194,7 @@

/** The names a module published, through the export map, under its bare specifier. */
function* published(specifier: string): Operation<string[]> {
const namespace: unknown = yield* until(import(specifier));

Check warning on line 197 in packages/git/tests/api-entrypoint.test.ts

View workflow job for this annotation

GitHub Actions / jsr

unable to analyze dynamic import
if (typeof namespace !== "object" || namespace === null) {
return [];
}
Expand All @@ -198,7 +203,7 @@

/** Every published name that is a contextual Api, sorted. */
function* seams(specifier: string): Operation<string[]> {
const namespace: unknown = yield* until(import(specifier));

Check warning on line 206 in packages/git/tests/api-entrypoint.test.ts

View workflow job for this annotation

GitHub Actions / jsr

unable to analyze dynamic import
if (typeof namespace !== "object" || namespace === null) {
return [];
}
Expand All @@ -209,7 +214,7 @@

/** The operation names one published Api answers to, sorted. */
function* operationsOf(specifier: string, name: string): Operation<string[]> {
const namespace: unknown = yield* until(import(specifier));

Check warning on line 217 in packages/git/tests/api-entrypoint.test.ts

View workflow job for this annotation

GitHub Actions / jsr

unable to analyze dynamic import
if (typeof namespace !== "object" || namespace === null) {
return [];
}
Expand Down
6 changes: 3 additions & 3 deletions packages/git/tests/git.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
8 changes: 7 additions & 1 deletion packages/testing/tests/assert-throws.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,13 @@ describe("<AssertThrows>", () => {
),
);
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* () {
Expand Down
38 changes: 29 additions & 9 deletions packages/workflow/tests/workspace-effect-transaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -199,18 +200,24 @@ function retainedRepositoryNames(path: string): string[] {
}
}

function* workspaceContent(read: Operation<string>): Operation<string | undefined> {
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,
Expand Down Expand Up @@ -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<string> {
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";
Expand Down Expand Up @@ -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";

Expand Down Expand Up @@ -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";

Expand Down Expand Up @@ -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`);
Expand Down
5 changes: 4 additions & 1 deletion specs/executable-mdx-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<File />` reads and never writes, and a paired `<File>` 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 — `<File.Delete>`

Expand Down Expand Up @@ -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

Expand Down
Loading