Signals: an admin chooses whether the server's TypeSafe key pays for everyone (#271)

With signals_on_server set and TYPESAFE_KEY present, the judge asks Jev
about everyone's Signals on the server's key; otherwise each person turns
Signals on in Settings with a key of their own, which is now checked with
TypeSafe before it is saved. The Signals folder shows only when they are on.

The browser suite's daemon inherited the shell's TYPESAFE_KEY, so fixture
feeds were sent to the real TypeSafe to be categorised; it is blanked now,
and an empty key counts as none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-17 13:18:03 +00:00
parent 33eb1f4db2
commit 9a41641eaa
15 changed files with 242 additions and 71 deletions

View File

@@ -199,3 +199,12 @@ test('Jev is offered the icons found for each word of a Signal', () => {
assert.deepEqual(offered('Apple'), ['brands/apple', 'solid/apple-whole']);
assert.equal(run(`signalCounts({items: 12, unread: 1})`), '12 items · 1 unread');
});
test('Signals are everyone\'s on the server\'s key, and yours on your own once a key is saved', () => {
const on = me => { run(`S.me = ${JSON.stringify(me)}`); return run('signalsOn()'); };
assert.equal(on({ signals: 'server', typesafe_key_set: false }), true);
assert.equal(on({ signals: 'own', typesafe_key_set: true }), true);
assert.equal(on({ signals: 'own', typesafe_key_set: false }), false);
assert.equal(on(null), false);
run('S.me = null');
});

View File

@@ -1143,7 +1143,8 @@ test('one action, one icon: the toolbar, the page and every dialog agree', async
() => page.locator('#content .acts [data-a="settings"]').click(),
() => page.locator('#content .acts [data-a="dl"]').click(),
() => page.locator('#content .acts [data-a="rm"]').click(),
async () => { await page.locator('#feedlist .sigs').click(); await page.locator('#content .acts [data-a="new"]').click(); },
// The Signals folder is not there without a key (#271); the dialog is the same either way.
() => page.evaluate(() => signalModal()),
];
for (const open of dialogs) {
await open();
@@ -2127,3 +2128,36 @@ test('a Signal with no key to judge it links to where one is got and where it go
await page.keyboard.press('Escape');
await page.request.delete(`/api/signals/${id}`);
});
test('Signals are turned on in Settings with a key TypeSafe takes, and off again by forgetting it', async ({ page }) => {
// #271. The test daemon has no key of its own, so each person brings theirs.
await page.request.patch('/api/me', { data: { typesafe_key: null } });
await page.reload();
await expect(page.locator('#feedlist .sigs')).toHaveCount(0);
await page.evaluate(() => prefsModal());
await expect(page.locator('#ssigkey')).toBeHidden();
await page.locator('#ssigon').check();
await page.locator('#stskey').fill('bad-key');
await page.locator('#stskey').press('Enter');
await page.locator('#stskey').blur();
await expect(page.locator('.toast', { hasText: 'TypeSafe did not accept that key' })).toBeVisible();
expect((await (await page.request.get('/api/me')).json()).typesafe_key_set).toBe(false);
await page.locator('#stskey').fill('test-key');
await page.locator('#stskey').blur();
await expect(page.locator('#feedlist .sigs')).toHaveCount(1);
page.once('dialog', d => d.accept());
await page.locator('#ssigon').uncheck();
await expect(page.locator('#feedlist .sigs')).toHaveCount(0);
expect((await (await page.request.get('/api/me')).json()).typesafe_key_set).toBe(false);
await page.keyboard.press('Escape');
});
test('the server pays for Signals only when it has a key to pay with', async ({ page }) => {
const settings = await (await page.request.get('/api/settings')).json();
expect(settings.typesafe_server_key).toBe(false);
expect((await page.request.patch('/api/settings', { data: { signals_on_server: true } })).status()).toBe(204);
expect((await (await page.request.get('/api/me')).json()).signals).toBe('own');
await page.goto('/admin#server');
await expect(page.locator('#gsig')).toBeDisabled();
await page.request.patch('/api/settings', { data: { signals_on_server: false } });
});

View File

@@ -13,6 +13,11 @@ http.createServer((req, res) => {
let body = '';
req.on('data', c => { body += c; });
req.on('end', () => {
// As TypeSafe answers a key it does not know (#271).
if (req.headers.authorization === 'Bearer bad-key') {
res.writeHead(401, { 'content-type': 'application/json' }).end('{"detail":"invalid api key"}');
return;
}
const { state, questions } = JSON.parse(body);
if (Object.values(questions).some(q => typeof q.criteria !== 'object')) {
res.writeHead(422, { 'content-type': 'application/json' }).end('{"detail":"criteria should be an object"}');
@@ -21,7 +26,8 @@ http.createServer((req, res) => {
const title = String(state).toLowerCase();
const answers = {};
for (const [id, q] of Object.entries(questions)) {
const words = /described by these words: ([^.]*)\./.exec(q.instructions)[1].split(', ');
// A key's check asks about no Signal's words.
const words = /described by these words: ([^.]*)\./.exec(q.instructions)?.[1].split(', ') ?? [];
answers[id] = { noul: words.some(w => title.includes(w.toLowerCase())) ? 0.9 : 0.1 };
}
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ answers }));