`mode` and `extraRoots` carried no `volatile()` marker, so the framework's
form projection dropped the whole entry from `settings.describe` and the plugin
had no card under Plugins at all. The marker is what creates the card.
It is also what turns each field into a reference rather than a value: the
resolved config hands `apply` a `{get(), [write]}` per volatile field, which is
how a settings write lands in a running plugin without re-applying it. So the
policy and the guard now read both fields at the point of use instead of
snapshotting them at mount, and `sectionOrder`/`contextOrder` stay unmarked
because a prompt section cannot be re-ordered without a remount.
A section is also registered once as a plain string, so the rule could not
follow a live `mode` without freezing at load. The mode-specific sentence moved
into the binding, which is a callback.
130 lines
4.7 KiB
JavaScript
130 lines
4.7 KiB
JavaScript
/**
|
|
* The host row: the deployment's policy and its lifetime.
|
|
*
|
|
* Short, but it is the half that is easiest to leave untested — it has no
|
|
* interesting logic and one interesting obligation, which is that a grant must
|
|
* not outlive the session that asked for it.
|
|
*/
|
|
|
|
import assert from 'node:assert/strict'
|
|
import { test } from 'node:test'
|
|
|
|
import { apply, Config } from '../index.js'
|
|
import { configValue } from '../lib/policy.js'
|
|
|
|
/**
|
|
* The symbol cosmokit marks a volatile config reference with, and the writer it
|
|
* carries. A settings write reaches a running plugin through exactly this
|
|
* function — `updateVolatile(target, source)` is `target[write](source.get())` —
|
|
* so a test can commit an edit the same way the framework does.
|
|
*/
|
|
const VOLATILE_WRITE = Symbol.for('cosmokit.volatile.write')
|
|
|
|
/** Whether a field arrived as a live reference rather than a plain value. */
|
|
const isReference = (value) => typeof value === 'object' && value !== null && VOLATILE_WRITE in value
|
|
|
|
/** The smallest context this row touches. */
|
|
function harness() {
|
|
const captured = { provided: undefined, onDisposed: undefined }
|
|
const ctx = {
|
|
provide: (name, value) => {
|
|
captured.provided = { name, value }
|
|
},
|
|
on: (event, listener) => {
|
|
if (event === 'session/disposed') captured.onDisposed = listener
|
|
},
|
|
}
|
|
apply(ctx, Config({ mode: 'ask' }))
|
|
return captured
|
|
}
|
|
|
|
test('the schema fills every default, so a bare row is a working row', () => {
|
|
const config = Config({})
|
|
assert.equal(configValue(config.mode), 'ask')
|
|
assert.deepEqual(configValue(config.extraRoots), [])
|
|
assert.equal(config.sectionOrder, 100)
|
|
assert.equal(config.contextOrder, 105)
|
|
})
|
|
|
|
test('the editable fields are volatile, which is the only reason they have a settings card', () => {
|
|
const config = Config({})
|
|
// The framework's form projection keeps only fields under a volatile ancestor
|
|
// and drops an entry with no such field from `settings.describe` entirely, so
|
|
// dropping either of these markers silently removes the card from the UI.
|
|
assert.equal(isReference(config.mode), true)
|
|
assert.equal(isReference(config.extraRoots), true)
|
|
|
|
// Placement is settled once at load: not settings anyone edits live, and not
|
|
// on the card.
|
|
assert.equal(isReference(config.sectionOrder), false)
|
|
assert.equal(isReference(config.contextOrder), false)
|
|
})
|
|
|
|
test('a settings edit is answered on the next call, without the plugin being re-applied', () => {
|
|
const provided = []
|
|
let applies = 0
|
|
const config = Config({ mode: 'ask', extraRoots: ['/w/src/shared'] })
|
|
apply(
|
|
{
|
|
provide: (name, value) => {
|
|
applies += 1
|
|
provided.push({ name, value })
|
|
},
|
|
on: () => {},
|
|
},
|
|
config,
|
|
)
|
|
const policy = provided[0].value
|
|
assert.equal(policy.mode, 'ask')
|
|
assert.deepEqual(policy.standing, ['/w/src/shared'])
|
|
|
|
// What the framework does on a settings write: it commits into the running
|
|
// references and deliberately does NOT re-apply the plugin. Everything the
|
|
// policy reports has to follow from the reference alone.
|
|
config.mode[VOLATILE_WRITE]('deny')
|
|
config.extraRoots[VOLATILE_WRITE](['/w/src/shared', '/w/src/types'])
|
|
|
|
assert.equal(applies, 1)
|
|
assert.equal(policy.mode, 'deny')
|
|
assert.deepEqual(policy.standing, ['/w/src/shared', '/w/src/types'])
|
|
assert.deepEqual(
|
|
policy.scopeFor({ session: { id: 's1', header: { cwd: '/w/src/room' } } }).extras,
|
|
['/w/src/shared', '/w/src/types'],
|
|
)
|
|
})
|
|
|
|
test('a bad mode fails loudly at load rather than at the first file call', () => {
|
|
assert.throws(() => Config({ mode: 'maybe' }))
|
|
})
|
|
|
|
test('the policy is published under the name the guard injects', () => {
|
|
const captured = harness()
|
|
assert.equal(captured.provided.name, 'facetPolicy')
|
|
assert.equal(captured.provided.value.mode, 'ask')
|
|
})
|
|
|
|
test('the service reads the configuration it was given', () => {
|
|
const provided = []
|
|
apply(
|
|
{ provide: (name, value) => provided.push({ name, value }), on: () => {} },
|
|
Config({ mode: 'deny', extraRoots: ['../../shared'], sectionOrder: 7, contextOrder: 8 }),
|
|
)
|
|
const policy = provided[0].value
|
|
assert.equal(policy.mode, 'deny')
|
|
assert.deepEqual(policy.standing, ['../../shared'])
|
|
assert.equal(policy.sectionOrder, 7)
|
|
assert.equal(policy.contextOrder, 8)
|
|
})
|
|
|
|
test('a disposed session does not leave its grant behind', () => {
|
|
const captured = harness()
|
|
const policy = captured.provided.value
|
|
policy.grant('session-1', ['../../shared'], '/w/src/room')
|
|
assert.deepEqual(policy.grantsFor('session-1'), ['/w/shared'])
|
|
|
|
assert.equal(typeof captured.onDisposed, 'function')
|
|
captured.onDisposed({ id: 'session-1' })
|
|
assert.deepEqual(policy.grantsFor('session-1'), [])
|
|
assert.deepEqual(policy.sessions, [])
|
|
})
|