morgana: row 205a build (sage-2288): trail and replies hide other users' thread messages from owner, reader and tester; hidden answers as unknown; #readable owner-only; design d2 rulings
This commit is contained in:
1 parent
f751505ada
commit
59f708ca21
4 files changed
+320
-2
No files matched your search
@@ -0,0 +1,63 @@
|
||||
# Row 205a (193b1a, #1703): the trail and replies message filter
|
||||
|
||||
Design: DESIGN.md d2 in this directory, with rulings in section 7 (sage-2288). Base: refactor 02da6a2ae. No schema
|
||||
change. Nothing has run yet: every test run waits for Sage's CLEAR.
|
||||
|
||||
## Changes
|
||||
|
||||
`packages/bus/src/broker.mjs`:
|
||||
- `#hiddenMessage(s, id)`: the message's thread (`message_threads` joined to `threads`) is owned by a `user:`
|
||||
other than `#owner(s)`, and `#readable(s, t)` is false. A message with no thread, or in the business human's
|
||||
or the tester's thread, is never hidden. A reader session owns nothing, so every user thread is hidden from it.
|
||||
The tester reads through its reader view, and `#owner` still gives `tester`.
|
||||
- `#readable(s, t)` returns false. Rule 1 (a share in force) can't hold before 193b2's schema 3j, so nobody
|
||||
reads another's thread. 193b2 adds the share rule and rules 2-4 here.
|
||||
- `#trail`: a hidden subject returns `[]` before anything is read, exactly as an unknown id does (ruling 3).
|
||||
After the rows are collected, it drops hidden `messages` rows, `deliveries` rows of a hidden message, and
|
||||
`events` rows whose subject is a hidden message. Decisions, decision events, task snapshots, run events and
|
||||
claims stay.
|
||||
- `replies`: a hidden id fails `message-not-found`, as an unknown id does. Hidden replies are left out in SQL
|
||||
(`NOT EXISTS` a thread whose owner starts with `user:` and isn't the caller), so the `REPLIES` limit counts
|
||||
only visible rows.
|
||||
- The SQL clause doesn't call `#readable`. That is exact while `#readable` is false. 193b2 must add the share
|
||||
exception to the clause and to `#readable` together.
|
||||
|
||||
`packages/bus/tests/message-filter.test.mjs` (new). It writes a `user:alice` thread and its messages
|
||||
straight to the store, as R3's `thread.send` and the run's `to: 'user'` reply will write them, because nothing
|
||||
makes a user session before R3.
|
||||
- `trail` on a hidden id equals `trail` on an unknown id, byte for byte, for the owner, a reader and the tester.
|
||||
- `replies` on a hidden id equals `replies` on an unknown id (code and message), for all three.
|
||||
- `trail` on a user-thread run's decision shows the decision and the owner's message, and drops the user
|
||||
thread message, its delivery and its event.
|
||||
- `replies` with `REPLIES` hidden replies ahead of a visible one returns the visible one.
|
||||
- Messages in the business human's and the tester's threads stay visible on both verbs (row 177 control).
|
||||
- The governance pin, one assertion per item in the user sentence: inbox entry, claim, task, project file and
|
||||
the closed decision in `decisions {list: 'recent'}` show to the owner. `trail` and `replies` hide the user
|
||||
message, and the decision's trail carries no user body.
|
||||
|
||||
## Mutants (each must fail its named test in message-filter.test.mjs)
|
||||
|
||||
| Mutant | Edit | Named test |
|
||||
|---|---|---|
|
||||
| M1 | `#readable` returns true | governance pin: the owner sees decisions, inbox entries, claims, tasks and project files, but not the user messages |
|
||||
| M11 | the trail drop loop iterates nothing | trail on a decision keeps the decision and drops the user thread messages, their deliveries and their events |
|
||||
| M12 | the replies `NOT EXISTS` clause removed | replies leaves out other users thread messages and still fills a page |
|
||||
| M13 | trail fails `message-not-found` for a hidden subject | trail on a hidden message id answers exactly as on an unknown id, for the owner, a reader and the tester |
|
||||
| M14 | replies returns `[]` for a hidden id | replies on a hidden message id answers exactly as on an unknown id, for the owner, a reader and the tester |
|
||||
|
||||
## Gate
|
||||
|
||||
The kit is /mnt/storage/scratch/morgana-r205 (`slot205.sh`, `mutate.py`, `busy.sh`), run as
|
||||
`bash busy.sh && CAND=<sha> bash slot205.sh` on Sage's CLEAR. It runs these files whole:
|
||||
- message-filter;
|
||||
- the bus files that call trail or replies: replies, tester, tester-threads, tester-writes, broker, decisions,
|
||||
decision-close, end-launch and task-read.
|
||||
|
||||
Then it runs the bus suite, then M1 and M11-M14, each on its named test and restored after each. It uses umask
|
||||
022 and TMPDIR from `mktemp -d /mnt/storage/scratch/tmp/g205XXXX`. It refuses unless HEAD is the candidate
|
||||
and the tree is clean, and it stops after two unexpected failures (mos-0823).
|
||||
|
||||
Sage's gate adds every `scripts/test-*.sh`. The webui tests that read trail or replies run there.
|
||||
|
||||
Static check: `node --check` on broker.mjs and the new test, run from `/`. Both parse. All five mutant anchors
|
||||
match exactly once.
|
||||
@@ -3,7 +3,7 @@
|
||||
Owner Morgana, reviewer Duckworth, gate owner Sage (sage-2280). Brief: `docs/plans/2026-10-20_human-users-build.md`,
|
||||
"Brief: 193b1, viewer verbs, board refusals and the message filter" (blob a73e1bba0, rev 3088). Design:
|
||||
row 193 DESIGN.md r5 (e2d161e2e, Duckworth approved), sections 4.3, 5.4, 5.5, 5.7, 8 and 9. Base: refactor
|
||||
02da6a2ae. Draft 1, not built.
|
||||
02da6a2ae. Draft 2: Sage's rulings (sage-2288) are in section 7. Sections 1-6 are d1 as sent (morgana-0063).
|
||||
|
||||
## 1. The dependency gap
|
||||
|
||||
@@ -140,3 +140,34 @@ files, any live access.json or `~/.mosaic`.
|
||||
2. Mutant 1 as "`#readable` returns true" in 205a, and mutant 3 moved to 193b2?
|
||||
3. A hidden `trail` subject returns `[]`, the same as an unknown id, in place of 5.7's `message-not-found`
|
||||
(3.2)? `replies` already fails `message-not-found` on an unknown id, so there the code stays.
|
||||
|
||||
## 7. Rulings (sage-2288, re morgana-0063)
|
||||
|
||||
1. **Split approved.** 205a is built now: the 5.7 `trail` and `replies` filter over owner, reader and tester
|
||||
sessions, `#readable` owner-only, the owner-side governance pin test, and mutants 11 and 12. 205b comes
|
||||
after R3's candidate and R4: viewer verbs, `covers` level checks, board reads refused to users, the
|
||||
user-side pin, and mutants 6 and 10. 205a's base is origin/refactor 02da6a2ae or later. R3 builds on
|
||||
205a's candidate and extends the filter to user sessions, so the filter isn't written twice.
|
||||
2. **Mutant 1** in 205a is "`#readable` returns true", killed by the pin test. **Mutant 3** moves to 193b2
|
||||
(row 206). Sage changes the brief to match.
|
||||
3. **Hidden equals unknown.** A hidden `trail` subject returns `[]`, exactly as an unknown id does. `replies`
|
||||
keeps `message-not-found` for both. The point of 5.7 is that a caller can't tell hidden from unknown.
|
||||
So 205a adds:
|
||||
- one test per verb comparing the hidden response with the unknown-id response byte for byte
|
||||
(`JSON.stringify` of the result, or of the error's code and message), for each of the three sessions;
|
||||
- a mutant M13 that makes the hidden path distinguishable: `trail` fails `message-not-found` for a
|
||||
hidden subject. The `trail` equality test must kill it. M14 does the same for `replies`: a hidden id
|
||||
returns `[]` instead of failing. The `replies` equality test must kill it.
|
||||
|
||||
### 7.1 205a mutant table (replaces 3.5)
|
||||
|
||||
| Mutant | Edit | Named test |
|
||||
|---|---|---|
|
||||
| M1 (design 1) | `#readable` returns true | governance pin: the owner sees decisions, inbox entries, claims, tasks and project files, but not the user's messages |
|
||||
| M11 (design 11) | the `trail` filter of hidden rows removed | trail on the decision hides the user's message, its delivery and its events |
|
||||
| M12 (design 12) | the `replies` filter removed | replies drops other users' thread messages and still fills a page |
|
||||
| M13 | `trail` fails `message-not-found` for a hidden subject | trail on a hidden id answers exactly as on an unknown id |
|
||||
| M14 | `replies` returns `[]` for a hidden id | replies on a hidden id answers exactly as on an unknown id |
|
||||
|
||||
Queue: Sage re-pins 205 to the split brief and adds 205b and the R3 row at the next gap. I claim after
|
||||
that. No run before Sage's CLEAR.
|
||||
@@ -549,6 +549,22 @@ export class Broker {
|
||||
#owner(s) {
|
||||
return s.tester ? 'tester' : s.human;
|
||||
}
|
||||
// Row 205 (193b1a, #1703, row 193 design 5.7): a message in a thread a user other than the caller
|
||||
// owns is hidden from trail and replies unless #readable holds. The business human's and the
|
||||
// tester's threads are never hidden, so row 177 is unchanged. A reader owns nothing.
|
||||
#hiddenMessage(s, id) {
|
||||
const t = this.#store.get(
|
||||
'SELECT t.* FROM message_threads mt JOIN threads t ON t.id=mt.thread WHERE mt.message=? AND t.business=?',
|
||||
id,
|
||||
s.business,
|
||||
);
|
||||
return Boolean(t) && t.owner.startsWith('user:') && t.owner !== this.#owner(s) && !this.#readable(s, t);
|
||||
}
|
||||
// Row 193 design 5.4. Rule 1 needs a share in force, and there is no thread_shares table before
|
||||
// 193b2 (schema 3j), so nobody reads another's thread yet. 193b2 adds the share rule and rules 2-4.
|
||||
#readable(s, t) {
|
||||
return false;
|
||||
}
|
||||
#agent(s) {
|
||||
if (s.tester) fail('tester-refused');
|
||||
if (s.reader) fail('read-only');
|
||||
@@ -3663,10 +3679,15 @@ export class Broker {
|
||||
identifier(a.id);
|
||||
if (!this.#store.get('SELECT id FROM messages WHERE id=? AND business=?', a.id, s.business))
|
||||
fail('message-not-found');
|
||||
// Row 205 (design 5.7): a hidden id fails as an unknown one does. Hidden replies are left out
|
||||
// in the query, so a page of them doesn't come back short; #readable is false until 193b2,
|
||||
// which adds the share exception to this clause too.
|
||||
if (this.#hiddenMessage(s, a.id)) fail('message-not-found');
|
||||
return this.#store.all(
|
||||
`SELECT * FROM messages WHERE business=? AND in_reply_to=? ORDER BY seq LIMIT ${REPLIES}`,
|
||||
`SELECT * FROM messages m WHERE business=? AND in_reply_to=? AND NOT EXISTS(SELECT 1 FROM message_threads mt JOIN threads t ON t.id=mt.thread WHERE mt.message=m.id AND substr(t.owner,1,5)='user:' AND t.owner IS NOT ?) ORDER BY seq LIMIT ${REPLIES}`,
|
||||
s.business,
|
||||
a.id,
|
||||
this.#owner(s) || null,
|
||||
);
|
||||
}
|
||||
default:
|
||||
@@ -3915,6 +3936,8 @@ export class Broker {
|
||||
for (const m of rows) this.#delivery(m.id, 'routed', h);
|
||||
}
|
||||
#trail(s, subject) {
|
||||
// Row 205 (design 5.7, sage-2288): a hidden subject answers exactly as an unknown id does.
|
||||
if (this.#hiddenMessage(s, subject)) return [];
|
||||
const rows = new Map();
|
||||
const add = (table, list) => {
|
||||
for (const row of list) {
|
||||
@@ -3996,6 +4019,18 @@ export class Broker {
|
||||
this.#store.all('SELECT * FROM role_claims WHERE business=? AND holder_run=?', s.business, run),
|
||||
);
|
||||
}
|
||||
// Row 205 (design 5.7): drop hidden messages, their deliveries and the events about them.
|
||||
// Decision rows and their events stay: the run's governance is the owner's to see.
|
||||
const hidden = new Set(
|
||||
[...rows.values()].filter((r) => r.table === 'messages' && this.#hiddenMessage(s, r.id)).map((r) => r.id),
|
||||
);
|
||||
for (const [key, r] of rows)
|
||||
if (
|
||||
(r.table === 'messages' && hidden.has(r.id)) ||
|
||||
(r.table === 'deliveries' && hidden.has(r.message)) ||
|
||||
(r.table === 'events' && hidden.has(r.subject))
|
||||
)
|
||||
rows.delete(key);
|
||||
return [...rows.values()].sort(
|
||||
(a, b) => a.at.localeCompare(b.at) || a.table.localeCompare(b.table) || a.seq - b.seq,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
// Row 205 (193b1a, #1703, row 193 design 5.7, sage-2288): a user's messages are private from the
|
||||
// business owner, reader sessions and the tester on trail and replies; the run's governance is not.
|
||||
// Nothing makes a user session before FED R3, so a user's thread and its messages are written to the
|
||||
// store as R3's thread.send and the run's reply will write them.
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { Store } from '../src/store.mjs';
|
||||
import { Broker, REPLIES } from '../src/broker.mjs';
|
||||
import { fixture } from './spec-fixtures.mjs';
|
||||
|
||||
const AT = '2026-10-21T00:00:00.000Z';
|
||||
const within = (grants) => ({ authority: { withinRole: grants, crossRole: [] } });
|
||||
const options = [{ key: 'yes', text: 'Yes' }, { key: 'no', text: 'No' }];
|
||||
|
||||
// A thread owned by `owner`, on the business's assistant instance.
|
||||
function threadOf(store, project, owner, name) {
|
||||
const id = randomUUID();
|
||||
store.run(
|
||||
'INSERT INTO threads(id,at,business,project,instance,owner,name) VALUES(?,?,?,?,?,?,?)',
|
||||
id, AT, 'demo', project, 'assistant', owner, name,
|
||||
);
|
||||
return id;
|
||||
}
|
||||
// A message with its thread row, its routed delivery and one event about it.
|
||||
function put(store, { thread = null, from_role, from_run, to_role, in_reply_to = null, decision = null, body }) {
|
||||
const id = randomUUID();
|
||||
store.run(
|
||||
'INSERT INTO messages(id,at,business,from_role,from_run,to_role,class,in_reply_to,decision,corrects,body) VALUES(?,?,?,?,?,?,?,?,?,?,?)',
|
||||
id, AT, 'demo', from_role, from_run, to_role, 'INFO', in_reply_to, decision, null, body,
|
||||
);
|
||||
if (thread) store.run('INSERT INTO message_threads(message,thread) VALUES(?,?)', id, thread);
|
||||
store.run('INSERT INTO deliveries(message,at,op,holder_run,transport,address) VALUES(?,?,?,?,?,?)', id, AT, 'routed', null, null, null);
|
||||
store.run(
|
||||
'INSERT INTO events(id,at,business,kind,actor_role,actor_run,subject,body) VALUES(?,?,?,?,?,?,?,?)',
|
||||
randomUUID(), AT, 'demo', 'action.allowed', from_role, from_run, id, '{}',
|
||||
);
|
||||
return id;
|
||||
}
|
||||
// From alice to the instance, as R3's thread.send writes it.
|
||||
const fromAlice = (store, thread, body, extra = {}) =>
|
||||
put(store, { thread, from_role: 'user', from_run: 'user:alice', to_role: 'assistant', body, ...extra });
|
||||
// The run's reply to alice, as R3's `to: 'user'` path writes it.
|
||||
const toAlice = (store, thread, body, extra = {}) =>
|
||||
put(store, { thread, from_role: 'assistant', from_run: 'assistant-run', to_role: 'user', body, ...extra });
|
||||
|
||||
function setup(t) {
|
||||
const root = mkdtempSync(join(tmpdir(), 'bus-message-filter-'));
|
||||
const store = new Store(root);
|
||||
t.after(() => {
|
||||
store.close();
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
const b = new Broker({
|
||||
store,
|
||||
businesses: {
|
||||
demo: {
|
||||
id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, vars: { 'bus.tester': true },
|
||||
roles: { chief: { definition: 'chief-of-staff', ...within(['message.send']) }, assistant: { definition: 'assistant', ...within(['message.send']) } },
|
||||
},
|
||||
},
|
||||
});
|
||||
const caps = {
|
||||
human: b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }),
|
||||
reader: b.bindReader({ business: 'demo' }),
|
||||
tester: b.bindTester({ business: 'demo' }),
|
||||
};
|
||||
const call = (who, verb, args = {}) => b.request(caps[who], { verb, args });
|
||||
const project = call('human', 'project.create', { name: 'shared work' }).id;
|
||||
const U = threadOf(store, project, 'user:alice', 'alice');
|
||||
return { b, store, caps, call, project, U };
|
||||
}
|
||||
const sessions = ['human', 'reader', 'tester'];
|
||||
// What a caller sees of a call: its result, or its refusal's code and message.
|
||||
const answer = (fn) => {
|
||||
try {
|
||||
return JSON.stringify({ result: fn() });
|
||||
} catch (e) {
|
||||
return JSON.stringify({ code: e.code, message: e.message });
|
||||
}
|
||||
};
|
||||
|
||||
test('trail on a hidden message id answers exactly as on an unknown id, for the owner, a reader and the tester', (t) => {
|
||||
const { store, call, U } = setup(t);
|
||||
const A = fromAlice(store, U, 'alice: my salary is private');
|
||||
const unknown = randomUUID();
|
||||
for (const who of sessions) {
|
||||
const hidden = answer(() => call(who, 'trail', { subject: A }));
|
||||
assert.equal(hidden, answer(() => call(who, 'trail', { subject: unknown })), who);
|
||||
assert.equal(hidden, JSON.stringify({ result: [] }), who);
|
||||
}
|
||||
});
|
||||
|
||||
test('replies on a hidden message id answers exactly as on an unknown id, for the owner, a reader and the tester', (t) => {
|
||||
const { store, call, U } = setup(t);
|
||||
const A = fromAlice(store, U, 'alice: hello');
|
||||
toAlice(store, U, 'hello alice', { in_reply_to: A });
|
||||
const unknown = randomUUID();
|
||||
for (const who of sessions) {
|
||||
const hidden = answer(() => call(who, 'replies', { id: A }));
|
||||
assert.equal(hidden, answer(() => call(who, 'replies', { id: unknown })), who);
|
||||
assert.equal(JSON.parse(hidden).code, 'message-not-found', who);
|
||||
}
|
||||
});
|
||||
|
||||
test('trail on a decision keeps the decision and drops the user thread messages, their deliveries and their events', (t) => {
|
||||
const { b, store, call, U } = setup(t);
|
||||
const run = b.bindLaunch({ business: 'demo', role: 'assistant', run: 'assistant-run', harness: 'pi', thread: U });
|
||||
b.request(run, { verb: 'role.claim' });
|
||||
const d = b.request(run, {
|
||||
verb: 'decision.raise',
|
||||
args: { action: 'deploy', question: 'Ship it?', options, recommendation: 'yes', blocking: false },
|
||||
}).id;
|
||||
const hidden = toAlice(store, U, 'alice, I asked the owner about shipping', { decision: d });
|
||||
const shown = put(store, { from_role: 'assistant', from_run: 'assistant-run', to_role: 'human', body: 'Ship it, owner?', decision: d });
|
||||
for (const who of sessions) {
|
||||
const rows = call(who, 'trail', { subject: d });
|
||||
assert.ok(rows.some((r) => r.table === 'decisions' && r.id === d), `${who}: the decision row shows`);
|
||||
assert.ok(rows.some((r) => r.table === 'messages' && r.id === shown), `${who}: the owner's message shows`);
|
||||
assert.ok(rows.some((r) => r.table === 'deliveries' && r.message === shown), `${who}: its delivery shows`);
|
||||
assert.ok(rows.some((r) => r.table === 'events' && r.subject === shown), `${who}: its event shows`);
|
||||
assert.equal(rows.filter((r) => r.table === 'messages' && r.id === hidden).length, 0, `${who}: no hidden message`);
|
||||
assert.equal(rows.filter((r) => r.table === 'deliveries' && r.message === hidden).length, 0, `${who}: no hidden delivery`);
|
||||
assert.equal(rows.filter((r) => r.table === 'events' && r.subject === hidden).length, 0, `${who}: no hidden event`);
|
||||
assert.equal(JSON.stringify(rows).includes('alice, I asked'), false, `${who}: the body appears nowhere`);
|
||||
}
|
||||
});
|
||||
|
||||
test('replies leaves out other users thread messages and still fills a page', (t) => {
|
||||
const { store, call, U } = setup(t);
|
||||
const P = put(store, { from_role: 'human', from_run: 'human-cli', to_role: 'chief', body: 'Status, everyone?' });
|
||||
for (let i = 0; i < REPLIES; i++) toAlice(store, U, `hidden ${i}`, { in_reply_to: P });
|
||||
const shown = put(store, { from_role: 'chief', from_run: 'chief-run', to_role: 'human', body: 'All green.', in_reply_to: P });
|
||||
for (const who of sessions) assert.deepEqual(call(who, 'replies', { id: P }).map((m) => m.id), [shown], who);
|
||||
});
|
||||
|
||||
test('messages in the business human and tester threads are not hidden (row 177 unchanged)', (t) => {
|
||||
const { store, call, project } = setup(t);
|
||||
const H = threadOf(store, project, 'jason', 'mine');
|
||||
const T = threadOf(store, project, 'tester', 'probe');
|
||||
const h = put(store, { thread: H, from_role: 'human', from_run: 'human-cli', to_role: 'assistant', body: 'from jason' });
|
||||
const x = put(store, { thread: T, from_role: 'tester', from_run: 'tester-cli', to_role: 'assistant', body: 'from tester' });
|
||||
const hr = put(store, { thread: H, from_role: 'assistant', from_run: 'r', to_role: 'human', body: 'to jason', in_reply_to: h });
|
||||
const xr = put(store, { thread: T, from_role: 'assistant', from_run: 'r', to_role: 'tester', body: 'to tester', in_reply_to: x });
|
||||
for (const who of sessions) {
|
||||
for (const id of [h, x]) assert.ok(call(who, 'trail', { subject: id }).some((r) => r.table === 'messages' && r.id === id), `${who} ${id}`);
|
||||
assert.deepEqual(call(who, 'replies', { id: h }).map((m) => m.id), [hr], who);
|
||||
assert.deepEqual(call(who, 'replies', { id: x }).map((m) => m.id), [xr], who);
|
||||
}
|
||||
});
|
||||
|
||||
// Row 193's user sentence, one assertion per item: "The business owner sees your thread's agent's
|
||||
// decisions, inbox entries, claims, tasks and project files, but not your messages."
|
||||
test('governance pin: the owner sees decisions, inbox entries, claims, tasks and project files, but not the user messages', async (t) => {
|
||||
const f = await fixture(t, { roles: { assistant: { definition: 'assistant', ...within(['message.send', 'record.write']) } } });
|
||||
const project = (await f.call(f.human, 'project.create', { name: 'alice work' })).id;
|
||||
const U = threadOf(f.store, project, 'user:alice', 'alice');
|
||||
const run = f.broker.bindLaunch({ business: 'demo', role: 'assistant', run: 'assistant-run', harness: 'pi', thread: U });
|
||||
await f.call(run, 'role.claim');
|
||||
const A = fromAlice(f.store, U, 'alice: please write up my plan');
|
||||
toAlice(f.store, U, 'alice, done', { in_reply_to: A });
|
||||
await f.call(run, 'record.write', { project: 'stack', path: 'docs/alice-plan.md', record: '# Plan\n' });
|
||||
const d = (await f.call(run, 'decision.raise', { action: 'deploy', question: 'Ship the plan?', options, recommendation: 'yes', blocking: false })).id;
|
||||
f.store.run(
|
||||
'INSERT INTO task_snapshots(at,business,task_ref,updated,digest,fields,source,via,read_at,role,run) VALUES(?,?,?,?,?,?,?,?,?,?,?)',
|
||||
AT, 'demo', 'vikunja:3/47', AT, 'a'.repeat(64), JSON.stringify({ bucket: 1, done: false }), 'self', null, null, 'assistant', 'assistant-run',
|
||||
);
|
||||
|
||||
const inbox = await f.call(f.human, 'inbox');
|
||||
assert.ok(inbox.some((x) => x.id === d), 'inbox entries: the run raise is in the owner inbox');
|
||||
const agents = await f.call(f.human, 'agents');
|
||||
assert.ok(agents.some((c) => c.role === 'assistant' && c.holder_run === 'assistant-run'), 'claims: the run claim shows');
|
||||
const tasks = await f.call(f.human, 'tasks');
|
||||
assert.ok(tasks.some((x) => x.task_ref === 'vikunja:3/47' && x.run === 'assistant-run'), 'tasks: the run task write shows');
|
||||
const file = await f.call(f.human, 'project.file', { project: 'stack', path: 'docs/alice-plan.md' });
|
||||
assert.equal(file.content, '# Plan\n', 'project files: the run record shows');
|
||||
await f.call(f.human, 'decision.resolve', { id: d, choice: 'yes' });
|
||||
const recent = await f.call(f.human, 'decisions', { list: 'recent' });
|
||||
assert.ok(recent.some((x) => x.id === d), 'decisions: the closed raise shows in recent');
|
||||
|
||||
assert.deepEqual(await f.call(f.human, 'trail', { subject: A }), [], 'messages: trail hides the user message');
|
||||
await assert.rejects(Promise.resolve().then(() => f.call(f.human, 'replies', { id: A })), { code: 'message-not-found' }, 'messages: replies hides it');
|
||||
const trail = await f.call(f.human, 'trail', { subject: d });
|
||||
assert.ok(trail.some((r) => r.table === 'decisions' && r.id === d), 'decisions: the trail keeps the decision');
|
||||
assert.equal(JSON.stringify(trail).includes('alice'), false, 'messages: no user body on the decision trail');
|
||||
});
|
||||
Reference in new issue
Block a user