Skip to content

Commit 624dba0

Browse files
committed
fix: preserve validation initialization and ordinary input failures
1 parent 3300588 commit 624dba0

7 files changed

Lines changed: 61 additions & 26 deletions

File tree

‎packages/cli/src/repl/elicitation.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export interface ReplFieldConstraint {
5959

6060
/** One field the form presents, in the order the schema declared it. */
6161
export interface ReplFormField extends ReplFieldConstraint {
62-
readonly secret: boolean;
62+
readonly secret?: boolean;
6363
/** The schema's own label for it, when it wrote one. */
6464
readonly title: string | undefined;
6565
readonly description: string | undefined;
@@ -421,7 +421,7 @@ export function readQuestionForm(schema: Json): ReplQuestionForm {
421421
choices: choicesOf(field, path),
422422
minLength: minLengthOf(field, path),
423423
required: required.includes(name),
424-
secret: field.writeOnly === true,
424+
...(field.writeOnly === true ? { secret: true } : {}),
425425
}),
426426
);
427427
}

‎packages/cli/tests/protected-input-forms.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ describe("protected local forms", () => {
2828
if (question === undefined) {
2929
throw new Error("No active question.");
3030
}
31-
expect(question.form.fields.map((field) => field.secret)).toEqual([true, false]);
31+
expect(question.form.fields.map((field) => field.secret === true)).toEqual([true, false]);
32+
expect(Object.hasOwn(question.form.fields[1], "secret")).toBe(false);
3233
const value = String(Math.floor(100000 + Math.random() * 900000));
3334
const invalid = question.submit({ code: value.slice(0, 2), public: "useful" });
3435
expect(invalid.kind).toBe("invalid");

‎packages/core/src/expand.ts‎

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { inputValues } from "./secrets/inputs.ts";
1+
import { inputValues, validateLiveReturnValue } from "./secrets/inputs.ts";
22
import type { ProtectedInput } from "./secrets/inputs.ts";
33
/**
44
* Expansion engine (spec §5).
@@ -147,12 +147,7 @@ import {
147147
} from "@executablemd/durable-streams";
148148
import type { DurablePosition, Workflow } from "@executablemd/durable-streams";
149149
import { SourceOwner } from "./source-owner.ts";
150-
import {
151-
SchemaValidationError,
152-
validateProps,
153-
validateReturnValue,
154-
validateLiveReturnValue,
155-
} from "./validate.ts";
150+
import { SchemaValidationError, validateProps, validateReturnValue } from "./validate.ts";
156151
import { isJsonObject, parseJson } from "./json.ts";
157152
import { healSegment } from "./heal.ts";
158153
import { scanSegments, splitTextFences } from "./scanner.ts";

‎packages/core/src/secrets/inputs.ts‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { compileReturnsSchema, ReturnValidationError, normalizeIssues } from "../validate.ts";
1+
import {
2+
compileReturnsSchema,
3+
ReturnValidationError,
4+
normalizeIssues,
5+
validateReturnValue,
6+
} from "../validate.ts";
27
import type { ReturnsSchema } from "../types.ts";
38
import { createContext, Err, Ok, ensure } from "effection";
49
import type { Operation, Result } from "effection";
@@ -193,7 +198,7 @@ export function* createInputOwner(
193198
known.add(value);
194199
candidates = [...known]
195200
.filter((entered) => entered.length > 0)
196-
.toSorted((left, right) => right.length - left.length);
201+
.sort((left, right) => right.length - left.length);
197202
return leaf;
198203
},
199204
record: (value) => attempt(() => encode(value)),
@@ -361,3 +366,15 @@ export function inspectInputOwner(owner: SecretInputOwner): InputValues {
361366
}
362367
return inspect(false);
363368
}
369+
370+
export function* validateLiveReturnValue(
371+
name: string,
372+
value: unknown,
373+
schema: ReturnsSchema,
374+
): Operation<unknown> {
375+
const inputs = yield* inputValues();
376+
if (inputs?.contains(value)) {
377+
return yield* inputs.validate(value, name, schema);
378+
}
379+
return yield* validateReturnValue(name, value, schema);
380+
}

‎packages/core/src/secrets/policy.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,12 @@ export function* clearsLiveOutput(content: string): Operation<boolean> {
253253
}
254254
}
255255

256-
function projectInputEvent(event: DurableEvent, inputs: InputValues): DurableEvent {
256+
function projectInputEvent(received: DurableEvent, inputs: InputValues): DurableEvent {
257+
const serialized = parseDurableEvent(serializeDurableEvent(received));
258+
if (!serialized.ok) {
259+
throw serialized.error;
260+
}
261+
const event = serialized.value;
257262
const project = (value: unknown) => {
258263
const safe = inputs.display(value);
259264
if (!safe.ok) {

‎packages/core/src/validate.ts‎

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { inputValues } from "./secrets/inputs.ts";
21
import { Ajv } from "ajv";
32
import type { ErrorObject, ValidateFunction } from "ajv";
43
import { createContext } from "effection";
@@ -316,15 +315,3 @@ function preciseInstancePath(issue: NormalizedIssue): string {
316315
function escapePointerToken(token: string): string {
317316
return token.replace(/~/g, "~0").replace(/\//g, "~1");
318317
}
319-
320-
export function* validateLiveReturnValue(
321-
name: string,
322-
value: unknown,
323-
schema: ReturnsSchema,
324-
): Operation<unknown> {
325-
const inputs = yield* inputValues();
326-
if (inputs?.contains(value)) {
327-
return yield* inputs.validate(value, name, schema);
328-
}
329-
return yield* validateReturnValue(name, value, schema);
330-
}

‎packages/core/tests/protected-input.test.ts‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -784,3 +784,33 @@ describe("the private process result boundary", () => {
784784
expect(safe).toBe(true);
785785
});
786786
});
787+
788+
import { useSecretDetection } from "../src/secrets/policy.ts";
789+
790+
describe("serialized failure projection", () => {
791+
it("projects the actual journal representation when optional error fields are undefined", function* () {
792+
const inputs = yield* attachInputOwner(yield* createInputOwner());
793+
const entered = String(Math.floor(100000 + Math.random() * 900000));
794+
inputs.protect({ coroutine: "root", index: 0, field: "code" }, entered);
795+
const stream = new InMemoryStream();
796+
const detection = yield* useSecretDetection(undefined, stream, inputs);
797+
yield* detection.journal.append({
798+
type: "yield",
799+
coroutineId: "root",
800+
description: { type: "failure-probe", name: "failure-probe" },
801+
result: { status: "err", error: { message: entered, name: undefined, stack: undefined } },
802+
});
803+
const event = stream.snapshot()[0];
804+
expect(
805+
event.type === "yield" &&
806+
event.result.status === "err" &&
807+
event.result.error.message === "[redacted]",
808+
).toBe(true);
809+
expect(
810+
event.result.status === "err" &&
811+
!Object.hasOwn(event.result.error, "stack") &&
812+
!Object.hasOwn(event.result.error, "name"),
813+
).toBe(true);
814+
expect(JSON.stringify(stream.snapshot()).includes(entered)).toBe(false);
815+
});
816+
});

0 commit comments

Comments
 (0)