Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions core/src/host/meetings.js
Original file line number Diff line number Diff line change
Expand Up @@ -498,8 +498,12 @@ export function install(rt) {
const head = `${meeting.id} [${meeting.status}] ${meeting.question}`
const lines = [head, `positions ${positions}/${meeting.participants.length}${meeting.owner ? ` | owner ${meeting.owner}` : ''}`]
// #1831: the card's level and recipients are part of what the caller is told, and a severe
// card says in the summary why it will not advance on its own.
lines.push(`level: ${decisionLevel(meeting.level)}${(meeting.recipients ?? []).length === 0 ? '' : ` | recipients: ${(meeting.recipients ?? []).join(', ')}`}`)
// card says in the summary why it will not advance on its own. #2592: this is the projection
// every action answers through, so a stored level no writer stores — a hand edit, or a record
// from an older build — renders as `ordinary`, the way the sibling readers treat it, instead
// of throwing the whole list away. The strict validator stays on the write path.
const level = DECISION_LEVELS.includes(meeting.level) ? meeting.level : 'ordinary'
lines.push(`level: ${level}${(meeting.recipients ?? []).length === 0 ? '' : ` | recipients: ${(meeting.recipients ?? []).join(', ')}`}`)
const paused = advanceRefusal(meeting)
if (paused !== undefined) lines.push(`paused: ${paused}`)
if (meeting.dissent !== undefined) lines.push(`dissent: ${meeting.dissent.text} — ${meeting.dissent.owner}`)
Expand Down
53 changes: 53 additions & 0 deletions core/test/meetings-unknown-level.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// #2592: `summary()` is the projection every meeting action answers through, and it called the
// strict `decisionLevel()` on the stored record. A `level` no writer stores — a hand edit, or a
// record from an older build — made `meeting action=list` throw `meeting/invalid` and lose every
// record, not only the bad one. The read side now renders an unknown level as `ordinary`, the way
// `advanceRefusal()` and the board's `decisionCardOf` already read it; the write path still refuses.
import assert from 'node:assert/strict'
import { test } from 'node:test'
import { install } from '../src/host/meetings.js'

const corrupt = {
id: 'm1',
status: 'decided',
question: 'Adopt the record?',
level: 'critical',
participants: [{ name: 'Dev', position: { stance: 'agree' } }],
owner: 'Chief',
decisionFile: 'decisions/x.md',
taskIds: [],
decidedAt: 12,
updatedAt: 12,
}

const boot = (meetings = {}) => {
const rt = {
state: { meetings },
load: async () => {},
save: async () => {},
ctx: { tools: { register() {} } },
}
install(rt)
return rt
}

test('#2592: the meeting list keeps its records when a stored level does not parse', async () => {
const rt = boot({ m1: corrupt })
const list = await rt.meetingEndpoint('list')
assert.match(list, /m1 \[decided\] Adopt the record\?/)
assert.match(list, /level: ordinary/)
})

test('#2592: reading the record itself does not throw either', async () => {
const rt = boot({ m1: corrupt })
const read = await rt.meetingEndpoint('read', { meeting: 'm1' })
assert.match(read, /m1 \[decided\] Adopt the record\?/)
})

test('#2592: the write path still refuses an unknown level', async () => {
const rt = boot()
await assert.rejects(
rt.meetingEndpoint('open', { question: 'Adopt the record?', participants: ['Dev'], level: 'critical' }),
/invalid: level must be one of ordinary, major, severe/,
)
})
8 changes: 6 additions & 2 deletions patches/ds-bot/files/src/host/meetings.js
Original file line number Diff line number Diff line change
Expand Up @@ -498,8 +498,12 @@ export function install(rt) {
const head = `${meeting.id} [${meeting.status}] ${meeting.question}`
const lines = [head, `positions ${positions}/${meeting.participants.length}${meeting.owner ? ` | owner ${meeting.owner}` : ''}`]
// #1831: the card's level and recipients are part of what the caller is told, and a severe
// card says in the summary why it will not advance on its own.
lines.push(`level: ${decisionLevel(meeting.level)}${(meeting.recipients ?? []).length === 0 ? '' : ` | recipients: ${(meeting.recipients ?? []).join(', ')}`}`)
// card says in the summary why it will not advance on its own. #2592: this is the projection
// every action answers through, so a stored level no writer stores — a hand edit, or a record
// from an older build — renders as `ordinary`, the way the sibling readers treat it, instead
// of throwing the whole list away. The strict validator stays on the write path.
const level = DECISION_LEVELS.includes(meeting.level) ? meeting.level : 'ordinary'
lines.push(`level: ${level}${(meeting.recipients ?? []).length === 0 ? '' : ` | recipients: ${(meeting.recipients ?? []).join(', ')}`}`)
const paused = advanceRefusal(meeting)
if (paused !== undefined) lines.push(`paused: ${paused}`)
if (meeting.dissent !== undefined) lines.push(`dissent: ${meeting.dissent.text} — ${meeting.dissent.owner}`)
Expand Down
53 changes: 53 additions & 0 deletions patches/ds-bot/files/test/meetings-unknown-level.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// #2592: `summary()` is the projection every meeting action answers through, and it called the
// strict `decisionLevel()` on the stored record. A `level` no writer stores — a hand edit, or a
// record from an older build — made `meeting action=list` throw `meeting/invalid` and lose every
// record, not only the bad one. The read side now renders an unknown level as `ordinary`, the way
// `advanceRefusal()` and the board's `decisionCardOf` already read it; the write path still refuses.
import assert from 'node:assert/strict'
import { test } from 'node:test'
import { install } from '../src/host/meetings.js'

const corrupt = {
id: 'm1',
status: 'decided',
question: 'Adopt the record?',
level: 'critical',
participants: [{ name: 'Dev', position: { stance: 'agree' } }],
owner: 'Chief',
decisionFile: 'decisions/x.md',
taskIds: [],
decidedAt: 12,
updatedAt: 12,
}

const boot = (meetings = {}) => {
const rt = {
state: { meetings },
load: async () => {},
save: async () => {},
ctx: { tools: { register() {} } },
}
install(rt)
return rt
}

test('#2592: the meeting list keeps its records when a stored level does not parse', async () => {
const rt = boot({ m1: corrupt })
const list = await rt.meetingEndpoint('list')
assert.match(list, /m1 \[decided\] Adopt the record\?/)
assert.match(list, /level: ordinary/)
})

test('#2592: reading the record itself does not throw either', async () => {
const rt = boot({ m1: corrupt })
const read = await rt.meetingEndpoint('read', { meeting: 'm1' })
assert.match(read, /m1 \[decided\] Adopt the record\?/)
})

test('#2592: the write path still refuses an unknown level', async () => {
const rt = boot()
await assert.rejects(
rt.meetingEndpoint('open', { question: 'Adopt the record?', participants: ['Dev'], level: 'critical' }),
/invalid: level must be one of ordinary, major, severe/,
)
})
Loading