Skip to content

Commit b5a2b3c

Browse files
authored
Merge pull request #916 from taras/agent/issue-914-core-diagnostics
🧪 Identify core refusals and retained test observations
2 parents f2007e6 + bafb473 commit b5a2b3c

10 files changed

Lines changed: 129 additions & 50 deletions

‎packages/core/tests/answer-identity.test.ts‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -584,14 +584,11 @@ describe("Tier CIV — an identity belongs to one answer of one import", () => {
584584
});
585585

586586
/**
587-
* Tier PA10b — two sibling spawns resolving at the same time.
587+
* Tier PA10b — serial component-answer capture.
588588
*
589-
* `<All>` lets two `<Spawn>` children have an import open together, so a
590-
* resolution is no longer the only one in flight. What makes an answer this
591-
* import's is the exact window it was asked in, and windows now belong to the
592-
* engine scope that opened them — so these rows are written with two real
593-
* scopes standing for two spawns, and never with one spawn's bookkeeping
594-
* standing in for the other's.
589+
* Each request retains the window that was open when its handler began. A
590+
* descendant scope preserves that correlation, and closing the window makes
591+
* the retained request unable to record an answer’s identity in a later one.
595592
*/
596593
describe("Tier PA10b — one serial resolution window", () => {
597594
/**

‎packages/core/tests/evaluate-component.test.ts‎

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -279,30 +279,65 @@ describe("protected generated component answers", () => {
279279
it("refuses copied, mutated, wrapped, unclaimed, misidentified and wrong-form answers", function* () {
280280
const source = `<Evaluate text={'<Syntax />'} />`;
281281
const ran: string[] = [];
282-
const transforms: ((answer: FunctionComponentDefinition) => FunctionComponentDefinition)[] = [
283-
(answer) => ({ ...answer }),
284-
(answer) => ({
285-
...answer,
286-
*fn(): Operation<string> {
287-
ran.push("same name");
288-
return "forged";
282+
const variants: {
283+
name: string;
284+
options: Parameters<typeof syntaxProfile>[0];
285+
reason: string;
286+
}[] = [
287+
{
288+
name: "copied",
289+
options: { transform: (answer) => ({ ...answer }) },
290+
reason: "carries no identity",
291+
},
292+
{
293+
name: "replaced body",
294+
options: {
295+
transform: (answer) => ({
296+
...answer,
297+
*fn(): Operation<string> {
298+
ran.push("same name");
299+
return "forged";
300+
},
301+
}),
289302
},
290-
}),
291-
independentWrapper,
292-
(answer) => {
293-
answer.props = { type: "object" };
294-
return answer;
303+
reason: "carries no identity",
304+
},
305+
{
306+
name: "wrapped",
307+
options: { transform: independentWrapper },
308+
reason: "carries no identity",
309+
},
310+
{
311+
name: "mutated",
312+
options: {
313+
transform: (answer) => {
314+
answer.props = { type: "object" };
315+
return answer;
316+
},
317+
},
318+
reason: "carries no identity",
319+
},
320+
{ name: "unclaimed", options: { unclaimed: true }, reason: "carries no identity" },
321+
{
322+
name: "misidentified",
323+
options: { revision: "2" },
324+
reason: "chain answered with test://syntax-provider#Syntax@2",
295325
},
296326
];
297-
for (const transform of transforms) {
298-
const stream = new InMemoryStream();
299-
expect(yield* refusal(run(source, [syntaxProfile({ transform })], stream))).toBeTruthy();
300-
expect(admissions(yield* stream.readAll())).toHaveLength(0);
301-
}
302-
for (const options of [{ unclaimed: true }, { revision: "2" }]) {
327+
for (const { name, options, reason } of variants) {
303328
const stream = new InMemoryStream();
304-
expect(yield* refusal(run(source, [syntaxProfile(options)], stream))).toBeTruthy();
305-
expect(admissions(yield* stream.readAll())).toHaveLength(0);
329+
expect({
330+
variant: name,
331+
refusal: yield* refusal(run(source, [syntaxProfile(options)], stream)),
332+
}).toEqual({
333+
variant: name,
334+
refusal: expect.stringContaining(reason),
335+
});
336+
expect({ variant: name, admissions: admissions(yield* stream.readAll()) }).toEqual({
337+
variant: name,
338+
admissions: [],
339+
});
340+
expect({ variant: name, ran }).toEqual({ variant: name, ran: [] });
306341
}
307342
expect(
308343
yield* refusal(run(`<Evaluate text={'<Syntax></Syntax>'} />`, [syntaxProfile()])),

‎packages/core/tests/execution-init.test.ts‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { createContext, ensure, scoped, spawn, suspend, withResolvers } from "ef
44
import type { Operation } from "effection";
55
import { InMemoryStream } from "@executablemd/durable-streams";
66
import { Execution } from "../api.ts";
7+
import { parseFilesFatal } from "@executablemd/runtime";
8+
import { fatalCause } from "../src/errors.ts";
79
import { collect, execute, Execution as RootExecution, inlineSource } from "../mod.ts";
810
import { executeInstalled, fileReadEntry, Markdown, sourceDigest } from "../host.ts";
911
import type { JournalAdmission } from "../host.ts";
@@ -240,7 +242,10 @@ describe("Tier EI — execution-owned initialization", () => {
240242
});
241243

242244
it("EI7: a no-op initializer invents no Files provider; explicit initialization supplies one", function* () {
243-
expect(String(yield* failure(run('<File path="note.md" />')))).toContain("Files");
245+
expect(parseFilesFatal(fatalCause(yield* failure(run('<File path="note.md" />'))))).toEqual({
246+
type: "executablemd.runtime.files-fatal/v1",
247+
kind: "provider-unavailable",
248+
});
244249
const files = recordedFiles({ "note.md": "INSTALLED BY INIT" });
245250
const output = yield* scoped(function* () {
246251
yield* Execution.around({
@@ -253,6 +258,9 @@ describe("Tier EI — execution-owned initialization", () => {
253258
});
254259
expect(output).toContain("INSTALLED BY INIT");
255260
expect(files.performed).toEqual(["read note.md"]);
256-
expect(String(yield* failure(run('<File path="note.md" />')))).toContain("Files");
261+
expect(parseFilesFatal(fatalCause(yield* failure(run('<File path="note.md" />'))))).toEqual({
262+
type: "executablemd.runtime.files-fatal/v1",
263+
kind: "provider-unavailable",
264+
});
257265
});
258266
});

‎packages/core/tests/expand.test.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, it } from "@executablemd/test-support/bdd";
22
import { expect } from "@executablemd/test-support/expect";
3-
import { ensure, scoped, sleep, spawn, withResolvers } from "effection";
3+
import { ensure, scoped, spawn, withResolvers } from "effection";
44
import type { Subscription } from "effection";
55
import { createBlockCounter, expandSegments } from "../src/expand.ts";
66
import { regionStream } from "../src/expansion-region.ts";
@@ -1469,6 +1469,7 @@ describe("Tier SR — expanding installed structural syntax", () => {
14691469
it("SR7: one demand permits one chunk, and later work waits for later demand", function* () {
14701470
const order: string[] = [];
14711471
const released = withResolvers<void>();
1472+
const firstDelivered = withResolvers<void>();
14721473
const environment = yield* installedEnvironment(
14731474
[structuralDeclaration(), regionDeclaration()],
14741475
function* (request) {
@@ -1483,6 +1484,7 @@ describe("Tier SR — expanding installed structural syntax", () => {
14831484
// element's text asks again — each ask is one more permission to work.
14841485
const first = yield* readText(subscription);
14851486
order.push(`first:${first}`);
1487+
firstDelivered.resolve();
14861488
yield* released.operation;
14871489
const second = yield* readText(subscription);
14881490
order.push(`second:${second}`);
@@ -1505,9 +1507,7 @@ describe("Tier SR — expanding installed structural syntax", () => {
15051507
yield* spawn(function* () {
15061508
// Released only once the first chunk has been delivered, so the second
15071509
// segment's authored work cannot have run before this point.
1508-
while (!order.some((entry) => entry.startsWith("first:"))) {
1509-
yield* sleep(0);
1510-
}
1510+
yield* firstDelivered.operation;
15111511
expect(order).not.toContain("ran:2");
15121512
released.resolve();
15131513
});

‎packages/core/tests/syntax-api.test.ts‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,11 +181,16 @@ describe("Tier SA — contextual Syntax", () => {
181181
.snapshot()
182182
.filter((event) => event.type === "yield" && event.description.type === "syntax_symbols");
183183
expect(records).toHaveLength(2);
184-
for (const record of records) {
184+
for (const [index, record] of records.entries()) {
185185
if (record.type !== "yield" || record.result.status !== "ok") {
186-
throw new Error("no retained Syntax answer");
186+
throw new Error(
187+
`Syntax occurrence ${index + 1} retained an unexpected result: ${JSON.stringify(record)}`,
188+
);
187189
}
188-
expect(JSON.stringify(record.result.value)).toContain("FINAL NAMED");
190+
expect({ occurrence: index + 1, retained: JSON.stringify(record.result.value) }).toEqual({
191+
occurrence: index + 1,
192+
retained: expect.stringContaining("FINAL NAMED"),
193+
});
189194
}
190195
});
191196

‎packages/git/tests/api-entrypoint.test.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222
* fails under Deno, one missing from `package.json` fails under Node and Bun,
2323
* and only running all three tells those apart.
2424
*
25+
* Emitted Git API identity composition across physical package copies is
26+
* exercised by "publishes contextual Git APIs that compose across copies and
27+
* loads a named Plugin" in `scripts/tests/cli-npm-bin.test.ts`; this file holds
28+
* the portable source export-map contract.
29+
*
2530
* Read through `Reflect.get` rather than a cast: what a module exported is
2631
* whatever it exported, and the members below are proven present rather than
2732
* assumed.

‎packages/git/tests/git.test.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
*
44
* `GitQuery.resolve()` verifies and resolves one revision expression in the
55
* contextual working directory. The default invokes the Git CLI; a host or a
6-
* test replaces it lexically. Nothing here shells out: every test either
7-
* replaces `GitQuery` or stubs the process boundary beneath it, so the suite
8-
* runs the same wherever it is checked out.
6+
* test replaces it lexically. GT1–GT5 replace `GitQuery` or stub the process
7+
* boundary. GT6–GT7 exercise the shipped default against temporary directories,
8+
* including a real Git checkout.
99
*/
1010

1111
import { describe, it } from "@executablemd/test-support/bdd";

‎packages/testing/tests/assert-throws.test.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,13 @@ describe("<AssertThrows>", () => {
119119
),
120120
);
121121
expect(run.results[0]?.status).toBe("fail");
122-
expect(run.results[0]?.error?.message ?? "").not.toContain("none was raised");
122+
expect(run.results[0]?.error).toMatchObject({
123+
kind: "assertion",
124+
actual: "1",
125+
expected: "2",
126+
});
127+
expect(run.output).toContain("**AssertEquals** failed");
128+
expect(run.output).not.toContain("**AssertThrows** failed");
123129
});
124130

