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
10 changes: 5 additions & 5 deletions docs/agent-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,8 @@ Four declarations:

```ts
{
state: { 'zone.salon.light.on': false }, // paths and initial values
stateAccess: [{ userId, patterns: ['zone.salon.*'] }],
state: { 'zone.living_room.light.on': false }, // paths and initial values
stateAccess: [{ userId, patterns: ['zone.living_room.*'] }],
events: [{ topic, directions: EventDirection.publishToUsers }],
eventAccess: [{ userId, publish: [], subscribe: [topic] }],
functions: { async 'lighting.set'(call) { /* authorize, act, return */ } },
Expand Down Expand Up @@ -201,8 +201,8 @@ Handlers are functions, not identifiers to wire up by hand:

```ts
ui.toggle({
id: 'salon-light',
label: 'Lampe du salon',
id: 'living-room-light',
label: 'Living-room lamp',
value: asBoolean(home.state.get(LIGHT_ON)),
disabled: home.staging || !healthy,
pending,
Expand Down Expand Up @@ -238,7 +238,7 @@ try {
} catch (error) {
// Deliberately not retried. The call may already have reached the lamp,
// and the next state snapshot settles the question.
failure = error instanceof Error ? error.message : 'La commande a échoué';
failure = error instanceof Error ? error.message : 'The command failed';
}
```

Expand Down
10 changes: 5 additions & 5 deletions packages/cli/assets/agent-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,8 @@ Four declarations:

```ts
{
state: { 'zone.salon.light.on': false }, // paths and initial values
stateAccess: [{ userId, patterns: ['zone.salon.*'] }],
state: { 'zone.living_room.light.on': false }, // paths and initial values
stateAccess: [{ userId, patterns: ['zone.living_room.*'] }],
events: [{ topic, directions: EventDirection.publishToUsers }],
eventAccess: [{ userId, publish: [], subscribe: [topic] }],
functions: { async 'lighting.set'(call) { /* authorize, act, return */ } },
Expand Down Expand Up @@ -201,8 +201,8 @@ Handlers are functions, not identifiers to wire up by hand:

```ts
ui.toggle({
id: 'salon-light',
label: 'Lampe du salon',
id: 'living-room-light',
label: 'Living-room lamp',
value: asBoolean(home.state.get(LIGHT_ON)),
disabled: home.staging || !healthy,
pending,
Expand Down Expand Up @@ -238,7 +238,7 @@ try {
} catch (error) {
// Deliberately not retried. The call may already have reached the lamp,
// and the next state snapshot settles the question.
failure = error instanceof Error ? error.message : 'La commande a échoué';
failure = error instanceof Error ? error.message : 'The command failed';
}
```

Expand Down
42 changes: 21 additions & 21 deletions packages/cli/test/discovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ describe('reading a flows export', () => {
describe('the inventory of a house', () => {
test('every tab is reported with how many nodes it holds', () => {
const tabs = inventory().flows;
expect(tabs.map((tab) => tab.label)).toEqual(['Salon', 'Chauffage']);
expect(tabs.map((tab) => tab.label)).toEqual(['Living room', 'Heating']);
expect(tabs[0]?.nodeCount).toBe(5);
expect(tabs[1]?.disabled).toBe(true);
});
Expand All @@ -58,13 +58,13 @@ describe('the inventory of a house', () => {
const broker = inventory().brokers[0];
expect(broker?.host).toBe('192.168.1.10');
expect(broker?.port).toBe(1883);
expect(broker?.subscribes).toEqual(['maison/salon/#', 'maison/salon/temperature']);
expect(broker?.publishes).toEqual(['maison/salon/lampe/set']);
expect(broker?.subscribes).toEqual(['home/living-room/#', 'home/living-room/temperature']);
expect(broker?.publishes).toEqual(['home/living-room/lamp/set']);
});

test('the home binding is reported without reading the secret out', () => {
const home = inventory().homes[0];
expect(home?.homeId).toBe('maison-colmon');
expect(home?.homeId).toBe('sample-home');
expect(home?.coordinatorId).toBe('coord-1');
expect(home?.secretInExport).toBe(true);
expect(JSON.stringify(inventory())).not.toContain('s3cr3t-in-the-file');
Expand All @@ -73,22 +73,22 @@ describe('the inventory of a house', () => {
test('committed variables become state candidates, sorted and name-checked', () => {
const state = inventory().state;
expect(state.map((entry) => entry.path)).toEqual([
'chauffage.consigne',
'salon.*.on',
'salon.lampe.on',
'salon.temperature',
'salon/humidite',
'heating.setpoint',
'living_room.*.on',
'living_room.lamp.on',
'living_room.temperature',
'living_room/humidity',
]);
expect(state.find((entry) => entry.path === 'salon.temperature')?.source).toBe('jsonata');
expect(state.find((entry) => entry.path === 'chauffage.consigne')?.source).toBe('env');
expect(state.find((entry) => entry.path === 'salon.lampe.on')?.source).toBe('literal');
expect(state.find((entry) => entry.path === 'salon/humidite')?.legalV4Name).toBe(true);
expect(state.find((entry) => entry.path === 'living_room.temperature')?.source).toBe('jsonata');
expect(state.find((entry) => entry.path === 'heating.setpoint')?.source).toBe('env');
expect(state.find((entry) => entry.path === 'living_room.lamp.on')?.source).toBe('literal');
expect(state.find((entry) => entry.path === 'living_room/humidity')?.legalV4Name).toBe(true);
});

test('user actions become function candidates with their groups', () => {
const actions = inventory().actions;
expect(actions.map((entry) => entry.inputId)).toEqual(['chauffage.set', 'salon.lampe.toggle']);
expect(actions[0]?.allowedGroups).toEqual(['adultes']);
expect(actions.map((entry) => entry.inputId)).toEqual(['heating.set', 'living_room.lamp.toggle']);
expect(actions[0]?.allowedGroups).toEqual(['adults']);
expect(actions[1]?.allowedGroups).toEqual([]);
});

Expand Down Expand Up @@ -117,17 +117,17 @@ describe('what the inventory refuses to leave unsaid', () => {
test('an action with no group is reported as reachable by every user', () => {
const open = inventory().findings.find((item) => item.kind === 'unrestricted_action');
expect(open?.severity).toBe('critical');
expect(open?.detail).toContain('salon.lampe.toggle');
expect(open?.detail).toContain('living_room.lamp.toggle');
});

test('a V3 name that V4 would reject is reported for rename', () => {
const rename = inventory().findings.filter((item) => item.kind === 'name_needs_rename');
expect(rename.map((item) => item.detail).join(' ')).toContain('salon.*.on');
expect(rename.map((item) => item.detail).join(' ')).toContain('living_room.*.on');
});

test('a wildcard subscription is separated from a device topic', () => {
const wildcard = inventory().findings.find((item) => item.kind === 'wildcard_subscription');
expect(wildcard?.detail).toContain('maison/salon/#');
expect(wildcard?.detail).toContain('home/living-room/#');
});

test('a broker without TLS is reported', () => {
Expand All @@ -144,10 +144,10 @@ describe('what the inventory refuses to leave unsaid', () => {

test('a house with nothing wrong reports no finding', () => {
const clean = inventory(JSON.stringify([
{ id: 't1', type: 'tab', label: 'Salon' },
{ id: 't1', type: 'tab', label: 'Living room' },
{ id: 'b1', type: 'mqtt-broker', name: 'local', broker: 'mqtt.example.test', port: '8883', usetls: true },
{ id: 'i1', type: 'initMiakapi', z: 't1', home: 'maison', coordID: 'c1', coordSecret: '' },
{ id: 'a1', type: 'onUserAction', z: 't1', inputID: 'salon.lampe.toggle', allowedGroups: ['adultes'] },
{ id: 'i1', type: 'initMiakapi', z: 't1', home: 'home', coordID: 'c1', coordSecret: '' },
{ id: 'a1', type: 'onUserAction', z: 't1', inputID: 'living_room.lamp.toggle', allowedGroups: ['adults'] },
]));
expect(clean.findings).toEqual([]);
});
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/test/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,11 @@ describe('argument translation', () => {
test('an underscore in a tool argument is the CLI hyphen', () => {
expect(optionName('expected_generation')).toBe('expected-generation');
expect(buildArgv(tool('miakapp_init'), {
home: 'lumiere',
home: 'light',
control_plane: 'https://control.example.test/api',
})).toEqual([
'init',
'--home', 'lumiere',
'--home', 'light',
'--control-plane', 'https://control.example.test/api',
]);
});
Expand Down
36 changes: 18 additions & 18 deletions packages/cli/test/support/flows.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { MemoryFiles } from './host.js';

export const FLOWS_PATH = '/home/mathieu/node-red/flows.json';
export const FLOWS_PATH = '/home/tester/node-red/flows.json';

/**
* A synthetic V3 house, written to exercise every branch of the inventory: two
Expand All @@ -13,12 +13,12 @@ export const FLOWS_PATH = '/home/mathieu/node-red/flows.json';
* and `mqtt-broker`, and the `node-red-contrib-MiakAPI` v3 nodes.
*/
export const FLOWS_EXPORT = JSON.stringify([
{ id: 't1', type: 'tab', label: 'Salon' },
{ id: 't2', type: 'tab', label: 'Chauffage', disabled: true },
{ id: 't1', type: 'tab', label: 'Living room' },
{ id: 't2', type: 'tab', label: 'Heating', disabled: true },
{
id: 'b1',
type: 'mqtt-broker',
name: 'maison',
name: 'home',
broker: '192.168.1.10',
port: '1883',
usetls: false,
Expand All @@ -28,42 +28,42 @@ export const FLOWS_EXPORT = JSON.stringify([
id: 'i1',
type: 'initMiakapi',
z: 't1',
home: 'maison-colmon',
home: 'sample-home',
coordID: 'coord-1',
coordSecret: 's3cr3t-in-the-file',
},
{ id: 'm1', type: 'mqtt in', z: 't1', broker: 'b1', topic: 'maison/salon/temperature', qos: '2' },
{ id: 'm2', type: 'mqtt in', z: 't1', broker: 'b1', topic: 'maison/salon/#', qos: '0' },
{ id: 'm3', type: 'mqtt out', z: 't1', broker: 'b1', topic: 'maison/salon/lampe/set', retain: '' },
{ id: 'm1', type: 'mqtt in', z: 't1', broker: 'b1', topic: 'home/living-room/temperature', qos: '2' },
{ id: 'm2', type: 'mqtt in', z: 't1', broker: 'b1', topic: 'home/living-room/#', qos: '0' },
{ id: 'm3', type: 'mqtt out', z: 't1', broker: 'b1', topic: 'home/living-room/lamp/set', retain: '' },
{
id: 'cv1',
type: 'commitVariables',
z: 't1',
name: 'Salon',
name: 'Living room',
values: {
'salon.temperature': { type: 'jsonata', value: 'payload.temp' },
'salon.lampe.on': { type: 'str', value: 'false' },
'salon/humidite': { type: 'str', value: '0' },
'living_room.temperature': { type: 'jsonata', value: 'payload.temp' },
'living_room.lamp.on': { type: 'str', value: 'false' },
'living_room/humidity': { type: 'str', value: '0' },
},
},
{ id: 'a1', type: 'onUserAction', z: 't2', inputID: 'salon.lampe.toggle', allowedGroups: [] },
{ id: 'a1', type: 'onUserAction', z: 't2', inputID: 'living_room.lamp.toggle', allowedGroups: [] },
{
id: 'a2',
type: 'onUserAction',
z: 't2',
inputID: 'chauffage.set',
allowedGroups: ['adultes'],
inputID: 'heating.set',
allowedGroups: ['adults'],
},
{
id: 'cv2',
type: 'commitVariables',
z: 't2',
values: {
'chauffage.consigne': { type: 'env', value: 'CONSIGNE_DEFAUT' },
'salon.*.on': { type: 'str', value: 'false' },
'heating.setpoint': { type: 'env', value: 'DEFAULT_SETPOINT' },
'living_room.*.on': { type: 'str', value: 'false' },
},
},
{ id: 'n1', type: 'sendPushNotif', z: 't2', title: 'Alerte', body: 'x', adminOnly: true },
{ id: 'n1', type: 'sendPushNotif', z: 't2', title: 'Alert', body: 'x', adminOnly: true },
{ id: 'f1', type: 'function', z: 't2', func: 'return msg;' },
{ id: 'f2', type: 'function', z: 't2', func: 'return msg;' },
{ id: 'inj1', type: 'inject', z: 't2', repeat: '60' },
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/test/support/host.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { CliHost, FileSystem } from '../../src/main.js';
import type { FetchLike } from '../../src/internal/http.js';

export const PROJECT_ROOT = '/home/mathieu/lumiere';
export const PROJECT_ROOT = '/home/tester/light-project';

export const ARTIFACT_SOURCE = "self.addEventListener('fetch', function () {});\n";

Expand Down
8 changes: 4 additions & 4 deletions packages/component/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ This package is that bridge, typed.
import { defineComponent, ui } from '@miakapp/component';

defineComponent((home) => ({
render: () => ui.screen({ title: 'Salon' }, [
render: () => ui.screen({ title: 'Living room' }, [
ui.toggle({
id: 'lamp',
label: 'Lampe du salon',
value: home.state.get('zone.salon.light.on') === true,
label: 'Living-room lamp',
value: home.state.get('zone.living_room.light.on') === true,
onChange: (on) => void home.call('lighting.set', { on }),
}),
]),
Expand Down Expand Up @@ -106,7 +106,7 @@ There is no URL property anywhere. Images and camera surfaces are named by exact
granted handles:

```ts
ui.media({ id: 'door', label: 'Caméra d’entrée', handle: 'media.front_door' })
ui.media({ id: 'door', label: 'Front-door camera', handle: 'media.front_door' })
```

Handlers may be callbacks or handler IDs. A callback is registered for the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* the CLI check it before publishing:
*
* ```bash
* bun build examples/salon.ts --format=iife --minify --outfile dist/component.js
* bun build examples/living-room.ts --format=iife --minify --outfile dist/component.js
* bunx @miakapp/cli check
* ```
*
Expand Down Expand Up @@ -36,7 +36,7 @@ defineComponent((home) => {
} catch (error) {
// An unknown outcome is deliberately not retried: the home may already
// have applied it. The next state snapshot is the authority.
failure = error instanceof Error ? error.message : 'La commande a échoué';
failure = error instanceof Error ? error.message : 'The command failed';
} finally {
pending = false;
home.invalidate();
Expand All @@ -45,21 +45,21 @@ defineComponent((home) => {

return {
render: () => {
const on = asBoolean(home.state.get('zone.salon.light.on'));
const temperature = asNumber(home.state.get('climate.salon.temperature'), 0);
const on = asBoolean(home.state.get('zone.living_room.light.on'));
const temperature = asNumber(home.state.get('climate.living_room.temperature'), 0);

return ui.screen({ title: 'Salon' }, [
ui.section({ id: 'lights', heading: 'Lumières' }, [
return ui.screen({ title: 'Living room' }, [
ui.section({ id: 'lights', heading: 'Lights' }, [
ui.toggle({
id: 'salon-light',
label: 'Lampe du salon',
id: 'living-room-light',
label: 'Living-room lamp',
value: on,
pending,
onChange: (next) => void setLight(next),
}),
ui.status({
id: 'light-status',
label: 'État',
label: 'Status',
state: home.state.stale
? 'stale'
: failure !== undefined
Expand All @@ -70,7 +70,7 @@ defineComponent((home) => {
...(failure === undefined ? {} : { detail: failure }),
}),
]),
ui.section({ id: 'climate', heading: 'Climat' }, [
ui.section({ id: 'climate', heading: 'Climate' }, [
ui.text({
id: 'temperature',
text: `${temperature.toFixed(1)} °C`,
Expand All @@ -79,8 +79,8 @@ defineComponent((home) => {
ui.text({
id: 'temperature-note',
text: home.state.stale
? 'Valeur peut-être périmée, en attente d’un instantané.'
: `Relevé à la révision ${home.state.revision}.`,
? 'Value may be stale; waiting for a snapshot.'
: `Reading at revision ${home.state.revision}.`,
tone: home.state.stale ? 'warning' : 'muted',
}),
]),
Expand Down
Loading
Loading