feat(loader): interpolate the entry disabled field

The Windows platform layer disables tool-bash and inserts the pwsh stack, but the shipped presets each mount a tool-bash row that re-enabled the tool on win32 — the session had both a PowerShell-backed bash tool and tool-pwsh, silently, because no spec pinned the composed preset layer.

The Loader now evaluates a disabled: !!js expression against the loader context at every mount decision; disabled is the only interpolated metadata field, and the raw node stays in the options so write-back keeps the !!js form. The standard/code/cordis presets gate tool-bash with process.platform === 'win32', verify-cordis-config allows expressions in disabled only, and the windows-shell spec pins the preset-level invariant.
This commit is contained in:
Huanqi Cao
2026-08-11 11:33:53 +08:00
parent db1028a84d
commit 6fb226ea24
16 changed files with 261 additions and 43 deletions

View File

@@ -187,6 +187,74 @@ describe('Loader config interpolation', () => {
})
})
describe('Loader entry disabled interpolation', () => {
it('evaluates a !!js disabled expression against the loader context', async () => {
const dir = tmp()
writeFileSync(join(dir, 'noop.mjs'), 'export function apply() {}\n')
writeFileSync(join(dir, 'cordis.yml'), [
'- id: expr-off',
' name: ./noop.mjs',
' disabled: !!js process.version.length > 0',
'- id: expr-on',
' name: ./noop.mjs',
' disabled: !!js process.version.length === 0',
'',
].join('\n'))
const ctx = await boot(NAME, join(dir, 'cordis.yml'))
try {
const off = [...ctx.loader.entries()].find(entry => entry.options.id === 'expr-off')
const on = [...ctx.loader.entries()].find(entry => entry.options.id === 'expr-on')
expect(off?.disabled).toBe(true)
expect(off?.fiber).toBeUndefined()
expect(on?.disabled).toBe(false)
expect(on?.fiber).toBeDefined()
} finally {
await ctx.fiber.dispose()
}
})
it('keeps the raw expression in the options so write-back preserves the !!js form', async () => {
const dir = tmp()
writeFileSync(join(dir, 'noop.mjs'), 'export function apply() {}\n')
writeFileSync(join(dir, 'cordis.yml'), '- id: expr\n name: ./noop.mjs\n disabled: !!js process.platform === "win32"\n')
const ctx = await boot(NAME, join(dir, 'cordis.yml'))
try {
const entry = [...ctx.loader.entries()].find(item => item.options.id === 'expr')
// The evaluated boolean drives the mount decision; the serialized
// expression node stays in the options for the file-backed tree.
expect(entry?.options.disabled).toEqual({ __jsExpr: 'process.platform === "win32"' })
expect(entry?.disabled).toBe(process.platform === 'win32')
} finally {
await ctx.fiber.dispose()
}
})
it('re-evaluates when update() replaces the expression, mounting and unmounting', async () => {
const dir = tmp()
writeFileSync(join(dir, 'noop.mjs'), 'export function apply() {}\n')
writeFileSync(join(dir, 'cordis.yml'), '- id: expr\n name: ./noop.mjs\n disabled: !!js process.version.length === 0\n')
const ctx = await boot(NAME, join(dir, 'cordis.yml'))
try {
const entry = [...ctx.loader.entries()].find(item => item.options.id === 'expr')
expect(entry?.disabled).toBe(false)
expect(entry?.fiber).toBeDefined()
// The expression form is the file dialect; the typed programmatic API
// carries booleans. Include reapplication feeds the raw node through
// the untyped file path — simulated here with the serialized shape.
const disabledTrue = { __jsExpr: 'process.version.length > 0' } as unknown as boolean
const disabledFalse = { __jsExpr: 'process.version.length === 0' } as unknown as boolean
await entry?.update({ disabled: disabledTrue })
expect(entry?.disabled).toBe(true)
expect(entry?.fiber).toBeUndefined()
await entry?.update({ disabled: disabledFalse })
expect(entry?.disabled).toBe(false)
expect(entry?.fiber).toBeDefined()
} finally {
await ctx.fiber.dispose()
}
})
})
describe('boot with user patches', () => {
it('applies id-targeted overrides, inserts, and interpolates !!js from the environment', async () => {
const dir = tmp()