@canonical/pragma-cli 0.32.0 → 0.34.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 +19 -15
- package/pragma.conf.ts +664 -0
- package/src/bin.ts +15 -6
- package/src/capabilities/block.parity.test.ts +250 -0
- package/src/capabilities/capabilities/capabilities.render.ts +3 -2
- package/src/capabilities/capabilities/capabilities.test.ts +10 -1
- package/src/capabilities/capabilities/capabilities.verb.ts +32 -14
- package/src/capabilities/capabilities/catalog.ts +20 -8
- package/src/capabilities/capabilities/hints.ts +21 -36
- package/src/capabilities/capabilities/types.ts +2 -0
- package/src/capabilities/colophon/collectColophon.ts +34 -19
- 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 +11 -4
- package/src/capabilities/colophon/markdownTerminal.ts +33 -14
- package/src/capabilities/colophon/types.ts +16 -7
- package/src/capabilities/config/collectConfigShow.test.ts +57 -0
- package/src/capabilities/config/collectConfigShow.ts +41 -4
- package/src/capabilities/config/fields.ts +10 -108
- package/src/capabilities/config/set.test.ts +6 -6
- package/src/capabilities/config/set.verb.ts +12 -8
- package/src/capabilities/config/show.render.test.ts +137 -0
- package/src/capabilities/config/show.render.ts +57 -16
- package/src/capabilities/config/show.verb.ts +9 -8
- package/src/capabilities/config/types.ts +8 -0
- package/src/capabilities/create/byteEquality.subprocess.test.ts +1 -1
- package/src/capabilities/create/byteEquality.test.ts +82 -101
- package/src/capabilities/create/compiledCreate.subprocess.test.ts +53 -16
- package/src/capabilities/create/constants.ts +75 -0
- package/src/capabilities/create/create.mcp.test.ts +2 -2
- package/src/capabilities/create/create.test.ts +192 -16
- package/src/capabilities/create/create.verb.ts +208 -79
- package/src/capabilities/create/index.ts +2 -10
- package/src/capabilities/create/pickGenerator.ts +34 -28
- package/src/capabilities/create/templates.embedded.generated.ts +4 -29
- package/src/capabilities/create/types.ts +10 -6
- package/src/capabilities/{standard/parity.test.ts → distribution.parity.test.ts} +24 -14
- package/src/capabilities/distribution.test.ts +120 -0
- package/src/capabilities/distribution.ts +59 -0
- package/src/capabilities/doctor/checks/checkConfigFile.ts +6 -5
- package/src/capabilities/doctor/checks/checkKeStore.ts +2 -2
- package/src/capabilities/doctor/checks/checkMcpCommands.ts +6 -0
- package/src/capabilities/doctor/checks/checkMcpConfigured.ts +15 -8
- package/src/capabilities/doctor/checks/checkPackageRefs.ts +79 -45
- package/src/capabilities/doctor/checks/checkPragmaVersion.ts +3 -2
- package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +61 -11
- package/src/capabilities/doctor/checks/checkShellCompletions.ts +63 -23
- package/src/capabilities/doctor/checks/checkSkillsSymlinked.ts +3 -2
- 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 +117 -34
- package/src/capabilities/doctor/doctor.test.ts +68 -9
- package/src/capabilities/doctor/doctor.verb.ts +6 -2
- package/src/capabilities/doctor/runChecks.ts +33 -9
- package/src/capabilities/doctor/types.ts +16 -1
- package/src/capabilities/emptyRecovery.test.ts +32 -15
- package/src/capabilities/graph/inspect.verb.ts +3 -2
- package/src/capabilities/graph/query.verb.ts +3 -2
- package/src/capabilities/graph/runQuery.ts +8 -5
- package/src/capabilities/index.ts +22 -18
- package/src/capabilities/info/collectInfo.ts +10 -6
- package/src/capabilities/info/info.render.ts +3 -2
- package/src/capabilities/info/info.verb.ts +3 -2
- package/src/capabilities/lazy.test.ts +93 -2
- package/src/capabilities/ontology/index.ts +10 -3
- package/src/capabilities/ontology/ontology.test.ts +157 -5
- package/src/capabilities/ontology/verbs.ts +203 -104
- 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 +68 -1
- package/src/capabilities/prompt/verbs.ts +24 -11
- package/src/capabilities/reference.test.ts +32 -0
- package/src/capabilities/resources/provider.ts +58 -25
- package/src/capabilities/resources/resources.test.ts +176 -21
- 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 +166 -63
- 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 +10 -4
- 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 +68 -9
- package/src/capabilities/setup/shell.ts +29 -3
- package/src/capabilities/setup/types.ts +59 -2
- package/src/capabilities/shared/bands.ts +16 -0
- package/src/capabilities/skill/discover.ts +11 -5
- package/src/capabilities/skill/render.ts +4 -5
- package/src/capabilities/skill/verbs.ts +7 -7
- package/src/capabilities/sources/collectStatus.ts +69 -49
- package/src/capabilities/sources/index.ts +1 -1
- package/src/capabilities/sources/installSkills.ts +2 -2
- package/src/capabilities/sources/runUpdate.ts +146 -98
- package/src/capabilities/sources/sources.test.ts +264 -84
- package/src/capabilities/sources/status.render.test.ts +94 -0
- package/src/capabilities/sources/status.render.ts +55 -33
- package/src/capabilities/sources/status.verb.ts +15 -8
- package/src/capabilities/sources/types.ts +18 -25
- package/src/capabilities/sources/update.render.ts +9 -2
- package/src/capabilities/sources/update.verb.ts +12 -23
- package/src/capabilities/surface.test.ts +65 -17
- package/src/capabilities/tier.parity.test.ts +144 -0
- package/src/capabilities/upgrade/upgrade.verb.ts +7 -3
- package/src/constants.test.ts +9 -7
- package/src/constants.ts +49 -23
- package/src/identity.test.ts +333 -0
- package/src/index.ts +2 -2
- package/src/kernel/completion/__snapshots__/emitScripts.test.ts.snap +18 -16
- package/src/kernel/completion/complete.test.ts +3 -5
- package/src/kernel/completion/emitScripts.test.ts +1 -1
- package/src/kernel/completion/emitScripts.ts +4 -2
- package/src/kernel/completion/entitySource.test.ts +185 -41
- package/src/kernel/completion/entitySource.ts +131 -113
- package/src/kernel/completion/model.test.ts +3 -3
- package/src/kernel/completion/model.ts +11 -1
- package/src/kernel/completion/parse.test.ts +2 -4
- package/src/kernel/completion/safety.test.ts +82 -36
- package/src/kernel/completion/shellDrive.test.ts +592 -0
- package/src/kernel/completion/templates/fish.ts +28 -6
- package/src/kernel/completion/types.ts +3 -3
- package/src/kernel/config/__snapshots__/firstRun.test.ts.snap +1 -1
- package/src/kernel/config/defaults.test.ts +85 -0
- package/src/kernel/config/defaults.ts +12 -23
- package/src/kernel/config/evaluateProjectConfig.ts +2 -3
- package/src/kernel/config/findProjectConfig.ts +4 -3
- package/src/kernel/config/firstRun.ts +12 -5
- package/src/kernel/config/globalConfig.ts +2 -2
- package/src/kernel/config/index.ts +4 -5
- package/src/kernel/config/paths.ts +17 -17
- package/src/kernel/config/readConfig.test.ts +242 -6
- package/src/kernel/config/readConfig.ts +8 -8
- package/src/kernel/config/schema.test.ts +50 -0
- package/src/kernel/config/schema.ts +97 -10
- package/src/kernel/config/types.ts +73 -19
- package/src/kernel/copy.test.ts +453 -0
- package/src/kernel/error/PragmaError.test.ts +5 -5
- package/src/kernel/error/constants.ts +3 -2
- package/src/kernel/error/index.ts +1 -1
- package/src/kernel/error/recovery.test.ts +22 -45
- package/src/kernel/error/recovery.ts +22 -27
- package/src/kernel/error/renderError.ts +1 -1
- package/src/kernel/error/types.ts +5 -4
- package/src/kernel/packs/collect.test.ts +195 -11
- package/src/kernel/packs/collect.ts +213 -58
- package/src/kernel/packs/compile.test.ts +163 -7
- package/src/kernel/packs/compile.ts +15 -12
- package/src/kernel/packs/disclosure.test.ts +5 -2
- package/src/kernel/packs/graphql/fetchGraphqlLookup.ts +11 -10
- package/src/kernel/packs/graphqlSource.test.ts +6 -3
- package/src/kernel/packs/iri.ts +5 -7
- package/src/kernel/packs/renderPack.ts +5 -3
- package/src/kernel/packs/resolveEntity.ts +21 -17
- package/src/kernel/packs/runBodies.ts +15 -14
- package/src/kernel/packs/schema.ts +96 -10
- package/src/kernel/packs/sparql/runSelect.test.ts +104 -10
- package/src/kernel/packs/sparql/runSelect.ts +73 -34
- package/src/kernel/packs/toolDescription.test.ts +10 -2
- package/src/kernel/packs/types.ts +39 -4
- package/src/kernel/packs/uniqueness.ts +4 -4
- package/src/kernel/project/cli/buildProgram.test.ts +60 -11
- package/src/kernel/project/cli/buildProgram.ts +11 -0
- package/src/kernel/project/cli/dispatch.test.ts +67 -0
- package/src/kernel/project/cli/dispatch.ts +34 -12
- package/src/kernel/project/cli/globalFlags.test.ts +12 -4
- package/src/kernel/project/cli/globalFlags.ts +14 -14
- package/src/kernel/project/cli/helpFormat.ts +1 -1
- package/src/kernel/project/cli/rootHelp.ts +109 -107
- package/src/kernel/project/mcp/buildServer.ts +11 -6
- package/src/kernel/project/mcp/instructions.ts +12 -7
- package/src/kernel/project/mcp/mcpError.ts +52 -0
- package/src/kernel/project/mcp/parity.test.ts +39 -1
- package/src/kernel/project/mcp/prompts/prompts.test.ts +128 -5
- package/src/kernel/project/mcp/prompts/provider.ts +51 -7
- package/src/kernel/project/mcp/prompts/source.ts +68 -67
- package/src/kernel/project/mcp/registerVerb.ts +25 -6
- package/src/kernel/project/mcp/serve.ts +11 -3
- package/src/kernel/project/mcp/toolDescriptions.test.ts +6 -2
- package/src/kernel/render/disclosure.ts +4 -2
- package/src/kernel/render/formatters.ts +5 -4
- package/src/kernel/render/prefixes.test.ts +36 -0
- package/src/kernel/render/prefixes.ts +40 -13
- package/src/kernel/render/renderers.test.ts +35 -1
- 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 +57 -3
- package/src/kernel/runtime/graphpack/buildIndex.test.ts +149 -0
- package/src/kernel/runtime/graphpack/buildIndex.ts +173 -50
- package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +5 -8
- package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +3 -4
- package/src/kernel/runtime/graphpack/embedded/pack.stories.generated.ts +3 -0
- package/src/kernel/runtime/graphpack/embedded.ts +37 -11
- package/src/kernel/runtime/graphpack/graphpack.test.ts +274 -6
- package/src/kernel/runtime/graphpack/hash.ts +3 -2
- package/src/kernel/runtime/graphpack/index.ts +1 -1
- package/src/kernel/runtime/graphpack/manifest.ts +114 -12
- package/src/kernel/runtime/graphpack/read.ts +32 -22
- package/src/kernel/runtime/graphpack/schemas.ts +74 -0
- package/src/kernel/runtime/graphpack/stories.ts +84 -0
- package/src/kernel/runtime/graphpack/types.ts +72 -45
- package/src/kernel/runtime/loadSession.ts +4 -12
- package/src/kernel/runtime/paths.ts +64 -15
- package/src/kernel/runtime/probe.test.ts +8 -4
- package/src/kernel/runtime/readEntity.ts +6 -5
- package/src/kernel/runtime/refs/gitOps.ts +1 -19
- package/src/kernel/runtime/refs/parseRef.ts +7 -7
- package/src/kernel/runtime/refs/resolve.ts +141 -65
- package/src/kernel/runtime/resolveSources.test.ts +63 -24
- package/src/kernel/runtime/resolveSources.ts +36 -25
- package/src/kernel/runtime/storeReadiness.ts +62 -0
- package/src/kernel/runtime/storeless.test.ts +1 -1
- package/src/kernel/runtime/types.ts +1 -1
- package/src/kernel/runtime/wasmEmbed.test.ts +25 -20
- package/src/kernel/spec/emitReference.test.ts +196 -0
- package/src/kernel/spec/emitReference.ts +624 -0
- package/src/kernel/spec/emitSurface.ts +13 -7
- package/src/kernel/spec/index.ts +2 -0
- package/src/kernel/spec/surfaceConformance.test.ts +2 -2
- package/src/kernel/spec/surfaceConformance.ts +8 -0
- package/src/kernel/spec/types.ts +29 -9
- package/src/kernel/spec/validate.ts +2 -1
- package/src/kernel/vocabulary.test.ts +164 -0
- package/src/kernel/vocabulary.ts +146 -0
- package/src/testing/PARITY_GAPS.ts +9 -3
- package/src/testing/behavioral/README.md +3 -3
- package/src/testing/behavioral/autoLlm.e2e.test.ts +12 -2
- package/src/testing/behavioral/completion.test.ts +21 -8
- package/src/testing/behavioral/disclosure.test.ts +12 -0
- package/src/testing/behavioral/docExamples.test.ts +283 -0
- package/src/testing/behavioral/errorMatrix.mcp.test.ts +4 -3
- package/src/testing/behavioral/errorMatrix.storeless.test.ts +3 -2
- package/src/testing/behavioral/exitCodes.e2e.test.ts +6 -5
- package/src/testing/behavioral/harness.foundation.test.ts +14 -5
- package/src/testing/behavioral/journeys.cli.test.ts +116 -73
- package/src/testing/behavioral/journeys.defaultPack.test.ts +477 -0
- package/src/testing/behavioral/journeys.firstInstall.test.ts +167 -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 +179 -0
- package/src/testing/behavioral/journeys.packageStories.test.ts +188 -0
- package/src/testing/behavioral/liveReadSurface.ts +4 -3
- package/src/testing/behavioral/mcpSurface.test.ts +10 -5
- package/src/testing/behavioral/staleCommands.test.ts +86 -0
- package/src/testing/eval/__snapshots__/eval.test.ts.snap +9 -9
- package/src/testing/eval/cases/assembly.ts +16 -9
- package/src/testing/eval/cases/effectful.ts +31 -16
- package/src/testing/eval/cases/readNouns.ts +23 -18
- package/src/testing/eval/cases/stable.ts +5 -1
- package/src/testing/eval/eval.test.ts +7 -1
- package/src/testing/fixtures/fixtureCapability.ts +201 -1
- package/src/testing/fixtures/graph/canonical.ts +6 -3
- package/src/testing/fixtures/graph/defaultPack.ts +179 -0
- package/src/testing/fixtures/graph/owlExport.ts +97 -0
- package/src/testing/helpers/extractSnippets.ts +29 -0
- package/src/testing/helpers/fixtureGraph.ts +33 -16
- package/src/testing/helpers/golden.ts +1 -1
- package/src/testing/helpers/packRuntime.ts +2 -3
- package/src/testing/helpers/projectCli.ts +2 -1
- package/src/testing/helpers/projectMcp.ts +6 -0
- package/src/testing/perf/budgets.test.ts +4 -0
- package/src/testing/perf/budgets.ts +22 -7
- package/src/testing/perf/globalSetup.ts +121 -6
- package/src/types.ts +2 -2
- package/src/capabilities/block/blockList.verb.ts +0 -150
- package/src/capabilities/block/index.ts +0 -25
- package/src/capabilities/block/pack.ts +0 -155
- package/src/capabilities/block/parity.test.ts +0 -180
- package/src/capabilities/block/tierChain.ts +0 -78
- package/src/capabilities/colophon/pragmaColophon.ts +0 -66
- package/src/capabilities/config/field.test.ts +0 -188
- package/src/capabilities/modifier/index.ts +0 -15
- package/src/capabilities/modifier/pack.ts +0 -64
- package/src/capabilities/standard/index.ts +0 -18
- package/src/capabilities/standard/pack.ts +0 -128
- package/src/capabilities/tier/index.ts +0 -19
- package/src/capabilities/tier/lookup.render.ts +0 -34
- package/src/capabilities/tier/lookup.test.ts +0 -68
- package/src/capabilities/tier/lookup.verb.ts +0 -44
- package/src/capabilities/tier/pack.ts +0 -28
- package/src/capabilities/tier/runLookup.ts +0 -59
- package/src/capabilities/token/addConfig.render.ts +0 -27
- package/src/capabilities/token/addConfig.test.ts +0 -62
- package/src/capabilities/token/addConfig.verb.ts +0 -40
- package/src/capabilities/token/index.ts +0 -19
- package/src/capabilities/token/pack.ts +0 -58
- package/src/capabilities/token/runAddConfig.ts +0 -95
- package/src/kernel/completion/bashDrive.test.ts +0 -73
- package/src/kernel/runtime/graphpack/embedded/sample.ttl +0 -39
- package/src/kernel/runtime/lock.ts +0 -90
|
@@ -0,0 +1,477 @@
|
|
|
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
|
+
* point, via `bootFixtureRuntime`) → boot → `graph query` → `block list` /
|
|
9
|
+
* `ontology list` (populated AND empty) → the error paths (malformed `.ttl`,
|
|
10
|
+
* a torn `schema.json`, a corrupt `schema.json`).
|
|
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 {
|
|
21
|
+
mkdirSync,
|
|
22
|
+
mkdtempSync,
|
|
23
|
+
rmSync,
|
|
24
|
+
truncateSync,
|
|
25
|
+
writeFileSync,
|
|
26
|
+
} from "node:fs";
|
|
27
|
+
import { tmpdir } from "node:os";
|
|
28
|
+
import { join } from "node:path";
|
|
29
|
+
import { afterAll, describe, expect, it } from "vitest";
|
|
30
|
+
import { storyModules } from "../../capabilities/distribution.js";
|
|
31
|
+
import { graphQueryVerb } from "../../capabilities/graph/query.verb.js";
|
|
32
|
+
import {
|
|
33
|
+
ontologyListVerb,
|
|
34
|
+
ontologyShowVerb,
|
|
35
|
+
} from "../../capabilities/ontology/verbs.js";
|
|
36
|
+
import { updateVerb } from "../../capabilities/sources/update.verb.js";
|
|
37
|
+
import {
|
|
38
|
+
entityTotal,
|
|
39
|
+
readPackIndex,
|
|
40
|
+
} from "../../kernel/completion/entitySource.js";
|
|
41
|
+
import { PragmaError } from "../../kernel/error/PragmaError.js";
|
|
42
|
+
import { executeVerb } from "../../kernel/project/cli/dispatch.js";
|
|
43
|
+
import { bootRuntime } from "../../kernel/runtime/boot.js";
|
|
44
|
+
import type { PackIndex } from "../../kernel/runtime/graphpack/types.js";
|
|
45
|
+
import { SCHEMA_FILE } from "../../kernel/runtime/graphpack/types.js";
|
|
46
|
+
import { packDir, readActivePack } from "../../kernel/runtime/paths.js";
|
|
47
|
+
import { resolveSources } from "../../kernel/runtime/resolveSources.js";
|
|
48
|
+
import type { GlobalFlags } from "../../kernel/runtime/types.js";
|
|
49
|
+
import type { VerbSpec } from "../../kernel/spec/types.js";
|
|
50
|
+
import {
|
|
51
|
+
DEFAULT_PACK_ALL_VISIBLE_CONFIG,
|
|
52
|
+
DEFAULT_PACK_CONFIG,
|
|
53
|
+
DEFAULT_PACK_TTL,
|
|
54
|
+
INSTANCE_ONLY_TTL,
|
|
55
|
+
MALFORMED_TTL,
|
|
56
|
+
NO_BLOCKS_TTL,
|
|
57
|
+
UNTIERED_BLOCK_NAME,
|
|
58
|
+
} from "../fixtures/graph/defaultPack.js";
|
|
59
|
+
import {
|
|
60
|
+
bootFixtureRuntime,
|
|
61
|
+
type FixtureGraph,
|
|
62
|
+
type FixtureGraphOptions,
|
|
63
|
+
} from "../helpers/fixtureGraph.js";
|
|
64
|
+
|
|
65
|
+
const JSON_FLAGS: GlobalFlags = {
|
|
66
|
+
llm: false,
|
|
67
|
+
autoLlm: false,
|
|
68
|
+
format: "json",
|
|
69
|
+
verbose: false,
|
|
70
|
+
};
|
|
71
|
+
const NO_MUTATION = { dryRun: false, undo: false, yes: false };
|
|
72
|
+
|
|
73
|
+
const blockModule = storyModules.get("block");
|
|
74
|
+
if (!blockModule) {
|
|
75
|
+
throw new Error('pragma.conf.ts declares no story for "block"');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const blockListVerb = blockModule.verbs.find(
|
|
79
|
+
(verb) => verb.path.join(" ") === "block list",
|
|
80
|
+
) as VerbSpec;
|
|
81
|
+
|
|
82
|
+
/** Track fixtures so a single `afterAll` disposes every temp dir + store. */
|
|
83
|
+
const fixtures: FixtureGraph[] = [];
|
|
84
|
+
|
|
85
|
+
/** Boot a tracked fixture from inline TTL (+ optional config), for auto-disposal. */
|
|
86
|
+
async function boot(
|
|
87
|
+
ttl: string,
|
|
88
|
+
config?: FixtureGraphOptions["config"],
|
|
89
|
+
): Promise<FixtureGraph> {
|
|
90
|
+
const fixture = await bootFixtureRuntime({
|
|
91
|
+
ttl,
|
|
92
|
+
...(config ? { config } : {}),
|
|
93
|
+
});
|
|
94
|
+
fixtures.push(fixture);
|
|
95
|
+
return fixture;
|
|
96
|
+
}
|
|
97
|
+
afterAll(async () => {
|
|
98
|
+
await Promise.all(fixtures.map((fixture) => fixture.dispose()));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
/** The index of the pack a fixture's boot decision names — what `info` reads. */
|
|
102
|
+
async function activeIndex(
|
|
103
|
+
fixture: FixtureGraph,
|
|
104
|
+
): Promise<PackIndex | undefined> {
|
|
105
|
+
return readPackIndex(
|
|
106
|
+
resolveSources(await fixture.runtime.loadConfig(), fixture.cwd),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The `--format json` envelope a read verb renders. */
|
|
111
|
+
interface Envelope {
|
|
112
|
+
readonly ok: boolean;
|
|
113
|
+
readonly data: unknown;
|
|
114
|
+
readonly meta: Record<string, unknown>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Run a read verb at a fixture's cwd and parse its JSON envelope. */
|
|
118
|
+
async function readVerb(
|
|
119
|
+
verb: VerbSpec,
|
|
120
|
+
params: Record<string, unknown>,
|
|
121
|
+
cwd: string,
|
|
122
|
+
): Promise<Envelope> {
|
|
123
|
+
const outcome = await executeVerb(
|
|
124
|
+
verb,
|
|
125
|
+
params,
|
|
126
|
+
NO_MUTATION,
|
|
127
|
+
bootRuntime(JSON_FLAGS, cwd),
|
|
128
|
+
);
|
|
129
|
+
return JSON.parse(outcome.stdout as string) as Envelope;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The sorted `name` column of a `block list` invocation. */
|
|
133
|
+
async function blockListNames(cwd: string): Promise<string[]> {
|
|
134
|
+
const envelope = await readVerb(blockListVerb, {}, cwd);
|
|
135
|
+
const rows = envelope.data as { name: string }[];
|
|
136
|
+
return rows.map((row) => row.name).sort();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
describe("default-pack journey — sources update, build, boot (E1)", () => {
|
|
140
|
+
it("bootFixtureRuntime runs the real resolve/build and points the project at the pack", async () => {
|
|
141
|
+
const fixture = await boot(DEFAULT_PACK_TTL, DEFAULT_PACK_CONFIG);
|
|
142
|
+
expect(readActivePack(fixture.cwd)).toMatch(/^[0-9a-f]{64}$/);
|
|
143
|
+
// The booted store answers a read against the freshly built pack.
|
|
144
|
+
const ask = await fixture.runtime.query.sparql(
|
|
145
|
+
"ASK { ds:button a ds:Component }",
|
|
146
|
+
);
|
|
147
|
+
expect(ask.type === "ask" && ask.result).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("drives the literal `sources update` verb over a cold project, then boots a read", async () => {
|
|
151
|
+
// A cold file-package project with nothing built yet — the real CLI path.
|
|
152
|
+
const pkg = mkdtempSync(join(tmpdir(), "e1-pkg-"));
|
|
153
|
+
mkdirSync(join(pkg, "definitions"), { recursive: true });
|
|
154
|
+
writeFileSync(join(pkg, "definitions", "pack.ttl"), DEFAULT_PACK_TTL);
|
|
155
|
+
const cwd = mkdtempSync(join(tmpdir(), "e1-proj-"));
|
|
156
|
+
writeFileSync(
|
|
157
|
+
join(cwd, "pragma.config.ts"),
|
|
158
|
+
`export default { packs: [{ name: "default", source: "file://${pkg}" }] };\n`,
|
|
159
|
+
);
|
|
160
|
+
try {
|
|
161
|
+
// `sources update` is a real mutation — resolve, build, point the project.
|
|
162
|
+
const outcome = await executeVerb(
|
|
163
|
+
updateVerb,
|
|
164
|
+
{},
|
|
165
|
+
NO_MUTATION,
|
|
166
|
+
bootRuntime(JSON_FLAGS, cwd),
|
|
167
|
+
);
|
|
168
|
+
expect(outcome.exitCode).toBe(0);
|
|
169
|
+
expect(readActivePack(cwd)).toMatch(/^[0-9a-f]{64}$/);
|
|
170
|
+
// A SEPARATE, fresh runtime boots the just-built pack from the pointer and
|
|
171
|
+
// answers a read over it. Since L-OPEN-9 the list is unfiltered, so the
|
|
172
|
+
// cold-boot read returns the pack's WHOLE block set — the beta-gated Beta
|
|
173
|
+
// Badge and the untiered Orphan Widget included. That makes this the
|
|
174
|
+
// strictest form of the assert (an exact set, not a tolerant subset).
|
|
175
|
+
expect(await blockListNames(cwd)).toEqual([
|
|
176
|
+
"Beta Badge",
|
|
177
|
+
"Button",
|
|
178
|
+
"Card",
|
|
179
|
+
UNTIERED_BLOCK_NAME,
|
|
180
|
+
]);
|
|
181
|
+
} finally {
|
|
182
|
+
rmSync(pkg, { recursive: true, force: true });
|
|
183
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("default-pack journey — graph query over the real pack (E1)", () => {
|
|
189
|
+
it("a SELECT resolves every ds:Component, including the untiered one", async () => {
|
|
190
|
+
const fixture = await boot(DEFAULT_PACK_TTL, DEFAULT_PACK_CONFIG);
|
|
191
|
+
const envelope = await readVerb(
|
|
192
|
+
graphQueryVerb,
|
|
193
|
+
{
|
|
194
|
+
sparql:
|
|
195
|
+
"SELECT ?name WHERE { ?c a ds:Component ; ds:name ?name } ORDER BY ?name",
|
|
196
|
+
},
|
|
197
|
+
fixture.cwd,
|
|
198
|
+
);
|
|
199
|
+
expect(envelope.ok).toBe(true);
|
|
200
|
+
const data = envelope.data as {
|
|
201
|
+
type: string;
|
|
202
|
+
bindings: { name: string }[];
|
|
203
|
+
};
|
|
204
|
+
expect(data.type).toBe("select");
|
|
205
|
+
// All THREE components are in the graph — the untiered Orphan Widget too.
|
|
206
|
+
expect(data.bindings.map((binding) => binding.name)).toEqual([
|
|
207
|
+
"Beta Badge",
|
|
208
|
+
"Button",
|
|
209
|
+
"Orphan Widget",
|
|
210
|
+
]);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("an ASK over a known individual returns true", async () => {
|
|
214
|
+
const fixture = await boot(DEFAULT_PACK_TTL, DEFAULT_PACK_CONFIG);
|
|
215
|
+
const envelope = await readVerb(
|
|
216
|
+
graphQueryVerb,
|
|
217
|
+
{ sparql: "ASK { ds:card a ds:Pattern }" },
|
|
218
|
+
fixture.cwd,
|
|
219
|
+
);
|
|
220
|
+
const data = envelope.data as { type: string; result: boolean };
|
|
221
|
+
expect(data.type).toBe("ask");
|
|
222
|
+
expect(data.result).toBe(true);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("a malformed query surfaces INVALID_INPUT, not an internal crash", async () => {
|
|
226
|
+
const fixture = await boot(DEFAULT_PACK_TTL, DEFAULT_PACK_CONFIG);
|
|
227
|
+
await expect(
|
|
228
|
+
executeVerb(
|
|
229
|
+
graphQueryVerb,
|
|
230
|
+
{ sparql: "SELECT ?s WHERE { ?s ?p" },
|
|
231
|
+
NO_MUTATION,
|
|
232
|
+
bootRuntime(JSON_FLAGS, fixture.cwd),
|
|
233
|
+
),
|
|
234
|
+
).rejects.toMatchObject({ code: "INVALID_INPUT" });
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe("default-pack journey — block list, populated and empty (E1)", () => {
|
|
239
|
+
/** The pack's WHOLE block set — what the declared list returns, always. */
|
|
240
|
+
const ALL_FOUR = ["Beta Badge", "Button", "Card", UNTIERED_BLOCK_NAME];
|
|
241
|
+
|
|
242
|
+
it("the `normal` channel lists the beta-gated block too (L-OPEN-9)", async () => {
|
|
243
|
+
// This used to answer ["Button", "Card"]: the hand-written query hid Beta
|
|
244
|
+
// Badge behind the channel and Orphan Widget behind the required tier join.
|
|
245
|
+
// The declared list has neither filter, so the signed-off consequence is
|
|
246
|
+
// pinned right where the old scoping was.
|
|
247
|
+
const fixture = await boot(DEFAULT_PACK_TTL, DEFAULT_PACK_CONFIG);
|
|
248
|
+
expect(await blockListNames(fixture.cwd)).toEqual(ALL_FOUR);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("the `prerelease` channel returns the SAME rows — channel scopes nothing", async () => {
|
|
252
|
+
const fixture = await boot(
|
|
253
|
+
DEFAULT_PACK_TTL,
|
|
254
|
+
DEFAULT_PACK_ALL_VISIBLE_CONFIG,
|
|
255
|
+
);
|
|
256
|
+
expect(await blockListNames(fixture.cwd)).toEqual(ALL_FOUR);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("a configured tier returns the SAME rows — there is no chain to walk", async () => {
|
|
260
|
+
const fixture = await boot(DEFAULT_PACK_TTL, {
|
|
261
|
+
tier: "apps",
|
|
262
|
+
channel: "prerelease",
|
|
263
|
+
});
|
|
264
|
+
// The hand-written verb expanded `apps` to [global, apps] and listed the
|
|
265
|
+
// union; the declared list never reads the tier at all, so `apps` and the
|
|
266
|
+
// unset default are indistinguishable.
|
|
267
|
+
expect(await blockListNames(fixture.cwd)).toEqual(ALL_FOUR);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("an ontology-only pack with no blocks lists empty — a calm exit 0", async () => {
|
|
271
|
+
// D10-A, MEASURED at the dispatch seam the binary uses: the declared list
|
|
272
|
+
// body (`makeListRun`) returns `[]` and never throws on emptiness, so the
|
|
273
|
+
// process still exits 0 — no exit-code delta against the hand-written verb.
|
|
274
|
+
// Only the empty MESSAGE moved (the story's `emptyRecovery` replaces the
|
|
275
|
+
// tier/channel wording and the `--all-tiers` hint).
|
|
276
|
+
const fixture = await boot(NO_BLOCKS_TTL);
|
|
277
|
+
const outcome = await executeVerb(
|
|
278
|
+
blockListVerb,
|
|
279
|
+
{},
|
|
280
|
+
NO_MUTATION,
|
|
281
|
+
bootRuntime(JSON_FLAGS, fixture.cwd),
|
|
282
|
+
);
|
|
283
|
+
expect(outcome.exitCode).toBe(0);
|
|
284
|
+
const envelope = JSON.parse(outcome.stdout as string) as Envelope;
|
|
285
|
+
expect(envelope.ok).toBe(true);
|
|
286
|
+
expect(envelope.data).toEqual([]);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe("default-pack journey — ontology list/show, populated and empty (E1)", () => {
|
|
291
|
+
it("lists the ds namespace with its class and property counts", async () => {
|
|
292
|
+
const fixture = await boot(DEFAULT_PACK_TTL, DEFAULT_PACK_CONFIG);
|
|
293
|
+
const envelope = await readVerb(ontologyListVerb, {}, fixture.cwd);
|
|
294
|
+
const summaries = envelope.data as {
|
|
295
|
+
prefix: string;
|
|
296
|
+
classCount: number;
|
|
297
|
+
propertyCount: number;
|
|
298
|
+
}[];
|
|
299
|
+
expect(summaries).toEqual([
|
|
300
|
+
{
|
|
301
|
+
prefix: "ds",
|
|
302
|
+
namespace: "https://ds.canonical.com/",
|
|
303
|
+
classCount: 5,
|
|
304
|
+
propertyCount: 4,
|
|
305
|
+
},
|
|
306
|
+
]);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("ontology show ds surfaces the two block domain classes", async () => {
|
|
310
|
+
const fixture = await boot(DEFAULT_PACK_TTL, DEFAULT_PACK_CONFIG);
|
|
311
|
+
const envelope = await readVerb(
|
|
312
|
+
ontologyShowVerb,
|
|
313
|
+
{ prefix: "ds" },
|
|
314
|
+
fixture.cwd,
|
|
315
|
+
);
|
|
316
|
+
const data = envelope.data as { classes: { uri: string }[] };
|
|
317
|
+
const localNames = data.classes.map((klass) => klass.uri.split("/").at(-1));
|
|
318
|
+
expect(localNames).toContain("Component");
|
|
319
|
+
expect(localNames).toContain("Pattern");
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("a pack of pure instance data lists NO ontology namespaces — empty, calm", async () => {
|
|
323
|
+
const fixture = await boot(INSTANCE_ONLY_TTL);
|
|
324
|
+
const envelope = await readVerb(ontologyListVerb, {}, fixture.cwd);
|
|
325
|
+
expect(envelope.ok).toBe(true);
|
|
326
|
+
expect(envelope.data).toEqual([]);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("ontology show of an unknown prefix is NOT_FOUND, not a crash", async () => {
|
|
330
|
+
const fixture = await boot(DEFAULT_PACK_TTL, DEFAULT_PACK_CONFIG);
|
|
331
|
+
await expect(
|
|
332
|
+
executeVerb(
|
|
333
|
+
ontologyShowVerb,
|
|
334
|
+
{ prefix: "nope" },
|
|
335
|
+
NO_MUTATION,
|
|
336
|
+
bootRuntime(JSON_FLAGS, fixture.cwd),
|
|
337
|
+
),
|
|
338
|
+
).rejects.toBeInstanceOf(PragmaError);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
describe("default-pack journey — error paths (E1)", () => {
|
|
343
|
+
it("a malformed .ttl fails the build with a NAMED CONFIG_ERROR, not INTERNAL", async () => {
|
|
344
|
+
// bootFixtureRuntime writes the ttl into `fixture/definitions/fixture.ttl`
|
|
345
|
+
// and runs the REAL sources-update build, which classifies the parser error.
|
|
346
|
+
await expect(
|
|
347
|
+
bootFixtureRuntime({ ttl: MALFORMED_TTL }),
|
|
348
|
+
).rejects.toMatchObject({ code: "CONFIG_ERROR" });
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("a torn (emptied) schema.json boots to STORE_UNAVAILABLE with the update recovery", async () => {
|
|
352
|
+
const fixture = await boot(DEFAULT_PACK_TTL, DEFAULT_PACK_CONFIG);
|
|
353
|
+
const active = readActivePack(fixture.cwd) ?? "";
|
|
354
|
+
// Simulate a torn/evicted extraction: truncate schema.json in the pack cache.
|
|
355
|
+
truncateSync(join(packDir(active), SCHEMA_FILE), 0);
|
|
356
|
+
// A brand-new runtime (fresh store memo) boots against the now-incomplete pack.
|
|
357
|
+
let caught: unknown;
|
|
358
|
+
try {
|
|
359
|
+
await executeVerb(
|
|
360
|
+
blockListVerb,
|
|
361
|
+
{},
|
|
362
|
+
NO_MUTATION,
|
|
363
|
+
bootRuntime(JSON_FLAGS, fixture.cwd),
|
|
364
|
+
);
|
|
365
|
+
} catch (error) {
|
|
366
|
+
caught = error;
|
|
367
|
+
}
|
|
368
|
+
expect(caught).toBeInstanceOf(PragmaError);
|
|
369
|
+
expect((caught as PragmaError).code).toBe("STORE_UNAVAILABLE");
|
|
370
|
+
expect((caught as PragmaError).recovery?.cli).toBe("pragma sources update");
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
describe("default-pack journey — real-data shapes the clean fixture masked (E1, hand-off to lanes A)", () => {
|
|
375
|
+
it("every block is co-typed owl:NamedIndividual and indexed as an individual (abox)", async () => {
|
|
376
|
+
const fixture = await boot(DEFAULT_PACK_TTL, DEFAULT_PACK_CONFIG);
|
|
377
|
+
const index = await activeIndex(fixture);
|
|
378
|
+
// The vendored pack indexes 17 distinct entities (5 classes + 4 properties +
|
|
379
|
+
// 2 tiers + 2 channels + 4 blocks).
|
|
380
|
+
expect(index?.entities).toHaveLength(17);
|
|
381
|
+
const button = index?.entities.find(
|
|
382
|
+
(entity) => entity.name === "ds:button",
|
|
383
|
+
);
|
|
384
|
+
expect(button?.box).toBe("abox");
|
|
385
|
+
expect(button?.types).toContain("owl:NamedIndividual");
|
|
386
|
+
expect(button?.types).toContain("ds:Component");
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("a multilingual rdfs:label is stored in BOTH languages and indexed tag-stripped (A7)", async () => {
|
|
390
|
+
const fixture = await boot(DEFAULT_PACK_TTL, DEFAULT_PACK_CONFIG);
|
|
391
|
+
// Both language variants are retrievable directly from the store.
|
|
392
|
+
const en = await fixture.runtime.query.sparql(
|
|
393
|
+
'SELECT ?l WHERE { ds:button rdfs:label ?l . FILTER(LANG(?l) = "en") }',
|
|
394
|
+
);
|
|
395
|
+
const fr = await fixture.runtime.query.sparql(
|
|
396
|
+
'SELECT ?l WHERE { ds:button rdfs:label ?l . FILTER(LANG(?l) = "fr") }',
|
|
397
|
+
);
|
|
398
|
+
const value = (result: typeof en): string | undefined =>
|
|
399
|
+
result.type === "select"
|
|
400
|
+
? (result.bindings.at(0) as { l?: string } | undefined)?.l
|
|
401
|
+
: undefined;
|
|
402
|
+
expect(value(en)).toBe("Button");
|
|
403
|
+
expect(value(fr)).toBe("Bouton");
|
|
404
|
+
// The single-valued index carries ONE label, tag-stripped (no `@en`/`@fr`).
|
|
405
|
+
// NOTE(A7): which language wins is store-order-arbitrary — the index picks
|
|
406
|
+
// whichever the store yields first (no `@en` preference). E3 (live oxigraph)
|
|
407
|
+
// is where that ordering is confirmed against the real pack; here we only
|
|
408
|
+
// assert the tag is stripped and the value is one of the two declared forms.
|
|
409
|
+
const label = (await activeIndex(fixture))?.entities.find(
|
|
410
|
+
(entity) => entity.name === "ds:button",
|
|
411
|
+
)?.label;
|
|
412
|
+
expect(label).not.toMatch(/@(en|fr)/);
|
|
413
|
+
expect(["Button", "Bouton"]).toContain(label);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// ----- HAND-OFFS (A1, A2): lane A's fixes have landed in this branch, so the
|
|
417
|
+
// former `it.fails` pins are now LIVE regression guards. -----
|
|
418
|
+
|
|
419
|
+
it("A1: info's entity total must not exceed the distinct entity count (owl:NamedIndividual double-count)", async () => {
|
|
420
|
+
const fixture = await boot(DEFAULT_PACK_TTL, DEFAULT_PACK_CONFIG);
|
|
421
|
+
const index = await activeIndex(fixture);
|
|
422
|
+
// `entityTotal` (the figure `info`/`doctor` report) once SUMMED per-type
|
|
423
|
+
// instance counts, double-counting each entity per asserted rdf:type (each
|
|
424
|
+
// block under owl:NamedIndividual AND its domain class; ds:tier/ds:release
|
|
425
|
+
// under owl:ObjectProperty AND owl:FunctionalProperty — 23 for 17 distinct
|
|
426
|
+
// entities). Lane A fixed the count, so the true total no longer exceeds the
|
|
427
|
+
// distinct count — guard it stays that way.
|
|
428
|
+
expect(entityTotal(index as NonNullable<typeof index>)).toBeLessThanOrEqual(
|
|
429
|
+
(index as NonNullable<typeof index>).entities.length,
|
|
430
|
+
);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it("A2: an untiered block must still appear in `block list`", async () => {
|
|
434
|
+
const fixture = await boot(
|
|
435
|
+
DEFAULT_PACK_TTL,
|
|
436
|
+
DEFAULT_PACK_ALL_VISIBLE_CONFIG,
|
|
437
|
+
);
|
|
438
|
+
// `block list`'s SELECT once inner-joined `?c ds:tier ?t`, dropping a
|
|
439
|
+
// ds:Component with no ds:tier even under --all-tiers (though `graph query`
|
|
440
|
+
// found it, proven above). Lane A made the tier join OPTIONAL; L-OPEN-9 then
|
|
441
|
+
// removed the filtering entirely, so the untiered block is visible in the
|
|
442
|
+
// ONE view that now exists — guard it stays that way.
|
|
443
|
+
expect(await blockListNames(fixture.cwd)).toContain(UNTIERED_BLOCK_NAME);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// KNOWN GAP / product follow-up (NOT a lane hand-off): a corrupt, NON-EMPTY
|
|
447
|
+
// `schema.json` currently surfaces UNCLASSIFIED. `packIsComplete` only checks
|
|
448
|
+
// size > 0, so torn-but-nonempty garbage bypasses the completeness guard and
|
|
449
|
+
// `compileFromExtraction` throws a raw `SyntaxError` (renders as INTERNAL
|
|
450
|
+
// "please report") instead of a classified error. It SHOULD degrade to
|
|
451
|
+
// STORE_UNAVAILABLE like the emptied case does — the fix belongs in
|
|
452
|
+
// `kernel/runtime/graphpack/read.ts` (guard the `schema.json` read/compile).
|
|
453
|
+
// No lane in this wave owns that, so rather than a false `it.fails` hand-off
|
|
454
|
+
// that would never flip, this is a plain test pinning the CURRENT behavior:
|
|
455
|
+
// when read.ts starts classifying it, this guard trips — swap the assertion
|
|
456
|
+
// to expect a STORE_UNAVAILABLE PragmaError then.
|
|
457
|
+
it("corrupt (non-empty, invalid) schema.json currently surfaces an UNCLASSIFIED error (known gap)", async () => {
|
|
458
|
+
const fixture = await boot(DEFAULT_PACK_TTL, DEFAULT_PACK_CONFIG);
|
|
459
|
+
const active = readActivePack(fixture.cwd) ?? "";
|
|
460
|
+
writeFileSync(join(packDir(active), SCHEMA_FILE), "{ not valid json ]");
|
|
461
|
+
let caught: unknown;
|
|
462
|
+
try {
|
|
463
|
+
await executeVerb(
|
|
464
|
+
blockListVerb,
|
|
465
|
+
{},
|
|
466
|
+
NO_MUTATION,
|
|
467
|
+
bootRuntime(JSON_FLAGS, fixture.cwd),
|
|
468
|
+
);
|
|
469
|
+
} catch (error) {
|
|
470
|
+
caught = error;
|
|
471
|
+
}
|
|
472
|
+
// It throws — but as a raw, unclassified error, NOT a PragmaError. Assert the
|
|
473
|
+
// gap explicitly so a future classification fix in read.ts trips this guard.
|
|
474
|
+
expect(caught).toBeInstanceOf(Error);
|
|
475
|
+
expect(caught).not.toBeInstanceOf(PragmaError);
|
|
476
|
+
});
|
|
477
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The first-install journey — this PR's whole point, pinned.
|
|
3
|
+
*
|
|
4
|
+
* A machine with no cache, no pointer, no network and an empty cwd must answer
|
|
5
|
+
* real store-backed reads from the embedded pack, and must say honestly where
|
|
6
|
+
* those answers come from. Equally: a project that declared its OWN packs and
|
|
7
|
+
* never built them must NOT be handed the distribution's graph — it is a
|
|
8
|
+
* different graph, and quietly serving it is the failure the boot decision
|
|
9
|
+
* exists to prevent.
|
|
10
|
+
*
|
|
11
|
+
* Assertions pin MEMBERSHIP, never counts: the row counts move whenever the
|
|
12
|
+
* upstream design system does, but a design system with no `Button`, or a tier
|
|
13
|
+
* hierarchy with no `Global`, is a change a human should be made to look at.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { describe, expect, it } from "vitest";
|
|
20
|
+
import { storyModules } from "../../capabilities/distribution.js";
|
|
21
|
+
import { checkPackageRefs } from "../../capabilities/doctor/checks/checkPackageRefs.js";
|
|
22
|
+
import { promptListVerb } from "../../capabilities/prompt/verbs.js";
|
|
23
|
+
import { collectStatus } from "../../capabilities/sources/collectStatus.js";
|
|
24
|
+
import { PragmaError } from "../../kernel/error/PragmaError.js";
|
|
25
|
+
import { executeVerb } from "../../kernel/project/cli/dispatch.js";
|
|
26
|
+
import { bootRuntime } from "../../kernel/runtime/boot.js";
|
|
27
|
+
import type { GlobalFlags } from "../../kernel/runtime/types.js";
|
|
28
|
+
import type { CapabilityModule, VerbSpec } from "../../kernel/spec/types.js";
|
|
29
|
+
|
|
30
|
+
const JSON_FLAGS: GlobalFlags = {
|
|
31
|
+
llm: false,
|
|
32
|
+
autoLlm: false,
|
|
33
|
+
format: "json",
|
|
34
|
+
verbose: false,
|
|
35
|
+
};
|
|
36
|
+
const NO_MUTATION = { dryRun: false, undo: false, yes: false };
|
|
37
|
+
|
|
38
|
+
const blockModule = storyModules.get("block");
|
|
39
|
+
if (!blockModule) {
|
|
40
|
+
throw new Error('pragma.conf.ts declares no story for "block"');
|
|
41
|
+
}
|
|
42
|
+
const standardModule = storyModules.get("standard");
|
|
43
|
+
if (!standardModule) {
|
|
44
|
+
throw new Error('pragma.conf.ts declares no story for "standard"');
|
|
45
|
+
}
|
|
46
|
+
const tierModule = storyModules.get("tier");
|
|
47
|
+
if (!tierModule) {
|
|
48
|
+
throw new Error('pragma.conf.ts declares no story for "tier"');
|
|
49
|
+
}
|
|
50
|
+
const tokenModule = storyModules.get("token");
|
|
51
|
+
if (!tokenModule) {
|
|
52
|
+
throw new Error('pragma.conf.ts declares no story for "token"');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const verbOf = (module: CapabilityModule, path: string): VerbSpec =>
|
|
56
|
+
module.verbs.find((verb) => verb.path.join(" ") === path) as VerbSpec;
|
|
57
|
+
|
|
58
|
+
/** A cwd with nothing in it — the cold-install shape (the XDG dirs are isolated). */
|
|
59
|
+
const emptyCwd = (): string => mkdtempSync(join(tmpdir(), "pragma-first-"));
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A cwd whose `pragma.config.ts` declares its own packs and has never been
|
|
63
|
+
* built — `origins.packs` is "project", so the embedded pack must not answer.
|
|
64
|
+
*/
|
|
65
|
+
function unbuiltProjectCwd(): string {
|
|
66
|
+
const cwd = mkdtempSync(join(tmpdir(), "pragma-unbuilt-"));
|
|
67
|
+
writeFileSync(
|
|
68
|
+
join(cwd, "pragma.config.ts"),
|
|
69
|
+
'export default { packs: [{ name: "mine", source: "file:///pragma-never-built" }] };\n',
|
|
70
|
+
);
|
|
71
|
+
return cwd;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Run a read verb at a cwd and return the parsed `data` payload. */
|
|
75
|
+
async function readData(verb: VerbSpec, cwd: string): Promise<unknown> {
|
|
76
|
+
const outcome = await executeVerb(
|
|
77
|
+
verb,
|
|
78
|
+
{},
|
|
79
|
+
NO_MUTATION,
|
|
80
|
+
bootRuntime(JSON_FLAGS, cwd),
|
|
81
|
+
);
|
|
82
|
+
return (JSON.parse(outcome.stdout as string) as { data: unknown }).data;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
describe("first install — an empty cwd answers real reads offline", () => {
|
|
86
|
+
it("block list resolves the design system's blocks", async () => {
|
|
87
|
+
const rows = (await readData(
|
|
88
|
+
verbOf(blockModule, "block list"),
|
|
89
|
+
emptyCwd(),
|
|
90
|
+
)) as { name: string }[];
|
|
91
|
+
expect(rows.map((row) => row.name)).toContain("Button");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("standard list resolves the code standards", async () => {
|
|
95
|
+
const rows = (await readData(
|
|
96
|
+
verbOf(standardModule, "standard list"),
|
|
97
|
+
emptyCwd(),
|
|
98
|
+
)) as { uri: string; name: string }[];
|
|
99
|
+
expect(rows.length).toBeGreaterThan(0);
|
|
100
|
+
// Every row is a code standard from the standards pack's own namespace.
|
|
101
|
+
for (const row of rows) {
|
|
102
|
+
expect(row.uri).toContain("codestandards#");
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("tier list resolves the tier hierarchy", async () => {
|
|
107
|
+
const rows = (await readData(
|
|
108
|
+
verbOf(tierModule, "tier list"),
|
|
109
|
+
emptyCwd(),
|
|
110
|
+
)) as { name: string }[];
|
|
111
|
+
expect(rows.map((row) => row.name)).toContain("Global");
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("first install — empty results are honest, not papered over", () => {
|
|
116
|
+
it("token list exits calmly with no rows (the graph carries no ds:Token)", async () => {
|
|
117
|
+
expect(
|
|
118
|
+
await readData(verbOf(tokenModule, "token list"), emptyCwd()),
|
|
119
|
+
).toEqual([]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("prompt list exits calmly with no prompts (the graph carries no ds:Prompt)", async () => {
|
|
123
|
+
expect(await readData(promptListVerb as VerbSpec, emptyCwd())).toEqual({
|
|
124
|
+
prompts: [],
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("first install — a project with its own unbuilt packs is refused", () => {
|
|
130
|
+
it("a read throws STORE_UNAVAILABLE naming `pragma sources update`", async () => {
|
|
131
|
+
let caught: unknown;
|
|
132
|
+
try {
|
|
133
|
+
await readData(verbOf(blockModule, "block list"), unbuiltProjectCwd());
|
|
134
|
+
} catch (error) {
|
|
135
|
+
caught = error;
|
|
136
|
+
}
|
|
137
|
+
expect(caught).toBeInstanceOf(PragmaError);
|
|
138
|
+
expect((caught as PragmaError).code).toBe("STORE_UNAVAILABLE");
|
|
139
|
+
expect((caught as PragmaError).recovery?.cli).toBe("pragma sources update");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("doctor reports it as a failure, not a healthy install", async () => {
|
|
143
|
+
const result = await checkPackageRefs(
|
|
144
|
+
bootRuntime(JSON_FLAGS, unbuiltProjectCwd()),
|
|
145
|
+
);
|
|
146
|
+
expect(result.status).toBe("fail");
|
|
147
|
+
expect(result.detail).toContain("the store has not been built");
|
|
148
|
+
expect(result.remedy).toBe("pragma sources update");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("first install — the surfaces say where the answers come from", () => {
|
|
153
|
+
it("sources status reports the embedded snapshot with its provenance", async () => {
|
|
154
|
+
const status = await collectStatus(bootRuntime(JSON_FLAGS, emptyCwd()));
|
|
155
|
+
expect(status.store).toBe("embedded");
|
|
156
|
+
// The upstream revisions the snapshot was compiled from — never "up to date".
|
|
157
|
+
expect(status.sourceRef).toContain("@canonical/design-system@");
|
|
158
|
+
expect(status.entityCount).toBeGreaterThan(0);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("doctor passes, naming the embedded snapshot rather than the config", async () => {
|
|
162
|
+
const result = await checkPackageRefs(bootRuntime(JSON_FLAGS, emptyCwd()));
|
|
163
|
+
expect(result.status).toBe("pass");
|
|
164
|
+
expect(result.detail).toContain("embedded snapshot @ ");
|
|
165
|
+
expect(result.detail).toContain("pragma sources update");
|
|
166
|
+
});
|
|
167
|
+
});
|