125131
it("stops expanding body children after the first raised error, rendering nothing", function* () {

‎packages/workflow/tests/workspace-effect-transaction.test.ts‎

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
type WorkflowWorkspaceMutation,
4444
type WorkflowWorkspaceTransaction,
4545
} from "../src/deno/workspace/effect.ts";
46+
import { journalableWorkspaceCode } from "../src/deno/workspace/errors.ts";
4647
import { readWorkflowWorkspace } from "../src/deno/workspace/inspect.ts";
4748
import type { WorkflowWorkspaceSnapshot } from "../src/deno/workspace/inspect.ts";
4849
import type {
@@ -199,18 +200,24 @@ function retainedRepositoryNames(path: string): string[] {
199200
}
200201
}
201202

203+
function* workspaceContent(read: Operation<string>): Operation<string | undefined> {
204+
try {
205+
return yield* read;
206+
} catch (error) {
207+
if (journalableWorkspaceCode(error) === "ENOENT") {
208+
return undefined;
209+
}
210+
throw error;
211+
}
212+
}
213+
202214
function* inspectWorkspace(
203215
database: WorkflowRunDatabase,
204216
path: string,
205217
): Operation<{ root: string; content: string | undefined }> {
206218
const inspected = yield* database.transact(function* (transaction) {
207219
return yield* withPrivateWorkspaceTransaction(database, transaction, function* (workspace) {
208-
let content: string | undefined;
209-
try {
210-
content = yield* workspace.filesystem.readTextFile(path);
211-
} catch {
212-
content = undefined;
213-
}
220+
const content = yield* workspaceContent(workspace.filesystem.readTextFile(path));
214221
return {
215222
root: yield* workspace.currentRoot(),
216223
content,
@@ -316,6 +323,19 @@ function setSqliteAuthorizer(
316323
}
317324

318325
describe("Tier WAC — atomic provider-level Workspace effects", () => {
326+
it("Workspace observation preserves a read failure that is not genuine absence", function* () {
327+
const failures = [
328+
Object.assign(new Error("Workspace read denied"), { code: "EACCES" }),
329+
Object.assign(new Error("Workspace absence impostor"), { code: "ENOENT" }),
330+
];
331+
for (const failure of failures) {
332+
const read = (function* (): Operation<string> {
333+
throw failure;
334+
})();
335+
expect(yield* raised(workspaceContent(read))).toBe(failure);
336+
}
337+
});
338+
319339
it("WAC1: mutation, root, filtered Yield and commit become visible together", function* () {
320340
const root = yield* useStorageRoot();
321341
const runId = "atomic-success";
@@ -1653,7 +1673,7 @@ describe("Tier WAC — atomic provider-level Workspace effects", () => {
16531673
});
16541674
});
16551675

1656-
it("WAC24: a row written through the storage view commits with the effect's bytes", function* () {
1676+
it("WAC27: a row written through the storage view commits with the effect's bytes", function* () {
16571677
const root = yield* useStorageRoot();
16581678
const runId = "atomic-storage-commit";
16591679

@@ -1697,7 +1717,7 @@ describe("Tier WAC — atomic provider-level Workspace effects", () => {
16971717
});
16981718
});
16991719

1700-
it("WAC25: a refused mutation takes its rows back with its bytes", function* () {
1720+
it("WAC28: a refused mutation takes its rows back with its bytes", function* () {
17011721
const root = yield* useStorageRoot();
17021722
const runId = "atomic-storage-rollback";
17031723

@@ -1753,7 +1773,7 @@ describe("Tier WAC — atomic provider-level Workspace effects", () => {
17531773
* its root is still open, which is exactly where "for this callback" has to
17541774
* be a fact rather than a convention.
17551775
*/
1756-
it("WAC26: every capability a mutation retained answers nothing once it returned", function* () {
1776+
it("WAC29: every capability a mutation retained answers nothing once it returned", function* () {
17571777
const root = yield* useStorageRoot();
17581778
const runId = "atomic-storage-retained";
17591779
const path = join(root, `${runId}.sqlite`);

‎specs/executable-mdx-spec.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11777,7 +11777,7 @@ visible warning blocks, gather into a separate error report).
1177711777
| 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 |
1177811778
| 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) |
1177911779
| FL36 | An honest chain | With no handler interfering, both forms reach the same providers and produce the same bytes |
11780-
| 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 |
11780+
| 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 |
1178111781

1178211782
### Tier FD — `<File.Delete>`
1178311783

@@ -12659,6 +12659,9 @@ workspaces](./workflow-workspace-spec.md) §13.
1265912659
| 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 |
1266012660
| 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 |
1266112661
| 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 |
12662+
| 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)) |
12663+
| 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)) |
12664+
| 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)) |
1266212665

1266312666
### Tier WTX — WorkflowRun savepoints and transaction ownership
1266412667

0 commit comments

Comments
 (0)