Skip to content
Merged
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
97 changes: 73 additions & 24 deletions packages/cli/tests/repl-lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@

import { beforeAll, describe, it } from "@executablemd/test-support/bdd";
import { expect } from "@executablemd/test-support/expect";
import { useTempFileCompiler } from "@executablemd/core";
import { Component, useTempFileCompiler } from "@executablemd/core";
import { InMemoryStream } from "@executablemd/durable-streams";
import { each, ensure, scoped, sleep, spawn } from "effection";
import { each, ensure, scoped, sleep, spawn, withResolvers } from "effection";
import type { Operation, Result } from "effection";

import { openReplSession } from "../src/repl/session.ts";
import type { ReplSession } from "../src/repl/session.ts";
import type { ReplExecution } from "../src/repl/journal.ts";
import type { ReplQuestion } from "../src/repl/elicitation.ts";
import type { ReplLifecycleReading, ReplOccurrence } from "../src/repl/lifecycle.ts";

function execution(): ReplExecution {
Expand All @@ -37,6 +38,25 @@ function accepted(result: Result<unknown>): void {
}
}

function nextQuestion(session: ReplSession): Operation<ReplQuestion> {
return scoped(function* () {
// Checking before subscribing could lose a question published in between.
const changes = yield* session.elicitation.changes;
const pending = session.elicitation.pending;
if (pending !== undefined) {
return pending;
}
let next = yield* changes.next();
while (!next.done) {
if (next.value !== undefined) {
return next.value;
}
next = yield* changes.next();
}
throw new Error("this execution asked nothing");
});
}

/** Every reading one session published, in order. */
interface Watched {
readonly readings: ReplLifecycleReading[];
Expand Down Expand Up @@ -116,24 +136,7 @@ describe("Tier L — the reading one entry leaves behind", () => {
'required: ["note"], additionalProperties: false }} as="answer">Answer this.</Elicit>\n',
),
);
// Waited for rather than sampled after a fixed number of turns. The phases
// a consumer is told travel through its own queue and the question is
// published from inside the element, so how many turns that takes depends
// on what else the runner has queued — and a fixed count samples whichever
// phase had arrived by then, which is `Elicit:enter:` whenever another
// suite is running beside this one.
//
// The signal is deliberately narrower than the claim below: this waits only
// for *some* element to report that it is waiting on something, and what it
// reports — which element, which phase, what it waits on, and that it is
// the only one — is then asserted whole. A wait for the expected value
// would assert nothing.
for (let turn = 0; turn < 1000; turn += 1) {
if (session.lifecycle.occurrences.some((one) => one.waiting.length > 0)) {
break;
}
yield* sleep(0);
}
const question = yield* nextQuestion(session);

const waiting = session.lifecycle.occurrences.filter((one) => one.waiting.length > 0);
// The element that is waiting, what it is waiting on, and that it is
Expand All @@ -143,10 +146,6 @@ describe("Tier L — the reading one entry leaves behind", () => {
).toEqual(["Elicit:active:question"]);
expect(waiting.map((one) => one.name)).toEqual(["Elicit"]);

const question = session.elicitation.pending;
if (question === undefined) {
throw new Error("this session is waiting on no question");
}
question.submit({ note: "answered" });
yield* session.join();
yield* sleep(0);
Expand All @@ -155,6 +154,56 @@ describe("Tier L — the reading one entry leaves behind", () => {
expect(session.live).toBe(false);
});

it("L2: an entered import has no question wait until it asks", function* () {
const arrived = withResolvers<void>();
const release = withResolvers<void>();
const session = opened(
yield* openReplSession({
execution: execution(),
installations: [
{
*install(): Operation<void> {
yield* Component.around({
*importComponent([name], next) {
if (name === "Elicit") {
arrived.resolve();
yield* release.operation;
}
return yield* next(name);
},
});
},
},
],
}),
);
accepted(
yield* session.submit(
'<Elicit schema={{ type: "object", properties: { note: { type: "string" } }, ' +
'required: ["note"], additionalProperties: false }} as="answer">Answer this.</Elicit>\n',
),
);
yield* arrived.operation;
expect(session.elicitation.pending).toBe(undefined);
expect(
session.lifecycle.occurrences.map((one) => `${one.name}:${one.phase}:${one.waiting.join()}`),
).toEqual(["Elicit:enter:"]);

const asking = yield* spawn(() => nextQuestion(session));
release.resolve();
const question = yield* asking;
const waiting = session.lifecycle.occurrences.filter((one) => one.waiting.length > 0);
expect(
session.lifecycle.occurrences.map((one) => `${one.name}:${one.phase}:${one.waiting.join()}`),
).toEqual(["Elicit:active:question"]);
expect(waiting.map((one) => one.name)).toEqual(["Elicit"]);

question.submit({ note: "answered" });
yield* session.join();
expect(session.lifecycle.occurrences).toEqual([]);
expect(session.live).toBe(false);
});

it("L2: ordinary work is not a wait", function* () {
const session = opened(yield* openReplSession({ execution: execution() }));
const watched = yield* watching(session);
Expand Down
Loading