mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
38719a6fa9
* feat(environment): explain values an application derives from other fields
Prompt-built environments withheld creates whose responses carry values the
application derives deterministically, which the write model kept as
constants or could not match across calls:
- A composed string is literal text plus parts read from the fields other
rules write: a field's value, its slug, or a clock's calendar digits. A
create composes a string only when another entity of the type spells it
the same way and none spells it otherwise, and never from two sources
serving could tell apart; an update composes an unsent change from members
stored as sent and unchanged fields that also spell its value before the
call. A composed field no longer pins the members it reads.
- A trimmed clock format drops trailing fraction zeros, as Go spells
nanoseconds. It is learned from the type's values, a later create or a
later update, and spreads to the plan's other clocks unless a value shows
a fixed width.
- A large integer no entity of the type holds, among distinct ones of the
same length and kind, is a generated unique value, served as a seeded
integer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(environment): record the derived-values verification
Gates, tests, write plans over 69 recorded captures and served write tasks on the baseline and on this branch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(environment): keep clock spellings, linked ids and word boundaries honest
- A later update's clocks take the plan's spelling, sibling-trimmed
clocks included, wherever the call's values fit it, so an update whose
later call trims only one of two clocks is served.
- A held value that ends its fraction in a zero keeps a create's clock
at a fixed width, whatever order the held spellings come in.
- A large integer that another field of the created entity also holds
is linked, as a repeated token is: it is never unique, and the create
is withheld as unexplained-change.
- Composed parts are delimited by whole characters through the shared
joinsBefore/joinsAfter, so an astral letter joins a match to its word.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(environment): keep a same-width fixed clock out of trimming
A held .123456780 beside a held .12345678 keeps the create's clock at
nine digits in either order; the cases fail on 8ce1127b5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(environment): check composed parts and clock fits before they shape a plan
- A composed string whose part has nothing to spell refuses the create
or update through the apply Result instead of keeping a stale value.
- A composition learned on a later call is rebuilt from each earlier
call's fields, so an earlier call that left the value otherwise
contradicts it.
- An update adopts its fitted clock spellings only after the
unknown-entity and contradiction checks, so a call on an entity an
unmodelled write left unknown no longer trims a served plan's clock.
- Sibling trimming on a later create also checks that call's fields,
so a sibling clock it shows with a trailing zero stays fixed-width.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(environment): split the derived-values view tests by rule
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(environment): keep fixed-width, zone and generated-integer evidence through a plan
- A plan keeps the first value that showed each clock at a fixed width,
from held entities and every accepted create or update. No later fit
or sibling trim widens that field, and a later call that shows it
shorter is withheld.
- Two stamped clocks are one composed source only when they share a
zone offset. A held entity that holds the target field as anything
the parts do not spell, a non-string included, vetoes the
composition. The token index is built once, and only when a string is
left to compose.
- A generated integer that another field repeats as a number at any
depth is linked. Compare treats two integers of one JSON kind and
length under a unique rule as generated values.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(setup): share registration prewarm host preparation
* fix(environment): retain write evidence and match trimmed response clocks
* fix(environment): withhold creates whose generated integer another string repeats
A unique integer whose digits show in another top-level string that no
template or composed rule rebuilds from it (an echoed serialText, or a
token like folder-<id>) withholds the create as unexplained-change,
since serving would seed the integer and keep the string. A templated
copy is still served.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(environment): hold later creates and compare to the composed and integer forms
- A later create whose response spells a composed field otherwise than
the plan rebuilds it is withheld as unexplained-change.
- A unique field learned from a generated integer accepts only an
integer of the same length and kind from a later create, as serving
seeds one.
- Compare counts a composed field as equal when each side's response
spells it from its own fields, which compare under their own rules.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(environment): preserve composed parts and unique generator families
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
618 lines
18 KiB
TypeScript
618 lines
18 KiB
TypeScript
import { readFile, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
|
|
import { describe, expect } from "vitest";
|
|
|
|
import {
|
|
analysisViewCaptureEvidence,
|
|
environmentManifestCaptureFixture,
|
|
timedCaptureEvidence,
|
|
type TimedCaptureRow,
|
|
} from "../../fixtures/analysisView.js";
|
|
import { environmentStateServeFixture } from "../../fixtures/environmentServe.js";
|
|
import {
|
|
BOOKMARK_ROWS,
|
|
bookmark,
|
|
TAG_ROWS,
|
|
} from "../../fixtures/environmentStateDerivedValues.js";
|
|
import {
|
|
BOOK_NOT_FOUND,
|
|
BOOK_ROWS,
|
|
FIRST_BOOK,
|
|
SECOND_BOOK,
|
|
THIRD_BOOK,
|
|
book,
|
|
numberedUuid,
|
|
renamedBook,
|
|
} from "../../fixtures/environmentStateWrites.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { cliTest } from "../../support/cli/cliFixture.js";
|
|
|
|
/** The books environment and a probe of the application re-seeded under other ids, written as Evidence files. */
|
|
const writeCompareFiles = async (root: string) => {
|
|
const fixture = environmentStateServeFixture(timedCaptureEvidence(BOOK_ROWS));
|
|
const first = book(101, "First Book", "2026-10-20T09:00:00Z");
|
|
const second = book(102, "Second Book", "2026-10-20T10:00:00Z");
|
|
const probe = timedCaptureEvidence([
|
|
["GET", "/api/books", [first, second]],
|
|
["DELETE", "/api/books/102", "", 204],
|
|
["GET", "/api/books/102", BOOK_NOT_FOUND, 404],
|
|
]);
|
|
const paths = {
|
|
manifest: join(root, "manifest.json"),
|
|
capture: join(root, "capture.json"),
|
|
probe: join(root, "probe.json"),
|
|
};
|
|
await writeFile(paths.manifest, JSON.stringify(fixture.manifest));
|
|
await writeFile(paths.capture, JSON.stringify(fixture.capture));
|
|
await writeFile(paths.probe, JSON.stringify(probe));
|
|
return paths;
|
|
};
|
|
|
|
/** The exact chronology and clock-format probes share one unchanged source capture. */
|
|
const writeExactClockFiles = async (
|
|
root: string,
|
|
): Promise<Map<string, string>> => {
|
|
const capture = timedCaptureEvidence([
|
|
["GET", "/api/books", [FIRST_BOOK]],
|
|
["GET", "/api/books/1", FIRST_BOOK],
|
|
["DELETE", "/api/books/1", "", 204],
|
|
["GET", "/api/books/1", BOOK_NOT_FOUND, 404],
|
|
]);
|
|
const fixture = environmentStateServeFixture(capture);
|
|
const chronologyProbe = (later: string, earlier: string) =>
|
|
analysisViewCaptureEvidence(
|
|
environmentManifestCaptureFixture([
|
|
{
|
|
startedDateTime: later,
|
|
request: { method: "GET", url: "http://app.example/api/books/1" },
|
|
response: {
|
|
status: 404,
|
|
content: {
|
|
mimeType: "application/json",
|
|
text: JSON.stringify(BOOK_NOT_FOUND),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
startedDateTime: earlier,
|
|
request: {
|
|
method: "DELETE",
|
|
url: "http://app.example/api/books/1",
|
|
},
|
|
response: {
|
|
status: 204,
|
|
content: { mimeType: "application/json", text: "" },
|
|
},
|
|
},
|
|
]),
|
|
);
|
|
const ordered = chronologyProbe(
|
|
"0001-01-01T00:00:00.0000000002Z",
|
|
"0001-01-01T01:00:00.0000000001+01:00",
|
|
);
|
|
const legacyOrdered = chronologyProbe(
|
|
"2026-10-20 10:00:00.0002+0100",
|
|
"2026-10-20 09:00:00.0001+0000",
|
|
);
|
|
const clocks = timedCaptureEvidence([
|
|
[
|
|
"GET",
|
|
"/api/books/1",
|
|
{ ...FIRST_BOOK, created_at: "0001-01-01 00:00:00.000Z" },
|
|
],
|
|
]);
|
|
const originals = new Map<string, string>();
|
|
for (const [name, value] of [
|
|
["manifest.json", fixture.manifest],
|
|
["capture.json", capture],
|
|
["order.json", ordered],
|
|
["legacy-order.json", legacyOrdered],
|
|
["clocks.json", clocks],
|
|
] as const) {
|
|
const path = join(root, name);
|
|
const text = JSON.stringify(value);
|
|
originals.set(path, text);
|
|
await writeFile(path, text);
|
|
}
|
|
return originals;
|
|
};
|
|
|
|
/** Capture-confirmed generated writes and literal/clock probes, retained as unchanged Evidence files. */
|
|
const writeValueContextFiles = async (
|
|
root: string,
|
|
): Promise<Map<string, string>> => {
|
|
const recorded = {
|
|
samples: [1700000000],
|
|
nested_samples: [[1700000000]],
|
|
scalar_samples: 1700000000,
|
|
sku: "product1",
|
|
catalog: ["product1"],
|
|
serial: numberedUuid(1),
|
|
updated_at: [1700000000],
|
|
nested: { updated_at: 1700000000 },
|
|
fixed_samples: [1700000000],
|
|
};
|
|
const clocks = {
|
|
...recorded,
|
|
updated_at: [1800000000],
|
|
nested: { updated_at: 1800000000 },
|
|
};
|
|
const changed = {
|
|
...clocks,
|
|
samples: [1800000000],
|
|
nested_samples: [[1800000000]],
|
|
scalar_samples: 1800000000,
|
|
sku: "product2",
|
|
catalog: ["product2"],
|
|
serial: numberedUuid(2),
|
|
};
|
|
const first = { ...FIRST_BOOK, ref: numberedUuid(1) };
|
|
const second = { ...SECOND_BOOK, ref: numberedUuid(2) };
|
|
const third = { ...THIRD_BOOK, ref: numberedUuid(3) };
|
|
const renamed = {
|
|
...renamedBook(first, "Renamed Book", 6),
|
|
ref: numberedUuid(4),
|
|
};
|
|
const missing = { error: "missing", ref: numberedUuid(99) };
|
|
const rows: TimedCaptureRow[] = [
|
|
["GET", "/api/config", recorded],
|
|
["GET", "/api/books", [first, second]],
|
|
["GET", "/api/books/1", first],
|
|
["GET", "/api/books/2", second],
|
|
["POST", "/api/books", third, 201, { name: "Third Book" }],
|
|
["GET", "/api/books/3", third],
|
|
["PATCH", "/api/books/1", renamed, 200, { name: "Renamed Book" }],
|
|
["GET", "/api/books/1", renamed],
|
|
["GET", "/api/books/99", missing, 404],
|
|
];
|
|
const capture = timedCaptureEvidence(rows);
|
|
const fixture = environmentStateServeFixture(capture);
|
|
expect(fixture.state.normalized_result).toMatchObject({
|
|
writes: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
tool: "post_books",
|
|
served: true,
|
|
response: { ordinal: 4, shape: "entity", pointer: "" },
|
|
rules: expect.arrayContaining([{ field: "ref", rule: "unique" }]),
|
|
}),
|
|
expect.objectContaining({
|
|
tool: "patch_books_by_id",
|
|
served: true,
|
|
response: { ordinal: 6, shape: "entity", pointer: "" },
|
|
rules: expect.arrayContaining([{ field: "ref", rule: "unique" }]),
|
|
}),
|
|
]),
|
|
});
|
|
const generated = {
|
|
...book(101, "Third Book", "2026-11-01T12:00:00Z"),
|
|
ref: numberedUuid(101),
|
|
};
|
|
const originals = new Map<string, string>();
|
|
for (const [name, value] of [
|
|
["manifest.json", fixture.manifest],
|
|
["capture.json", capture],
|
|
["changed.json", timedCaptureEvidence([["GET", "/api/config", changed]])],
|
|
["clocks.json", timedCaptureEvidence([["GET", "/api/config", clocks]])],
|
|
[
|
|
"generated.json",
|
|
timedCaptureEvidence([
|
|
["POST", "/api/books", generated, 201, { name: "Third Book" }],
|
|
[
|
|
"PATCH",
|
|
"/api/books/1",
|
|
{ ...renamed, ref: numberedUuid(102) },
|
|
200,
|
|
{ name: "Renamed Book" },
|
|
],
|
|
]),
|
|
],
|
|
[
|
|
"missing.json",
|
|
timedCaptureEvidence([
|
|
[
|
|
"PATCH",
|
|
"/api/books/99",
|
|
{ ...missing, ref: numberedUuid(100) },
|
|
404,
|
|
{ name: "Ghost" },
|
|
],
|
|
]),
|
|
],
|
|
] as const) {
|
|
const path = join(root, name);
|
|
const text = JSON.stringify(value);
|
|
originals.set(path, text);
|
|
await writeFile(path, text);
|
|
}
|
|
return originals;
|
|
};
|
|
|
|
describe("rea environment-compare value context", () => {
|
|
cliTest(
|
|
"keeps business values and numeric measurements literal while normalizing confirmed generation and clocks",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory(
|
|
"rea-environment-value-context-",
|
|
);
|
|
const originals = await writeValueContextFiles(root);
|
|
const result = await cli.run({
|
|
arguments: [
|
|
"environment-compare",
|
|
join(root, "manifest.json"),
|
|
"--evidence",
|
|
join(root, "capture.json"),
|
|
"--probe",
|
|
join(root, "changed.json"),
|
|
"--probe",
|
|
join(root, "clocks.json"),
|
|
"--probe",
|
|
join(root, "generated.json"),
|
|
"--probe",
|
|
join(root, "missing.json"),
|
|
"--json",
|
|
],
|
|
});
|
|
expect(result.exitCode, result.stderr).toBe(0);
|
|
expect(result.json).toMatchObject({
|
|
status: "complete",
|
|
summary: {
|
|
probes: 4,
|
|
steps: 5,
|
|
equal: 3,
|
|
agreement: 0.6,
|
|
normalised: { generated: 2 },
|
|
},
|
|
probes: [
|
|
{
|
|
label: "changed.json",
|
|
normalised: { generated: 0, clocks: 2, ids: 0 },
|
|
results: [
|
|
{
|
|
equal: false,
|
|
categories: ["value"],
|
|
differences: [
|
|
{
|
|
category: "value",
|
|
pointer: "/samples/0",
|
|
live: "1800000000",
|
|
env: "1700000000",
|
|
},
|
|
{
|
|
category: "value",
|
|
pointer: "/nested_samples/0/0",
|
|
live: "1800000000",
|
|
env: "1700000000",
|
|
},
|
|
{
|
|
category: "value",
|
|
pointer: "/scalar_samples",
|
|
live: "1800000000",
|
|
env: "1700000000",
|
|
},
|
|
{
|
|
category: "value",
|
|
pointer: "/sku",
|
|
live: '"product2"',
|
|
env: '"product1"',
|
|
},
|
|
{
|
|
category: "value",
|
|
pointer: "/catalog/0",
|
|
live: '"product2"',
|
|
env: '"product1"',
|
|
},
|
|
{
|
|
category: "value",
|
|
pointer: "/serial",
|
|
live: JSON.stringify(numberedUuid(2)),
|
|
env: JSON.stringify(numberedUuid(1)),
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
label: "clocks.json",
|
|
normalised: { clocks: 2, generated: 0 },
|
|
results: [{ equal: true, differences: [] }],
|
|
},
|
|
{
|
|
label: "generated.json",
|
|
normalised: { generated: 2, ids: 1 },
|
|
results: [
|
|
{ equal: true, differences: [] },
|
|
{ equal: true, differences: [] },
|
|
],
|
|
},
|
|
{
|
|
label: "missing.json",
|
|
normalised: { generated: 0 },
|
|
results: [
|
|
{
|
|
tool: "patch_books_by_id",
|
|
live_status: 404,
|
|
env_status: 404,
|
|
equal: false,
|
|
differences: [
|
|
{
|
|
category: "value",
|
|
pointer: "/ref",
|
|
live: JSON.stringify(numberedUuid(100)),
|
|
env: JSON.stringify(numberedUuid(99)),
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
for (const [path, text] of originals)
|
|
expect(await readFile(path, "utf8")).toBe(text);
|
|
},
|
|
);
|
|
});
|
|
|
|
describe("rea environment-compare recorded clock spelling", () => {
|
|
for (const scenario of [
|
|
{
|
|
name: "variable fractional widths under a confirmed trimmed write plan",
|
|
rows: TAG_ROWS,
|
|
probe: [
|
|
[
|
|
"POST",
|
|
"/api/tags",
|
|
{
|
|
id: numberedUuid(20),
|
|
name: "Spare",
|
|
createdAt: "2026-10-20T10:00:00.12345678Z",
|
|
updatedAt: "2026-10-20T10:00:00.12345678Z",
|
|
},
|
|
201,
|
|
{ name: "Spare" },
|
|
],
|
|
] as const,
|
|
generated: 0,
|
|
},
|
|
{
|
|
name: "a composed bookmark URL through its creation clock",
|
|
rows: BOOKMARK_ROWS,
|
|
probe: [
|
|
[
|
|
"POST",
|
|
"/api/bookmarks",
|
|
bookmark(
|
|
20,
|
|
"https://pier.example.test/inspection",
|
|
"Pier",
|
|
"2026-10-20T10:00:00Z",
|
|
),
|
|
201,
|
|
{ url: "https://pier.example.test/inspection", title: "Pier" },
|
|
],
|
|
] as const,
|
|
generated: 1,
|
|
},
|
|
]) {
|
|
cliTest(`accepts ${scenario.name}`, async ({ cli }) => {
|
|
const root = await createTestTempDirectory(
|
|
"rea-environment-derived-compare-",
|
|
);
|
|
const capture = timedCaptureEvidence(scenario.rows);
|
|
const fixture = environmentStateServeFixture(capture);
|
|
const probe = timedCaptureEvidence(scenario.probe);
|
|
const originals = new Map<string, string>();
|
|
for (const [name, value] of [
|
|
["manifest.json", fixture.manifest],
|
|
["capture.json", capture],
|
|
["probe.json", probe],
|
|
] as const) {
|
|
const path = join(root, name);
|
|
const text = JSON.stringify(value);
|
|
originals.set(path, text);
|
|
await writeFile(path, text);
|
|
}
|
|
const result = await cli.run({
|
|
arguments: [
|
|
"environment-compare",
|
|
join(root, "manifest.json"),
|
|
"--evidence",
|
|
join(root, "capture.json"),
|
|
"--probe",
|
|
join(root, "probe.json"),
|
|
"--json",
|
|
],
|
|
});
|
|
expect(result.exitCode, result.stderr).toBe(0);
|
|
expect(result.json).toMatchObject({
|
|
status: "complete",
|
|
summary: { probes: 1, steps: 1, equal: 1, agreement: 1 },
|
|
probes: [
|
|
{
|
|
normalised: { clocks: 2, generated: scenario.generated },
|
|
results: [{ method: "POST", equal: true, differences: [] }],
|
|
},
|
|
],
|
|
});
|
|
for (const [path, text] of originals)
|
|
expect(await readFile(path, "utf8")).toBe(text);
|
|
});
|
|
}
|
|
|
|
cliTest(
|
|
"replays compact-offset submillisecond probe timestamps in exact order",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory(
|
|
"rea-environment-legacy-chronology-",
|
|
);
|
|
const originals = await writeExactClockFiles(root);
|
|
const result = await cli.run({
|
|
arguments: [
|
|
"environment-compare",
|
|
join(root, "manifest.json"),
|
|
"--evidence",
|
|
join(root, "capture.json"),
|
|
"--probe",
|
|
join(root, "legacy-order.json"),
|
|
"--json",
|
|
],
|
|
});
|
|
expect(result.exitCode, result.stderr).toBe(0);
|
|
expect(result.json).toMatchObject({
|
|
status: "complete",
|
|
environment: { state: "served" },
|
|
summary: { probes: 1, steps: 2, equal: 2, agreement: 1 },
|
|
probes: [
|
|
{
|
|
label: "legacy-order.json",
|
|
results: [
|
|
{
|
|
ordinal: 1,
|
|
method: "DELETE",
|
|
live_status: 204,
|
|
env_status: 204,
|
|
equal: true,
|
|
},
|
|
{
|
|
ordinal: 0,
|
|
method: "GET",
|
|
live_status: 404,
|
|
env_status: 404,
|
|
equal: true,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
for (const [path, text] of originals)
|
|
expect(await readFile(path, "utf8")).toBe(text);
|
|
},
|
|
);
|
|
});
|
|
|
|
describe("rea environment-compare replay and chronology", () => {
|
|
cliTest(
|
|
"preserves exact probe chronology and first-century clock-format differences",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory(
|
|
"rea-environment-exact-clock-",
|
|
);
|
|
const originals = await writeExactClockFiles(root);
|
|
const result = await cli.run({
|
|
arguments: [
|
|
"environment-compare",
|
|
join(root, "manifest.json"),
|
|
"--evidence",
|
|
join(root, "capture.json"),
|
|
"--probe",
|
|
join(root, "order.json"),
|
|
"--probe",
|
|
join(root, "clocks.json"),
|
|
"--json",
|
|
],
|
|
});
|
|
expect(result.exitCode, result.stderr).toBe(0);
|
|
expect(result.json).toMatchObject({
|
|
status: "complete",
|
|
environment: { state: "served" },
|
|
summary: { probes: 2, steps: 3, equal: 2 },
|
|
probes: [
|
|
{
|
|
label: "order.json",
|
|
results: [
|
|
{
|
|
ordinal: 1,
|
|
method: "DELETE",
|
|
live_status: 204,
|
|
env_status: 204,
|
|
equal: true,
|
|
},
|
|
{
|
|
ordinal: 0,
|
|
method: "GET",
|
|
live_status: 404,
|
|
env_status: 404,
|
|
equal: true,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
label: "clocks.json",
|
|
results: [
|
|
{
|
|
categories: ["clock-format"],
|
|
differences: [
|
|
{
|
|
category: "clock-format",
|
|
pointer: "/created_at",
|
|
live: '"0001-01-01 00:00:00.000Z"',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
for (const [path, text] of originals)
|
|
expect(await readFile(path, "utf8")).toBe(text);
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"compares the served environment with a probe of the re-seeded application and prints the report",
|
|
async ({ cli }) => {
|
|
const files = await writeCompareFiles(
|
|
await createTestTempDirectory("rea-environment-compare-cli-"),
|
|
);
|
|
const result = await cli.run({
|
|
arguments: [
|
|
"environment-compare",
|
|
files.manifest,
|
|
"--evidence",
|
|
files.capture,
|
|
"--probe",
|
|
files.probe,
|
|
"--json",
|
|
],
|
|
timeoutMs: 30_000,
|
|
});
|
|
expect(result.exitCode).toBe(0);
|
|
expect(result.json).toMatchObject({
|
|
status: "complete",
|
|
environment: { state: "served" },
|
|
summary: { probes: 1, steps: 3, equal: 3, agreement: 1 },
|
|
probes: [{ label: "probe.json", ids_mapped: 2 }],
|
|
});
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"refuses a probe that is not capture Evidence and names it",
|
|
async ({ cli }) => {
|
|
const files = await writeCompareFiles(
|
|
await createTestTempDirectory("rea-environment-compare-refusal-cli-"),
|
|
);
|
|
const result = await cli.run({
|
|
arguments: [
|
|
"environment-compare",
|
|
files.manifest,
|
|
"--evidence",
|
|
files.capture,
|
|
"--probe",
|
|
files.manifest,
|
|
"--json",
|
|
],
|
|
timeoutMs: 30_000,
|
|
});
|
|
expect(result.exitCode).toBe(1);
|
|
expect(result.json).toMatchObject({
|
|
code: "invalid_request",
|
|
details: {
|
|
operation: "environment-compare",
|
|
issues: [{ path: ["probe", 0] }],
|
|
},
|
|
});
|
|
},
|
|
);
|
|
});
|