@canonical/pragma-cli 0.32.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -0
- package/dist/pragma +0 -0
- package/package.json +14 -13
- package/src/bin.ts +2 -2
- package/src/capabilities/block/blockList.test.ts +79 -0
- package/src/capabilities/block/blockList.verb.ts +18 -5
- package/src/capabilities/block/pack.ts +12 -1
- package/src/capabilities/block/parity.test.ts +9 -2
- package/src/capabilities/capabilities/capabilities.test.ts +10 -1
- package/src/capabilities/capabilities/capabilities.verb.ts +1 -1
- package/src/capabilities/capabilities/catalog.ts +7 -3
- package/src/capabilities/capabilities/hints.ts +8 -17
- package/src/capabilities/capabilities/types.ts +2 -0
- package/src/capabilities/colophon/colophon.render.test.ts +31 -4
- package/src/capabilities/colophon/colophon.render.ts +13 -4
- package/src/capabilities/colophon/colophon.test.ts +3 -3
- package/src/capabilities/colophon/colophon.verb.ts +4 -1
- package/src/capabilities/colophon/markdownTerminal.ts +33 -14
- package/src/capabilities/colophon/pragmaColophon.ts +4 -4
- package/src/capabilities/colophon/types.ts +1 -1
- package/src/capabilities/config/fields.ts +21 -92
- package/src/capabilities/config/set.test.ts +6 -6
- package/src/capabilities/config/set.verb.ts +5 -5
- package/src/capabilities/config/show.render.test.ts +65 -0
- package/src/capabilities/config/show.render.ts +56 -10
- package/src/capabilities/config/show.verb.ts +5 -5
- package/src/capabilities/create/byteEquality.subprocess.test.ts +1 -1
- package/src/capabilities/create/byteEquality.test.ts +7 -4
- package/src/capabilities/create/compiledCreate.subprocess.test.ts +1 -1
- package/src/capabilities/create/create.test.ts +32 -3
- package/src/capabilities/create/create.verb.ts +69 -11
- package/src/capabilities/doctor/checks/checkConfigFile.ts +1 -1
- package/src/capabilities/doctor/checks/checkMcpCommands.ts +6 -0
- package/src/capabilities/doctor/checks/checkMcpConfigured.ts +10 -4
- package/src/capabilities/doctor/checks/deriveBand.test.ts +42 -0
- package/src/capabilities/doctor/checks/deriveBand.ts +28 -0
- package/src/capabilities/doctor/doctor.render.test.ts +170 -0
- package/src/capabilities/doctor/doctor.render.ts +116 -34
- package/src/capabilities/doctor/doctor.test.ts +31 -1
- package/src/capabilities/doctor/runChecks.ts +28 -4
- package/src/capabilities/doctor/types.ts +15 -0
- package/src/capabilities/ontology/index.ts +10 -3
- package/src/capabilities/ontology/ontology.test.ts +157 -5
- package/src/capabilities/ontology/verbs.ts +192 -93
- package/src/capabilities/prompt/prompt.render.test.ts +94 -0
- package/src/capabilities/prompt/prompt.render.ts +23 -6
- package/src/capabilities/prompt/prompt.test.ts +61 -0
- package/src/capabilities/reference.test.ts +32 -0
- package/src/capabilities/resources/provider.ts +28 -14
- package/src/capabilities/resources/resources.test.ts +99 -0
- package/src/capabilities/scopeTypeSync.test.ts +37 -0
- package/src/capabilities/setup/index.ts +7 -1
- package/src/capabilities/setup/operations/setupCompletions.ts +49 -16
- package/src/capabilities/setup/operations/setupGenerator.ts +157 -61
- package/src/capabilities/setup/operations/setupLsp.ts +56 -12
- package/src/capabilities/setup/operations/setupMcp.ts +216 -39
- package/src/capabilities/setup/operations/setupSkills.ts +7 -1
- package/src/capabilities/setup/setup.render.test.ts +109 -0
- package/src/capabilities/setup/setup.render.ts +61 -14
- package/src/capabilities/setup/setup.test.ts +352 -1
- package/src/capabilities/setup/setup.verb.ts +57 -5
- package/src/capabilities/setup/types.ts +59 -2
- package/src/capabilities/shared/bands.ts +16 -0
- package/src/capabilities/sources/collectStatus.ts +19 -4
- package/src/capabilities/sources/runUpdate.ts +58 -1
- package/src/capabilities/sources/sources.test.ts +187 -1
- package/src/capabilities/sources/status.render.test.ts +65 -0
- package/src/capabilities/sources/status.render.ts +50 -23
- package/src/capabilities/surface.test.ts +34 -9
- package/src/capabilities/tier/lookup.render.test.ts +72 -0
- package/src/capabilities/tier/lookup.render.ts +33 -8
- package/src/capabilities/tier/lookup.test.ts +15 -0
- package/src/capabilities/tier/runLookup.ts +28 -0
- package/src/constants.test.ts +1 -1
- package/src/constants.ts +2 -2
- package/src/kernel/completion/__snapshots__/emitScripts.test.ts.snap +13 -14
- package/src/kernel/completion/bashDrive.test.ts +3 -3
- package/src/kernel/completion/complete.test.ts +3 -5
- package/src/kernel/completion/entitySource.ts +14 -6
- package/src/kernel/completion/model.test.ts +2 -2
- package/src/kernel/completion/model.ts +0 -1
- package/src/kernel/completion/parse.test.ts +2 -4
- package/src/kernel/error/PragmaError.test.ts +4 -4
- package/src/kernel/error/renderError.ts +1 -1
- package/src/kernel/packs/compile.ts +1 -1
- package/src/kernel/project/cli/buildProgram.test.ts +51 -7
- package/src/kernel/project/cli/buildProgram.ts +11 -0
- package/src/kernel/project/cli/globalFlags.test.ts +12 -4
- package/src/kernel/project/cli/globalFlags.ts +14 -14
- package/src/kernel/project/cli/rootHelp.ts +4 -2
- package/src/kernel/project/mcp/instructions.ts +1 -1
- package/src/kernel/project/mcp/mcpError.ts +52 -0
- package/src/kernel/project/mcp/prompts/prompts.test.ts +8 -0
- package/src/kernel/project/mcp/prompts/provider.ts +36 -6
- package/src/kernel/render/formatters.ts +5 -4
- package/src/kernel/render/renderers.test.ts +34 -0
- package/src/kernel/render/renderers.ts +25 -7
- package/src/kernel/render/style.test.ts +31 -0
- package/src/kernel/render/style.ts +74 -0
- package/src/kernel/runtime/graphpack/build.ts +28 -0
- package/src/kernel/runtime/graphpack/buildIndex.test.ts +103 -0
- package/src/kernel/runtime/graphpack/buildIndex.ts +124 -36
- package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +3 -6
- package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +1 -2
- package/src/kernel/runtime/graphpack/graphpack.test.ts +69 -1
- package/src/kernel/runtime/graphpack/read.ts +28 -13
- package/src/kernel/runtime/graphpack/types.ts +14 -0
- package/src/kernel/runtime/loadSession.ts +4 -12
- package/src/kernel/runtime/refs/resolve.ts +63 -3
- package/src/kernel/runtime/storeReadiness.ts +61 -0
- package/src/kernel/runtime/types.ts +1 -1
- package/src/kernel/runtime/wasmEmbed.test.ts +3 -1
- package/src/kernel/spec/emitReference.test.ts +196 -0
- package/src/kernel/spec/emitReference.ts +491 -0
- package/src/kernel/spec/emitSurface.ts +4 -2
- package/src/kernel/spec/index.ts +2 -0
- package/src/kernel/spec/surfaceConformance.test.ts +2 -2
- package/src/testing/PARITY_GAPS.ts +1 -1
- package/src/testing/behavioral/autoLlm.e2e.test.ts +12 -2
- package/src/testing/behavioral/completion.test.ts +2 -2
- package/src/testing/behavioral/disclosure.test.ts +12 -0
- package/src/testing/behavioral/docExamples.test.ts +283 -0
- package/src/testing/behavioral/errorMatrix.storeless.test.ts +2 -1
- package/src/testing/behavioral/journeys.cli.test.ts +3 -1
- package/src/testing/behavioral/journeys.defaultPack.test.ts +571 -0
- package/src/testing/behavioral/journeys.interactiveTty.e2e.test.ts +66 -0
- package/src/testing/behavioral/journeys.livePack.e2e.test.ts +129 -0
- package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +176 -0
- package/src/testing/behavioral/mcpSurface.test.ts +1 -1
- package/src/testing/behavioral/staleCommands.test.ts +76 -0
- package/src/testing/eval/__snapshots__/eval.test.ts.snap +2 -2
- package/src/testing/eval/cases/assembly.ts +3 -3
- package/src/testing/eval/cases/effectful.ts +6 -3
- package/src/testing/eval/cases/readNouns.ts +4 -1
- package/src/testing/fixtures/fixtureCapability.ts +156 -0
- package/src/testing/fixtures/graph/defaultPack.ts +178 -0
- package/src/testing/fixtures/graph/owlExport.ts +95 -0
- package/src/testing/helpers/extractSnippets.ts +29 -0
- package/src/testing/helpers/golden.ts +1 -1
- package/src/capabilities/config/field.test.ts +0 -188
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E1 (AV-231, Backlog E) — the hermetic default-pack journey.
|
|
3
|
+
*
|
|
4
|
+
* The root method fix: the pre-existing suite tested specs against a clean,
|
|
5
|
+
* single-typed 6-entity fixture, never the PRODUCT against a cold, real,
|
|
6
|
+
* multi-typed pack. This file drives the REAL path end to end over the vendored
|
|
7
|
+
* {@link DEFAULT_PACK_TTL} default pack — `sources update` (resolve → build →
|
|
8
|
+
* lock, via `bootFixtureRuntime`) → boot → `graph query` → `block list` /
|
|
9
|
+
* `ontology list` (populated AND empty) → the error paths (malformed `.ttl`,
|
|
10
|
+
* corrupt `schema.json`, a missing pinned commit).
|
|
11
|
+
*
|
|
12
|
+
* It also pins, in-process, the real-data shapes the clean fixture masked — the
|
|
13
|
+
* `entityTotal` double-count (A1), the untiered block that `block list` drops
|
|
14
|
+
* (A2), and the multilingual label (A7). These were HAND-OFFS to lane A, pinned
|
|
15
|
+
* with `it.fails`; lane A's fixes have landed, so they are now LIVE regression
|
|
16
|
+
* guards. This is the durable fix the issue asks for: "we'd have caught this in
|
|
17
|
+
* use" becomes "CI catches it".
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { execFileSync } from "node:child_process";
|
|
21
|
+
import {
|
|
22
|
+
mkdirSync,
|
|
23
|
+
mkdtempSync,
|
|
24
|
+
rmSync,
|
|
25
|
+
truncateSync,
|
|
26
|
+
writeFileSync,
|
|
27
|
+
} from "node:fs";
|
|
28
|
+
import { tmpdir } from "node:os";
|
|
29
|
+
import { join } from "node:path";
|
|
30
|
+
import { runTask } from "@canonical/task/node";
|
|
31
|
+
import { afterAll, describe, expect, it } from "vitest";
|
|
32
|
+
import { blockModule } from "../../capabilities/block/index.js";
|
|
33
|
+
import { graphQueryVerb } from "../../capabilities/graph/query.verb.js";
|
|
34
|
+
import {
|
|
35
|
+
ontologyListVerb,
|
|
36
|
+
ontologyShowVerb,
|
|
37
|
+
} from "../../capabilities/ontology/verbs.js";
|
|
38
|
+
import { buildUpdateTask } from "../../capabilities/sources/runUpdate.js";
|
|
39
|
+
import { updateVerb } from "../../capabilities/sources/update.verb.js";
|
|
40
|
+
import { VERSION } from "../../constants.js";
|
|
41
|
+
import {
|
|
42
|
+
entityTotal,
|
|
43
|
+
readPackIndex,
|
|
44
|
+
} from "../../kernel/completion/entitySource.js";
|
|
45
|
+
import type { ConfigLayers, PackageEntry } from "../../kernel/config/types.js";
|
|
46
|
+
import { PragmaError } from "../../kernel/error/PragmaError.js";
|
|
47
|
+
import { executeVerb } from "../../kernel/project/cli/dispatch.js";
|
|
48
|
+
import { bootRuntime } from "../../kernel/runtime/boot.js";
|
|
49
|
+
import { createQueryFacade } from "../../kernel/runtime/facade.js";
|
|
50
|
+
import { SCHEMA_FILE } from "../../kernel/runtime/graphpack/types.js";
|
|
51
|
+
import { readLock } from "../../kernel/runtime/lock.js";
|
|
52
|
+
import { packDir } from "../../kernel/runtime/paths.js";
|
|
53
|
+
import { createLazyStore } from "../../kernel/runtime/store.js";
|
|
54
|
+
import type { GlobalFlags, PragmaRuntime } from "../../kernel/runtime/types.js";
|
|
55
|
+
import type { VerbSpec } from "../../kernel/spec/types.js";
|
|
56
|
+
import {
|
|
57
|
+
DEFAULT_PACK_ALL_VISIBLE_CONFIG,
|
|
58
|
+
DEFAULT_PACK_CONFIG,
|
|
59
|
+
DEFAULT_PACK_TTL,
|
|
60
|
+
INSTANCE_ONLY_TTL,
|
|
61
|
+
MALFORMED_TTL,
|
|
62
|
+
NO_BLOCKS_TTL,
|
|
63
|
+
UNTIERED_BLOCK_NAME,
|
|
64
|
+
} from "../fixtures/graph/defaultPack.js";
|
|
65
|
+
import {
|
|
66
|
+
bootFixtureRuntime,
|
|
67
|
+
type FixtureGraph,
|
|
68
|
+
type FixtureGraphOptions,
|
|
69
|
+
} from "../helpers/fixtureGraph.js";
|
|
70
|
+
|
|
71
|
+
const JSON_FLAGS: GlobalFlags = {
|
|
72
|
+
llm: false,
|
|
73
|
+
autoLlm: false,
|
|
74
|
+
format: "json",
|
|
75
|
+
verbose: false,
|
|
76
|
+
};
|
|
77
|
+
const NO_MUTATION = { dryRun: false, undo: false, yes: false };
|
|
78
|
+
|
|
79
|
+
const blockListVerb = blockModule.verbs.find(
|
|
80
|
+
(verb) => verb.path.join(" ") === "block list",
|
|
81
|
+
) as VerbSpec;
|
|
82
|
+
|
|
83
|
+
/** Track fixtures so a single `afterAll` disposes every temp dir + store. */
|
|
84
|
+
const fixtures: FixtureGraph[] = [];
|
|
85
|
+
|
|
86
|
+
/** Boot a tracked fixture from inline TTL (+ optional config), for auto-disposal. */
|
|
87
|
+
async function boot(
|
|
88
|
+
ttl: string,
|
|
89
|
+
config?: FixtureGraphOptions["config"],
|
|
90
|
+
): Promise<FixtureGraph> {
|
|
91
|
+
const fixture = await bootFixtureRuntime({
|
|
92
|
+
ttl,
|
|
93
|
+
...(config ? { config } : {}),
|
|
94
|
+
});
|
|
95
|
+
fixtures.push(fixture);
|
|
96
|
+
return fixture;
|
|
97
|
+
}
|
|
98
|
+
afterAll(async () => {
|
|
99
|
+
await Promise.all(fixtures.map((fixture) => fixture.dispose()));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
/** The `--format json` envelope a read verb renders. */
|
|
103
|
+
interface Envelope {
|
|
104
|
+
readonly ok: boolean;
|
|
105
|
+
readonly data: unknown;
|
|
106
|
+
readonly meta: Record<string, unknown>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Run a read verb at a fixture's cwd and parse its JSON envelope. */
|
|
110
|
+
async function readVerb(
|
|
111
|
+
verb: VerbSpec,
|
|
112
|
+
params: Record<string, unknown>,
|
|
113
|
+
cwd: string,
|
|
114
|
+
): Promise<Envelope> {
|
|
115
|
+
const outcome = await executeVerb(
|
|
116
|
+
verb,
|
|
117
|
+
params,
|
|
118
|
+
NO_MUTATION,
|
|
119
|
+
bootRuntime(JSON_FLAGS, cwd),
|
|
120
|
+
);
|
|
121
|
+
return JSON.parse(outcome.stdout as string) as Envelope;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The sorted `name` column of a `block list` invocation. */
|
|
125
|
+
async function blockListNames(
|
|
126
|
+
cwd: string,
|
|
127
|
+
allTiers: boolean,
|
|
128
|
+
): Promise<string[]> {
|
|
129
|
+
const envelope = await readVerb(blockListVerb, { allTiers }, cwd);
|
|
130
|
+
const rows = envelope.data as { name: string }[];
|
|
131
|
+
return rows.map((row) => row.name).sort();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
describe("default-pack journey — sources update, build, boot (E1)", () => {
|
|
135
|
+
it("bootFixtureRuntime runs the real resolve/build/lock and writes a content-addressed lock", async () => {
|
|
136
|
+
const fixture = await boot(DEFAULT_PACK_TTL, DEFAULT_PACK_CONFIG);
|
|
137
|
+
const lock = readLock(fixture.cwd);
|
|
138
|
+
expect(lock?.contentHash).toMatch(/^[0-9a-f]{64}$/);
|
|
139
|
+
expect(lock?.packs).toHaveLength(1);
|
|
140
|
+
// The booted store answers a read against the freshly built pack.
|
|
141
|
+
const ask = await fixture.runtime.query.sparql(
|
|
142
|
+
"ASK { ds:button a ds:Component }",
|
|
143
|
+
);
|
|
144
|
+
expect(ask.type === "ask" && ask.result).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("drives the literal `sources update` verb over a cold project, then boots a read", async () => {
|
|
148
|
+
// A cold file-package project with NO lock yet — the real CLI command path.
|
|
149
|
+
const pkg = mkdtempSync(join(tmpdir(), "e1-pkg-"));
|
|
150
|
+
mkdirSync(join(pkg, "definitions"), { recursive: true });
|
|
151
|
+
writeFileSync(join(pkg, "definitions", "pack.ttl"), DEFAULT_PACK_TTL);
|
|
152
|
+
const cwd = mkdtempSync(join(tmpdir(), "e1-proj-"));
|
|
153
|
+
writeFileSync(
|
|
154
|
+
join(cwd, "pragma.config.ts"),
|
|
155
|
+
`export default { packages: [{ name: "default", source: "file://${pkg}" }] };\n`,
|
|
156
|
+
);
|
|
157
|
+
try {
|
|
158
|
+
// `sources update` is a real mutation — resolve, build, write the lock.
|
|
159
|
+
const outcome = await executeVerb(
|
|
160
|
+
updateVerb,
|
|
161
|
+
{},
|
|
162
|
+
NO_MUTATION,
|
|
163
|
+
bootRuntime(JSON_FLAGS, cwd),
|
|
164
|
+
);
|
|
165
|
+
expect(outcome.exitCode).toBe(0);
|
|
166
|
+
expect(readLock(cwd)?.contentHash).toMatch(/^[0-9a-f]{64}$/);
|
|
167
|
+
// A SEPARATE, fresh runtime boots the just-built pack from the lock. No
|
|
168
|
+
// channel is configured, so the default `normal` channel hides the
|
|
169
|
+
// beta-gated Beta Badge. Assert the two global blocks are present while
|
|
170
|
+
// staying TOLERANT of the untiered Orphan Widget: whether it appears under
|
|
171
|
+
// --all-tiers is the A2 behavior, asserted in exactly one place below (the
|
|
172
|
+
// A2 regression guard). Filtering Orphan out keeps this test focused on the
|
|
173
|
+
// two global blocks, and still pins that Beta Badge is hidden.
|
|
174
|
+
const globalBlocks = (await blockListNames(cwd, true)).filter(
|
|
175
|
+
(name) => name !== UNTIERED_BLOCK_NAME,
|
|
176
|
+
);
|
|
177
|
+
expect(globalBlocks).toEqual(["Button", "Card"]);
|
|
178
|
+
} finally {
|
|
179
|
+
rmSync(pkg, { recursive: true, force: true });
|
|
180
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("default-pack journey — graph query over the real pack (E1)", () => {
|
|
186
|
+
it("a SELECT resolves every ds:Component, including the untiered one", async () => {
|
|
187
|
+
const fixture = await boot(DEFAULT_PACK_TTL, DEFAULT_PACK_CONFIG);
|
|
188
|
+
const envelope = await readVerb(
|
|
189
|
+
graphQueryVerb,
|
|
190
|
+
{
|
|
191
|
+
sparql:
|
|
192
|
+
"SELECT ?name WHERE { ?c a ds:Component ; ds:name ?name } ORDER BY ?name",
|
|
193
|
+
},
|
|
194
|
+
fixture.cwd,
|
|
195
|
+
);
|
|
196
|
+
expect(envelope.ok).toBe(true);
|
|
197
|
+
const data = envelope.data as {
|
|
198
|
+
type: string;
|
|
199
|
+
bindings: { name: string }[];
|
|
200
|
+
};
|
|
201
|
+
expect(data.type).toBe("select");
|
|
202
|
+
// All THREE components are in the graph — the untiered Orphan Widget too.
|
|
203
|
+
expect(data.bindings.map((binding) => binding.name)).toEqual([
|
|
204
|
+
"Beta Badge",
|
|
205
|
+
"Button",
|
|
206
|
+
"Orphan Widget",
|
|
207
|
+
]);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("an ASK over a known individual returns true", async () => {
|
|
211
|
+
const fixture = await boot(DEFAULT_PACK_TTL, DEFAULT_PACK_CONFIG);
|
|
212
|
+
const envelope = await readVerb(
|
|
213
|
+
graphQueryVerb,
|
|
214
|
+
{ sparql: "ASK { ds:card a ds:Pattern }" },
|
|
215
|
+
fixture.cwd,
|
|
216
|
+
);
|
|
217
|
+
const data = envelope.data as { type: string; result: boolean };
|
|
218
|
+
expect(data.type).toBe("ask");
|
|
219
|
+
expect(data.result).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("a malformed query surfaces INVALID_INPUT, not an internal crash", async () => {
|
|
223
|
+
const fixture = await boot(DEFAULT_PACK_TTL, DEFAULT_PACK_CONFIG);
|
|
224
|
+
await expect(
|
|
225
|
+
executeVerb(
|
|
226
|
+
graphQueryVerb,
|
|
227
|
+
{ sparql: "SELECT ?s WHERE { ?s ?p" },
|
|
228
|
+
NO_MUTATION,
|
|
229
|
+
bootRuntime(JSON_FLAGS, fixture.cwd),
|
|
230
|
+
),
|
|
231
|
+
).rejects.toMatchObject({ code: "INVALID_INPUT" });
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("default-pack journey — block list, populated and empty (E1)", () => {
|
|
236
|
+
it("normal channel lists the two global blocks, hiding the beta-gated one", async () => {
|
|
237
|
+
const fixture = await boot(DEFAULT_PACK_TTL, DEFAULT_PACK_CONFIG);
|
|
238
|
+
expect(await blockListNames(fixture.cwd, false)).toEqual([
|
|
239
|
+
"Button",
|
|
240
|
+
"Card",
|
|
241
|
+
]);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("prerelease channel reveals the beta-gated block across the tier chain", async () => {
|
|
245
|
+
const fixture = await boot(
|
|
246
|
+
DEFAULT_PACK_TTL,
|
|
247
|
+
DEFAULT_PACK_ALL_VISIBLE_CONFIG,
|
|
248
|
+
);
|
|
249
|
+
// Same inputs as the A2 regression guard below (ALL_VISIBLE + allTiers).
|
|
250
|
+
// Assert the tiered blocks (prerelease reveals the beta-gated Beta Badge)
|
|
251
|
+
// while staying TOLERANT of the untiered Orphan Widget — its --all-tiers
|
|
252
|
+
// visibility is the A2 truth, asserted in exactly one place below. Filtering
|
|
253
|
+
// Orphan out keeps this green pre-A2 (absent) and post-A2 (present), so it
|
|
254
|
+
// never contradicts that hand-off.
|
|
255
|
+
const tieredBlocks = (await blockListNames(fixture.cwd, true)).filter(
|
|
256
|
+
(name) => name !== UNTIERED_BLOCK_NAME,
|
|
257
|
+
);
|
|
258
|
+
expect(tieredBlocks).toEqual(["Beta Badge", "Button", "Card"]);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("the apps tier inherits its parent chain (global) plus its own block", async () => {
|
|
262
|
+
const fixture = await boot(DEFAULT_PACK_TTL, {
|
|
263
|
+
tier: "apps",
|
|
264
|
+
channel: "prerelease",
|
|
265
|
+
});
|
|
266
|
+
// resolveTierChain("apps") = [global, apps]: Button/Card (global, inherited)
|
|
267
|
+
// + Beta Badge (apps, own).
|
|
268
|
+
expect(await blockListNames(fixture.cwd, false)).toEqual([
|
|
269
|
+
"Beta Badge",
|
|
270
|
+
"Button",
|
|
271
|
+
"Card",
|
|
272
|
+
]);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("an ontology-only pack with no tiered blocks lists empty — a calm exit 0", async () => {
|
|
276
|
+
const fixture = await boot(NO_BLOCKS_TTL);
|
|
277
|
+
const envelope = await readVerb(
|
|
278
|
+
blockListVerb,
|
|
279
|
+
{ allTiers: true },
|
|
280
|
+
fixture.cwd,
|
|
281
|
+
);
|
|
282
|
+
expect(envelope.ok).toBe(true);
|
|
283
|
+
expect(envelope.data).toEqual([]);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe("default-pack journey — ontology list/show, populated and empty (E1)", () => {
|
|
288
|
+
it("lists the ds namespace with its class and property counts", async () => {
|
|
289
|
+
const fixture = await boot(DEFAULT_PACK_TTL, DEFAULT_PACK_CONFIG);
|
|
290
|
+
const envelope = await readVerb(ontologyListVerb, {}, fixture.cwd);
|
|
291
|
+
const summaries = envelope.data as {
|
|
292
|
+
prefix: string;
|
|
293
|
+
classCount: number;
|
|
294
|
+
propertyCount: number;
|
|
295
|
+
}[];
|
|
296
|
+
expect(summaries).toEqual([
|
|
297
|
+
{
|
|
298
|
+
prefix: "ds",
|
|
299
|
+
namespace: "https://ds.canonical.com/",
|
|
300
|
+
classCount: 5,
|
|
301
|
+
propertyCount: 4,
|
|
302
|
+
},
|
|
303
|
+
]);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("ontology show ds surfaces the two block domain classes", async () => {
|
|
307
|
+
const fixture = await boot(DEFAULT_PACK_TTL, DEFAULT_PACK_CONFIG);
|
|
308
|
+
const envelope = await readVerb(
|
|
309
|
+
ontologyShowVerb,
|
|
310
|
+
{ prefix: "ds" },
|
|
311
|
+
fixture.cwd,
|
|
312
|
+
);
|
|
313
|
+
const data = envelope.data as { classes: { uri: string }[] };
|
|
314
|
+
const localNames = data.classes.map((klass) => klass.uri.split("/").at(-1));
|
|
315
|
+
expect(localNames).toContain("Component");
|
|
316
|
+
expect(localNames).toContain("Pattern");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("a pack of pure instance data lists NO ontology namespaces — empty, calm", async () => {
|
|
320
|
+
const fixture = await boot(INSTANCE_ONLY_TTL);
|
|
321
|
+
const envelope = await readVerb(ontologyListVerb, {}, fixture.cwd);
|
|
322
|
+
expect(envelope.ok).toBe(true);
|
|
323
|
+
expect(envelope.data).toEqual([]);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("ontology show of an unknown prefix is NOT_FOUND, not a crash", async () => {
|
|
327
|
+
const fixture = await boot(DEFAULT_PACK_TTL, DEFAULT_PACK_CONFIG);
|
|
328
|
+
await expect(
|
|
329
|
+
executeVerb(
|
|
330
|
+
ontologyShowVerb,
|
|
331
|
+
{ prefix: "nope" },
|
|
332
|
+
NO_MUTATION,
|
|
333
|
+
bootRuntime(JSON_FLAGS, fixture.cwd),
|
|
334
|
+
),
|
|
335
|
+
).rejects.toBeInstanceOf(PragmaError);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe("default-pack journey — error paths (E1)", () => {
|
|
340
|
+
it("a malformed .ttl fails the build with a NAMED CONFIG_ERROR, not INTERNAL", async () => {
|
|
341
|
+
// bootFixtureRuntime writes the ttl into `fixture/definitions/fixture.ttl`
|
|
342
|
+
// and runs the REAL sources-update build, which classifies the parser error.
|
|
343
|
+
await expect(
|
|
344
|
+
bootFixtureRuntime({ ttl: MALFORMED_TTL }),
|
|
345
|
+
).rejects.toMatchObject({ code: "CONFIG_ERROR" });
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("a torn (emptied) schema.json boots to STORE_UNAVAILABLE with the update recovery", async () => {
|
|
349
|
+
const fixture = await boot(DEFAULT_PACK_TTL, DEFAULT_PACK_CONFIG);
|
|
350
|
+
const lock = readLock(fixture.cwd);
|
|
351
|
+
// Simulate a torn/evicted extraction: truncate schema.json in the pack cache.
|
|
352
|
+
truncateSync(join(packDir(lock?.contentHash ?? ""), SCHEMA_FILE), 0);
|
|
353
|
+
// A brand-new runtime (fresh store memo) boots against the now-incomplete pack.
|
|
354
|
+
let caught: unknown;
|
|
355
|
+
try {
|
|
356
|
+
await executeVerb(
|
|
357
|
+
blockListVerb,
|
|
358
|
+
{ allTiers: true },
|
|
359
|
+
NO_MUTATION,
|
|
360
|
+
bootRuntime(JSON_FLAGS, fixture.cwd),
|
|
361
|
+
);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
caught = error;
|
|
364
|
+
}
|
|
365
|
+
expect(caught).toBeInstanceOf(PragmaError);
|
|
366
|
+
expect((caught as PragmaError).code).toBe("STORE_UNAVAILABLE");
|
|
367
|
+
expect((caught as PragmaError).recovery?.cli).toBe("pragma sources update");
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("a --frozen update whose pinned commit is gone fails with a named CONFIG_ERROR", async () => {
|
|
371
|
+
const runGit = (repo: string, args: string[]): string =>
|
|
372
|
+
execFileSync("git", args, {
|
|
373
|
+
cwd: repo,
|
|
374
|
+
stdio: "pipe",
|
|
375
|
+
env: {
|
|
376
|
+
...process.env,
|
|
377
|
+
GIT_AUTHOR_NAME: "t",
|
|
378
|
+
GIT_AUTHOR_EMAIL: "t@t",
|
|
379
|
+
GIT_COMMITTER_NAME: "t",
|
|
380
|
+
GIT_COMMITTER_EMAIL: "t@t",
|
|
381
|
+
},
|
|
382
|
+
}).toString();
|
|
383
|
+
|
|
384
|
+
const repo = mkdtempSync(join(tmpdir(), "e1-repo-"));
|
|
385
|
+
const cwd = mkdtempSync(join(tmpdir(), "e1-frozen-"));
|
|
386
|
+
try {
|
|
387
|
+
runGit(repo, ["init", "-b", "main"]);
|
|
388
|
+
mkdirSync(join(repo, "definitions"), { recursive: true });
|
|
389
|
+
writeFileSync(join(repo, "definitions", "pack.ttl"), NO_BLOCKS_TTL);
|
|
390
|
+
runGit(repo, ["add", "-A"]);
|
|
391
|
+
runGit(repo, ["commit", "-m", "one"]);
|
|
392
|
+
|
|
393
|
+
const runtime = gitRuntimeFor(cwd, [
|
|
394
|
+
{ name: "pkg-git", source: `git+file://${repo}#main` },
|
|
395
|
+
]);
|
|
396
|
+
// First update locks the current commit SHA.
|
|
397
|
+
await runTask(await buildUpdateTask(runtime, false));
|
|
398
|
+
expect(readLock(cwd)?.packs.at(0)?.resolved).toMatch(/^[0-9a-f]{40}$/);
|
|
399
|
+
|
|
400
|
+
// Orphan the pinned commit: amend main, then expire the reflog + prune so
|
|
401
|
+
// the previously-locked SHA is unreachable.
|
|
402
|
+
writeFileSync(
|
|
403
|
+
join(repo, "definitions", "pack.ttl"),
|
|
404
|
+
`${NO_BLOCKS_TTL}\nds:extra a owl:Class .\n`,
|
|
405
|
+
);
|
|
406
|
+
runGit(repo, ["add", "-A"]);
|
|
407
|
+
runGit(repo, ["commit", "--amend", "-m", "amended"]);
|
|
408
|
+
runGit(repo, ["reflog", "expire", "--expire=now", "--all"]);
|
|
409
|
+
runGit(repo, ["gc", "--prune=now"]);
|
|
410
|
+
|
|
411
|
+
// A --frozen update must re-resolve the pinned (now-missing) SHA and refuse.
|
|
412
|
+
let caught: unknown;
|
|
413
|
+
try {
|
|
414
|
+
await runTask(await buildUpdateTask(runtime, true));
|
|
415
|
+
} catch (error) {
|
|
416
|
+
caught = error;
|
|
417
|
+
}
|
|
418
|
+
expect(caught).toBeInstanceOf(PragmaError);
|
|
419
|
+
expect((caught as PragmaError).code).toBe("CONFIG_ERROR");
|
|
420
|
+
expect((caught as PragmaError).message).toContain("pkg-git");
|
|
421
|
+
} finally {
|
|
422
|
+
rmSync(repo, { recursive: true, force: true });
|
|
423
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
describe("default-pack journey — real-data shapes the clean fixture masked (E1, hand-off to lanes A)", () => {
|
|
429
|
+
it("every block is co-typed owl:NamedIndividual and indexed as an individual (abox)", async () => {
|
|
430
|
+
const fixture = await boot(DEFAULT_PACK_TTL, DEFAULT_PACK_CONFIG);
|
|
431
|
+
const index = readPackIndex(fixture.cwd);
|
|
432
|
+
// The vendored pack indexes 17 distinct entities (5 classes + 4 properties +
|
|
433
|
+
// 2 tiers + 2 channels + 4 blocks).
|
|
434
|
+
expect(index?.entities).toHaveLength(17);
|
|
435
|
+
const button = index?.entities.find(
|
|
436
|
+
(entity) => entity.name === "ds:button",
|
|
437
|
+
);
|
|
438
|
+
expect(button?.box).toBe("abox");
|
|
439
|
+
expect(button?.types).toContain("owl:NamedIndividual");
|
|
440
|
+
expect(button?.types).toContain("ds:Component");
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it("a multilingual rdfs:label is stored in BOTH languages and indexed tag-stripped (A7)", async () => {
|
|
444
|
+
const fixture = await boot(DEFAULT_PACK_TTL, DEFAULT_PACK_CONFIG);
|
|
445
|
+
// Both language variants are retrievable directly from the store.
|
|
446
|
+
const en = await fixture.runtime.query.sparql(
|
|
447
|
+
'SELECT ?l WHERE { ds:button rdfs:label ?l . FILTER(LANG(?l) = "en") }',
|
|
448
|
+
);
|
|
449
|
+
const fr = await fixture.runtime.query.sparql(
|
|
450
|
+
'SELECT ?l WHERE { ds:button rdfs:label ?l . FILTER(LANG(?l) = "fr") }',
|
|
451
|
+
);
|
|
452
|
+
const value = (result: typeof en): string | undefined =>
|
|
453
|
+
result.type === "select"
|
|
454
|
+
? (result.bindings.at(0) as { l?: string } | undefined)?.l
|
|
455
|
+
: undefined;
|
|
456
|
+
expect(value(en)).toBe("Button");
|
|
457
|
+
expect(value(fr)).toBe("Bouton");
|
|
458
|
+
// The single-valued index carries ONE label, tag-stripped (no `@en`/`@fr`).
|
|
459
|
+
// NOTE(A7): which language wins is store-order-arbitrary — the index picks
|
|
460
|
+
// whichever the store yields first (no `@en` preference). E3 (live oxigraph)
|
|
461
|
+
// is where that ordering is confirmed against the real pack; here we only
|
|
462
|
+
// assert the tag is stripped and the value is one of the two declared forms.
|
|
463
|
+
const label = readPackIndex(fixture.cwd)?.entities.find(
|
|
464
|
+
(entity) => entity.name === "ds:button",
|
|
465
|
+
)?.label;
|
|
466
|
+
expect(label).not.toMatch(/@(en|fr)/);
|
|
467
|
+
expect(["Button", "Bouton"]).toContain(label);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// ----- HAND-OFFS (A1, A2): lane A's fixes have landed in this branch, so the
|
|
471
|
+
// former `it.fails` pins are now LIVE regression guards. -----
|
|
472
|
+
|
|
473
|
+
it("A1: info's entity total must not exceed the distinct entity count (owl:NamedIndividual double-count)", async () => {
|
|
474
|
+
const fixture = await boot(DEFAULT_PACK_TTL, DEFAULT_PACK_CONFIG);
|
|
475
|
+
const index = readPackIndex(fixture.cwd);
|
|
476
|
+
// `entityTotal` (the figure `info`/`doctor` report) once SUMMED per-type
|
|
477
|
+
// instance counts, double-counting each entity per asserted rdf:type (each
|
|
478
|
+
// block under owl:NamedIndividual AND its domain class; ds:tier/ds:release
|
|
479
|
+
// under owl:ObjectProperty AND owl:FunctionalProperty — 23 for 17 distinct
|
|
480
|
+
// entities). Lane A fixed the count, so the true total no longer exceeds the
|
|
481
|
+
// distinct count — guard it stays that way.
|
|
482
|
+
expect(entityTotal(index as NonNullable<typeof index>)).toBeLessThanOrEqual(
|
|
483
|
+
(index as NonNullable<typeof index>).entities.length,
|
|
484
|
+
);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it("A2: an untiered block must still appear in `block list --all-tiers`", async () => {
|
|
488
|
+
const fixture = await boot(
|
|
489
|
+
DEFAULT_PACK_TTL,
|
|
490
|
+
DEFAULT_PACK_ALL_VISIBLE_CONFIG,
|
|
491
|
+
);
|
|
492
|
+
// `block list`'s SELECT once inner-joined `?c ds:tier ?t`, dropping a
|
|
493
|
+
// ds:Component with no ds:tier even under --all-tiers (though `graph query`
|
|
494
|
+
// found it, proven above). Lane A made the tier join OPTIONAL, so an
|
|
495
|
+
// untiered block now appears under --all-tiers — guard it stays visible.
|
|
496
|
+
expect(await blockListNames(fixture.cwd, true)).toContain(
|
|
497
|
+
UNTIERED_BLOCK_NAME,
|
|
498
|
+
);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// KNOWN GAP / product follow-up (NOT a lane hand-off): a corrupt, NON-EMPTY
|
|
502
|
+
// `schema.json` currently surfaces UNCLASSIFIED. `packIsComplete` only checks
|
|
503
|
+
// size > 0, so torn-but-nonempty garbage bypasses the completeness guard and
|
|
504
|
+
// `compileFromExtraction` throws a raw `SyntaxError` (renders as INTERNAL
|
|
505
|
+
// "please report") instead of a classified error. It SHOULD degrade to
|
|
506
|
+
// STORE_UNAVAILABLE like the emptied case does — the fix belongs in
|
|
507
|
+
// `kernel/runtime/graphpack/read.ts` (guard the `schema.json` read/compile).
|
|
508
|
+
// No lane in this wave owns that, so rather than a false `it.fails` hand-off
|
|
509
|
+
// that would never flip, this is a plain test pinning the CURRENT behavior:
|
|
510
|
+
// when read.ts starts classifying it, this guard trips — swap the assertion
|
|
511
|
+
// to expect a STORE_UNAVAILABLE PragmaError then.
|
|
512
|
+
it("corrupt (non-empty, invalid) schema.json currently surfaces an UNCLASSIFIED error (known gap)", async () => {
|
|
513
|
+
const fixture = await boot(DEFAULT_PACK_TTL, DEFAULT_PACK_CONFIG);
|
|
514
|
+
const lock = readLock(fixture.cwd);
|
|
515
|
+
writeFileSync(
|
|
516
|
+
join(packDir(lock?.contentHash ?? ""), SCHEMA_FILE),
|
|
517
|
+
"{ not valid json ]",
|
|
518
|
+
);
|
|
519
|
+
let caught: unknown;
|
|
520
|
+
try {
|
|
521
|
+
await executeVerb(
|
|
522
|
+
blockListVerb,
|
|
523
|
+
{ allTiers: true },
|
|
524
|
+
NO_MUTATION,
|
|
525
|
+
bootRuntime(JSON_FLAGS, fixture.cwd),
|
|
526
|
+
);
|
|
527
|
+
} catch (error) {
|
|
528
|
+
caught = error;
|
|
529
|
+
}
|
|
530
|
+
// It throws — but as a raw, unclassified error, NOT a PragmaError. Assert the
|
|
531
|
+
// gap explicitly so a future classification fix in read.ts trips this guard.
|
|
532
|
+
expect(caught).toBeInstanceOf(Error);
|
|
533
|
+
expect(caught).not.toBeInstanceOf(PragmaError);
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* A synthetic runtime whose config is a fixed package list — drives the
|
|
539
|
+
* git-source build Task without a config file (mirrors sources.test.ts).
|
|
540
|
+
*
|
|
541
|
+
* @param cwd - The project directory the lock is written to.
|
|
542
|
+
* @param packages - The configured package entries.
|
|
543
|
+
* @returns A runtime wired to a lazy store over `cwd`.
|
|
544
|
+
* @note Impure — constructs a store handle over the working directory.
|
|
545
|
+
*/
|
|
546
|
+
function gitRuntimeFor(cwd: string, packages: PackageEntry[]): PragmaRuntime {
|
|
547
|
+
const layers: ConfigLayers = {
|
|
548
|
+
config: { channel: "normal", packages },
|
|
549
|
+
origins: {
|
|
550
|
+
tier: "default",
|
|
551
|
+
channel: "default",
|
|
552
|
+
detail: "default",
|
|
553
|
+
packages: "project",
|
|
554
|
+
stories: "default",
|
|
555
|
+
prefixes: "default",
|
|
556
|
+
prompts: "default",
|
|
557
|
+
},
|
|
558
|
+
global: { path: "/nonexistent", exists: false },
|
|
559
|
+
project: { exists: false },
|
|
560
|
+
};
|
|
561
|
+
const loadConfig = async (): Promise<ConfigLayers> => layers;
|
|
562
|
+
const store = createLazyStore({ cwd, loadConfig });
|
|
563
|
+
return {
|
|
564
|
+
cwd,
|
|
565
|
+
version: VERSION,
|
|
566
|
+
globalFlags: JSON_FLAGS,
|
|
567
|
+
loadConfig,
|
|
568
|
+
store,
|
|
569
|
+
query: createQueryFacade(store),
|
|
570
|
+
};
|
|
571
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E4 (AV-231, Backlog E) — the interactive TTY journey. SCAFFOLD.
|
|
3
|
+
*
|
|
4
|
+
* No test in the suite ever drove a REAL terminal, so the interactive-cancel and
|
|
5
|
+
* no-freeze guarantees (H1–H3, C3/C4) were never exercised end to end. Confirming
|
|
6
|
+
* them needs a pseudo-terminal: spawn the compiled `pragma` under a PTY, send raw
|
|
7
|
+
* keystrokes (Ctrl-C = `\x03`, EOF = `\x04`), and observe the exit code + output.
|
|
8
|
+
*
|
|
9
|
+
* `node-pty` is NOT available in this environment (it is a native addon, absent
|
|
10
|
+
* from the dependency set — verified at authoring time), so the journey is
|
|
11
|
+
* scaffolded as `it.todo` behind a `node-pty` availability gate rather than
|
|
12
|
+
* blocking the P0 work. Each todo names the exact behaviour and the exit contract
|
|
13
|
+
* the CLI already promises (see `kernel/project/cli/dispatch.ts`):
|
|
14
|
+
*
|
|
15
|
+
* - H1: an at-prompt Ctrl-C is a DECLINE — a clean cancel, "Cancelled." on
|
|
16
|
+
* stderr, exit 0 (`EXIT.OK`).
|
|
17
|
+
* - H2: a Ctrl-C DURING execution is an INTERRUPT — aborts work underway,
|
|
18
|
+
* "Cancelled." on stderr, exit 130 (`EXIT.INTERRUPTED`, 128 + SIGINT).
|
|
19
|
+
* - H3: `create 2>/dev/null` must not freeze — interactivity gates on stderr
|
|
20
|
+
* being a TTY, so a redirected stderr picks the non-interactive strategy
|
|
21
|
+
* instead of mounting an invisible wizard that blocks on stdin.
|
|
22
|
+
* - C3: EOF (`\x04`) on a prompt resolves to a usage error, never a hang.
|
|
23
|
+
* - C4: an empty select (Enter on a no-default list) does not hang.
|
|
24
|
+
*
|
|
25
|
+
* TODO(AV-231/E4): when `node-pty` (or an equivalent PTY shim) is on the
|
|
26
|
+
* devDependency set, replace each `it.todo` with a PTY-driven `it` that spawns
|
|
27
|
+
* `dist/pragma create …` under a pty, writes the keystroke, and asserts the exit
|
|
28
|
+
* code + "Cancelled." line. The compiled binary is already provisioned for the
|
|
29
|
+
* spawn-e2e layer by `testing/perf/globalSetup.ts`.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { describe, it } from "vitest";
|
|
33
|
+
|
|
34
|
+
/** True when a real PTY driver is importable (kept async-free: a resolve probe). */
|
|
35
|
+
async function ptyAvailable(): Promise<boolean> {
|
|
36
|
+
try {
|
|
37
|
+
await import("node-pty" as string);
|
|
38
|
+
return true;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Authoring-time: `node-pty` is absent, so this resolves to `describe.skip` and
|
|
45
|
+
// the todos below stand in as the recorded, unblocked plan. The probe is awaited
|
|
46
|
+
// lazily so a future environment with node-pty flips the gate with no edit here.
|
|
47
|
+
const hasPty = await ptyAvailable();
|
|
48
|
+
const ptyGate = hasPty ? describe : describe.skip;
|
|
49
|
+
|
|
50
|
+
ptyGate(
|
|
51
|
+
"interactive TTY journey — cancel + no-freeze (E4, node-pty) [SCAFFOLD]",
|
|
52
|
+
() => {
|
|
53
|
+
// These become PTY-driven `it`s once node-pty is available (see module TODO).
|
|
54
|
+
it.todo(
|
|
55
|
+
"H1: Ctrl-C at a wizard prompt cancels cleanly (stderr 'Cancelled.', exit 0)",
|
|
56
|
+
);
|
|
57
|
+
it.todo(
|
|
58
|
+
"H2: Ctrl-C during execution aborts the run (stderr 'Cancelled.', exit 130)",
|
|
59
|
+
);
|
|
60
|
+
it.todo(
|
|
61
|
+
"H3: `create 2>/dev/null` does not freeze (non-TTY stderr -> non-interactive)",
|
|
62
|
+
);
|
|
63
|
+
it.todo("C3: EOF on a prompt resolves to a usage error, never a hang");
|
|
64
|
+
it.todo("C4: an empty select does not hang");
|
|
65
|
+
},
|
|
66
|
+
);
|