@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
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
mkdtempSync,
|
|
6
6
|
readFileSync,
|
|
7
7
|
rmSync,
|
|
8
|
+
symlinkSync,
|
|
8
9
|
writeFileSync,
|
|
9
10
|
} from "node:fs";
|
|
10
11
|
import type { createRequire } from "node:module";
|
|
@@ -13,19 +14,30 @@ import { join } from "node:path";
|
|
|
13
14
|
import { runTask } from "@canonical/task/node";
|
|
14
15
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
15
16
|
import { VERSION } from "../../constants.js";
|
|
16
|
-
import type {
|
|
17
|
+
import type {
|
|
18
|
+
ConfigLayers,
|
|
19
|
+
PackDeclaration,
|
|
20
|
+
} from "../../kernel/config/types.js";
|
|
17
21
|
import { PragmaError } from "../../kernel/error/PragmaError.js";
|
|
18
22
|
import { executeVerb } from "../../kernel/project/cli/dispatch.js";
|
|
19
23
|
import { bootRuntime } from "../../kernel/runtime/boot.js";
|
|
20
24
|
import { createQueryFacade } from "../../kernel/runtime/facade.js";
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
|
|
25
|
+
import { readManifest } from "../../kernel/runtime/graphpack/manifest.js";
|
|
26
|
+
import {
|
|
27
|
+
activePackPath,
|
|
28
|
+
packDir,
|
|
29
|
+
readActivePack,
|
|
30
|
+
} from "../../kernel/runtime/paths.js";
|
|
31
|
+
import {
|
|
32
|
+
detectPrefixClashes,
|
|
33
|
+
resolvePackageJson,
|
|
34
|
+
} from "../../kernel/runtime/refs/resolve.js";
|
|
24
35
|
import { createLazyStore } from "../../kernel/runtime/store.js";
|
|
25
36
|
import type { GlobalFlags, PragmaRuntime } from "../../kernel/runtime/types.js";
|
|
26
37
|
import type { VerbSpec } from "../../kernel/spec/types.js";
|
|
27
38
|
import { projectMcp } from "../../testing/helpers/projectMcp.js";
|
|
28
39
|
import { discoverSkills } from "../skill/discover.js";
|
|
40
|
+
import { collectStatus } from "./collectStatus.js";
|
|
29
41
|
import { sourcesModule } from "./index.js";
|
|
30
42
|
import { buildUpdateTask } from "./runUpdate.js";
|
|
31
43
|
|
|
@@ -53,18 +65,17 @@ const tmp = (prefix: string): string => {
|
|
|
53
65
|
return dir;
|
|
54
66
|
};
|
|
55
67
|
|
|
56
|
-
/** A runtime whose config is the given
|
|
57
|
-
function runtimeFor(cwd: string,
|
|
68
|
+
/** A runtime whose config is the given pack list (no config files needed). */
|
|
69
|
+
function runtimeFor(cwd: string, packs: PackDeclaration[]): PragmaRuntime {
|
|
58
70
|
const layers: ConfigLayers = {
|
|
59
|
-
config: { channel: "normal",
|
|
71
|
+
config: { channel: "normal", packs },
|
|
60
72
|
origins: {
|
|
61
73
|
tier: "default",
|
|
62
74
|
channel: "default",
|
|
63
75
|
detail: "default",
|
|
64
|
-
|
|
76
|
+
packs: "project",
|
|
65
77
|
stories: "default",
|
|
66
78
|
prefixes: "default",
|
|
67
|
-
prompts: "default",
|
|
68
79
|
},
|
|
69
80
|
global: { path: "/nonexistent", exists: false },
|
|
70
81
|
project: { exists: false },
|
|
@@ -96,30 +107,24 @@ function filePackage(): string {
|
|
|
96
107
|
return pkg;
|
|
97
108
|
}
|
|
98
109
|
|
|
99
|
-
describe("sources
|
|
100
|
-
it("file source: builds
|
|
110
|
+
describe("sources update round-trip (PROTECTED)", () => {
|
|
111
|
+
it("file source: builds and points the project at the built pack", async () => {
|
|
101
112
|
const pkg = filePackage();
|
|
102
113
|
const cwd = tmp("pragma-proj-");
|
|
103
114
|
const runtime = runtimeFor(cwd, [
|
|
104
115
|
{ name: "pkg-a", source: `file://${pkg}` },
|
|
105
116
|
]);
|
|
106
117
|
|
|
107
|
-
const result = await runTask(await buildUpdateTask(runtime
|
|
118
|
+
const result = await runTask(await buildUpdateTask(runtime));
|
|
108
119
|
expect(result.contentHash).toMatch(/^[0-9a-f]{64}$/);
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
expect(
|
|
114
|
-
expect(lock?.contentHash).toBe(result.contentHash);
|
|
115
|
-
|
|
116
|
-
const firstBytes = readFileSync(lockPath(cwd), "utf-8");
|
|
117
|
-
// A --frozen re-run reproduces the locked state byte-for-byte.
|
|
118
|
-
await runTask(await buildUpdateTask(runtime, true));
|
|
119
|
-
expect(readFileSync(lockPath(cwd), "utf-8")).toBe(firstBytes);
|
|
120
|
+
expect(result.packs).toEqual([
|
|
121
|
+
{ name: "pkg-a", resolved: pkg, sourceCount: 1, storyCount: 0 },
|
|
122
|
+
]);
|
|
123
|
+
// The pointer the next boot reads names exactly the pack just built.
|
|
124
|
+
expect(readActivePack(cwd)).toBe(result.contentHash);
|
|
120
125
|
});
|
|
121
126
|
|
|
122
|
-
it("git source: resolves
|
|
127
|
+
it("git source: resolves the ref to a commit SHA", async () => {
|
|
123
128
|
const repo = tmp("pragma-repo-");
|
|
124
129
|
const git = (args: string[]) =>
|
|
125
130
|
execFileSync("git", args, {
|
|
@@ -144,25 +149,83 @@ describe("sources lock round-trip (PROTECTED)", () => {
|
|
|
144
149
|
{ name: "pkg-git", source: `git+file://${repo}#main` },
|
|
145
150
|
]);
|
|
146
151
|
|
|
147
|
-
await runTask(await buildUpdateTask(runtime
|
|
148
|
-
const
|
|
149
|
-
expect(
|
|
152
|
+
const result = await runTask(await buildUpdateTask(runtime));
|
|
153
|
+
const sha = result.packs.at(0)?.resolved;
|
|
154
|
+
expect(sha).toMatch(/^[0-9a-f]{40}$/);
|
|
155
|
+
expect(readActivePack(cwd)).toBe(result.contentHash);
|
|
156
|
+
// The revision reaches the manifest's provenance label, which is the only
|
|
157
|
+
// place `sources status` and `doctor` can read it from now that no lock
|
|
158
|
+
// records it. Same `<name>@<kind>:<resolved>` shape the bundler writes.
|
|
159
|
+
expect(readManifest(packDir(result.contentHash))?.sourceRef).toBe(
|
|
160
|
+
`pkg-git@git:${sha}`,
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("a re-run over an unchanged source leaves the pointer byte-identical (L1)", async () => {
|
|
165
|
+
// On base the lock file carried a `resolvedAt` timestamp, so a no-op re-run
|
|
166
|
+
// rewrote it and dirtied the tree. The pointer holds only the content hash,
|
|
167
|
+
// which is a pure function of the sources — so this is now true by
|
|
168
|
+
// construction. The guard stays because it is the property users cared
|
|
169
|
+
// about: repeating an update changes nothing when nothing changed.
|
|
170
|
+
const pkg = filePackage();
|
|
171
|
+
const cwd = tmp("pragma-proj-");
|
|
172
|
+
const runtime = runtimeFor(cwd, [
|
|
173
|
+
{ name: "pkg-a", source: `file://${pkg}` },
|
|
174
|
+
]);
|
|
175
|
+
await runTask(await buildUpdateTask(runtime));
|
|
176
|
+
const firstBytes = readFileSync(activePackPath(cwd), "utf-8");
|
|
177
|
+
await runTask(await buildUpdateTask(runtime));
|
|
178
|
+
expect(readFileSync(activePackPath(cwd), "utf-8")).toBe(firstBytes);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("follows a symlinked .ttl into the build (L6)", async () => {
|
|
182
|
+
// pnpm / workspace trees symlink their sources; Dirent.isFile is false for a
|
|
183
|
+
// symlink, so on base the only `.ttl` was skipped → 0 sources → the empty
|
|
184
|
+
// build. Following the link ingests it and the build succeeds.
|
|
185
|
+
const pkg = tmp("pragma-symlink-pkg-");
|
|
186
|
+
mkdirSync(join(pkg, "definitions"), { recursive: true });
|
|
187
|
+
const realTtl = join(pkg, "real.ttl");
|
|
188
|
+
writeFileSync(realTtl, TTL);
|
|
189
|
+
symlinkSync(realTtl, join(pkg, "definitions", "linked.ttl"));
|
|
190
|
+
const cwd = tmp("pragma-proj-");
|
|
191
|
+
const runtime = runtimeFor(cwd, [
|
|
192
|
+
{ name: "pkg-a", source: `file://${pkg}` },
|
|
193
|
+
]);
|
|
194
|
+
|
|
195
|
+
const result = await runTask(await buildUpdateTask(runtime));
|
|
196
|
+
expect(result.contentHash).toMatch(/^[0-9a-f]{64}$/);
|
|
197
|
+
expect(readActivePack(cwd)).toBe(result.contentHash);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("does not recurse into a symlinked directory cycle (L6 safety)", async () => {
|
|
201
|
+
// The L6 fix follows symlinked FILES but must NOT recurse into a symlinked
|
|
202
|
+
// DIRECTORY: a link to an ancestor dir is a cycle that, if walked, recurses
|
|
203
|
+
// without bound → a stack-overflow RangeError (an INTERNAL "please report").
|
|
204
|
+
// The real `.ttl` is still ingested and the build completes.
|
|
205
|
+
const pkg = tmp("pragma-symlink-cycle-");
|
|
206
|
+
mkdirSync(join(pkg, "definitions"), { recursive: true });
|
|
207
|
+
writeFileSync(join(pkg, "definitions", "widget.ttl"), TTL);
|
|
208
|
+
// `definitions/loop` → `definitions` (its own parent): a symlink cycle.
|
|
209
|
+
symlinkSync(join(pkg, "definitions"), join(pkg, "definitions", "loop"));
|
|
210
|
+
const cwd = tmp("pragma-proj-");
|
|
211
|
+
const runtime = runtimeFor(cwd, [
|
|
212
|
+
{ name: "pkg-a", source: `file://${pkg}` },
|
|
213
|
+
]);
|
|
150
214
|
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
expect(
|
|
215
|
+
const result = await runTask(await buildUpdateTask(runtime));
|
|
216
|
+
expect(result.contentHash).toMatch(/^[0-9a-f]{64}$/);
|
|
217
|
+
expect(readActivePack(cwd)).toBe(result.contentHash);
|
|
154
218
|
});
|
|
155
219
|
|
|
156
|
-
it("undo
|
|
220
|
+
it("undo removes the pointer when the project had none before", async () => {
|
|
157
221
|
const pkg = filePackage();
|
|
158
222
|
const cwd = tmp("pragma-proj-");
|
|
159
223
|
const runtime = runtimeFor(cwd, [
|
|
160
224
|
{ name: "pkg-a", source: `file://${pkg}` },
|
|
161
225
|
]);
|
|
162
226
|
const { runUndo } = await import("@canonical/task/node");
|
|
163
|
-
await runUndo(await buildUpdateTask(runtime
|
|
164
|
-
|
|
165
|
-
expect(readLock(cwd)).toBeUndefined();
|
|
227
|
+
await runUndo(await buildUpdateTask(runtime));
|
|
228
|
+
expect(readActivePack(cwd)).toBeUndefined();
|
|
166
229
|
});
|
|
167
230
|
});
|
|
168
231
|
|
|
@@ -178,7 +241,7 @@ describe("sources update — package-declared prefixes (M1)", () => {
|
|
|
178
241
|
{ name: "pkg-a", source: `file://${pkg}` },
|
|
179
242
|
]);
|
|
180
243
|
|
|
181
|
-
const result = await runTask(await buildUpdateTask(runtime
|
|
244
|
+
const result = await runTask(await buildUpdateTask(runtime));
|
|
182
245
|
const index = JSON.parse(
|
|
183
246
|
readFileSync(join(packDir(result.contentHash), "index.json"), "utf-8"),
|
|
184
247
|
) as {
|
|
@@ -200,6 +263,114 @@ describe("sources update — package-declared prefixes (M1)", () => {
|
|
|
200
263
|
});
|
|
201
264
|
});
|
|
202
265
|
|
|
266
|
+
describe("sources update — refuses an empty store (A4)", () => {
|
|
267
|
+
/** A local package that ships NO `.ttl` (an empty definitions directory). */
|
|
268
|
+
function emptyPackage(): string {
|
|
269
|
+
const pkg = tmp("pragma-empty-pkg-");
|
|
270
|
+
mkdirSync(join(pkg, "definitions"), { recursive: true });
|
|
271
|
+
return pkg;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
it("a package with no .ttl is refused, and no pointer is written", async () => {
|
|
275
|
+
// On base this builds a 0-triple pack whose empty data.nq fails the
|
|
276
|
+
// completeness gate — so the "successful" update boots to a PERMANENT
|
|
277
|
+
// STORE_UNAVAILABLE loop. The fix refuses before writing the pointer.
|
|
278
|
+
const pkg = emptyPackage();
|
|
279
|
+
const cwd = tmp("pragma-proj-");
|
|
280
|
+
const runtime = runtimeFor(cwd, [
|
|
281
|
+
{ name: "pkg-a", source: `file://${pkg}` },
|
|
282
|
+
]);
|
|
283
|
+
|
|
284
|
+
await expect(buildUpdateTask(runtime)).rejects.toMatchObject({
|
|
285
|
+
code: "CONFIG_ERROR",
|
|
286
|
+
});
|
|
287
|
+
// No pointer → the embedded fallback / prior state survives, no boot loop.
|
|
288
|
+
expect(readActivePack(cwd)).toBeUndefined();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("no configured packs is refused rather than pointing at an empty pack", async () => {
|
|
292
|
+
const cwd = tmp("pragma-proj-");
|
|
293
|
+
const runtime = runtimeFor(cwd, []);
|
|
294
|
+
await expect(buildUpdateTask(runtime)).rejects.toMatchObject({
|
|
295
|
+
code: "CONFIG_ERROR",
|
|
296
|
+
});
|
|
297
|
+
expect(readActivePack(cwd)).toBeUndefined();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("a comment-only .ttl (0 triples) is refused too", async () => {
|
|
301
|
+
const pkg = tmp("pragma-noTriples-pkg-");
|
|
302
|
+
mkdirSync(join(pkg, "definitions"), { recursive: true });
|
|
303
|
+
// Parses fine, but yields no triples — the post-build 0-triple guard.
|
|
304
|
+
writeFileSync(
|
|
305
|
+
join(pkg, "definitions", "empty.ttl"),
|
|
306
|
+
"# only a comment, no triples\n",
|
|
307
|
+
);
|
|
308
|
+
const cwd = tmp("pragma-proj-");
|
|
309
|
+
const runtime = runtimeFor(cwd, [
|
|
310
|
+
{ name: "pkg-a", source: `file://${pkg}` },
|
|
311
|
+
]);
|
|
312
|
+
await expect(buildUpdateTask(runtime)).rejects.toMatchObject({
|
|
313
|
+
code: "CONFIG_ERROR",
|
|
314
|
+
});
|
|
315
|
+
expect(readActivePack(cwd)).toBeUndefined();
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe("sources update — conflicting @prefix detection (A5)", () => {
|
|
320
|
+
it("flags a label bound to two different IRIs across packages", () => {
|
|
321
|
+
const clashes = detectPrefixClashes([
|
|
322
|
+
{ content: "@prefix ex: <https://a.test/#> .\nex:One a ex:T ." },
|
|
323
|
+
{ content: "@prefix ex: <https://b.test/#> .\nex:Two a ex:T ." },
|
|
324
|
+
]);
|
|
325
|
+
expect(clashes).toHaveLength(1);
|
|
326
|
+
expect(clashes.at(0)?.label).toBe("ex");
|
|
327
|
+
expect(clashes.at(0)?.iris).toEqual([
|
|
328
|
+
"https://a.test/#",
|
|
329
|
+
"https://b.test/#",
|
|
330
|
+
]);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("does NOT flag harmless same-label/same-IRI redeclarations", () => {
|
|
334
|
+
const clashes = detectPrefixClashes([
|
|
335
|
+
{ content: "@prefix ex: <https://a.test/#> .\nex:One a ex:T ." },
|
|
336
|
+
{ content: "@prefix ex: <https://a.test/#> .\nex:Two a ex:T ." },
|
|
337
|
+
]);
|
|
338
|
+
expect(clashes).toEqual([]);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("warns loudly on a cross-package prefix clash during update", async () => {
|
|
342
|
+
const owl = "@prefix owl: <http://www.w3.org/2002/07/owl#> .\n";
|
|
343
|
+
const pkgA = tmp("pragma-clash-a-");
|
|
344
|
+
mkdirSync(join(pkgA, "definitions"), { recursive: true });
|
|
345
|
+
writeFileSync(
|
|
346
|
+
join(pkgA, "definitions", "a.ttl"),
|
|
347
|
+
`@prefix ex: <https://a.test/#> .\n${owl}ex:Thing a owl:Class .\nex:one a ex:Thing .\n`,
|
|
348
|
+
);
|
|
349
|
+
const pkgB = tmp("pragma-clash-b-");
|
|
350
|
+
mkdirSync(join(pkgB, "definitions"), { recursive: true });
|
|
351
|
+
writeFileSync(
|
|
352
|
+
join(pkgB, "definitions", "b.ttl"),
|
|
353
|
+
`@prefix ex: <https://b.test/#> .\n${owl}ex:Widget a owl:Class .\nex:two a ex:Widget .\n`,
|
|
354
|
+
);
|
|
355
|
+
const cwd = tmp("pragma-proj-");
|
|
356
|
+
const reports: string[] = [];
|
|
357
|
+
const runtime: PragmaRuntime = {
|
|
358
|
+
...runtimeFor(cwd, [
|
|
359
|
+
{ name: "pkg-a", source: `file://${pkgA}` },
|
|
360
|
+
{ name: "pkg-b", source: `file://${pkgB}` },
|
|
361
|
+
]),
|
|
362
|
+
report: (message: string) => reports.push(message),
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
await runTask(await buildUpdateTask(runtime));
|
|
366
|
+
expect(
|
|
367
|
+
reports.some(
|
|
368
|
+
(r) => r.includes('Prefix "ex:"') && r.includes("conflicting"),
|
|
369
|
+
),
|
|
370
|
+
).toBe(true);
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
203
374
|
describe("sources update — network-free preview (M2)", () => {
|
|
204
375
|
const updateVerb = sourcesModule.verbs[1] as VerbSpec;
|
|
205
376
|
const DRY_RUN = { dryRun: true, undo: false, yes: false };
|
|
@@ -218,11 +389,12 @@ describe("sources update — network-free preview (M2)", () => {
|
|
|
218
389
|
|
|
219
390
|
// A successful plan — the unreachable clone was never attempted.
|
|
220
391
|
expect(outcome.exitCode).toBe(0);
|
|
221
|
-
expect(outcome.stdout).toContain("Resolve and build 1
|
|
392
|
+
expect(outcome.stdout).toContain("Resolve and build 1 pack(s)");
|
|
222
393
|
expect(outcome.stdout).toContain(UNREACHABLE);
|
|
223
|
-
// The
|
|
224
|
-
|
|
225
|
-
expect(
|
|
394
|
+
// The project mutation is previewed, not performed: the plan names the
|
|
395
|
+
// exact pointer file a real run would write, and no pointer landed.
|
|
396
|
+
expect(outcome.stdout).toContain(activePackPath(cwd));
|
|
397
|
+
expect(readActivePack(cwd)).toBeUndefined();
|
|
226
398
|
// The store was never even asked for.
|
|
227
399
|
expect(runtime.store.booted).toBe(false);
|
|
228
400
|
});
|
|
@@ -231,7 +403,7 @@ describe("sources update — network-free preview (M2)", () => {
|
|
|
231
403
|
const cwd = tmp("pragma-proj-");
|
|
232
404
|
writeFileSync(
|
|
233
405
|
join(cwd, "pragma.config.ts"),
|
|
234
|
-
`export default {
|
|
406
|
+
`export default { packs: [{ name: "pkg-remote", source: "${UNREACHABLE}" }] };\n`,
|
|
235
407
|
);
|
|
236
408
|
|
|
237
409
|
const mcp = await projectMcp([sourcesModule], cwd);
|
|
@@ -245,25 +417,8 @@ describe("sources update — network-free preview (M2)", () => {
|
|
|
245
417
|
});
|
|
246
418
|
const plan = (envelope.data as { plan: string[] }).plan;
|
|
247
419
|
expect(plan.some((line) => line.includes(UNREACHABLE))).toBe(true);
|
|
248
|
-
// Plan-first withheld the write — no
|
|
249
|
-
expect(
|
|
250
|
-
});
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
describe("sources update — reproducible-or-fail (m5)", () => {
|
|
254
|
-
it("--frozen refuses a package that has no lock entry", async () => {
|
|
255
|
-
const pkg = filePackage();
|
|
256
|
-
const cwd = tmp("pragma-proj-");
|
|
257
|
-
const runtime = runtimeFor(cwd, [
|
|
258
|
-
{ name: "pkg-a", source: `file://${pkg}` },
|
|
259
|
-
]);
|
|
260
|
-
|
|
261
|
-
// No prior lock → nothing to reproduce → refuse rather than silently
|
|
262
|
-
// advance. The throw is raised during setup, before any Task is returned.
|
|
263
|
-
await expect(buildUpdateTask(runtime, true)).rejects.toMatchObject({
|
|
264
|
-
code: "CONFIG_ERROR",
|
|
265
|
-
});
|
|
266
|
-
expect(readLock(cwd)).toBeUndefined();
|
|
420
|
+
// Plan-first withheld the write — no pointer landed.
|
|
421
|
+
expect(readActivePack(cwd)).toBeUndefined();
|
|
267
422
|
});
|
|
268
423
|
});
|
|
269
424
|
|
|
@@ -331,7 +486,7 @@ describe("sources update — data-failure classification (U6)", () => {
|
|
|
331
486
|
|
|
332
487
|
let caught: unknown;
|
|
333
488
|
try {
|
|
334
|
-
await buildUpdateTask(runtime
|
|
489
|
+
await buildUpdateTask(runtime);
|
|
335
490
|
} catch (error) {
|
|
336
491
|
caught = error;
|
|
337
492
|
}
|
|
@@ -349,8 +504,8 @@ describe("sources update — data-failure classification (U6)", () => {
|
|
|
349
504
|
expect(err.recovery?.message ?? "").not.toContain("report this issue");
|
|
350
505
|
// The recovery points the user at a runnable, useful next step.
|
|
351
506
|
expect(err.recovery?.cli).toBe("pragma sources update --verbose");
|
|
352
|
-
// Nothing was
|
|
353
|
-
expect(
|
|
507
|
+
// Nothing was pointed at on failure.
|
|
508
|
+
expect(readActivePack(cwd)).toBeUndefined();
|
|
354
509
|
});
|
|
355
510
|
|
|
356
511
|
it("classifies a git clone failure, naming the package (not INTERNAL)", async () => {
|
|
@@ -367,7 +522,7 @@ describe("sources update — data-failure classification (U6)", () => {
|
|
|
367
522
|
|
|
368
523
|
let caught: unknown;
|
|
369
524
|
try {
|
|
370
|
-
await buildUpdateTask(runtime
|
|
525
|
+
await buildUpdateTask(runtime);
|
|
371
526
|
} catch (error) {
|
|
372
527
|
caught = error;
|
|
373
528
|
}
|
|
@@ -376,7 +531,7 @@ describe("sources update — data-failure classification (U6)", () => {
|
|
|
376
531
|
expect(err.code).toBe("CONFIG_ERROR");
|
|
377
532
|
expect(err.message).toContain("pkg-remote");
|
|
378
533
|
expect(err.message).not.toContain("Internal error");
|
|
379
|
-
expect(
|
|
534
|
+
expect(readActivePack(cwd)).toBeUndefined();
|
|
380
535
|
});
|
|
381
536
|
|
|
382
537
|
it("names the SPECIFIC bad file among several good ones", async () => {
|
|
@@ -390,12 +545,12 @@ describe("sources update — data-failure classification (U6)", () => {
|
|
|
390
545
|
const cwd = tmp("pragma-proj-");
|
|
391
546
|
const runtime = runtimeFor(cwd, [{ name: "mix", source: `file://${pkg}` }]);
|
|
392
547
|
|
|
393
|
-
await expect(buildUpdateTask(runtime
|
|
548
|
+
await expect(buildUpdateTask(runtime)).rejects.toMatchObject({
|
|
394
549
|
code: "CONFIG_ERROR",
|
|
395
550
|
});
|
|
396
551
|
let caught: unknown;
|
|
397
552
|
try {
|
|
398
|
-
await buildUpdateTask(runtime
|
|
553
|
+
await buildUpdateTask(runtime);
|
|
399
554
|
} catch (error) {
|
|
400
555
|
caught = error;
|
|
401
556
|
}
|
|
@@ -427,9 +582,9 @@ describe("sources update — hidden files and --skip-invalid", () => {
|
|
|
427
582
|
{ name: "pkg-a", source: `file://${pkg}` },
|
|
428
583
|
]);
|
|
429
584
|
|
|
430
|
-
const result = await runTask(await buildUpdateTask(runtime
|
|
585
|
+
const result = await runTask(await buildUpdateTask(runtime));
|
|
431
586
|
expect(result.contentHash).toMatch(/^[0-9a-f]{64}$/);
|
|
432
|
-
expect(
|
|
587
|
+
expect(readActivePack(cwd)).toMatch(/^[0-9a-f]{64}$/);
|
|
433
588
|
});
|
|
434
589
|
|
|
435
590
|
it("without --skip-invalid, one malformed source fails the whole update", async () => {
|
|
@@ -438,10 +593,10 @@ describe("sources update — hidden files and --skip-invalid", () => {
|
|
|
438
593
|
const runtime = runtimeFor(cwd, [
|
|
439
594
|
{ name: "pkg-a", source: `file://${pkg}` },
|
|
440
595
|
]);
|
|
441
|
-
await expect(buildUpdateTask(runtime, false
|
|
596
|
+
await expect(buildUpdateTask(runtime, false)).rejects.toMatchObject({
|
|
442
597
|
code: "CONFIG_ERROR",
|
|
443
598
|
});
|
|
444
|
-
expect(
|
|
599
|
+
expect(readActivePack(cwd)).toBeUndefined();
|
|
445
600
|
});
|
|
446
601
|
|
|
447
602
|
it("with --skip-invalid, drops the bad source, warns loudly, and builds from the rest", async () => {
|
|
@@ -453,10 +608,10 @@ describe("sources update — hidden files and --skip-invalid", () => {
|
|
|
453
608
|
report: (message: string) => reports.push(message),
|
|
454
609
|
};
|
|
455
610
|
|
|
456
|
-
const result = await runTask(await buildUpdateTask(runtime,
|
|
611
|
+
const result = await runTask(await buildUpdateTask(runtime, true));
|
|
457
612
|
// Built from the good widget.ttl, not aborted.
|
|
458
613
|
expect(result.contentHash).toMatch(/^[0-9a-f]{64}$/);
|
|
459
|
-
expect(
|
|
614
|
+
expect(readActivePack(cwd)).toMatch(/^[0-9a-f]{64}$/);
|
|
460
615
|
// Loud per-source warning names the dropped file …
|
|
461
616
|
expect(
|
|
462
617
|
reports.some(
|
|
@@ -476,10 +631,10 @@ describe("sources update — hidden files and --skip-invalid", () => {
|
|
|
476
631
|
...runtimeFor(cwd, [{ name: "pkg-a", source: `file://${pkg}` }]),
|
|
477
632
|
report: () => {},
|
|
478
633
|
};
|
|
479
|
-
await expect(buildUpdateTask(runtime,
|
|
634
|
+
await expect(buildUpdateTask(runtime, true)).rejects.toMatchObject({
|
|
480
635
|
code: "CONFIG_ERROR",
|
|
481
636
|
});
|
|
482
|
-
expect(
|
|
637
|
+
expect(readActivePack(cwd)).toBeUndefined();
|
|
483
638
|
});
|
|
484
639
|
});
|
|
485
640
|
|
|
@@ -573,7 +728,7 @@ describe("sources update — installs package skills (U10)", () => {
|
|
|
573
728
|
{ name: "pkg-a", source: `file://${pkg}` },
|
|
574
729
|
]);
|
|
575
730
|
|
|
576
|
-
await runTask(await buildUpdateTask(runtime
|
|
731
|
+
await runTask(await buildUpdateTask(runtime));
|
|
577
732
|
|
|
578
733
|
// Installed as a symlink into $XDG_DATA_HOME/pragma/skills …
|
|
579
734
|
const linked = join(dataHome, "pragma", "skills", "foo");
|
|
@@ -595,7 +750,7 @@ describe("sources update — installs package skills (U10)", () => {
|
|
|
595
750
|
const runtime = runtimeFor(cwd, [
|
|
596
751
|
{ name: "pkg-a", source: `file://${pkg}` },
|
|
597
752
|
]);
|
|
598
|
-
await runTask(await buildUpdateTask(runtime
|
|
753
|
+
await runTask(await buildUpdateTask(runtime));
|
|
599
754
|
|
|
600
755
|
const shared = discoverSkills(cwd).filter((s) => s.name === "shared");
|
|
601
756
|
expect(shared).toHaveLength(1);
|
|
@@ -609,16 +764,42 @@ describe("sources update — installs package skills (U10)", () => {
|
|
|
609
764
|
{ name: "pkg-a", source: `file://${pkg}` },
|
|
610
765
|
]);
|
|
611
766
|
const { runUndo } = await import("@canonical/task/node");
|
|
612
|
-
await runUndo(await buildUpdateTask(runtime
|
|
767
|
+
await runUndo(await buildUpdateTask(runtime));
|
|
613
768
|
|
|
614
769
|
expect(existsSync(join(dataHome, "pragma", "skills", "bar"))).toBe(false);
|
|
615
770
|
});
|
|
616
771
|
});
|
|
617
772
|
|
|
773
|
+
describe("sources status — entityCount from the manifest (A10)", () => {
|
|
774
|
+
it("reports the count from the manifest even when index.json is unreadable", async () => {
|
|
775
|
+
// A pack of its OWN (unique TTL ⇒ unique content hash), so corrupting its
|
|
776
|
+
// cached index cannot reach any other test's pack in the shared cache.
|
|
777
|
+
const pkg = tmp("pragma-a10-pkg-");
|
|
778
|
+
mkdirSync(join(pkg, "definitions"), { recursive: true });
|
|
779
|
+
writeFileSync(
|
|
780
|
+
join(pkg, "definitions", "widget.ttl"),
|
|
781
|
+
`${TTL}ex:a10only a ex:Widget ; rdfs:label "A10" .\n`,
|
|
782
|
+
);
|
|
783
|
+
const cwd = tmp("pragma-proj-");
|
|
784
|
+
const runtime = runtimeFor(cwd, [
|
|
785
|
+
{ name: "pkg-a10", source: `file://${pkg}` },
|
|
786
|
+
]);
|
|
787
|
+
const result = await runTask(await buildUpdateTask(runtime));
|
|
788
|
+
|
|
789
|
+
// Corrupt index.json but keep it non-empty, so the pack still reads as
|
|
790
|
+
// complete: any path that PARSED it to count would now yield null.
|
|
791
|
+
writeFileSync(join(packDir(result.contentHash), "index.json"), "not json");
|
|
792
|
+
|
|
793
|
+
const status = await collectStatus(bootRuntime(flagsJson, cwd));
|
|
794
|
+
// Two abox individuals (ex:one, ex:a10only); ex:Widget is tbox, not counted.
|
|
795
|
+
expect(status.entityCount).toBe(2);
|
|
796
|
+
});
|
|
797
|
+
});
|
|
798
|
+
|
|
618
799
|
describe("sources status CLI-json == MCP tool (PROTECTED)", () => {
|
|
619
|
-
it("a
|
|
620
|
-
// Both surfaces boot from the SAME cwd + isolated config (default
|
|
621
|
-
//
|
|
800
|
+
it("a fresh install's status is byte-equal on both surfaces", async () => {
|
|
801
|
+
// Both surfaces boot from the SAME cwd + isolated config (default packs,
|
|
802
|
+
// nothing built), so the storeless status must be identical.
|
|
622
803
|
const cwd = tmp("pragma-proj-");
|
|
623
804
|
const cli = await executeVerb(
|
|
624
805
|
statusVerb,
|
|
@@ -634,9 +815,8 @@ describe("sources status CLI-json == MCP tool (PROTECTED)", () => {
|
|
|
634
815
|
|
|
635
816
|
expect(cliEnvelope).toEqual(mcpEnvelope);
|
|
636
817
|
expect(cliEnvelope.ok).toBe(true);
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
);
|
|
640
|
-
expect((cliEnvelope.data as { cached: boolean }).cached).toBe(false);
|
|
818
|
+
// Nothing built here, and the packs are the distribution's own — so the
|
|
819
|
+
// embedded snapshot is what answers reads, and status says exactly that.
|
|
820
|
+
expect((cliEnvelope.data as { store: string }).store).toBe("embedded");
|
|
641
821
|
});
|
|
642
822
|
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { type RenderStyle, styleFor } from "../../kernel/render/style.js";
|
|
3
|
+
import { renderSourcesStatusPlain } from "./status.render.js";
|
|
4
|
+
import type { SourcesStatusData } from "./types.js";
|
|
5
|
+
|
|
6
|
+
/** A project reading from its own built pack, with two configured sources. */
|
|
7
|
+
const BUILT: SourcesStatusData = {
|
|
8
|
+
cwd: "/repo",
|
|
9
|
+
store: "built",
|
|
10
|
+
contentHash: "abcdef012345aaaa",
|
|
11
|
+
sourceRef: "ds,extra",
|
|
12
|
+
builtAt: "2026-01-01",
|
|
13
|
+
entityCount: 42,
|
|
14
|
+
sources: [
|
|
15
|
+
{ name: "ds", ref: "git+https://example.test/ds.git#main" },
|
|
16
|
+
{ name: "extra", ref: "extra@1" },
|
|
17
|
+
],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** A sentinel styler — enabled, each color wrapped in a visible tag. */
|
|
21
|
+
const TAGGED: RenderStyle = {
|
|
22
|
+
enabled: true,
|
|
23
|
+
bold: (t) => `<b>${t}</b>`,
|
|
24
|
+
dim: (t) => `<d>${t}</d>`,
|
|
25
|
+
cyan: (t) => `<c>${t}</c>`,
|
|
26
|
+
green: (t) => `<g>${t}</g>`,
|
|
27
|
+
yellow: (t) => `<y>${t}</y>`,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
describe("renderSourcesStatusPlain", () => {
|
|
31
|
+
it("is byte-identical to the plain form when the styler is disabled", () => {
|
|
32
|
+
expect(renderSourcesStatusPlain(BUILT, styleFor(false))).toBe(
|
|
33
|
+
[
|
|
34
|
+
"Store: ready",
|
|
35
|
+
" pack: abcdef012345 — 42 entities, built 2026-01-01",
|
|
36
|
+
" from: ds,extra",
|
|
37
|
+
"",
|
|
38
|
+
"Sources:",
|
|
39
|
+
" ds git+https://example.test/ds.git#main",
|
|
40
|
+
" extra extra@1",
|
|
41
|
+
].join("\n"),
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("bolds the heading, aligns names, and dims refs on a TTY", () => {
|
|
46
|
+
const out = renderSourcesStatusPlain(BUILT, TAGGED);
|
|
47
|
+
const nameWidth = "extra".length;
|
|
48
|
+
expect(out).toContain("<b>Sources:</b>");
|
|
49
|
+
expect(out).toContain(
|
|
50
|
+
` ${"ds".padEnd(nameWidth)} <d>git+https://example.test/ds.git#main</d>`,
|
|
51
|
+
);
|
|
52
|
+
expect(out).toContain(` ${"extra".padEnd(nameWidth)} <d>extra@1</d>`);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("names the embedded snapshot AND the update that would replace it", () => {
|
|
56
|
+
// The embedded pack answers reads on a fresh install, but it IS a snapshot —
|
|
57
|
+
// rendering it as "ready" would read as "up to date", which it is not.
|
|
58
|
+
const out = renderSourcesStatusPlain(
|
|
59
|
+
{
|
|
60
|
+
...BUILT,
|
|
61
|
+
store: "embedded",
|
|
62
|
+
sourceRef: "@canonical/design-system@git:abc",
|
|
63
|
+
},
|
|
64
|
+
styleFor(false),
|
|
65
|
+
);
|
|
66
|
+
expect(out).toContain("Store: embedded snapshot");
|
|
67
|
+
expect(out).toContain("pragma sources update");
|
|
68
|
+
expect(out).toContain(" from: @canonical/design-system@git:abc");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("reports an unbuilt store with its recovery and no pack line", () => {
|
|
72
|
+
const out = renderSourcesStatusPlain(
|
|
73
|
+
{
|
|
74
|
+
...BUILT,
|
|
75
|
+
store: "unavailable",
|
|
76
|
+
contentHash: null,
|
|
77
|
+
sourceRef: null,
|
|
78
|
+
builtAt: null,
|
|
79
|
+
entityCount: null,
|
|
80
|
+
},
|
|
81
|
+
styleFor(false),
|
|
82
|
+
);
|
|
83
|
+
expect(out).toContain("Store: not built (run `pragma sources update`)");
|
|
84
|
+
expect(out).not.toContain(" pack: ");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("says so when no packs are configured", () => {
|
|
88
|
+
const out = renderSourcesStatusPlain(
|
|
89
|
+
{ ...BUILT, sources: [] },
|
|
90
|
+
styleFor(false),
|
|
91
|
+
);
|
|
92
|
+
expect(out).toContain(" (none configured)");
|
|
93
|
+
});
|
|
94
|
+
});
|