diff --git a/packages/bus/tests/grant-executor.test.mjs b/packages/bus/tests/grant-executor.test.mjs index e449b7d77..46235d16b 100644 --- a/packages/bus/tests/grant-executor.test.mjs +++ b/packages/bus/tests/grant-executor.test.mjs @@ -1047,7 +1047,10 @@ test('sage-1908: once the agent of an applied grant is a creative role, host.run const f = await setup(t); assert.equal((await f.approve()).end.action, 'grant.applied'); const r = await restart(t, f, creativeOps(f.business)); - const cap = r.broker.bindLaunch({ business: 'demo', role: 'ops', run: 'ops-run', harness: 'pi', address: 'ops' }); + const human = r.broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }); + const project = r.broker.request(human, { verb: 'project.create', args: { name: 'host grant check' } }).id; + const thread = r.broker.request(human, { verb: 'thread.create', args: { project, name: 'ops', instance: 'ops' } }).id; + const cap = r.broker.bindLaunch({ business: 'demo', role: 'ops', run: 'ops-run', harness: 'pi', address: 'ops', thread }); r.broker.request(cap, { verb: 'role.claim' }); const executor = hostExecutor({ credentials: r.credentials, root: join(f.dir, 'host-run') }); const before = f.requests().length; diff --git a/packages/bus/tests/project-runtime.test.mjs b/packages/bus/tests/project-runtime.test.mjs index a6086e56c..7275ad902 100644 --- a/packages/bus/tests/project-runtime.test.mjs +++ b/packages/bus/tests/project-runtime.test.mjs @@ -32,7 +32,8 @@ test('Broker keeps the sandbox client import closure: only the listed imports, n // (#1692, D2.4) reads unhireableAction, the hire veto's action name. // Row 236 (#1742, #436b) reads giteaName and parseGiteaUrl for the Gitea grant path. // FED R3 r5 (row 274, #414) reads covers, a user's grant check, from it. - "import { ID_PATTERN, THREAD_DEFINITION, END_PROTECTED, forkProblem, forkUrl, credentialKind, validArgv, commandAllowed, HOST_ID, CREATIVE_DEFINITIONS, unhireableAction, giteaName, parseGiteaUrl, covers } from '../../business/src/vocabulary.mjs';", + // Row 219 reads THREAD_DEFINITIONS from the same vocabulary for creative thread admission. + "import { ID_PATTERN, THREAD_DEFINITIONS, END_PROTECTED, forkProblem, forkUrl, credentialKind, validArgv, commandAllowed, HOST_ID, CREATIVE_DEFINITIONS, unhireableAction, giteaName, parseGiteaUrl, covers } from '../../business/src/vocabulary.mjs';", ]); assert.doesNotMatch(source, /\bimport\s*\(/); assert.doesNotMatch(readFileSync(new URL('../src/landing.mjs', import.meta.url), 'utf8'), /\bimport\b/); diff --git a/packages/bus/tests/threads.test.mjs b/packages/bus/tests/threads.test.mjs index 1410ec3fa..280e03184 100644 --- a/packages/bus/tests/threads.test.mjs +++ b/packages/bus/tests/threads.test.mjs @@ -640,3 +640,17 @@ test('C2-09a: explicit missing, non-thread and malformed selectors create no thr const legacy = ctx.call('human', 'thread.create', { project, name: 'Default' }); assert.equal(legacy.instance, 'assistant'); }); + + +test('C2-09a: creative bindLaunch without a thread refuses thread-required', t => { + const creative = ['companion', 'ideas', 'research', 'proofreader', 'fact-checker', 'continuity']; + const extra = Object.fromEntries(creative.map((definition, i) => [`creative${i}`, { definition, authority: { withinRole: ['message.send'], crossRole: [] } }])); + const ctx = setup(t, demo({ roles: { ...roles, ...extra }, definitions: { + ...definitions, ...Object.fromEntries(creative.map(definition => [definition, {}])), + } })); + for (const instance of Object.keys(extra)) { + assert.throws(() => ctx.launch(`unthreaded-${instance}`, instance), { code: 'thread-required' }); + } + assert.equal(ctx.count('SELECT count(*) n FROM threads'), 0); + assert.equal(ctx.count('SELECT count(*) n FROM role_claims'), 0); +});