@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,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E4 (AV-231, Backlog E) — the interactive TTY journey. SCAFFOLD.
|
|
3
|
+
*
|
|
4
|
+
* No test in the suite ever drove a REAL terminal, so the interactive-cancel and
|
|
5
|
+
* no-freeze guarantees (H1–H3, C3/C4) were never exercised end to end. Confirming
|
|
6
|
+
* them needs a pseudo-terminal: spawn the compiled `pragma` under a PTY, send raw
|
|
7
|
+
* keystrokes (Ctrl-C = `\x03`, EOF = `\x04`), and observe the exit code + output.
|
|
8
|
+
*
|
|
9
|
+
* `node-pty` is NOT available in this environment (it is a native addon, absent
|
|
10
|
+
* from the dependency set — verified at authoring time), so the journey is
|
|
11
|
+
* scaffolded as `it.todo` behind a `node-pty` availability gate rather than
|
|
12
|
+
* blocking the P0 work. Each todo names the exact behaviour and the exit contract
|
|
13
|
+
* the CLI already promises (see `kernel/project/cli/dispatch.ts`):
|
|
14
|
+
*
|
|
15
|
+
* - H1: an at-prompt Ctrl-C is a DECLINE — a clean cancel, "Cancelled." on
|
|
16
|
+
* stderr, exit 0 (`EXIT.OK`).
|
|
17
|
+
* - H2: a Ctrl-C DURING execution is an INTERRUPT — aborts work underway,
|
|
18
|
+
* "Cancelled." on stderr, exit 130 (`EXIT.INTERRUPTED`, 128 + SIGINT).
|
|
19
|
+
* - H3: `create 2>/dev/null` must not freeze — interactivity gates on stderr
|
|
20
|
+
* being a TTY, so a redirected stderr picks the non-interactive strategy
|
|
21
|
+
* instead of mounting an invisible wizard that blocks on stdin.
|
|
22
|
+
* - C3: EOF (`\x04`) on a prompt resolves to a usage error, never a hang.
|
|
23
|
+
* - C4: an empty select (Enter on a no-default list) does not hang.
|
|
24
|
+
*
|
|
25
|
+
* TODO(AV-231/E4): when `node-pty` (or an equivalent PTY shim) is on the
|
|
26
|
+
* devDependency set, replace each `it.todo` with a PTY-driven `it` that spawns
|
|
27
|
+
* `dist/pragma create …` under a pty, writes the keystroke, and asserts the exit
|
|
28
|
+
* code + "Cancelled." line. The compiled binary is already provisioned for the
|
|
29
|
+
* spawn-e2e layer by `testing/perf/globalSetup.ts`.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { describe, it } from "vitest";
|
|
33
|
+
|
|
34
|
+
/** True when a real PTY driver is importable (kept async-free: a resolve probe). */
|
|
35
|
+
async function ptyAvailable(): Promise<boolean> {
|
|
36
|
+
try {
|
|
37
|
+
await import("node-pty" as string);
|
|
38
|
+
return true;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Authoring-time: `node-pty` is absent, so this resolves to `describe.skip` and
|
|
45
|
+
// the todos below stand in as the recorded, unblocked plan. The probe is awaited
|
|
46
|
+
// lazily so a future environment with node-pty flips the gate with no edit here.
|
|
47
|
+
const hasPty = await ptyAvailable();
|
|
48
|
+
const ptyGate = hasPty ? describe : describe.skip;
|
|
49
|
+
|
|
50
|
+
ptyGate(
|
|
51
|
+
"interactive TTY journey — cancel + no-freeze (E4, node-pty) [SCAFFOLD]",
|
|
52
|
+
() => {
|
|
53
|
+
// These become PTY-driven `it`s once node-pty is available (see module TODO).
|
|
54
|
+
it.todo(
|
|
55
|
+
"H1: Ctrl-C at a wizard prompt cancels cleanly (stderr 'Cancelled.', exit 0)",
|
|
56
|
+
);
|
|
57
|
+
it.todo(
|
|
58
|
+
"H2: Ctrl-C during execution aborts the run (stderr 'Cancelled.', exit 130)",
|
|
59
|
+
);
|
|
60
|
+
it.todo(
|
|
61
|
+
"H3: `create 2>/dev/null` does not freeze (non-TTY stderr -> non-interactive)",
|
|
62
|
+
);
|
|
63
|
+
it.todo("C3: EOF on a prompt resolves to a usage error, never a hang");
|
|
64
|
+
it.todo("C4: an empty select does not hang");
|
|
65
|
+
},
|
|
66
|
+
);
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E3 (AV-231, Backlog E) — the network-gated, NON-BLOCKING live-pack job.
|
|
3
|
+
*
|
|
4
|
+
* A handful of behaviours can only be confirmed against a REAL oxigraph store
|
|
5
|
+
* built from the REAL default pack: result ordering (A3/A9), which language a
|
|
6
|
+
* multilingual label resolves to under real store order (A7), ke truncation of
|
|
7
|
+
* long literals, and `buildIndex`'s `Promise.all` re-entrancy when the same pack
|
|
8
|
+
* is built concurrently. Those depend on the network (fetching the shipped pack)
|
|
9
|
+
* and on real build cost, so this suite is GATED OFF by default — it never runs
|
|
10
|
+
* (or flakes) in the ordinary `bun test` pass. Set `PRAGMA_LIVE_PACK=1` to run
|
|
11
|
+
* it (a dedicated, non-required CI job).
|
|
12
|
+
*
|
|
13
|
+
* The assertions here run against the VENDORED default pack as a faithful local
|
|
14
|
+
* proxy for the shipped one, so a maintainer who flips the gate gets real signal
|
|
15
|
+
* today. Each carries a TODO for wiring the ACTUAL network pack fetch — the one
|
|
16
|
+
* piece this environment cannot provide.
|
|
17
|
+
*
|
|
18
|
+
* TODO(AV-231/E3): resolve the real shipped default pack (git/npm source) here
|
|
19
|
+
* instead of the vendored TTL, so ordering/truncation are asserted against the
|
|
20
|
+
* bytes agents actually receive.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { describe, expect, it } from "vitest";
|
|
24
|
+
import { buildPack } from "../../kernel/runtime/graphpack/build.js";
|
|
25
|
+
import {
|
|
26
|
+
DEFAULT_PACK_PREFIXES,
|
|
27
|
+
DEFAULT_PACK_TTL,
|
|
28
|
+
} from "../fixtures/graph/defaultPack.js";
|
|
29
|
+
import {
|
|
30
|
+
bootFixtureRuntime,
|
|
31
|
+
type FixtureGraph,
|
|
32
|
+
} from "../helpers/fixtureGraph.js";
|
|
33
|
+
|
|
34
|
+
/** Non-blocking gate: skipped unless a maintainer opts in (or CI's live job). */
|
|
35
|
+
const liveGate = process.env.PRAGMA_LIVE_PACK ? describe : describe.skip;
|
|
36
|
+
|
|
37
|
+
liveGate(
|
|
38
|
+
"live-pack journey — real oxigraph ordering + build re-entrancy (E3)",
|
|
39
|
+
() => {
|
|
40
|
+
it("a SELECT ... ORDER BY returns a STABLE, deterministic ordering (A3/A9)", async () => {
|
|
41
|
+
const fixture = await bootFixtureRuntime({ ttl: DEFAULT_PACK_TTL });
|
|
42
|
+
try {
|
|
43
|
+
const query =
|
|
44
|
+
"SELECT ?name WHERE { ?c a ds:Component ; ds:name ?name } ORDER BY ?name";
|
|
45
|
+
const first = await fixture.runtime.query.sparql(query);
|
|
46
|
+
const second = await fixture.runtime.query.sparql(query);
|
|
47
|
+
const names = (result: typeof first): string[] =>
|
|
48
|
+
result.type === "select"
|
|
49
|
+
? result.bindings.map((binding) => String(binding.name))
|
|
50
|
+
: [];
|
|
51
|
+
// Repeated evaluation is identical, and ORDER BY is honoured.
|
|
52
|
+
expect(names(first)).toEqual(names(second));
|
|
53
|
+
expect(names(first)).toEqual(["Beta Badge", "Button", "Orphan Widget"]);
|
|
54
|
+
} finally {
|
|
55
|
+
await fixture.dispose();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("a multilingual label resolves to a SINGLE, tag-stripped value under real store order (A7)", async () => {
|
|
60
|
+
// TODO(AV-231/E3): against the live pack, pin the EXACT language the store
|
|
61
|
+
// yields first, so the index-label choice is a confirmed contract rather than
|
|
62
|
+
// the store-order accident it is today (see the A7 note in the E1 journey).
|
|
63
|
+
const fixture = await bootFixtureRuntime({ ttl: DEFAULT_PACK_TTL });
|
|
64
|
+
try {
|
|
65
|
+
const result = await fixture.runtime.query.sparql(
|
|
66
|
+
"SELECT ?l WHERE { ds:button rdfs:label ?l } ORDER BY ?l",
|
|
67
|
+
);
|
|
68
|
+
const labels =
|
|
69
|
+
result.type === "select"
|
|
70
|
+
? result.bindings.map((binding) => String(binding.l))
|
|
71
|
+
: [];
|
|
72
|
+
expect(labels).toEqual(["Bouton", "Button"]);
|
|
73
|
+
} finally {
|
|
74
|
+
await fixture.dispose();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("building the same sources CONCURRENTLY is re-entrant — one hash, no torn pack (buildIndex Promise.all)", async () => {
|
|
79
|
+
// buildIndex fans out four bulk SPARQL queries under Promise.all, and buildPack
|
|
80
|
+
// publishes atomically; N concurrent builds of identical sources must converge
|
|
81
|
+
// on ONE content hash with every pack complete (no half-written cache).
|
|
82
|
+
const inputs = [
|
|
83
|
+
{ path: "concurrent/pack.ttl", content: DEFAULT_PACK_TTL },
|
|
84
|
+
];
|
|
85
|
+
const options = {
|
|
86
|
+
name: "pragma",
|
|
87
|
+
version: "0.0.0-e3",
|
|
88
|
+
sourceRef: "e3-reentrancy",
|
|
89
|
+
prefixes: DEFAULT_PACK_PREFIXES,
|
|
90
|
+
};
|
|
91
|
+
const results = await Promise.all(
|
|
92
|
+
Array.from({ length: 5 }, () => buildPack(inputs, options)),
|
|
93
|
+
);
|
|
94
|
+
const hashes = new Set(results.map((result) => result.contentHash));
|
|
95
|
+
expect(hashes.size).toBe(1);
|
|
96
|
+
for (const result of results) {
|
|
97
|
+
expect(result.contentHash).toMatch(/^[0-9a-f]{64}$/);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("a long literal survives the build round-trip without silent ke truncation", async () => {
|
|
102
|
+
// TODO(AV-231/E3): confirm against the live pack, whose real summaries/guidelines
|
|
103
|
+
// are long enough to trip any downstream ke truncation (A7/A9 tail).
|
|
104
|
+
const longText = "x".repeat(20_000);
|
|
105
|
+
const ttl = `@prefix ds: <https://ds.canonical.com/> .
|
|
106
|
+
@prefix owl: <http://www.w3.org/2002/07/owl#> .
|
|
107
|
+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
|
108
|
+
ds:Component a owl:Class .
|
|
109
|
+
ds:summary a owl:DatatypeProperty ; rdfs:range xsd:string .
|
|
110
|
+
ds:big a ds:Component ; ds:summary "${longText}" .`;
|
|
111
|
+
let fixture: FixtureGraph | undefined;
|
|
112
|
+
try {
|
|
113
|
+
fixture = await bootFixtureRuntime({ ttl });
|
|
114
|
+
const result = await fixture.runtime.query.sparql(
|
|
115
|
+
"SELECT ?s WHERE { ds:big ds:summary ?s }",
|
|
116
|
+
);
|
|
117
|
+
const value =
|
|
118
|
+
result.type === "select"
|
|
119
|
+
? String(
|
|
120
|
+
(result.bindings.at(0) as { s?: string } | undefined)?.s ?? "",
|
|
121
|
+
)
|
|
122
|
+
: "";
|
|
123
|
+
expect(value.length).toBe(longText.length);
|
|
124
|
+
} finally {
|
|
125
|
+
await fixture?.dispose();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
},
|
|
129
|
+
);
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2 (AV-231, Backlog E) — the MCP lifecycle journey over a LONG-LIVED,
|
|
3
|
+
* in-process server.
|
|
4
|
+
*
|
|
5
|
+
* Fresh-runtime unit tests structurally cannot catch the MCP memoization bugs:
|
|
6
|
+
* they build a new runtime per assertion, so a store/config memo that outlives a
|
|
7
|
+
* mutation is never exercised. The real MCP server boots ONE runtime for its
|
|
8
|
+
* whole lifetime and shares it across every tool call — the exact condition
|
|
9
|
+
* under which a cold-store rejection could be memoized forever, or a stale config
|
|
10
|
+
* layer could survive a `config_set`. This file drives that server the way an
|
|
11
|
+
* agent does, over a single connection:
|
|
12
|
+
*
|
|
13
|
+
* - COLD read → STORE_UNAVAILABLE → `sources_update` (confirm) → the SAME
|
|
14
|
+
* server's retry read SUCCEEDS. Guards C1: a rejected boot must NOT be
|
|
15
|
+
* memoized, and a real mutation must invalidate the shared store so the next
|
|
16
|
+
* read re-boots against the freshly built pack.
|
|
17
|
+
* - `config_set` → a subsequent `config_show` on the same server sees the new
|
|
18
|
+
* value. Guards C2: the mutation must drop the config memo the store boot
|
|
19
|
+
* depends on.
|
|
20
|
+
* - The discovery sequence resolves from a COLD store (the storeless
|
|
21
|
+
* `capabilities` catalog boots with no pack), then browse → inspect works once
|
|
22
|
+
* the store is warm.
|
|
23
|
+
*
|
|
24
|
+
* Uses the in-process `projectMcp` harness (SDK in-memory transport) — no stdio,
|
|
25
|
+
* no network — over the vendored default pack.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
29
|
+
import { tmpdir } from "node:os";
|
|
30
|
+
import { join } from "node:path";
|
|
31
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
32
|
+
import { capabilities } from "../../capabilities/index.js";
|
|
33
|
+
import { readActivePack } from "../../kernel/runtime/paths.js";
|
|
34
|
+
import { DEFAULT_PACK_TTL } from "../fixtures/graph/defaultPack.js";
|
|
35
|
+
import { type McpHarness, projectMcp } from "../helpers/projectMcp.js";
|
|
36
|
+
|
|
37
|
+
/** The transient roots one test creates; removed in `afterEach`. */
|
|
38
|
+
let roots: string[] = [];
|
|
39
|
+
const tempDir = (prefix: string): string => {
|
|
40
|
+
const dir = mkdtempSync(join(tmpdir(), prefix));
|
|
41
|
+
roots.push(dir);
|
|
42
|
+
return dir;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A COLD project directory: a `pragma.config.ts` pointing at a real file
|
|
47
|
+
* package, but nothing built yet — so the store boots cold until `sources_update`.
|
|
48
|
+
*/
|
|
49
|
+
function coldProject(ttl: string): string {
|
|
50
|
+
const pkg = tempDir("e2-pkg-");
|
|
51
|
+
mkdirSync(join(pkg, "definitions"), { recursive: true });
|
|
52
|
+
writeFileSync(join(pkg, "definitions", "pack.ttl"), ttl);
|
|
53
|
+
const cwd = tempDir("e2-proj-");
|
|
54
|
+
writeFileSync(
|
|
55
|
+
join(cwd, "pragma.config.ts"),
|
|
56
|
+
`export default { packs: [{ name: "default", source: "file://${pkg}" }] };\n`,
|
|
57
|
+
);
|
|
58
|
+
return cwd;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let mcp: McpHarness | undefined;
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
roots = [];
|
|
64
|
+
});
|
|
65
|
+
afterEach(async () => {
|
|
66
|
+
await mcp?.cleanup();
|
|
67
|
+
mcp = undefined;
|
|
68
|
+
for (const dir of roots) rmSync(dir, { recursive: true, force: true });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("MCP lifecycle — cold store recovers after sources_update (E2, C1)", () => {
|
|
72
|
+
it("a cold read fails STORE_UNAVAILABLE with the sources_update recovery, then the retry succeeds", async () => {
|
|
73
|
+
const cwd = coldProject(DEFAULT_PACK_TTL);
|
|
74
|
+
mcp = await projectMcp(capabilities, cwd);
|
|
75
|
+
|
|
76
|
+
// 1) COLD: the store has no pack — a store-backed read must fail cleanly,
|
|
77
|
+
// pointing the agent at the recovery TOOL (not just a CLI string).
|
|
78
|
+
const cold = await mcp.callTool("block_list");
|
|
79
|
+
expect(cold.ok).toBe(false);
|
|
80
|
+
const error = cold.error as {
|
|
81
|
+
code: string;
|
|
82
|
+
recovery?: { mcp?: { tool?: string } };
|
|
83
|
+
};
|
|
84
|
+
expect(error.code).toBe("STORE_UNAVAILABLE");
|
|
85
|
+
expect(error.recovery?.mcp?.tool).toBe("sources_update");
|
|
86
|
+
expect(readActivePack(cwd)).toBeUndefined();
|
|
87
|
+
|
|
88
|
+
// 2) The agent runs the recovery tool for real (confirm) — it builds the pack.
|
|
89
|
+
const update = await mcp.callTool("sources_update", { confirm: true });
|
|
90
|
+
expect(update.ok).toBe(true);
|
|
91
|
+
// A real execution, not a plan-only preview.
|
|
92
|
+
expect(update.meta).not.toMatchObject({ planOnly: true });
|
|
93
|
+
expect((update.data as { contentHash: string }).contentHash).toMatch(
|
|
94
|
+
/^[0-9a-f]{64}$/,
|
|
95
|
+
);
|
|
96
|
+
expect(readActivePack(cwd)).toMatch(/^[0-9a-f]{64}$/);
|
|
97
|
+
|
|
98
|
+
// 3) RETRY on the SAME server: the shared store was invalidated by the
|
|
99
|
+
// mutation, so the memoized cold rejection is gone and the read re-boots
|
|
100
|
+
// against the new pack. This is the guarantee a fresh-runtime test cannot
|
|
101
|
+
// make.
|
|
102
|
+
const warm = await mcp.callTool("block_list");
|
|
103
|
+
expect(warm.ok).toBe(true);
|
|
104
|
+
const names = (warm.data as { name: string }[])
|
|
105
|
+
.map((row) => row.name)
|
|
106
|
+
.sort();
|
|
107
|
+
// The whole pack — `block_list` is the declared, unfiltered list since
|
|
108
|
+
// L-OPEN-9, so no channel hides Beta Badge and no tier join hides the
|
|
109
|
+
// untiered Orphan Widget.
|
|
110
|
+
expect(names).toEqual(["Beta Badge", "Button", "Card", "Orphan Widget"]);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("MCP lifecycle — config_set is visible to the next read (E2, C2)", () => {
|
|
115
|
+
it("config_set updates the layer a subsequent config_show reads on the same server", async () => {
|
|
116
|
+
const cwd = tempDir("e2-cfg-");
|
|
117
|
+
mcp = await projectMcp(capabilities, cwd);
|
|
118
|
+
|
|
119
|
+
// Before: the default channel, from the default (unset) origin.
|
|
120
|
+
const before = await mcp.callTool("config_show");
|
|
121
|
+
const beforeData = before.data as {
|
|
122
|
+
config: { channel: string };
|
|
123
|
+
origins: { channel: string };
|
|
124
|
+
};
|
|
125
|
+
expect(beforeData.config.channel).toBe("normal");
|
|
126
|
+
expect(beforeData.origins.channel).toBe("default");
|
|
127
|
+
|
|
128
|
+
// Mutate: set the channel for real (confirm).
|
|
129
|
+
const set = await mcp.callTool("config_set", {
|
|
130
|
+
key: "channel",
|
|
131
|
+
value: "experimental",
|
|
132
|
+
confirm: true,
|
|
133
|
+
});
|
|
134
|
+
expect(set.ok).toBe(true);
|
|
135
|
+
|
|
136
|
+
// After: the SAME long-lived server's read reflects the write — proving the
|
|
137
|
+
// mutation dropped the config memo the boot depends on (not a stale layer).
|
|
138
|
+
const after = await mcp.callTool("config_show");
|
|
139
|
+
const afterData = after.data as {
|
|
140
|
+
config: { channel: string };
|
|
141
|
+
origins: { channel: string };
|
|
142
|
+
};
|
|
143
|
+
expect(afterData.config.channel).toBe("experimental");
|
|
144
|
+
// The write landed in the global layer, so the origin flips accordingly.
|
|
145
|
+
expect(afterData.origins.channel).toBe("global");
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("MCP lifecycle — discovery from a cold store (E2)", () => {
|
|
150
|
+
it("the capabilities catalog resolves cold, then browse -> inspect works once warm", async () => {
|
|
151
|
+
const cwd = coldProject(DEFAULT_PACK_TTL);
|
|
152
|
+
mcp = await projectMcp(capabilities, cwd);
|
|
153
|
+
|
|
154
|
+
// Discovery is storeless: the capabilities catalog answers even with a cold
|
|
155
|
+
// store, so an agent can orient before the pack exists.
|
|
156
|
+
const catalog = await mcp.callTool("capabilities");
|
|
157
|
+
expect(catalog.ok).toBe(true);
|
|
158
|
+
const catalogData = catalog.data as {
|
|
159
|
+
discovery_sequence: unknown;
|
|
160
|
+
tools: unknown;
|
|
161
|
+
};
|
|
162
|
+
expect(catalogData.discovery_sequence).toBeDefined();
|
|
163
|
+
expect(catalogData.tools).toBeDefined();
|
|
164
|
+
|
|
165
|
+
// Warm the store, then follow a browse -> inspect discovery step end to end.
|
|
166
|
+
await mcp.callTool("sources_update", { confirm: true });
|
|
167
|
+
const list = await mcp.callTool("block_list");
|
|
168
|
+
expect(list.ok).toBe(true);
|
|
169
|
+
const first = (list.data as { name: string }[]).at(0);
|
|
170
|
+
expect(first?.name).toBeDefined();
|
|
171
|
+
|
|
172
|
+
const lookup = await mcp.callTool("block_lookup", {
|
|
173
|
+
name: [first?.name ?? ""],
|
|
174
|
+
});
|
|
175
|
+
expect(lookup.ok).toBe(true);
|
|
176
|
+
const results = (lookup.data as { results: { name: string }[] }).results;
|
|
177
|
+
expect(results.at(0)?.name).toBe(first?.name);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The package tier, end to end: a noun the binary never shipped.
|
|
3
|
+
*
|
|
4
|
+
* A package that ships `stories/*.json` and is declared as a `file:` pack
|
|
5
|
+
* contributes a working command to a project that declares it — resolved by
|
|
6
|
+
* `sources update`, hashed into the pack, written into the pack directory, read
|
|
7
|
+
* back storelessly at dispatch, and answered from the fixture graph. Proven both
|
|
8
|
+
* in-process and through the COMPILED binary in a real subprocess.
|
|
9
|
+
*
|
|
10
|
+
* The same package also ships a malformed story and a schema-invalid one. Those
|
|
11
|
+
* must be DROPPED, not fatal: package stories reach dispatch before the command
|
|
12
|
+
* tree exists, so a throw there fails every command — including `sources update`
|
|
13
|
+
* and `doctor`, the only two that can recover from it. Both of those are run
|
|
14
|
+
* here after the bad stories are in the pack.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { runTask } from "@canonical/task/node";
|
|
20
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
21
|
+
import { runChecks } from "../../capabilities/doctor/runChecks.js";
|
|
22
|
+
import { capabilities } from "../../capabilities/index.js";
|
|
23
|
+
import { buildUpdateTask } from "../../capabilities/sources/runUpdate.js";
|
|
24
|
+
import { loadEffectiveModules } from "../../kernel/packs/collect.js";
|
|
25
|
+
import { packDir, readActivePack } from "../../kernel/runtime/paths.js";
|
|
26
|
+
import type { VerbSpec } from "../../kernel/spec/types.js";
|
|
27
|
+
import {
|
|
28
|
+
bootFixtureRuntime,
|
|
29
|
+
type FixtureGraph,
|
|
30
|
+
} from "../helpers/fixtureGraph.js";
|
|
31
|
+
import { runCli } from "../helpers/runCli.js";
|
|
32
|
+
|
|
33
|
+
const TTL = `
|
|
34
|
+
@prefix ex: <https://example.org/kitchen#> .
|
|
35
|
+
@prefix owl: <http://www.w3.org/2002/07/owl#> .
|
|
36
|
+
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
|
37
|
+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
|
38
|
+
|
|
39
|
+
ex:Recipe a owl:Class .
|
|
40
|
+
ex:name a owl:DatatypeProperty ; rdfs:domain ex:Recipe ; rdfs:range xsd:string .
|
|
41
|
+
|
|
42
|
+
ex:soup a ex:Recipe ; ex:name "Soup" .
|
|
43
|
+
ex:stew a ex:Recipe ; ex:name "Stew" .
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
/** A valid read story — the noun the binary never shipped. */
|
|
47
|
+
const RECIPE_STORY = JSON.stringify({
|
|
48
|
+
noun: "recipe",
|
|
49
|
+
description: "List recipes.",
|
|
50
|
+
colophon: "Recipes are modelled as ex:Recipe individuals.",
|
|
51
|
+
list: {
|
|
52
|
+
query:
|
|
53
|
+
"SELECT ?uri ?name WHERE { ?uri a ex:Recipe ; ex:name ?name } ORDER BY ?name",
|
|
54
|
+
columns: [{ field: "uri" }, { field: "name" }],
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/** Schema-invalid: a well-formed JSON document the pack grammar rejects. */
|
|
59
|
+
const INVALID_STORY = JSON.stringify({ noun: "Bad Noun", list: {} });
|
|
60
|
+
|
|
61
|
+
let graph: FixtureGraph;
|
|
62
|
+
beforeAll(async () => {
|
|
63
|
+
graph = await bootFixtureRuntime({
|
|
64
|
+
ttl: TTL,
|
|
65
|
+
stories: {
|
|
66
|
+
"recipe.json": RECIPE_STORY,
|
|
67
|
+
// Not JSON at all.
|
|
68
|
+
"broken.json": "{ this is not json",
|
|
69
|
+
"invalid.json": INVALID_STORY,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
}, 60_000);
|
|
73
|
+
afterAll(async () => {
|
|
74
|
+
await graph.dispose();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("a package's stories reach the pack", () => {
|
|
78
|
+
it("sources update carries all three story files into the pack, verbatim", () => {
|
|
79
|
+
const hash = readActivePack(graph.cwd);
|
|
80
|
+
expect(hash).toBeDefined();
|
|
81
|
+
const carried = JSON.parse(
|
|
82
|
+
readFileSync(join(packDir(hash as string), "stories.json"), "utf-8"),
|
|
83
|
+
) as { source: string; content: string }[];
|
|
84
|
+
expect(carried.map((record) => record.source)).toEqual([
|
|
85
|
+
"fixture/stories/broken.json",
|
|
86
|
+
"fixture/stories/invalid.json",
|
|
87
|
+
"fixture/stories/recipe.json",
|
|
88
|
+
]);
|
|
89
|
+
expect(
|
|
90
|
+
carried.find((r) => r.source === "fixture/stories/recipe.json")?.content,
|
|
91
|
+
).toBe(RECIPE_STORY);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("a package-declared noun answers reads (in-process)", () => {
|
|
96
|
+
it("recipe list returns rows from the package's own graph", async () => {
|
|
97
|
+
const { modules } = await loadEffectiveModules(capabilities, graph.cwd);
|
|
98
|
+
const recipe = modules.find((module) => module.name === "recipe");
|
|
99
|
+
expect(recipe?.story).toBe(true);
|
|
100
|
+
expect(recipe?.colophon).toBe(
|
|
101
|
+
"Recipes are modelled as ex:Recipe individuals.",
|
|
102
|
+
);
|
|
103
|
+
const list = recipe?.verbs.find(
|
|
104
|
+
(verb) => verb.path.join(" ") === "recipe list",
|
|
105
|
+
) as VerbSpec;
|
|
106
|
+
const rows = (await list.run({}, graph.runtime)) as { name: string }[];
|
|
107
|
+
expect(rows.map((row) => row.name)).toEqual(["Soup", "Stew"]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("names the unusable stories without failing the load", async () => {
|
|
111
|
+
const { problems } = await loadEffectiveModules(capabilities, graph.cwd);
|
|
112
|
+
expect(problems.map((problem) => problem.source).sort()).toEqual([
|
|
113
|
+
"fixture/stories/broken.json",
|
|
114
|
+
"fixture/stories/invalid.json",
|
|
115
|
+
]);
|
|
116
|
+
// Each says WHY, not just that something was wrong.
|
|
117
|
+
const invalid = problems.find((p) => p.source.endsWith("invalid.json"));
|
|
118
|
+
expect(invalid?.message).toMatch(/Invalid story/);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("the two recoveries still run with bad stories in the pack", () => {
|
|
123
|
+
it("sources update re-resolves and reports the carried story count", async () => {
|
|
124
|
+
const result = await runTask(await buildUpdateTask(graph.runtime));
|
|
125
|
+
expect(result.packs).toEqual([
|
|
126
|
+
{
|
|
127
|
+
name: "fixture",
|
|
128
|
+
resolved: expect.any(String),
|
|
129
|
+
sourceCount: 1,
|
|
130
|
+
storyCount: 3,
|
|
131
|
+
},
|
|
132
|
+
]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("doctor passes `pack refs` and lists each ignored story as a failing item", async () => {
|
|
136
|
+
const data = await runChecks(graph.runtime);
|
|
137
|
+
const check = data.checks.find((entry) => entry.name === "pack refs");
|
|
138
|
+
// The pack DOES answer reads, so the check passes…
|
|
139
|
+
expect(check?.status).toBe("pass");
|
|
140
|
+
expect(check?.detail).toContain("2 stories ignored");
|
|
141
|
+
// …with the unusable stories named underneath it.
|
|
142
|
+
expect(check?.items?.map((item) => item.label).sort()).toEqual([
|
|
143
|
+
"fixture/stories/broken.json",
|
|
144
|
+
"fixture/stories/invalid.json",
|
|
145
|
+
]);
|
|
146
|
+
for (const item of check?.items ?? []) expect(item.status).toBe("fail");
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("the compiled binary answers the package's noun (subprocess)", () => {
|
|
151
|
+
it("pragma recipe list --format json returns the package's rows", () => {
|
|
152
|
+
const result = runCli(["recipe", "list", "--format", "json"], {
|
|
153
|
+
cwd: graph.cwd,
|
|
154
|
+
});
|
|
155
|
+
expect(result.exitCode).toBe(0);
|
|
156
|
+
const rows = (JSON.parse(result.stdout) as { data: { name: string }[] })
|
|
157
|
+
.data;
|
|
158
|
+
expect(rows.map((row) => row.name)).toEqual(["Soup", "Stew"]);
|
|
159
|
+
// The bad stories are named on stderr, never on stdout — `--format json`
|
|
160
|
+
// stays machine-readable.
|
|
161
|
+
expect(result.stderr).toContain("fixture/stories/broken.json");
|
|
162
|
+
expect(result.stderr).toContain("fixture/stories/invalid.json");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("pragma capabilities lists the package's tools — the surface agents read", () => {
|
|
166
|
+
// The MCP server registers its tools from the effective modules, so a
|
|
167
|
+
// catalog built from the STATIC registry would omit exactly the nouns
|
|
168
|
+
// `tools/list` advertises. `config show` points agents here for the verbs a
|
|
169
|
+
// story produces; that pointer has to be true.
|
|
170
|
+
const result = runCli(["capabilities", "--format", "json"], {
|
|
171
|
+
cwd: graph.cwd,
|
|
172
|
+
});
|
|
173
|
+
expect(result.exitCode).toBe(0);
|
|
174
|
+
const { data } = JSON.parse(result.stdout) as {
|
|
175
|
+
data: { tools: { name: string }[] };
|
|
176
|
+
};
|
|
177
|
+
expect(data.tools.map((tool) => tool.name)).toContain("recipe_list");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("pragma --help does NOT list recipe — the documented fast-path limit", () => {
|
|
181
|
+
// `--help` reads the STATIC capability set (no config, no pack read), which
|
|
182
|
+
// is what keeps its budget. A package-declared noun is dispatch-only.
|
|
183
|
+
const help = runCli(["--help"], { cwd: graph.cwd });
|
|
184
|
+
expect(help.exitCode).toBe(0);
|
|
185
|
+
expect(help.stdout).not.toContain("recipe");
|
|
186
|
+
// …and the command it does not advertise still runs (asserted above).
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -61,9 +61,10 @@ export const liveVerbs: readonly LiveVerb[] = buildLiveVerbs();
|
|
|
61
61
|
/**
|
|
62
62
|
* Read nouns' `lookup` verbs, MCP-exposed, store-backed — restricted to the
|
|
63
63
|
* PACK-lookup contract (a variadic `<name...>` positional), which the B-tier
|
|
64
|
-
* list→lookup→batch suites drive uniformly.
|
|
65
|
-
* (`
|
|
66
|
-
*
|
|
64
|
+
* list→lookup→batch suites drive uniformly. The one bespoke single-name lookup
|
|
65
|
+
* left (`prompt_lookup`) has its own tests, so it is excluded here structurally
|
|
66
|
+
* (by param shape, not by naming the noun — R2). `tier_lookup` joined the pack
|
|
67
|
+
* contract in L-OPEN-9, so it is swept here like every other declared lookup.
|
|
67
68
|
*/
|
|
68
69
|
export const lookupVerbs: readonly LiveVerb[] = liveVerbs.filter(
|
|
69
70
|
(v) =>
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
22
22
|
import { capabilities } from "../../capabilities/index.js";
|
|
23
23
|
import { readPackIndex } from "../../kernel/completion/entitySource.js";
|
|
24
|
+
import type { PackIndex } from "../../kernel/runtime/graphpack/types.js";
|
|
25
|
+
import { resolveSources } from "../../kernel/runtime/resolveSources.js";
|
|
24
26
|
import {
|
|
25
27
|
ALL_VISIBLE_CONFIG,
|
|
26
28
|
CANONICAL_TTL,
|
|
@@ -41,6 +43,8 @@ let mcp: Awaited<ReturnType<typeof projectMcp>>;
|
|
|
41
43
|
let firstNameByNoun: Map<string, string>;
|
|
42
44
|
/** The registered tool list, fetched once (annotations are call-invariant). */
|
|
43
45
|
let registeredTools: Awaited<ReturnType<typeof mcp.listTools>>;
|
|
46
|
+
/** The index of the pack the fixture's boot decision names. */
|
|
47
|
+
let activeIndex: PackIndex | undefined;
|
|
44
48
|
|
|
45
49
|
beforeAll(async () => {
|
|
46
50
|
fixture = await bootFixtureRuntime({
|
|
@@ -49,6 +53,9 @@ beforeAll(async () => {
|
|
|
49
53
|
});
|
|
50
54
|
mcp = await projectMcp(capabilities, fixture.cwd);
|
|
51
55
|
registeredTools = await mcp.listTools();
|
|
56
|
+
activeIndex = readPackIndex(
|
|
57
|
+
resolveSources(await fixture.runtime.loadConfig(), fixture.cwd),
|
|
58
|
+
);
|
|
52
59
|
|
|
53
60
|
firstNameByNoun = new Map();
|
|
54
61
|
for (const v of listVerbs) {
|
|
@@ -81,18 +88,16 @@ function representativeValue(
|
|
|
81
88
|
param: LiveVerb["spec"]["params"][number],
|
|
82
89
|
): unknown {
|
|
83
90
|
if (param.name === "prefix") {
|
|
84
|
-
const prefix = Object.keys(
|
|
91
|
+
const prefix = Object.keys(activeIndex?.prefixes ?? {}).find(
|
|
85
92
|
(p) => p !== "owl" && p !== "rdfs" && p !== "rdf" && p !== "xsd",
|
|
86
93
|
);
|
|
87
94
|
return prefix ?? "ds";
|
|
88
95
|
}
|
|
89
96
|
if (param.name === "uri") {
|
|
90
|
-
const entity =
|
|
91
|
-
(e) => e.box === "abox",
|
|
92
|
-
);
|
|
97
|
+
const entity = activeIndex?.entities.find((e) => e.box === "abox");
|
|
93
98
|
return entity?.prefixed ?? entity?.name ?? "";
|
|
94
99
|
}
|
|
95
|
-
// An enum positional (e.g. `config
|
|
100
|
+
// An enum positional (e.g. `config set <key>`) must
|
|
96
101
|
// get a real member — a placeholder would fail the tool's zod schema at the
|
|
97
102
|
// SDK layer, before the handler, yielding a protocol error rather than a
|
|
98
103
|
// well-formed envelope.
|