diff --git a/packages/cli/tests/stack-release.test.mjs b/packages/cli/tests/stack-release.test.mjs index 412865d45..264fe4fdd 100644 --- a/packages/cli/tests/stack-release.test.mjs +++ b/packages/cli/tests/stack-release.test.mjs @@ -306,7 +306,7 @@ test('#811 migration readers follow the engine order and refuse malformed pairs' assert.notEqual(engineSource,source); const engine=await import(`data:text/javascript;base64,${Buffer.from(engineSource).toString('base64')}`); const pairs=engine.migrations.map(({from,to})=>[from,to]); - assert.equal(pairs.length,18);assert.deepEqual(pairs.at(-1),['3s','3t']); + assert.equal(pairs.length,19);assert.deepEqual(pairs.at(-1),['3t','3u']); const release=readFileSync(join(ROOT,'scripts/stack-release.sh'),'utf8'); const fn=(name)=>{const m=release.match(new RegExp(`^${name}\\(\\) \{[\\s\\S]*?^\}`, 'm'));assert.ok(m,name);return m[0]}; const functions=['copy_schema','target_schema','plan_steps'].map(fn).join('\n'); @@ -885,7 +885,7 @@ const COUNTED=/^COUNTED="([^"]*)"$/m.exec(srText)[1].split(' '); const STORE_JS=/^STORE_JS='([\s\S]*?)'$/m.exec(srText)[1]; // sage-1163's list 2, written out so that a table dropped from the script's COUNTED fails T45 as // well as T46. -const LISTED=['role_claims','thread_claims','decision_events','messages','deliveries','task_snapshots','projects','project_archives','threads','sessions','session_defaults','thread_opens','thread_shares','instance_names','usage_records','session_ops','message_cancels','request_events','request_effects','request_snapshots','request_snapshot_events','request_project_events']; +const LISTED=['role_claims','thread_claims','decision_events','messages','deliveries','task_snapshots','projects','project_archives','threads','sessions','session_defaults','thread_opens','thread_shares','instance_names','usage_records','session_ops','message_cancels','request_events','request_effects','request_snapshots','request_snapshot_events','request_project_events','project_system_types']; const LATE='2999-01-01T00:00:00.000Z'; const addRow=(file,table,at=LATE)=>{const db=new DatabaseSync(file);db.exec(`CREATE TABLE IF NOT EXISTS ${table} (seq INTEGER PRIMARY KEY, at TEXT NOT NULL)`);db.prepare(`INSERT INTO ${table} (at) VALUES (?)`).run(at);db.close()}; @@ -903,7 +903,7 @@ test('T45 row 181 items 2 and 3: the restore guard counts events and every COUNT both(store('3f.sqlite',['events'],['events','instance_names']),'1\t1 event(s)\n'); both(store('none.sqlite',[],['instance_names']),'0\t0 event(s)\n'); }); - for(const x of ['instance_names','usage_records'])await t.test(`one ${x} row and no event refuses before any stop; --events 1 restores`,async(t)=>{ + for(const x of ['instance_names','usage_records','project_system_types'])await t.test(`one ${x} row and no event refuses before any stop; --events 1 restores`,async(t)=>{ const e=await mk(t);await seed(e); assert.equal((await run(e,'activate',C,'acme','--migrate')).code,0);const act=pointer(e).activation; addRow(e.store,x); @@ -925,10 +925,10 @@ test('T45 row 181 items 2 and 3: the restore guard counts events and every COUNT assert.equal(r.code,3,r.out+r.err);assert.ok(r.err.endsWith(`stack-release: refusing restore-mismatch: cannot count the rows after ${stamp}\n`),r.err); assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);assert.equal(logLines(e).at(-1).code,'restore-mismatch'); }); - await t.test('the in-run recount counts the listed tables too',async(t)=>{ + for(const x of ['thread_opens','project_system_types'])await t.test(`the in-run recount counts the listed tables too (${x})`,async(t)=>{ const e=await mk(t);await seed(e); assert.equal((await run(e,'activate',C,'acme','--migrate')).code,0);const act=pointer(e).activation; - fault(e,'stop-writes-row','thread_opens'); + fault(e,'stop-writes-row',x); const r=await run(e,'rollback','acme','--restore',act);assert.equal(r.code,1,r.out+r.err); assert.equal(r.err,rolledBack('post-backup-events',C)); assert.ok(r.out.includes('; 0 event(s) were written after the backup\n'),r.out); diff --git a/packages/cli/tests/system-type.test.mjs b/packages/cli/tests/system-type.test.mjs index f3f83cf35..7ac1c7ca3 100644 --- a/packages/cli/tests/system-type.test.mjs +++ b/packages/cli/tests/system-type.test.mjs @@ -17,3 +17,37 @@ test('org installed system types and project selection reads print JSON', async assert.deepEqual(calls.map(x => x[0]), ['project.list', 'project.system-types']); assert.deepEqual(JSON.parse(output).active, ['coding']); }); + +// sys1-review P2: both commands go through run() and the real CLI channel module, so a verb the +// channel does not admit fails here. A disabled channel and an agent run still refuse. +test('project system-types and system-types-set pass the CLI channel', async t => { + const { run } = await import('../src/cli.mjs'); + const { fixture, humanEnv, io: mkio, tmp } = await import('./helpers.mjs'); + const root = tmp(t), f = fixture(root); + const P = '10000000-0000-4000-8000-000000000001'; + const transportFor = channels => { + const calls = []; + const transport = () => async (verb, args) => { + if (verb === 'plugins') return { business: 'acme', revision: 'a'.repeat(64), plugins: { channel: channels } }; + calls.push([verb, args]); + if (verb === 'project.list') return [{ id: P, at: '2026-11-09T00:00:00.000Z', name: 'work', repo: null }]; + if (verb === 'project.system-types') return { project: P, selected: ['coding'], active: ['coding'], inactive: [] }; + if (verb === 'project.system-types.set') return { project: P, selected: args.systemTypes, active: args.systemTypes, inactive: [] }; + throw Object.assign(new Error(verb), { code: 'bus-refused' }); + }; + return { calls, transport }; + }; + const read = transportFor(['cli']), rx = mkio(humanEnv(f.env)); + assert.equal(await run(['project', 'system-types', 'work', '--business', 'acme'], rx, { transport: read.transport }), 0, rx.err?.text); + assert.deepEqual(read.calls, [['project.list', { includeArchived: true }], ['project.system-types', { project: P }]]); + assert.deepEqual(JSON.parse(rx.out.text).active, ['coding']); + const write = transportFor(['cli']), wx = mkio(humanEnv(f.env)); + assert.equal(await run(['project', 'system-types-set', 'work', '--types', 'coding,marketing', '--business', 'acme'], wx, { transport: write.transport }), 0, wx.err?.text); + assert.deepEqual(write.calls.at(-1), ['project.system-types.set', { project: P, systemTypes: ['coding', 'marketing'] }]); + const off = transportFor(['webui']), ox = mkio(humanEnv(f.env)); + assert.equal(await run(['project', 'system-types', 'work', '--business', 'acme'], ox, { transport: off.transport }), 3); + assert.deepEqual(off.calls, []); + const agent = transportFor(['cli']); + assert.notEqual(await run(['project', 'system-types-set', 'work', '--types', 'coding', '--business', 'acme'], mkio({ ...humanEnv(f.env), CODEX_THREAD_ID: 'fixture-agent' }), { transport: agent.transport }), 0); + assert.deepEqual(agent.calls, []); +}); diff --git a/packages/plugins/channel/cli/index.mjs b/packages/plugins/channel/cli/index.mjs index ce3b8a956..6e66a9200 100644 --- a/packages/plugins/channel/cli/index.mjs +++ b/packages/plugins/channel/cli/index.mjs @@ -6,6 +6,8 @@ const VERBS = new Set([ "project.create", "project.archive", "project.unarchive", + "project.system-types", + "project.system-types.set", "thread.list", "thread.create", "thread.messages", diff --git a/scripts/stack-release.sh b/scripts/stack-release.sh index beb959c85..babc193f6 100755 --- a/scripts/stack-release.sh +++ b/scripts/stack-release.sh @@ -421,7 +421,7 @@ plugin_checks() { # Row 181: the tables a bus writer fills without an event in the same transaction, in schema # order. A restore drops their rows after STAMP as it drops events, so the guard counts them too. # BUILD.md for row 181 names each writer; the T46 guard classifies every table in the schema. -COUNTED="role_claims thread_claims decision_events messages deliveries task_snapshots projects project_archives threads sessions session_defaults thread_opens thread_shares instance_names usage_records session_ops message_cancels request_events request_effects request_snapshots request_snapshot_events request_project_events" +COUNTED="role_claims thread_claims decision_events messages deliveries task_snapshots projects project_archives threads sessions session_defaults thread_opens thread_shares instance_names usage_records session_ops message_cancels request_events request_effects request_snapshots request_snapshot_events request_project_events project_system_types" STORE_JS=' const {DatabaseSync}=require("node:sqlite");const {pathToFileURL}=require("node:url");