fix(sys1): count project system types in the restore guard and admit the CLI verbs

Sol review sys1-review-r1 (changes 3):
- P1: project_system_types joins stack-release.sh COUNTED and the T45 LISTED pin; the
  post-backup refusal and in-run recount tests now cover it.
- P2: the CLI channel admits project.system-types and project.system-types.set; a new test
  drives both through run() and the real channel module (1/1 red without the fix).
- P2: the #811 migration-reader pin moves to 19 steps ending 3t-3u.
This commit was merged in pull request #1890.
This commit is contained in:
jason.woltje committed 2026-11-09 18:46:36 -06:00
1 parent f4bfd3a744
commit 2c6556ab1e
4 files changed
+42 -6

No files matched your search

+5 -5
View File
@@ -306,7 +306,7 @@ test('#811 migration readers follow the engine order and refuse malformed pairs'
assert.notEqual(engineSource,source); assert.notEqual(engineSource,source);
const engine=await import(`data:text/javascript;base64,${Buffer.from(engineSource).toString('base64')}`); const engine=await import(`data:text/javascript;base64,${Buffer.from(engineSource).toString('base64')}`);
const pairs=engine.migrations.map(({from,to})=>[from,to]); 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 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 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'); 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]; 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 // sage-1163's list 2, written out so that a table dropped from the script's COUNTED fails T45 as
// well as T46. // 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 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()}; 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('3f.sqlite',['events'],['events','instance_names']),'1\t1 event(s)\n');
both(store('none.sqlite',[],['instance_names']),'0\t0 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); const e=await mk(t);await seed(e);
assert.equal((await run(e,'activate',C,'acme','--migrate')).code,0);const act=pointer(e).activation; assert.equal((await run(e,'activate',C,'acme','--migrate')).code,0);const act=pointer(e).activation;
addRow(e.store,x); 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.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'); 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); const e=await mk(t);await seed(e);
assert.equal((await run(e,'activate',C,'acme','--migrate')).code,0);const act=pointer(e).activation; 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); 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.equal(r.err,rolledBack('post-backup-events',C));
assert.ok(r.out.includes('; 0 event(s) were written after the backup\n'),r.out); assert.ok(r.out.includes('; 0 event(s) were written after the backup\n'),r.out);
+34
View File
@@ -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(calls.map(x => x[0]), ['project.list', 'project.system-types']);
assert.deepEqual(JSON.parse(output).active, ['coding']); 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, []);
});
+2
View File
@@ -6,6 +6,8 @@ const VERBS = new Set([
"project.create", "project.create",
"project.archive", "project.archive",
"project.unarchive", "project.unarchive",
"project.system-types",
"project.system-types.set",
"thread.list", "thread.list",
"thread.create", "thread.create",
"thread.messages", "thread.messages",
+1 -1
View File
@@ -421,7 +421,7 @@ plugin_checks() {
# Row 181: the tables a bus writer fills without an event in the same transaction, in schema # 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. # 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. # 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=' STORE_JS='
const {DatabaseSync}=require("node:sqlite");const {pathToFileURL}=require("node:url"); const {DatabaseSync}=require("node:sqlite");const {pathToFileURL}=require("node:url");