@canonical/pragma-cli 0.29.0 → 0.30.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 +67 -15
- package/dist/pragma +0 -0
- package/package.json +13 -13
- package/src/config/defaultConfig.test.ts +33 -0
- package/src/config/defaultConfig.ts +59 -0
- package/src/config/defaults.json +17 -0
- package/src/config/findProjectConfigPath.test.ts +53 -0
- package/src/config/findProjectConfigPath.ts +36 -0
- package/src/config/index.ts +17 -2
- package/src/config/mergeConfigFileUpdate.ts +58 -0
- package/src/config/parseConfigValues.ts +215 -0
- package/src/config/readConfig.test.ts +104 -13
- package/src/config/readConfig.ts +12 -153
- package/src/config/readConfigLayers.test.ts +107 -0
- package/src/config/readConfigLayers.ts +115 -0
- package/src/config/resolveGlobalConfigPath.ts +13 -0
- package/src/config/resolveWriteConfigPath.test.ts +53 -0
- package/src/config/resolveWriteConfigPath.ts +31 -0
- package/src/config/types.ts +76 -1
- package/src/config/writeConfig.ts +25 -55
- package/src/domains/block/commands/list.ts +5 -90
- package/src/domains/block/commands/lookup.ts +8 -167
- package/src/domains/block/mcp/specs.ts +16 -196
- package/src/domains/block/orchestration/resolveBlockLookup.ts +2 -1
- package/src/domains/block/stories.test.ts +28 -0
- package/src/domains/block/stories.ts +254 -0
- package/src/domains/config/commands/channel.test.ts +14 -3
- package/src/domains/config/commands/channel.ts +11 -7
- package/src/domains/config/commands/configScope.ts +46 -0
- package/src/domains/config/commands/framework.test.ts +18 -1
- package/src/domains/config/commands/framework.ts +13 -7
- package/src/domains/config/commands/show.ts +5 -45
- package/src/domains/config/commands/tier.test.ts +13 -2
- package/src/domains/config/commands/tier.ts +11 -7
- package/src/domains/config/commands/trace.test.ts +19 -2
- package/src/domains/config/commands/trace.ts +6 -3
- package/src/domains/config/formatters/channel.test.ts +20 -6
- package/src/domains/config/formatters/channel.ts +14 -11
- package/src/domains/config/formatters/framework.ts +14 -11
- package/src/domains/config/formatters/show.test.ts +27 -0
- package/src/domains/config/formatters/show.ts +27 -4
- package/src/domains/config/formatters/tier.test.ts +26 -10
- package/src/domains/config/formatters/tier.ts +19 -11
- package/src/domains/config/formatters/trace.ts +7 -7
- package/src/domains/config/mcp/specs.ts +33 -53
- package/src/domains/config/operations/show.test.ts +18 -0
- package/src/domains/config/operations/show.ts +7 -1
- package/src/domains/config/operations/types.ts +7 -0
- package/src/domains/config/stories.ts +42 -0
- package/src/domains/config/tasks/index.ts +1 -0
- package/src/domains/config/tasks/setChannel.ts +13 -28
- package/src/domains/config/tasks/setFramework.ts +11 -30
- package/src/domains/config/tasks/setTier.ts +10 -29
- package/src/domains/config/tasks/setTrace.ts +10 -28
- package/src/domains/config/tasks/writeConfigField.ts +41 -0
- package/src/domains/create/commands/component.ts +5 -5
- package/src/domains/doctor/operations/checks/checkConfigFile.ts +19 -5
- package/src/domains/doctor/operations/checks/checkMcpCommands.ts +130 -0
- package/src/domains/doctor/operations/checks/checkMcpConfigured.ts +1 -1
- package/src/domains/doctor/operations/checks/checkPackageRefs.ts +2 -14
- package/src/domains/doctor/operations/checks/checkSkillsSymlinked.ts +1 -1
- package/src/domains/doctor/operations/checks/index.ts +1 -0
- package/src/domains/doctor/operations/checks.test.ts +158 -7
- package/src/domains/doctor/operations/runChecks.test.ts +15 -6
- package/src/domains/doctor/operations/runChecks.ts +2 -0
- package/src/domains/graph/helpers/resolveUri.test.ts +50 -0
- package/src/domains/graph/helpers/resolveUri.ts +28 -21
- package/src/domains/graphql/commands/build.test.ts +5 -3
- package/src/domains/graphql/commands/build.ts +8 -16
- package/src/domains/graphql/commands/check.test.ts +5 -3
- package/src/domains/graphql/commands/check.ts +7 -13
- package/src/domains/graphql/commands/index.ts +1 -0
- package/src/domains/graphql/commands/serve.test.ts +40 -0
- package/src/domains/graphql/commands/serve.ts +131 -0
- package/src/domains/graphql/constants.ts +10 -0
- package/src/domains/graphql/helpers/gatherSources.test.ts +92 -0
- package/src/domains/graphql/helpers/gatherSources.ts +58 -0
- package/src/domains/graphql/helpers/index.ts +2 -0
- package/src/domains/graphql/helpers/routeGraphql.test.ts +43 -0
- package/src/domains/graphql/helpers/routeGraphql.ts +28 -0
- package/src/domains/graphql/index.ts +9 -6
- package/src/domains/graphql/operations/compileSchema.test.ts +21 -45
- package/src/domains/graphql/operations/compileSchema.ts +23 -25
- package/src/domains/graphql/operations/createSchemaServer.test.ts +50 -0
- package/src/domains/graphql/operations/createSchemaServer.ts +75 -0
- package/src/domains/graphql/operations/index.ts +5 -0
- package/src/domains/info/operations/collectInfo.test.ts +14 -3
- package/src/domains/info/operations/collectInfo.ts +12 -15
- package/src/domains/llm/capabilities.test.ts +4 -4
- package/src/domains/llm/data/toolCatalog.ts +6 -0
- package/src/domains/modifier/commands/list.ts +5 -38
- package/src/domains/modifier/commands/lookup.ts +8 -79
- package/src/domains/modifier/mcp/specs.ts +15 -94
- package/src/domains/modifier/orchestration/resolveModifierLookup.ts +2 -1
- package/src/domains/modifier/stories.test.ts +20 -0
- package/src/domains/modifier/stories.ts +77 -0
- package/src/domains/ontology/commands/list.ts +5 -58
- package/src/domains/ontology/commands/show.ts +6 -58
- package/src/domains/ontology/formatters/show.ts +7 -1
- package/src/domains/ontology/mcp/specs.ts +7 -84
- package/src/domains/ontology/stories.test.ts +36 -0
- package/src/domains/ontology/stories.ts +108 -0
- package/src/domains/refs/operations/updateRefs.ts +2 -37
- package/src/domains/setup/commands/skills.test.ts +6 -2
- package/src/domains/setup/commands/skills.ts +6 -2
- package/src/domains/setup/helpers/runSetupTask.test.ts +148 -150
- package/src/domains/setup/helpers/runSetupTask.ts +62 -70
- package/src/domains/setup/operations/setupMcp.test.ts +6 -1
- package/src/domains/shared/bootStore.ts +6 -3
- package/src/domains/shared/compactUri.test.ts +11 -0
- package/src/domains/shared/compactUri.ts +10 -5
- package/src/domains/shared/context.ts +2 -2
- package/src/domains/shared/contracts.ts +11 -0
- package/src/domains/shared/createInteractivePromptSession.test.ts +439 -0
- package/src/domains/shared/createInteractivePromptSession.ts +236 -0
- package/src/domains/shared/loaders/bundledLoader.test.ts +248 -0
- package/src/domains/shared/loaders/bundledLoader.ts +110 -12
- package/src/domains/shared/loaders/gitLoader.ts +2 -2
- package/src/domains/shared/loaders/index.ts +4 -1
- package/src/domains/shared/loaders/localLoader.ts +18 -4
- package/src/domains/shared/loaders/readPackageDir.ts +36 -1
- package/src/domains/shared/loaders/storyAssetsPlugin.test.ts +84 -0
- package/src/domains/shared/loaders/storyAssetsPlugin.ts +46 -0
- package/src/domains/shared/lookupMany.test.ts +120 -0
- package/src/domains/shared/lookupMany.ts +34 -2
- package/src/domains/shared/mergeAndParseRefs.test.ts +89 -0
- package/src/domains/shared/mergeAndParseRefs.ts +44 -0
- package/src/domains/shared/packages.ts +10 -17
- package/src/domains/shared/resolveConfiguredGraphs.test.ts +27 -0
- package/src/domains/shared/resolveConfiguredGraphs.ts +41 -0
- package/src/domains/shared/runtime.ts +4 -63
- package/src/domains/shared/semanticPackage.ts +17 -1
- package/src/domains/shared/stories/compileLookupCommand.test.ts +145 -0
- package/src/domains/shared/stories/compileLookupCommand.ts +100 -0
- package/src/domains/shared/stories/compileLookupTool.test.ts +105 -0
- package/src/domains/shared/stories/compileLookupTool.ts +96 -0
- package/src/domains/shared/stories/compileReadCommand.test.ts +121 -0
- package/src/domains/shared/stories/compileReadCommand.ts +48 -0
- package/src/domains/shared/stories/compileReadTool.test.ts +64 -0
- package/src/domains/shared/stories/compileReadTool.ts +38 -0
- package/src/domains/shared/stories/condense.test.ts +18 -0
- package/src/domains/shared/stories/condense.ts +15 -0
- package/src/domains/shared/stories/index.ts +20 -0
- package/src/domains/shared/stories/normalizeNames.test.ts +20 -0
- package/src/domains/shared/stories/normalizeNames.ts +22 -0
- package/src/domains/shared/stories/pack/applyPackFilters.test.ts +102 -0
- package/src/domains/shared/stories/pack/applyPackFilters.ts +76 -0
- package/src/domains/shared/stories/pack/buildLookupQuery.test.ts +51 -0
- package/src/domains/shared/stories/pack/buildLookupQuery.ts +80 -0
- package/src/domains/shared/stories/pack/collectPackStories.test.ts +108 -0
- package/src/domains/shared/stories/pack/collectPackStories.ts +76 -0
- package/src/domains/shared/stories/pack/compilePackCommands.ts +40 -0
- package/src/domains/shared/stories/pack/compilePackStories.test.ts +146 -0
- package/src/domains/shared/stories/pack/compilePackStories.ts +253 -0
- package/src/domains/shared/stories/pack/compilePackToolSpecs.ts +43 -0
- package/src/domains/shared/stories/pack/index.ts +28 -0
- package/src/domains/shared/stories/pack/runSelectQuery.ts +31 -0
- package/src/domains/shared/stories/pack/standardParity.test.ts +308 -0
- package/src/domains/shared/stories/pack/types.ts +104 -0
- package/src/domains/shared/stories/pack/validateStoryPackDefinition.test.ts +240 -0
- package/src/domains/shared/stories/pack/validateStoryPackDefinition.ts +408 -0
- package/src/domains/shared/stories/projectCliParameters.test.ts +59 -0
- package/src/domains/shared/stories/projectCliParameters.ts +57 -0
- package/src/domains/shared/stories/projectToolParams.test.ts +50 -0
- package/src/domains/shared/stories/projectToolParams.ts +32 -0
- package/src/domains/shared/stories/requirePragmaContext.test.ts +23 -0
- package/src/domains/shared/stories/requirePragmaContext.ts +23 -0
- package/src/domains/shared/stories/resolveLookupDetailed.test.ts +57 -0
- package/src/domains/shared/stories/resolveLookupDetailed.ts +27 -0
- package/src/domains/shared/stories/types.ts +171 -0
- package/src/domains/shared/suggestions/expandGlob.ts +1 -0
- package/src/domains/shared/suggestions/suggestNames.ts +43 -10
- package/src/domains/skill/commands/index.ts +3 -1
- package/src/domains/skill/commands/list.test.ts +6 -2
- package/src/domains/skill/commands/list.ts +5 -63
- package/src/domains/skill/commands/lookup.test.ts +118 -0
- package/src/domains/skill/commands/lookup.ts +14 -0
- package/src/domains/skill/formatters/index.ts +1 -0
- package/src/domains/skill/formatters/lookup.test.ts +46 -0
- package/src/domains/skill/formatters/lookup.ts +57 -0
- package/src/domains/skill/helpers/resolveSkillSources.ts +0 -1
- package/src/domains/skill/index.ts +7 -3
- package/src/domains/skill/mcp/specs.ts +10 -29
- package/src/domains/skill/operations/discover.test.ts +61 -30
- package/src/domains/skill/operations/discover.ts +28 -37
- package/src/domains/skill/operations/index.ts +2 -1
- package/src/domains/skill/operations/list.test.ts +15 -7
- package/src/domains/skill/operations/lookup.test.ts +91 -0
- package/src/domains/skill/operations/lookup.ts +70 -0
- package/src/domains/skill/stories.test.ts +43 -0
- package/src/domains/skill/stories.ts +103 -0
- package/src/domains/skill/types.ts +8 -0
- package/src/domains/standard/commands/list.ts +5 -84
- package/src/domains/standard/commands/lookup.ts +8 -93
- package/src/domains/standard/mcp/specs.ts +16 -171
- package/src/domains/standard/orchestration/resolveStandardLookup.ts +2 -1
- package/src/domains/standard/stories.test.ts +24 -0
- package/src/domains/standard/stories.ts +151 -0
- package/src/domains/tier/commands/list.ts +5 -55
- package/src/domains/tier/mcp/specs.ts +5 -31
- package/src/domains/tier/stories.test.ts +22 -0
- package/src/domains/tier/stories.ts +52 -0
- package/src/domains/token/mcp/specs.ts +5 -1
- package/src/domains/token/orchestration/resolveTokenLookup.ts +2 -1
- package/src/domains/trace/tracePlugin.test.ts +1 -1
- package/src/mcp/registerResources.test.ts +382 -74
- package/src/mcp/registerResources.ts +5 -348
- package/src/mcp/resources/buildGraphIndex.ts +167 -0
- package/src/mcp/resources/buildResourceList.test.ts +97 -0
- package/src/mcp/resources/buildResourceList.ts +176 -0
- package/src/mcp/resources/classifyEntity.test.ts +40 -0
- package/src/mcp/resources/classifyEntity.ts +27 -0
- package/src/mcp/resources/constants.ts +105 -0
- package/src/mcp/resources/displayPredicates.ts +62 -0
- package/src/mcp/resources/pickFirstValue.test.ts +29 -0
- package/src/mcp/resources/pickFirstValue.ts +26 -0
- package/src/mcp/resources/pickPrimaryType.test.ts +41 -0
- package/src/mcp/resources/pickPrimaryType.ts +26 -0
- package/src/mcp/resources/rankUriCompletions.test.ts +52 -0
- package/src/mcp/resources/rankUriCompletions.ts +64 -0
- package/src/mcp/resources/readEntity.ts +169 -0
- package/src/mcp/resources/registerResources.ts +126 -0
- package/src/mcp/resources/types.ts +128 -0
- package/src/mcp/tools/index.ts +9 -0
- package/src/mcp/tools/registerTools.test.ts +65 -8
- package/src/mcp/types.ts +4 -0
- package/src/pipeline/collectCommands.ts +8 -1
- package/src/pipeline/runCli.ts +40 -6
- package/src/testing/createTestMcpClient.ts +4 -1
- package/src/testing/index.ts +10 -0
- package/src/testing/integration/__snapshots__/executor-golden.test.ts.snap +1207 -0
- package/src/testing/integration/cross-binary-byte-equality.test.ts +129 -0
- package/src/testing/integration/executor-execution.test.ts +101 -0
- package/src/testing/integration/executor-golden.test.ts +145 -0
- package/src/testing/integration/mcp-surface.test.ts +1 -1
- package/src/testing/integration/parity.test.ts +17 -12
- package/src/testing/regression/0001-skill-discovery-granularity.test.ts +71 -0
- package/src/testing/setupXdgIsolation.ts +15 -0
- package/src/testing/standardParityFixtures.ts +92 -0
- package/src/testing/storyFixtures.ts +63 -0
- package/src/pipeline/runInteractiveCommand.ts +0 -198
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ontology read stories — the single declaration of `ontology list` and
|
|
3
|
+
* `ontology show` for both surfaces.
|
|
4
|
+
*
|
|
5
|
+
* The story kernel compiles these into the CLI commands and MCP tools.
|
|
6
|
+
* Both surfaces now render through the same formatters, so MCP condensed
|
|
7
|
+
* output matches CLI `--llm` output by construction (previously the MCP
|
|
8
|
+
* specs re-implemented the Markdown inline and had drifted).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { PragmaError } from "#error";
|
|
12
|
+
import { createListView } from "#tui";
|
|
13
|
+
import type { ColumnDef } from "../shared/contracts.js";
|
|
14
|
+
import {
|
|
15
|
+
type ReadStory,
|
|
16
|
+
requirePragmaContext,
|
|
17
|
+
} from "../shared/stories/index.js";
|
|
18
|
+
import type {
|
|
19
|
+
OntologyDetailed,
|
|
20
|
+
OntologySummary,
|
|
21
|
+
} from "../shared/types/index.js";
|
|
22
|
+
import { listFormatters, showFormatters } from "./formatters/index.js";
|
|
23
|
+
import { listOntologies, showOntology } from "./operations/index.js";
|
|
24
|
+
|
|
25
|
+
const ontologyListColumns: readonly ColumnDef<OntologySummary>[] = [
|
|
26
|
+
{ key: "prefix", label: "Prefix" },
|
|
27
|
+
{ key: "namespace", label: "Namespace" },
|
|
28
|
+
{ key: "classCount", label: "Classes" },
|
|
29
|
+
{ key: "propertyCount", label: "Properties" },
|
|
30
|
+
{ key: "anatomyCount", label: "Anatomies" },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/** The `ontology list` / `ontology_list` read story. */
|
|
34
|
+
export const ontologyListStory: ReadStory<
|
|
35
|
+
OntologySummary[],
|
|
36
|
+
readonly OntologySummary[]
|
|
37
|
+
> = {
|
|
38
|
+
noun: "ontology",
|
|
39
|
+
verb: "list",
|
|
40
|
+
description: "List loaded ontologies",
|
|
41
|
+
toolDescription:
|
|
42
|
+
"List all ontologies loaded in the knowledge graph with class and property counts.",
|
|
43
|
+
params: [],
|
|
44
|
+
examples: ["pragma ontology list", "pragma ontology list --llm"],
|
|
45
|
+
resolve: (rt) => listOntologies(rt.store),
|
|
46
|
+
toOutput: (ontologies) => ontologies,
|
|
47
|
+
formatters: listFormatters,
|
|
48
|
+
toEnvelope: (ontologies) => ({
|
|
49
|
+
data: ontologies,
|
|
50
|
+
meta: { count: ontologies.length },
|
|
51
|
+
}),
|
|
52
|
+
emptyError: (ontologies) =>
|
|
53
|
+
ontologies.length === 0
|
|
54
|
+
? PragmaError.emptyResults("ontology", {
|
|
55
|
+
recovery: {
|
|
56
|
+
message: "Ensure design system packages are installed.",
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
: undefined,
|
|
60
|
+
renderInk: (ontologies) =>
|
|
61
|
+
createListView({
|
|
62
|
+
heading: "Ontologies",
|
|
63
|
+
domain: "ontology",
|
|
64
|
+
items: ontologies,
|
|
65
|
+
columns: ontologyListColumns,
|
|
66
|
+
}),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** The `ontology show <prefix>` / `ontology_show` read story. */
|
|
70
|
+
export const ontologyShowStory: ReadStory<OntologyDetailed, OntologyDetailed> =
|
|
71
|
+
{
|
|
72
|
+
noun: "ontology",
|
|
73
|
+
verb: "show",
|
|
74
|
+
description: "Show ontology schema details",
|
|
75
|
+
toolDescription:
|
|
76
|
+
"Show detailed schema for an ontology including classes and properties.",
|
|
77
|
+
params: [
|
|
78
|
+
{
|
|
79
|
+
name: "prefix",
|
|
80
|
+
type: "string",
|
|
81
|
+
description: "Ontology prefix (e.g. 'ds', 'cs')",
|
|
82
|
+
positional: true,
|
|
83
|
+
required: true,
|
|
84
|
+
complete: async (partial, cmdCtx) => {
|
|
85
|
+
const ctx = requirePragmaContext(cmdCtx);
|
|
86
|
+
const all = await listOntologies(ctx.store);
|
|
87
|
+
return all
|
|
88
|
+
.map((o) => o.prefix)
|
|
89
|
+
.filter((p) => p.toLowerCase().startsWith(partial.toLowerCase()));
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
examples: ["pragma ontology show ds", "pragma ontology show cs --llm"],
|
|
94
|
+
resolve: (rt, params) => showOntology(rt.store, params.prefix as string),
|
|
95
|
+
toOutput: (ontology) => ontology,
|
|
96
|
+
formatters: showFormatters,
|
|
97
|
+
toEnvelope: (ontology) => ({ data: ontology }),
|
|
98
|
+
guardParams: (params) =>
|
|
99
|
+
typeof params.prefix === "string" && params.prefix.length > 0
|
|
100
|
+
? undefined
|
|
101
|
+
: PragmaError.invalidInput("prefix", "(empty)", {
|
|
102
|
+
recovery: {
|
|
103
|
+
message: "List loaded ontologies to find valid prefixes.",
|
|
104
|
+
cli: "pragma ontology list",
|
|
105
|
+
mcp: { tool: "ontology_list" },
|
|
106
|
+
},
|
|
107
|
+
}),
|
|
108
|
+
};
|
|
@@ -8,12 +8,11 @@
|
|
|
8
8
|
import { existsSync, readdirSync } from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { readConfig } from "#config";
|
|
11
|
-
import {
|
|
11
|
+
import { mergeAndParseRefs } from "../../shared/mergeAndParseRefs.js";
|
|
12
12
|
import { cloneRef, fetchRef, pruneCache } from "./gitOps.js";
|
|
13
13
|
import type { PackageRef, RawPackageEntry } from "./parseRef.js";
|
|
14
14
|
import { parsePackageEntry } from "./parseRef.js";
|
|
15
15
|
import { cacheRoot, gitCacheDir } from "./paths.js";
|
|
16
|
-
import readGlobalRefs from "./readGlobalRefs.js";
|
|
17
16
|
|
|
18
17
|
// ---------------------------------------------------------------------------
|
|
19
18
|
// Types
|
|
@@ -56,7 +55,7 @@ export default async function updateRefs(
|
|
|
56
55
|
options: UpdateRefsOptions,
|
|
57
56
|
): Promise<UpdateResult[]> {
|
|
58
57
|
const config = readConfig(options.cwd);
|
|
59
|
-
const refs =
|
|
58
|
+
const refs = mergeAndParseRefs(config.packages);
|
|
60
59
|
const results: UpdateResult[] = [];
|
|
61
60
|
|
|
62
61
|
for (const ref of refs) {
|
|
@@ -137,40 +136,6 @@ export default async function updateRefs(
|
|
|
137
136
|
// Helpers
|
|
138
137
|
// ---------------------------------------------------------------------------
|
|
139
138
|
|
|
140
|
-
/** Merge global + project entries, parse into PackageRef[]. */
|
|
141
|
-
function mergeEntries(
|
|
142
|
-
projectPackages?: ReadonlyArray<RawPackageEntry>,
|
|
143
|
-
): PackageRef[] {
|
|
144
|
-
const globalEntries = readGlobalRefs();
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
(!projectPackages || projectPackages.length === 0) &&
|
|
148
|
-
globalEntries.length === 0
|
|
149
|
-
) {
|
|
150
|
-
return DEFAULT_PACKAGES.map((pkg) => parsePackageEntry(pkg));
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const merged = new Map<string, RawPackageEntry>();
|
|
154
|
-
|
|
155
|
-
for (const entry of DEFAULT_PACKAGES) {
|
|
156
|
-
const name = typeof entry === "string" ? entry : entry.name;
|
|
157
|
-
merged.set(name, entry);
|
|
158
|
-
}
|
|
159
|
-
for (const entry of globalEntries) {
|
|
160
|
-
const name = typeof entry === "string" ? entry : entry.name;
|
|
161
|
-
merged.set(name, entry);
|
|
162
|
-
}
|
|
163
|
-
if (projectPackages) {
|
|
164
|
-
merged.clear();
|
|
165
|
-
for (const entry of projectPackages) {
|
|
166
|
-
const name = typeof entry === "string" ? entry : entry.name;
|
|
167
|
-
merged.set(name, entry);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return [...merged.values()].map(parsePackageEntry);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
139
|
/**
|
|
175
140
|
* Remove cache directories that don't match any configured git ref.
|
|
176
141
|
* Returns list of removed directories.
|
|
@@ -7,9 +7,13 @@ import buildSkillsCommand from "./skills.js";
|
|
|
7
7
|
|
|
8
8
|
const TMP_ROOT = join(tmpdir(), `pragma-setup-cmd-${Date.now()}`);
|
|
9
9
|
|
|
10
|
+
/** One source per skill directory, matching the loader granularity. */
|
|
10
11
|
const TEST_SOURCES = [
|
|
11
12
|
{
|
|
12
|
-
dir: join(
|
|
13
|
+
dir: join(
|
|
14
|
+
TMP_ROOT,
|
|
15
|
+
"node_modules/@canonical/design-system/skills/design-audit",
|
|
16
|
+
),
|
|
13
17
|
packageName: "@canonical/design-system",
|
|
14
18
|
},
|
|
15
19
|
];
|
|
@@ -21,7 +25,7 @@ vi.mock("../../skill/helpers/resolveSkillSources.js", () => ({
|
|
|
21
25
|
beforeAll(() => {
|
|
22
26
|
mkdirSync(TMP_ROOT, { recursive: true });
|
|
23
27
|
|
|
24
|
-
const dir =
|
|
28
|
+
const dir = TEST_SOURCES.at(0)?.dir ?? "";
|
|
25
29
|
mkdirSync(dir, { recursive: true });
|
|
26
30
|
writeFileSync(
|
|
27
31
|
join(dir, "SKILL.md"),
|
|
@@ -2,9 +2,11 @@ import {
|
|
|
2
2
|
type CommandDefinition,
|
|
3
3
|
type CommandResult,
|
|
4
4
|
createOutputResult,
|
|
5
|
+
runGeneratorTask,
|
|
5
6
|
} from "@canonical/cli-core";
|
|
6
7
|
import { detectHarnesses } from "@canonical/harnesses";
|
|
7
|
-
import { collectEffects
|
|
8
|
+
import { collectEffects } from "@canonical/task";
|
|
9
|
+
import { runTask } from "@canonical/task/node";
|
|
8
10
|
import { PragmaError } from "#error";
|
|
9
11
|
import type { PragmaContext } from "../../shared/context.js";
|
|
10
12
|
import { selectFormatter } from "../../shared/formatters.js";
|
|
@@ -94,7 +96,9 @@ export default function buildSkillsCommand(): CommandDefinition {
|
|
|
94
96
|
});
|
|
95
97
|
}
|
|
96
98
|
|
|
97
|
-
|
|
99
|
+
// Run the symlink task through the shared execution core, like the
|
|
100
|
+
// other setup commands.
|
|
101
|
+
const result = await runGeneratorTask(task);
|
|
98
102
|
|
|
99
103
|
const output: SetupSkillsOutput = { result, dryRun: false };
|
|
100
104
|
|
|
@@ -1,33 +1,44 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { answerPromptWithDefaults } from "@canonical/cli-core";
|
|
2
|
+
import { pure, type Task, writeFile } from "@canonical/task";
|
|
2
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
-
|
|
4
|
-
const { createInterfaceMock, dryRunMock, runTaskMock, runUndoMock } =
|
|
5
|
-
vi.hoisted(() => ({
|
|
6
|
-
createInterfaceMock: vi.fn(),
|
|
7
|
-
dryRunMock: vi.fn(() => ({ effects: [] })),
|
|
8
|
-
runTaskMock: vi.fn(),
|
|
9
|
-
runUndoMock: vi.fn(),
|
|
10
|
-
}));
|
|
11
|
-
|
|
12
|
-
async function loadRunSetupTask() {
|
|
13
|
-
vi.resetModules();
|
|
14
|
-
vi.doMock("node:readline", () => ({
|
|
15
|
-
createInterface: createInterfaceMock,
|
|
16
|
-
}));
|
|
17
|
-
vi.doMock("@canonical/task", async (importOriginal) => {
|
|
18
|
-
const actual = await importOriginal<typeof import("@canonical/task")>();
|
|
19
|
-
return {
|
|
20
|
-
...actual,
|
|
21
|
-
dryRun: dryRunMock,
|
|
22
|
-
runTask: runTaskMock,
|
|
23
|
-
runUndo: runUndoMock,
|
|
24
|
-
};
|
|
25
|
-
});
|
|
26
|
-
return (await import("./runSetupTask.js")).default;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
4
|
import runSetupTask from "./runSetupTask.js";
|
|
30
5
|
|
|
6
|
+
const {
|
|
7
|
+
runGeneratorTaskMock,
|
|
8
|
+
runUndoMock,
|
|
9
|
+
sessionAnswerMock,
|
|
10
|
+
sessionDisposeMock,
|
|
11
|
+
} = vi.hoisted(() => ({
|
|
12
|
+
runGeneratorTaskMock: vi.fn(),
|
|
13
|
+
runUndoMock: vi.fn(),
|
|
14
|
+
sessionAnswerMock: vi.fn(),
|
|
15
|
+
sessionDisposeMock: vi.fn(),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
// runSetupTask orchestrates: dry-run/undo use real formatting and runUndo,
|
|
19
|
+
// while production runs through the shared execution core. Mock the core,
|
|
20
|
+
// runUndo, and the interactive prompt session, so the wiring and lifecycle
|
|
21
|
+
// stay under test with real effect formatting.
|
|
22
|
+
vi.mock("@canonical/cli-core", async (importOriginal) => ({
|
|
23
|
+
...(await importOriginal<typeof import("@canonical/cli-core")>()),
|
|
24
|
+
runGeneratorTask: runGeneratorTaskMock,
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
vi.mock("@canonical/task/node", async (importOriginal) => ({
|
|
28
|
+
...(await importOriginal<typeof import("@canonical/task/node")>()),
|
|
29
|
+
runUndo: runUndoMock,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
vi.mock("../../shared/createInteractivePromptSession.js", () => ({
|
|
33
|
+
default: () => ({
|
|
34
|
+
answerPrompt: sessionAnswerMock,
|
|
35
|
+
wasInterrupted: () => false,
|
|
36
|
+
dispose: sessionDisposeMock,
|
|
37
|
+
}),
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
const succeed = async (): Promise<undefined> => undefined;
|
|
41
|
+
|
|
31
42
|
describe("runSetupTask", () => {
|
|
32
43
|
let stderrSpy: ReturnType<typeof vi.spyOn>;
|
|
33
44
|
|
|
@@ -40,13 +51,16 @@ describe("runSetupTask", () => {
|
|
|
40
51
|
|
|
41
52
|
afterEach(() => {
|
|
42
53
|
stderrSpy.mockRestore();
|
|
43
|
-
vi.doUnmock("node:readline");
|
|
44
|
-
vi.doUnmock("@canonical/task");
|
|
45
54
|
});
|
|
46
55
|
|
|
56
|
+
// ===========================================================================
|
|
57
|
+
// Dry-run mode
|
|
58
|
+
// ===========================================================================
|
|
59
|
+
|
|
47
60
|
it("returns output result in dry-run mode", async () => {
|
|
48
|
-
const
|
|
49
|
-
|
|
61
|
+
const result = await runSetupTask(writeFile("/tmp/test.txt", "hello"), {
|
|
62
|
+
dryRun: true,
|
|
63
|
+
});
|
|
50
64
|
|
|
51
65
|
expect(result.tag).toBe("output");
|
|
52
66
|
if (result.tag === "output") {
|
|
@@ -54,176 +68,152 @@ describe("runSetupTask", () => {
|
|
|
54
68
|
expect(text).toContain("Dry run");
|
|
55
69
|
expect(text).toContain("/tmp/test.txt");
|
|
56
70
|
}
|
|
71
|
+
expect(runGeneratorTaskMock).not.toHaveBeenCalled();
|
|
57
72
|
});
|
|
58
73
|
|
|
59
74
|
it("returns json output in dry-run + json mode", async () => {
|
|
60
|
-
const
|
|
61
|
-
const result = await runSetupTask(task, {
|
|
75
|
+
const result = await runSetupTask(writeFile("/tmp/test.txt", "hello"), {
|
|
62
76
|
dryRun: true,
|
|
63
77
|
format: "json",
|
|
64
78
|
});
|
|
65
79
|
|
|
66
80
|
expect(result.tag).toBe("output");
|
|
67
81
|
if (result.tag === "output") {
|
|
68
|
-
const
|
|
69
|
-
const parsed = JSON.parse(text);
|
|
82
|
+
const parsed = JSON.parse(result.render.plain(result.value));
|
|
70
83
|
expect(Array.isArray(parsed)).toBe(true);
|
|
71
84
|
expect(parsed[0].action).toBe("WriteFile");
|
|
72
85
|
}
|
|
73
86
|
});
|
|
74
87
|
|
|
75
88
|
it("returns llm markdown in dry-run + llm mode", async () => {
|
|
76
|
-
const
|
|
77
|
-
|
|
89
|
+
const result = await runSetupTask(writeFile("/tmp/test.txt", "hello"), {
|
|
90
|
+
dryRun: true,
|
|
91
|
+
llm: true,
|
|
92
|
+
});
|
|
78
93
|
|
|
79
94
|
expect(result.tag).toBe("output");
|
|
80
95
|
if (result.tag === "output") {
|
|
81
|
-
|
|
82
|
-
expect(text).toContain("## Dry Run");
|
|
83
|
-
expect(text).toContain("WriteFile");
|
|
96
|
+
expect(result.render.plain(result.value)).toContain("Dry Run");
|
|
84
97
|
}
|
|
85
98
|
});
|
|
86
99
|
|
|
87
|
-
it("
|
|
88
|
-
const
|
|
89
|
-
|
|
100
|
+
it("shows the no-visible-effects message when a dry-run collects none", async () => {
|
|
101
|
+
const result = await runSetupTask(pure(undefined) as Task<void>, {
|
|
102
|
+
dryRun: true,
|
|
103
|
+
verbose: false,
|
|
104
|
+
});
|
|
90
105
|
|
|
91
|
-
expect(result.tag).toBe("
|
|
92
|
-
if (result.tag === "
|
|
93
|
-
expect(result.
|
|
106
|
+
expect(result.tag).toBe("output");
|
|
107
|
+
if (result.tag === "output") {
|
|
108
|
+
expect(result.render.plain(result.value)).toContain("no visible effects");
|
|
94
109
|
}
|
|
95
110
|
});
|
|
96
111
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
112
|
+
// ===========================================================================
|
|
113
|
+
// Production execution — through the shared execution core
|
|
114
|
+
// ===========================================================================
|
|
115
|
+
|
|
116
|
+
it("runs production through the shared execution core and returns exit 0", async () => {
|
|
117
|
+
runGeneratorTaskMock.mockImplementation(succeed);
|
|
100
118
|
|
|
119
|
+
const result = await runSetupTask(writeFile("/tmp/x", "y"), { yes: true });
|
|
120
|
+
|
|
121
|
+
expect(runGeneratorTaskMock).toHaveBeenCalledTimes(1);
|
|
101
122
|
expect(result.tag).toBe("exit");
|
|
102
|
-
if (result.tag === "exit")
|
|
103
|
-
expect(result.code).toBe(0);
|
|
104
|
-
}
|
|
123
|
+
if (result.tag === "exit") expect(result.code).toBe(0);
|
|
105
124
|
});
|
|
106
125
|
|
|
107
|
-
it("
|
|
108
|
-
|
|
109
|
-
|
|
126
|
+
it("wires the auto-confirm handler in yes mode", async () => {
|
|
127
|
+
runGeneratorTaskMock.mockImplementation(async (_task, options) => {
|
|
128
|
+
expect(options.promptHandler).toBe(answerPromptWithDefaults);
|
|
129
|
+
return succeed();
|
|
130
|
+
});
|
|
110
131
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const text = result.render.plain(result.value);
|
|
114
|
-
expect(text).toContain("no visible effects");
|
|
115
|
-
}
|
|
132
|
+
await runSetupTask(writeFile("/tmp/x", "y"), { yes: true });
|
|
133
|
+
expect(runGeneratorTaskMock).toHaveBeenCalledTimes(1);
|
|
116
134
|
});
|
|
117
135
|
|
|
118
|
-
it("
|
|
119
|
-
|
|
120
|
-
|
|
136
|
+
it("wires an interactive prompt session when not in yes mode and disposes it", async () => {
|
|
137
|
+
runGeneratorTaskMock.mockImplementation(async (_task, options) => {
|
|
138
|
+
expect(options.promptHandler).toBe(sessionAnswerMock);
|
|
139
|
+
expect(sessionDisposeMock).not.toHaveBeenCalled();
|
|
140
|
+
return succeed();
|
|
141
|
+
});
|
|
121
142
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
expect(stderrSpy).toHaveBeenCalledWith("Nothing to undo.\n");
|
|
143
|
+
await runSetupTask(writeFile("/tmp/x", "y"), {});
|
|
144
|
+
expect(runGeneratorTaskMock).toHaveBeenCalledTimes(1);
|
|
145
|
+
expect(sessionDisposeMock).toHaveBeenCalledTimes(1);
|
|
127
146
|
});
|
|
128
147
|
|
|
129
|
-
it("
|
|
130
|
-
|
|
131
|
-
expect(
|
|
132
|
-
await options.promptHandler({
|
|
133
|
-
_tag: "Prompt",
|
|
134
|
-
question: { type: "confirm", default: false, message: "Continue?" },
|
|
135
|
-
}),
|
|
136
|
-
).toBe(false);
|
|
137
|
-
expect(
|
|
138
|
-
await options.promptHandler({
|
|
139
|
-
_tag: "Prompt",
|
|
140
|
-
question: {
|
|
141
|
-
type: "select",
|
|
142
|
-
choices: [{ value: "zsh" }],
|
|
143
|
-
message: "Shell?",
|
|
144
|
-
},
|
|
145
|
-
}),
|
|
146
|
-
).toBe("zsh");
|
|
147
|
-
expect(
|
|
148
|
-
await options.promptHandler({
|
|
149
|
-
_tag: "Prompt",
|
|
150
|
-
question: { type: "multiselect", message: "Pick many" },
|
|
151
|
-
}),
|
|
152
|
-
).toEqual([]);
|
|
153
|
-
expect(
|
|
154
|
-
await options.promptHandler({
|
|
155
|
-
_tag: "Prompt",
|
|
156
|
-
question: { type: "input", message: "Name?" },
|
|
157
|
-
}),
|
|
158
|
-
).toBe("");
|
|
159
|
-
});
|
|
148
|
+
it("disposes the prompt session even when production execution fails", async () => {
|
|
149
|
+
runGeneratorTaskMock.mockRejectedValue(new Error("boom"));
|
|
160
150
|
|
|
161
|
-
const
|
|
162
|
-
const result = await runSetupTaskWithMocks({} as never, { yes: true });
|
|
151
|
+
const result = await runSetupTask(writeFile("/tmp/x", "y"), {});
|
|
163
152
|
|
|
164
153
|
expect(result.tag).toBe("exit");
|
|
165
|
-
|
|
166
|
-
expect(result.code).toBe(0);
|
|
167
|
-
}
|
|
154
|
+
expect(sessionDisposeMock).toHaveBeenCalledTimes(1);
|
|
168
155
|
});
|
|
169
156
|
|
|
170
|
-
it("
|
|
171
|
-
|
|
172
|
-
question: (_prompt: string, cb: (answer: string) => void) => cb(""),
|
|
173
|
-
close: vi.fn(),
|
|
174
|
-
});
|
|
175
|
-
runTaskMock.mockImplementation(async (_task, options) => {
|
|
176
|
-
expect(
|
|
177
|
-
await options.promptHandler({
|
|
178
|
-
_tag: "Prompt",
|
|
179
|
-
question: { type: "confirm", default: true, message: "Continue?" },
|
|
180
|
-
}),
|
|
181
|
-
).toBe(true);
|
|
157
|
+
it("suppresses debug logs unless verbose during production", async () => {
|
|
158
|
+
runGeneratorTaskMock.mockImplementation(async (_task, options) => {
|
|
182
159
|
options.onLog("debug", "hidden");
|
|
183
160
|
options.onLog("info", "shown");
|
|
161
|
+
return succeed();
|
|
184
162
|
});
|
|
185
163
|
|
|
186
|
-
|
|
187
|
-
const result = await runSetupTaskWithMocks({} as never, {});
|
|
164
|
+
await runSetupTask(writeFile("/tmp/x", "y"), { yes: true });
|
|
188
165
|
|
|
189
|
-
expect(result.tag).toBe("exit");
|
|
190
166
|
expect(stderrSpy).toHaveBeenCalledWith("shown\n");
|
|
191
167
|
expect(stderrSpy).not.toHaveBeenCalledWith("hidden\n");
|
|
192
168
|
});
|
|
193
169
|
|
|
194
|
-
it("
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
170
|
+
it("emits debug logs when verbose during production", async () => {
|
|
171
|
+
runGeneratorTaskMock.mockImplementation(async (_task, options) => {
|
|
172
|
+
options.onLog("debug", "shown-debug");
|
|
173
|
+
return succeed();
|
|
198
174
|
});
|
|
199
|
-
runTaskMock.mockImplementation(async (_task, options) => {
|
|
200
|
-
expect(
|
|
201
|
-
await options.promptHandler({
|
|
202
|
-
_tag: "Prompt",
|
|
203
|
-
question: { type: "confirm", default: false, message: "Continue?" },
|
|
204
|
-
}),
|
|
205
|
-
).toBe(false);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
const runSetupTaskWithMocks = await loadRunSetupTask();
|
|
209
|
-
const result = await runSetupTaskWithMocks({} as never, {});
|
|
210
175
|
|
|
211
|
-
|
|
176
|
+
await runSetupTask(writeFile("/tmp/x", "y"), { yes: true, verbose: true });
|
|
177
|
+
expect(stderrSpy).toHaveBeenCalledWith("shown-debug\n");
|
|
212
178
|
});
|
|
213
179
|
|
|
214
180
|
it("returns exit 1 when production execution fails", async () => {
|
|
215
|
-
|
|
181
|
+
runGeneratorTaskMock.mockRejectedValue(new Error("permission denied"));
|
|
216
182
|
|
|
217
|
-
const
|
|
218
|
-
const result = await runSetupTaskWithMocks({} as never, { yes: true });
|
|
183
|
+
const result = await runSetupTask(writeFile("/tmp/x", "y"), { yes: true });
|
|
219
184
|
|
|
220
185
|
expect(result.tag).toBe("exit");
|
|
221
|
-
if (result.tag === "exit")
|
|
222
|
-
expect(result.code).toBe(1);
|
|
223
|
-
}
|
|
186
|
+
if (result.tag === "exit") expect(result.code).toBe(1);
|
|
224
187
|
expect(stderrSpy).toHaveBeenCalledWith("Setup failed: permission denied\n");
|
|
225
188
|
});
|
|
226
189
|
|
|
190
|
+
it("stringifies a non-Error production rejection", async () => {
|
|
191
|
+
runGeneratorTaskMock.mockRejectedValue("raw failure");
|
|
192
|
+
|
|
193
|
+
const result = await runSetupTask(writeFile("/tmp/x", "y"), { yes: true });
|
|
194
|
+
|
|
195
|
+
expect(result.tag).toBe("exit");
|
|
196
|
+
if (result.tag === "exit") expect(result.code).toBe(1);
|
|
197
|
+
expect(stderrSpy).toHaveBeenCalledWith("Setup failed: raw failure\n");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// ===========================================================================
|
|
201
|
+
// Undo mode
|
|
202
|
+
// ===========================================================================
|
|
203
|
+
|
|
204
|
+
it("reports nothing to undo when no undos are collected", async () => {
|
|
205
|
+
runUndoMock.mockResolvedValue({ undoCount: 0 });
|
|
206
|
+
|
|
207
|
+
const result = await runSetupTask(writeFile("/tmp/x", "y"), {
|
|
208
|
+
undo: true,
|
|
209
|
+
yes: true,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
expect(result.tag).toBe("exit");
|
|
213
|
+
if (result.tag === "exit") expect(result.code).toBe(0);
|
|
214
|
+
expect(stderrSpy).toHaveBeenCalledWith("Nothing to undo.\n");
|
|
215
|
+
});
|
|
216
|
+
|
|
227
217
|
it("writes verbose undo logs and the singular completion message", async () => {
|
|
228
218
|
runUndoMock.mockImplementation(async (_task, options) => {
|
|
229
219
|
options.onLog("debug", "debug line");
|
|
@@ -231,8 +221,7 @@ describe("runSetupTask", () => {
|
|
|
231
221
|
return { undoCount: 1 };
|
|
232
222
|
});
|
|
233
223
|
|
|
234
|
-
const
|
|
235
|
-
const result = await runSetupTaskWithMocks({} as never, {
|
|
224
|
+
const result = await runSetupTask(writeFile("/tmp/x", "y"), {
|
|
236
225
|
undo: true,
|
|
237
226
|
yes: true,
|
|
238
227
|
verbose: true,
|
|
@@ -249,8 +238,7 @@ describe("runSetupTask", () => {
|
|
|
249
238
|
it("writes the plural undo completion message", async () => {
|
|
250
239
|
runUndoMock.mockResolvedValue({ undoCount: 2 });
|
|
251
240
|
|
|
252
|
-
const
|
|
253
|
-
const result = await runSetupTaskWithMocks({} as never, {
|
|
241
|
+
const result = await runSetupTask(writeFile("/tmp/x", "y"), {
|
|
254
242
|
undo: true,
|
|
255
243
|
yes: true,
|
|
256
244
|
});
|
|
@@ -264,16 +252,26 @@ describe("runSetupTask", () => {
|
|
|
264
252
|
it("returns exit 1 when undo fails", async () => {
|
|
265
253
|
runUndoMock.mockRejectedValue(new Error("cannot undo"));
|
|
266
254
|
|
|
267
|
-
const
|
|
268
|
-
const result = await runSetupTaskWithMocks({} as never, {
|
|
255
|
+
const result = await runSetupTask(writeFile("/tmp/x", "y"), {
|
|
269
256
|
undo: true,
|
|
270
257
|
yes: true,
|
|
271
258
|
});
|
|
272
259
|
|
|
273
260
|
expect(result.tag).toBe("exit");
|
|
274
|
-
if (result.tag === "exit")
|
|
275
|
-
expect(result.code).toBe(1);
|
|
276
|
-
}
|
|
261
|
+
if (result.tag === "exit") expect(result.code).toBe(1);
|
|
277
262
|
expect(stderrSpy).toHaveBeenCalledWith("Undo failed: cannot undo\n");
|
|
278
263
|
});
|
|
264
|
+
|
|
265
|
+
it("stringifies a non-Error undo rejection", async () => {
|
|
266
|
+
runUndoMock.mockRejectedValue("raw undo failure");
|
|
267
|
+
|
|
268
|
+
const result = await runSetupTask(writeFile("/tmp/x", "y"), {
|
|
269
|
+
undo: true,
|
|
270
|
+
yes: true,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
expect(result.tag).toBe("exit");
|
|
274
|
+
if (result.tag === "exit") expect(result.code).toBe(1);
|
|
275
|
+
expect(stderrSpy).toHaveBeenCalledWith("Undo failed: raw undo failure\n");
|
|
276
|
+
});
|
|
279
277
|
});
|