Merge feat/modules-name-no-node into bulk-b
This commit is contained in:
@@ -105,7 +105,7 @@ export function hasField(schema: readonly SchemaField[] | null | undefined, name
|
||||
// **Nothing loses its connection without someone seeing it.** Before Home Assistant is touched the
|
||||
// broker itself is asked whether it takes the delivered login (the provisioner creates it within
|
||||
// seconds of the grant): if not, nothing is written and the step fails saying why, and Home
|
||||
// Assistant keeps the login it has — the carried `luffy` on ace, which mosquitto keeps. If Home
|
||||
// Assistant keeps the login it has — the carried `luffy` on node-a, which mosquitto keeps. If Home
|
||||
// Assistant's own connection test refuses the new settings, the flow saves nothing, and the step
|
||||
// fails with Home Assistant's reason. A login that may not subscribe to the discovery topics is said
|
||||
// as a warning: discovery would find nothing.
|
||||
@@ -299,8 +299,8 @@ export function describe(r: { type: string; reason?: string; errors?: Record<str
|
||||
// is the bound one and which is loaded: already as the mesh says. The key needs no digest here:
|
||||
// a Servarr app has one key, so an entry loaded against the app holds the key the app took.
|
||||
// 5. A working entry at a different URL that reaches **the same app** — the same process, by the
|
||||
// app's own status (start time, data folder, version) — is left as it is and said: ace's entries
|
||||
// say `127.0.0.1:<port>` and the binding says `ace.internal:<port>`, one Sonarr either way.
|
||||
// app's own status (start time, data folder, version) — is left as it is and said: node-a's entries
|
||||
// say `127.0.0.1:<port>` and the binding says `node-a.internal:<port>`, one Sonarr either way.
|
||||
// 6. Anything else is refused, loudly, with what the operator can do; nothing is removed.
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// minted value before the operator accepts the app's) is never written.
|
||||
//
|
||||
// Home Assistant and the apps are fakes answering as the real ones do (flow shapes checked against
|
||||
// ghcr.io/home-assistant/home-assistant 2026.9.3, the build ace runs).
|
||||
// ghcr.io/home-assistant/home-assistant 2026.9.3, the build node-a runs).
|
||||
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
@@ -21,7 +21,7 @@ const PWD_NOT_CHANGED = "__**password_not_changed**__";
|
||||
const MINTED = "mesh-minted-password";
|
||||
|
||||
function mqttBinding(): Binding {
|
||||
return { provision: "mqtt-topic", from: "ace", at: "ace.internal", as: "mesh_ace_hass", serves: { scheme: "mqtt", port: 1883 } };
|
||||
return { provision: "mqtt-topic", from: "node-a", at: "node-a.internal", as: "mesh_node-a_hass", serves: { scheme: "mqtt", port: 1883 } };
|
||||
}
|
||||
|
||||
/** The MQTT reconfigure form as Home Assistant serializes it, pre-filled from an entry. */
|
||||
@@ -138,29 +138,29 @@ function memoryMarks(): Marks & { store: Map<string, string> } {
|
||||
return { store, get: async (k) => store.get(k), set: async (k, v) => void store.set(k, v) };
|
||||
}
|
||||
|
||||
const takes = (suback = 0): Probe => async (_h, _p, user, pass) => ({ connack: user === "mesh_ace_hass" && pass === MINTED ? 0 : 5, suback });
|
||||
const takes = (suback = 0): Probe => async (_h, _p, user, pass) => ({ connack: user === "mesh_node-a_hass" && pass === MINTED ? 0 : 5, suback });
|
||||
|
||||
const aceMqttEntry = () => ({
|
||||
const nodeAMqttEntry = () => ({
|
||||
entry_id: "7d1e", domain: "mqtt", state: "loaded",
|
||||
data: { broker: "127.0.0.1", port: 1883, protocol: "5", username: "luffy", password: "luffys-password" },
|
||||
});
|
||||
|
||||
test("mqtt: the broker is asked first; a login it does not take is never written", async () => {
|
||||
const f = fakeHass({ entries: { mqtt: [aceMqttEntry()] } });
|
||||
const f = fakeHass({ entries: { mqtt: [nodeAMqttEntry()] } });
|
||||
const out = await reconcileMqtt({ hass: f.hass, probe: async () => ({ connack: 5 }), marks: memoryMarks() }, mqttBinding(), MINTED);
|
||||
assert.equal(out.result, "refused");
|
||||
assert.match((out as { problem: string }).problem, /does not \(yet\) take the login mesh_ace_hass/);
|
||||
assert.match((out as { problem: string }).problem, /does not \(yet\) take the login mesh_node-a_hass/);
|
||||
assert.deepEqual(f.calls, []); // Home Assistant not even asked
|
||||
assert.equal(f.entries.mqtt[0].data.username, "luffy");
|
||||
});
|
||||
|
||||
test("mqtt: ace's entry (127.0.0.1, luffy) is moved to the bound broker and login, every other setting kept", async () => {
|
||||
const f = fakeHass({ entries: { mqtt: [aceMqttEntry()] } });
|
||||
test("mqtt: node-a's entry (127.0.0.1, luffy) is moved to the bound broker and login, every other setting kept", async () => {
|
||||
const f = fakeHass({ entries: { mqtt: [nodeAMqttEntry()] } });
|
||||
const marks = memoryMarks();
|
||||
const out = await reconcileMqtt({ hass: f.hass, probe: takes(), marks }, mqttBinding(), `${MINTED}\n`);
|
||||
assert.deepEqual(out, { what: "mqtt", result: "written", fields: ["broker", "username", "password"] });
|
||||
assert.deepEqual(f.entries.mqtt[0].data, {
|
||||
broker: "ace.internal", port: 1883, protocol: "5", username: "mesh_ace_hass", password: MINTED,
|
||||
broker: "node-a.internal", port: 1883, protocol: "5", username: "mesh_node-a_hass", password: MINTED,
|
||||
other_settings: { keepalive: 60, transport: "tcp", set_ca_cert: "off", set_client_cert: false },
|
||||
});
|
||||
assert.ok(marks.store.get("mqtt"));
|
||||
@@ -175,7 +175,7 @@ test("mqtt: ace's entry (127.0.0.1, luffy) is moved to the bound broker and logi
|
||||
});
|
||||
|
||||
test("mqtt: a new password alone is written (the digest tells)", async () => {
|
||||
const f = fakeHass({ entries: { mqtt: [aceMqttEntry()] } });
|
||||
const f = fakeHass({ entries: { mqtt: [nodeAMqttEntry()] } });
|
||||
const marks = memoryMarks();
|
||||
await reconcileMqtt({ hass: f.hass, probe: takes(), marks }, mqttBinding(), MINTED);
|
||||
const rotated: Probe = async () => ({ connack: 0, suback: 0 });
|
||||
@@ -185,7 +185,7 @@ test("mqtt: a new password alone is written (the digest tells)", async () => {
|
||||
});
|
||||
|
||||
test("mqtt: Home Assistant's own connection test refusing saves nothing and fails loudly", async () => {
|
||||
const f = fakeHass({ entries: { mqtt: [aceMqttEntry()] }, accepts: () => false });
|
||||
const f = fakeHass({ entries: { mqtt: [nodeAMqttEntry()] }, accepts: () => false });
|
||||
const marks = memoryMarks();
|
||||
const out = await reconcileMqtt({ hass: f.hass, probe: takes(), marks }, mqttBinding(), MINTED);
|
||||
assert.equal(out.result, "refused");
|
||||
@@ -199,11 +199,11 @@ test("mqtt: a fresh Home Assistant gets an entry; a grant without the discovery
|
||||
const out = await reconcileMqtt({ hass: f.hass, probe: takes(0x80), marks: memoryMarks() }, mqttBinding(), MINTED);
|
||||
assert.equal(out.result, "written");
|
||||
assert.match((out as { note?: string }).note ?? "", /may not subscribe to homeassistant\/#/);
|
||||
assert.equal(f.entries.mqtt[0].data.broker, "ace.internal");
|
||||
assert.equal(f.entries.mqtt[0].data.broker, "node-a.internal");
|
||||
});
|
||||
|
||||
test("mqtt: two entries, or a binding without a port, are refused rather than guessed", async () => {
|
||||
const f = fakeHass({ entries: { mqtt: [aceMqttEntry(), { ...aceMqttEntry(), entry_id: "other" }] } });
|
||||
const f = fakeHass({ entries: { mqtt: [nodeAMqttEntry(), { ...nodeAMqttEntry(), entry_id: "other" }] } });
|
||||
assert.equal((await reconcileMqtt({ hass: f.hass, probe: takes(), marks: memoryMarks() }, mqttBinding(), MINTED)).result, "refused");
|
||||
const noPort = { ...mqttBinding(), serves: {} };
|
||||
assert.match(((await reconcileMqtt({ hass: f.hass, probe: takes(), marks: memoryMarks() }, noPort, MINTED)) as { problem: string }).problem, /no usable port/);
|
||||
@@ -214,9 +214,9 @@ test("mqtt: two entries, or a binding without a port, are refused rather than gu
|
||||
const SONARR = APPS.find((a) => a.domain === "sonarr") as ServarrApp;
|
||||
const RADARR = APPS.find((a) => a.domain === "radarr") as ServarrApp;
|
||||
const KEY = "the-apps-own-key";
|
||||
const servarrBinding = (port: number, at = "ace.internal"): Binding => ({ provision: "sonarr-api", from: "ace", at, as: "mesh_ace_hass", serves: { scheme: "http", port, "url-base": "" } });
|
||||
const servarrBinding = (port: number, at = "node-a.internal"): Binding => ({ provision: "sonarr-api", from: "node-a", at, as: "mesh_node-a_hass", serves: { scheme: "http", port, "url-base": "" } });
|
||||
|
||||
/** One running Sonarr, answering on several addresses (127.0.0.1 and ace.internal are one host). */
|
||||
/** One running Sonarr, answering on several addresses (127.0.0.1 and node-a.internal are one host). */
|
||||
function apps(instances: Record<string, { startTime: string }>): Http & { asked: string[] } {
|
||||
const asked: string[] = [];
|
||||
return {
|
||||
@@ -231,18 +231,18 @@ function apps(instances: Record<string, { startTime: string }>): Http & { asked:
|
||||
},
|
||||
};
|
||||
}
|
||||
const oneSonarr = () => apps({ "ace.internal:8989": { startTime: "t1" }, "127.0.0.1:8989": { startTime: "t1" } });
|
||||
const oneSonarr = () => apps({ "node-a.internal:8989": { startTime: "t1" }, "127.0.0.1:8989": { startTime: "t1" } });
|
||||
const sonarrEntry = (state = "loaded") => ({ entry_id: "5a1d", domain: "sonarr", state, data: { url: "http://127.0.0.1:8989", api_key: KEY } });
|
||||
|
||||
test("servarr: the mesh's minted key is never written; the remedy names the accept", async () => {
|
||||
const f = fakeHass({ entries: { sonarr: [sonarrEntry()] } });
|
||||
const out = await reconcileApp({ hass: f.hass, http: oneSonarr() }, SONARR, servarrBinding(8989), "minted-by-the-mesh");
|
||||
assert.equal(out.result, "refused");
|
||||
assert.match((out as { problem: string }).problem, /secret accept <this node> home-assistant sonarr-api --provider ace/);
|
||||
assert.match((out as { problem: string }).problem, /secret accept <this node> home-assistant sonarr-api --provider node-a/);
|
||||
assert.deepEqual(f.calls, []);
|
||||
});
|
||||
|
||||
test("servarr: ace's entry at 127.0.0.1 reaches the same Sonarr the mesh bound at ace.internal — left, and said", async () => {
|
||||
test("servarr: node-a's entry at 127.0.0.1 reaches the same Sonarr the mesh bound at node-a.internal — left, and said", async () => {
|
||||
const f = fakeHass({ entries: { sonarr: [sonarrEntry()] }, devices: [{ id: "d", config_entries: ["5a1d"], configuration_url: "http://127.0.0.1:8989" }] });
|
||||
const out = await reconcileApp({ hass: f.hass, http: oneSonarr() }, SONARR, servarrBinding(8989), KEY);
|
||||
assert.equal(out.result, "equivalent");
|
||||
@@ -250,13 +250,13 @@ test("servarr: ace's entry at 127.0.0.1 reaches the same Sonarr the mesh bound a
|
||||
});
|
||||
|
||||
test("servarr: an entry already at the bound URL is unchanged", async () => {
|
||||
const f = fakeHass({ entries: { sonarr: [sonarrEntry()] }, devices: [{ id: "d", config_entries: ["5a1d"], configuration_url: "http://ace.internal:8989" }] });
|
||||
const f = fakeHass({ entries: { sonarr: [sonarrEntry()] }, devices: [{ id: "d", config_entries: ["5a1d"], configuration_url: "http://node-a.internal:8989" }] });
|
||||
assert.deepEqual(await reconcileApp({ hass: f.hass, http: oneSonarr() }, SONARR, servarrBinding(8989), KEY), { what: "sonarr", result: "unchanged" });
|
||||
});
|
||||
|
||||
test("servarr: a working entry that reaches a different app is refused, and nothing is removed", async () => {
|
||||
const f = fakeHass({ entries: { sonarr: [sonarrEntry()] }, devices: [{ id: "d", config_entries: ["5a1d"], configuration_url: "http://127.0.0.1:8989" }] });
|
||||
const two = apps({ "ace.internal:8989": { startTime: "t1" }, "127.0.0.1:8989": { startTime: "another" } });
|
||||
const two = apps({ "node-a.internal:8989": { startTime: "t1" }, "127.0.0.1:8989": { startTime: "another" } });
|
||||
const out = await reconcileApp({ hass: f.hass, http: two }, SONARR, servarrBinding(8989), KEY);
|
||||
assert.equal(out.result, "refused");
|
||||
assert.match((out as { problem: string }).problem, /never does/);
|
||||
@@ -267,7 +267,7 @@ test("servarr: no entry — one is made at the bound URL through the user flow",
|
||||
const f = fakeHass({ entries: {} });
|
||||
const out = await reconcileApp({ hass: f.hass, http: oneSonarr() }, SONARR, servarrBinding(8989), KEY);
|
||||
assert.deepEqual(out, { what: "sonarr", result: "written", fields: ["entry"] });
|
||||
assert.deepEqual(f.submitted[0], { url: "http://ace.internal:8989", api_key: KEY, more_options: { verify_ssl: false } });
|
||||
assert.deepEqual(f.submitted[0], { url: "http://node-a.internal:8989", api_key: KEY, more_options: { verify_ssl: false } });
|
||||
});
|
||||
|
||||
test("servarr: a reauth Home Assistant started is finished with the bound URL and key", async () => {
|
||||
@@ -276,10 +276,10 @@ test("servarr: a reauth Home Assistant started is finished with the bound URL an
|
||||
entries: { radarr: [entry] },
|
||||
reauth: [{ flow_id: "r1", handler: "radarr", step_id: "reauth_confirm", context: { source: "reauth", entry_id: "1955" } }],
|
||||
});
|
||||
const radarr = apps({ "ace.internal:7878": { startTime: "t" } });
|
||||
const radarr = apps({ "node-a.internal:7878": { startTime: "t" } });
|
||||
const out = await reconcileApp({ hass: f.hass, http: radarr }, RADARR, { ...servarrBinding(7878), provision: "radarr-api" }, KEY);
|
||||
assert.deepEqual(out, { what: "radarr", result: "written", fields: ["api_key", "url"] });
|
||||
assert.deepEqual(f.submitted[0], { url: "http://ace.internal:7878", api_key: KEY, verify_ssl: false });
|
||||
assert.deepEqual(f.submitted[0], { url: "http://node-a.internal:7878", api_key: KEY, verify_ssl: false });
|
||||
});
|
||||
|
||||
test("form values: suggested first, then default, sections nested", () => {
|
||||
@@ -287,7 +287,7 @@ test("form values: suggested first, then default, sections nested", () => {
|
||||
broker: "b", port: 1, protocol: "5", username: "u", password: PWD_NOT_CHANGED,
|
||||
other_settings: { keepalive: 60, transport: "tcp", set_ca_cert: "off", set_client_cert: false },
|
||||
});
|
||||
assert.ok(sameUrl("http://ace.internal:8989/", "http://ace.internal:8989"));
|
||||
assert.ok(sameUrl("http://ACE.internal", "http://ace.internal:80"));
|
||||
assert.ok(!sameUrl("http://127.0.0.1:8989", "http://ace.internal:8989"));
|
||||
assert.ok(sameUrl("http://node-a.internal:8989/", "http://node-a.internal:8989"));
|
||||
assert.ok(sameUrl("http://NODE-A.internal", "http://node-a.internal:80"));
|
||||
assert.ok(!sameUrl("http://127.0.0.1:8989", "http://node-a.internal:8989"));
|
||||
});
|
||||
|
||||
@@ -170,15 +170,15 @@ func clientWrites(t *testing.T) *memStore {
|
||||
if _, _, err := Publish(s); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.write(t, Needs, "g14.dunst.config.font-size", `{"at":"2026-10-14T20:00:00Z"}`)
|
||||
s.write(t, Needs, "g14.dunst.secret.token", `{}`)
|
||||
s.write(t, Needs, "shanks.dunst.config.font-size", `{}`)
|
||||
s.write(t, Needs, "shanks.gitea.bound.database.user", `{}`)
|
||||
s.write(t, Applied, "g14.dunst", `{"build":"b1","outcome":"applied"}`)
|
||||
s.write(t, Applied, "shanks.dunst", `{"build":"b1","outcome":"failed","reason":"waits for config font-size"}`)
|
||||
s.write(t, Collect, "g14.zsh-snippets.git", `{}`)
|
||||
s.write(t, Collect, "g14.bar-blocks.disk-load", `{}`)
|
||||
s.write(t, Collect, "g14.shell=3Azsh=3Anormal.mesh-cli", `{}`)
|
||||
s.write(t, Needs, "node-b.dunst.config.font-size", `{"at":"2026-10-14T20:00:00Z"}`)
|
||||
s.write(t, Needs, "node-b.dunst.secret.token", `{}`)
|
||||
s.write(t, Needs, "node-d.dunst.config.font-size", `{}`)
|
||||
s.write(t, Needs, "node-d.gitea.bound.database.user", `{}`)
|
||||
s.write(t, Applied, "node-b.dunst", `{"build":"b1","outcome":"applied"}`)
|
||||
s.write(t, Applied, "node-d.dunst", `{"build":"b1","outcome":"failed","reason":"waits for config font-size"}`)
|
||||
s.write(t, Collect, "node-b.zsh-snippets.git", `{}`)
|
||||
s.write(t, Collect, "node-b.bar-blocks.disk-load", `{}`)
|
||||
s.write(t, Collect, "node-b.shell=3Azsh=3Anormal.mesh-cli", `{}`)
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -199,16 +199,16 @@ func TestEachBucketIsReadByItsShapeAndNarrowed(t *testing.T) {
|
||||
}{
|
||||
{Query{}, []string{"access", "backup", "bound", "collected", "config", "consumers", "contribution", "dir", "environment",
|
||||
"machine", "port", "seat", "secret", "shell"}},
|
||||
{Query{What: Needs}, []string{"g14.dunst.config.font-size", "g14.dunst.secret.token",
|
||||
"shanks.dunst.config.font-size", "shanks.gitea.bound.database.user"}},
|
||||
{Query{What: Needs, Node: "g14"}, []string{"g14.dunst.config.font-size", "g14.dunst.secret.token"}},
|
||||
{Query{What: Needs, Module: "dunst", Node: "shanks"}, []string{"shanks.dunst.config.font-size"}},
|
||||
{Query{What: Applied, Module: "dunst"}, []string{"g14.dunst", "shanks.dunst"}},
|
||||
{Query{What: Applied, Node: "ace"}, nil},
|
||||
{Query{What: Collect, Collection: "zsh-snippets"}, []string{"g14.zsh-snippets.git"}},
|
||||
{Query{What: Collect, Module: "disk-load"}, []string{"g14.bar-blocks.disk-load"}},
|
||||
{Query{What: Needs}, []string{"node-b.dunst.config.font-size", "node-b.dunst.secret.token",
|
||||
"node-d.dunst.config.font-size", "node-d.gitea.bound.database.user"}},
|
||||
{Query{What: Needs, Node: "node-b"}, []string{"node-b.dunst.config.font-size", "node-b.dunst.secret.token"}},
|
||||
{Query{What: Needs, Module: "dunst", Node: "node-d"}, []string{"node-d.dunst.config.font-size"}},
|
||||
{Query{What: Applied, Module: "dunst"}, []string{"node-b.dunst", "node-d.dunst"}},
|
||||
{Query{What: Applied, Node: "node-a"}, nil},
|
||||
{Query{What: Collect, Collection: "zsh-snippets"}, []string{"node-b.zsh-snippets.git"}},
|
||||
{Query{What: Collect, Module: "disk-load"}, []string{"node-b.bar-blocks.disk-load"}},
|
||||
// An old namespace's collection, by its name, not its key's token (design 57 L1).
|
||||
{Query{What: Collect, Collection: "shell:zsh:normal"}, []string{"g14.shell=3Azsh=3Anormal.mesh-cli"}},
|
||||
{Query{What: Collect, Collection: "shell:zsh:normal"}, []string{"node-b.shell=3Azsh=3Anormal.mesh-cli"}},
|
||||
} {
|
||||
r, err := Do(s, c.q)
|
||||
if err != nil {
|
||||
@@ -218,15 +218,15 @@ func TestEachBucketIsReadByItsShapeAndNarrowed(t *testing.T) {
|
||||
t.Errorf("%+v: read %v (%d), want %v", c.q, got, r.Count, c.want)
|
||||
}
|
||||
}
|
||||
r, _ := Do(s, Query{What: Needs, Node: "shanks", Module: "gitea"})
|
||||
if row := r.Rows[0]; row.Node != "shanks" || row.Module != "gitea" || row.Kind != "bound" || row.Name != "database.user" {
|
||||
r, _ := Do(s, Query{What: Needs, Node: "node-d", Module: "gitea"})
|
||||
if row := r.Rows[0]; row.Node != "node-d" || row.Module != "gitea" || row.Kind != "bound" || row.Name != "database.user" {
|
||||
t.Errorf("a need's parts: %+v", row)
|
||||
}
|
||||
r, _ = Do(s, Query{What: Collect, Collection: "bar-blocks"})
|
||||
if row := r.Rows[0]; row.Node != "g14" || row.Collection != "bar-blocks" || row.Module != "disk-load" {
|
||||
if row := r.Rows[0]; row.Node != "node-b" || row.Collection != "bar-blocks" || row.Module != "disk-load" {
|
||||
t.Errorf("an item's parts: %+v", row)
|
||||
}
|
||||
r, _ = Do(s, Query{What: Applied, Node: "shanks"})
|
||||
r, _ = Do(s, Query{What: Applied, Node: "node-d"})
|
||||
if string(r.Rows[0].Value) != `{"build":"b1","outcome":"failed","reason":"waits for config font-size"}` {
|
||||
t.Errorf("what is applied is read as the client wrote it: %s", r.Rows[0].Value)
|
||||
}
|
||||
@@ -235,12 +235,12 @@ func TestEachBucketIsReadByItsShapeAndNarrowed(t *testing.T) {
|
||||
// A key out of its bucket's shape is said, never dropped quietly.
|
||||
func TestAKeyOutOfItsBucketsShapeIsSaid(t *testing.T) {
|
||||
s := clientWrites(t)
|
||||
s.write(t, Applied, "g14.dunst.extra", `{}`)
|
||||
s.write(t, Applied, "node-b.dunst.extra", `{}`)
|
||||
r, err := Do(s, Query{What: Applied})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r.Count != 2 || !strings.Contains(r.Said, "g14.dunst.extra") || !strings.Contains(r.Said, "<node>.<module>") {
|
||||
if r.Count != 2 || !strings.Contains(r.Said, "node-b.dunst.extra") || !strings.Contains(r.Said, "<node>.<module>") {
|
||||
t.Errorf("an unshaped key: %d rows, said %q", r.Count, r.Said)
|
||||
}
|
||||
}
|
||||
@@ -253,21 +253,21 @@ func TestTheVerbsArgumentsAreReadStrictly(t *testing.T) {
|
||||
t.Fatalf("the verb is served as %s", Tools(s)[0].Name)
|
||||
}
|
||||
for _, args := range []map[string]any{
|
||||
{"what": "needs", "caller": "g14.dunst"},
|
||||
{"what": "needs", "caller": "node-b.dunst"},
|
||||
{"node": 3},
|
||||
{"what": "secrets"},
|
||||
{"node": "g14"},
|
||||
{"node": "node-b"},
|
||||
{"what": "needs", "collection": "zsh-snippets"},
|
||||
} {
|
||||
if _, err := run(args); err == nil {
|
||||
t.Errorf("%v was taken", args)
|
||||
}
|
||||
}
|
||||
got, err := run(map[string]any{"what": "applied", "node": " g14 "})
|
||||
got, err := run(map[string]any{"what": "applied", "node": " node-b "})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r := got.(Read); r.Count != 1 || r.Said != "1 in applied for node g14" {
|
||||
if r := got.(Read); r.Count != 1 || r.Said != "1 in applied for node node-b" {
|
||||
t.Errorf("a read: %+v", r)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import assert from "node:assert/strict";
|
||||
import { filterProblem, missingAcls, parseRoleAcls, staleAcls, topicFilters, wantedAcls } from "../topics.ts";
|
||||
|
||||
test("a consumer that contributed nothing gets its own subtree", () => {
|
||||
assert.deepEqual(topicFilters({}, "mesh_ace_hass"), { ok: true, filters: ["mesh_ace_hass/#"], own: true });
|
||||
assert.deepEqual(topicFilters({}, "mesh_node_a_hass"), { ok: true, filters: ["mesh_node_a_hass/#"], own: true });
|
||||
assert.deepEqual(topicFilters(undefined, "x"), { ok: true, filters: ["x/#"], own: true });
|
||||
// Settings merge into every contribution: keys that are not `topics` change nothing.
|
||||
assert.deepEqual(topicFilters({ endpoints: { web: {} } }, "x"), { ok: true, filters: ["x/#"], own: true });
|
||||
|
||||
@@ -212,7 +212,7 @@ func TestWithTheRealResticAHandTakenBackupOutlivesTheNightTheSameDay(t *testing.
|
||||
must(t, os.Mkdir(data, 0o700))
|
||||
must(t, os.WriteFile(filepath.Join(data, "library"), []byte("before the migration\n"), 0o600))
|
||||
where := placed(t, "# plex\npath "+data+"\n")
|
||||
// A machine whose counts are full, as ace's plex was: restic keeps the oldest restore point of a period not
|
||||
// A machine whose counts are full, as node-a's plex was: restic keeps the oldest restore point of a period not
|
||||
// yet filled, which would hide the issue on a fresh repository.
|
||||
conf, err := os.ReadFile(filepath.Join(where.State, RetentionFile))
|
||||
must(t, err)
|
||||
|
||||
Reference in New Issue
Block a user