Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions hypaware-core/smoke/flows/cli_bundled_plugins_activated.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ import { defaultConfigPath } from '../../../src/core/config/schema.js'
* 3. `hyp attach --client codex --dry-run` reaches the Codex adapter
* (same shape).
* 4. `hyp status --json` emits a stable JSON document listing the
* configured sources, sinks, clients, and active plugins, *without*
* mentioning `@hypaware/central` or `@hypaware/gascity` (both
* remain loadable for developers but are excluded from the V1
* default surface).
* configured sources, sinks, clients, and active plugins. Because
* neither `@hypaware/central` nor `@hypaware/gascity` is in this
* config, they must not appear — they are excluded from default
* activation but remain discoverable through the plugin catalog and
* activatable via explicit config or init presets.
*
* Telemetry contract (per bead):
* - One `kernel.boot` root span per dispatch boot.
Expand All @@ -48,9 +49,9 @@ export async function run({ harness, expect }) {
// Stage a v2 config that selects six of the nine V1-bundled
// plugins. `@hypaware/format-jsonl`, `@hypaware/s3`, and
// `@hypaware/format-iceberg` are intentionally omitted so the smoke
// can assert the "skipped" log surface, and we exclude
// `@hypaware/central` / `@hypaware/gascity` because they are not on
// the V1 default surface in the first place.
// can assert the "skipped" log surface. `@hypaware/central` and
// `@hypaware/gascity` are not in this config — they are excluded from
// default activation but activatable via explicit config.
const configPath = defaultConfigPath(harness.hypHome)
await fs.mkdir(path.dirname(configPath), { recursive: true })
await fs.writeFile(configPath, JSON.stringify({
Expand Down Expand Up @@ -131,7 +132,7 @@ export async function run({ harness, expect }) {
(rows) => Array.isArray(rows) && rows.every((/** @type {any} */ r) => r.source === 'bundled')
)
expect.that(
'plugins: no excluded-from-default plugin (central/gascity) appears',
'plugins: unconfigured plugins (central/gascity) absent from active list',
(listed.plugins ?? []).map((/** @type {any} */ p) => p.name),
(v) =>
Array.isArray(v) &&
Expand Down Expand Up @@ -219,12 +220,12 @@ export async function run({ harness, expect }) {
typeof v.state === 'string'
)
expect.that(
'status: JSON does not reference @hypaware/central',
'status: unconfigured @hypaware/central absent from status JSON',
statusStdout.text(),
(v) => typeof v === 'string' && !v.includes('@hypaware/central')
)
expect.that(
'status: JSON does not reference @hypaware/gascity',
'status: unconfigured @hypaware/gascity absent from status JSON',
statusStdout.text(),
(v) => typeof v === 'string' && !v.includes('@hypaware/gascity')
)
Expand Down
59 changes: 45 additions & 14 deletions hypaware-core/smoke/flows/gascity_attach_writes_partition.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,17 @@ import { activatePlugins } from '../../../src/core/runtime/loader.js'
import { loadManifests } from '../../../src/core/manifest.js'

/**
* Phase 8.6 smoke. Boots `@hypaware/gascity` from the in-repo
* workspace, attaches an in-process fixture supervisor through
* `hyp gascity attach`, drives a few SSE-shaped frames, and asserts
* the §Phase 8.6 contract from the implementation plan:
* Gascity plugin-surface acceptance smoke. Boots `@hypaware/gascity`
* from the in-repo workspace and exercises the full plugin lifecycle
* through plugin-owned contributions:
*
* - traces: a `source.start` span tagged `hyp_plugin=@hypaware/gascity`
* appears on the first `gascity attach`
* - traces: a `source.reload` span tagged `hyp_plugin=@hypaware/gascity`
* appears on the second `gascity attach`
* - query: `select count(*) from gascity_messages` returns the number
* of frames the fixture pushed
* - cache: at least one `cache.append` span landed for
* `hyp_dataset=gascity_messages`
* - `gascity attach` starts/reloads the source through plugin code
* - `gascity list` shows attached city state
* - `select count(*) from gascity_messages` returns captured rows
* - `gascity detach` removes a city and reloads cleanly
* - traces: `source.start` tagged `hyp_plugin=@hypaware/gascity`
* - traces: `source.reload` on subsequent attach/detach
* - cache: `cache.append` spans for `hyp_dataset=gascity_messages`
*
* The fixture supervisor lives entirely in this file and is wired to
* the plugin via `globalThis[Symbol.for('hypaware-gascity:transport')]`
Expand Down Expand Up @@ -171,6 +169,39 @@ export async function run({ harness, expect }) {
(v) => v === true
)

// Detach hypburb and verify the source reloads cleanly.
const detachOut = await dispatchCommand(
['gascity', 'detach', 'hypburb'],
{ kernel, registry, harness }
)
expect.that(
"stdout: detach prints confirmation for 'hypburb'",
detachOut.includes('hypburb'),
(v) => v === true
)

// After detach, list should still show hyptown but not hypburb.
const list2Stdout = makeBuf()
const list2Stderr = makeBuf()
await dispatch(['gascity', 'list'], {
stdout: list2Stdout,
stderr: list2Stderr,
kernel,
registry,
env: smokeEnv(harness),
})
const list2Text = list2Stdout.text()
expect.that(
'stdout: gascity list after detach still includes hyptown',
list2Text.includes('hyptown'),
(v) => v === true
)
expect.that(
'stdout: gascity list after detach no longer includes hypburb',
list2Text.includes('hypburb'),
(v) => v === false
)

await obs.shutdown()
fixture.uninstall()

Expand Down Expand Up @@ -199,9 +230,9 @@ export async function run({ harness, expect }) {
(/** @type {any} */ t) => t.name === 'source.reload'
)
expect.that(
'traces: at least one source.reload span emitted (from the second attach)',
'traces: at least 2 source.reload spans emitted (attach + detach)',
reloadSpans,
(rows) => rows.length >= 1
(rows) => rows.length >= 2
)
expect.that(
'traces: source.reload tagged hyp_plugin=@hypaware/gascity',
Expand Down
10 changes: 8 additions & 2 deletions src/core/plugin_catalog.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,14 @@

/**
* Build a plugin catalog from loaded manifests. The catalog derives
* capability metadata, known datasets, and contribution summaries
* from the manifest files themselves rather than a hardcoded table.
* capability metadata, known datasets, client descriptors, and
* contribution summaries from the manifest files themselves rather
* than a hardcoded table.
*
* Callers should pass both `bundled.loaded` and `bundled.excluded`
* manifests so excluded plugins (like `@hypaware/gascity`) remain
* visible for config validation and descriptor resolution even though
* they are not activated by default.
*
* Duplicate plugin names are resolved by first-writer-wins: the first
* manifest array is treated as authoritative (bundled plugins), so
Expand Down
10 changes: 4 additions & 6 deletions src/core/runtime/boot.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,10 @@ export async function bootKernel(opts = {}) {
const loadedConfig = configPath ? await loadConfigFile(configPath) : null
const config = loadedConfig?.ok ? loadedConfig.config : null

// The full plugin pool the kernel knows about: V1 allowlist plus
// the excluded-from-default set (so developers can still activate
// `@hypaware/central` or `@hypaware/gascity` by naming them in
// config), plus every plugin in `plugin-lock.json` whose manifest
// loaded. `all-bundled` boots intentionally skip the excluded and
// installed sets so the picker only sees the V1 default surface.
// Full plugin pool: V1 allowlist + excluded-from-default set +
// installed plugins. Excluded plugins are in the pool so they
// activate when named in config or an init preset — the allowlist
// only governs default activation, not discoverability.
const installedNames = new Set(installed.loaded.map((m) => m.manifest.name))
const excludedAvailable = discovered.excluded.filter(
(m) => !installedNames.has(/** @type {PluginName} */ (m.manifest.name))
Expand Down
30 changes: 18 additions & 12 deletions src/core/runtime/bundled.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ import { loadManifests } from '../manifest.js'
*/

/**
* V1 bundled plugin allowlist (finish-v1.md §Phase 2). A plugin must
* appear here to be discoverable through the default boot path. The
* allowlist exists so the V1 default install does not pull
* `@hypaware/central` or `@hypaware/gascity` into the picker, the
* default config, or the V1 smokes — both remain on disk for
* developers but are excluded from V1 acceptance gates.
* V1 bundled plugin allowlist. A plugin must appear here to be
* activated by the default boot profiles (`all-bundled`,
* `all-available`). Excluded plugins (`@hypaware/central`,
* `@hypaware/gascity`) are still discoverable through the plugin
* catalog — their manifest contributions (datasets, client
* descriptors, capability metadata) are visible to config validation
* and the walkthrough. They are activatable via explicit config or
* init presets; the allowlist only governs default activation.
*
* @type {ReadonlySet<PluginName>}
*/
Expand All @@ -34,10 +36,12 @@ export const V1_BUNDLED_PLUGIN_ALLOWLIST = new Set(/** @type {PluginName[]} */ (
]))

/**
* Bundled plugins present in the repo workspace but excluded from the
* V1 default surface. They remain loadable for developers (via
* explicit manifest discovery) but never appear in the V1 picker,
* default configs, V1 docs, or V1 smokes.
* Bundled plugins excluded from default activation. Their manifests
* are still loaded by `discoverBundledPlugins` (in the `excluded`
* bucket) so the plugin catalog can derive datasets, client
* descriptors, and capability metadata for config validation.
* Activation requires explicit config (`{ name: '@hypaware/gascity' }`)
* or an init preset — the picker and default boot profiles skip them.
*
* @type {ReadonlySet<PluginName>}
*/
Expand Down Expand Up @@ -67,8 +71,10 @@ export function defaultBundledWorkspaceDir() {
* - `loaded` — manifests whose `name` is in the V1 allowlist.
* - `excluded` — manifests whose `name` is in the V1 exclude set
* (`@hypaware/central`, `@hypaware/gascity`). These
* remain available for developers but are not surfaced
* by the default boot path.
* are excluded from default activation but their
* manifests feed the plugin catalog so datasets,
* client descriptors, and capability metadata remain
* visible to config validation and the walkthrough.
* - `unknownDirs` — directories that hold a parseable manifest under
* a name the kernel doesn't recognise as bundled.
*
Expand Down
30 changes: 30 additions & 0 deletions test/core/config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,36 @@ test('buildPluginCatalog collects known datasets from manifest contributions', a
assert.ok(catalog.knownDatasets.has('logs'))
assert.ok(catalog.knownDatasets.has('traces'))
assert.ok(catalog.knownDatasets.has('metrics'))
assert.ok(
catalog.knownDatasets.has('gascity_messages'),
'catalog includes gascity_messages from excluded plugin manifest'
)
})

test('buildPluginCatalog includes excluded gascity plugin as a catalog entry', async () => {
const bundled = await discoverBundledPlugins()
const catalog = buildPluginCatalog([...bundled.loaded, ...bundled.excluded])

const entry = catalog.plugins.get('@hypaware/gascity')
assert.ok(entry, 'gascity must be in catalog when excluded manifests are included')
assert.equal(entry.name, '@hypaware/gascity')
assert.ok(entry.contributes, 'gascity catalog entry must carry its contributions')
assert.ok(
entry.contributes.sources?.some((s) => s.name === 'gascity'),
'gascity contributes a "gascity" source'
)
assert.ok(
entry.contributes.commands?.some((c) => c.name === 'gascity attach'),
'gascity contributes a "gascity attach" command'
)
assert.ok(
entry.contributes.init_presets?.some((p) => p.name === 'gascity'),
'gascity contributes a "gascity" init preset'
)
assert.ok(
entry.contributes.skills?.some((s) => s.name === 'hypaware-gascity'),
'gascity contributes the hypaware-gascity skill'
)
})

test('validateConfig uses catalog-derived metadata for sink validation', async () => {
Expand Down