@openparachute/agent 0.2.2 → 0.2.3-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.parachute/module.json +3 -3
- package/package.json +4 -1
- package/src/agent-defs.ts +9 -0
- package/src/auth.ts +182 -14
- package/src/backends/registry.ts +65 -27
- package/src/daemon.ts +311 -12
- package/src/def-vault-triggers.ts +317 -0
- package/src/preflight.ts +139 -0
- package/src/spawn-agent.ts +16 -0
- package/src/step-up.ts +316 -0
- package/src/terminal-ui.ts +73 -0
- package/src/transports/http-ui.ts +10 -8
- package/src/transports/vault.ts +40 -22
- package/src/ui-kit.ts +6 -3
- package/src/ui-ticket.ts +121 -0
- package/web/ui/dist/assets/index-Dhr5Kl_d.css +1 -0
- package/web/ui/dist/assets/index-Di5MmFZR.js +60 -0
- package/web/ui/dist/index.html +2 -2
- package/src/_parked/interactive-spawn.test.ts +0 -324
- package/src/_parked/interactive-spawn.ts +0 -701
- package/src/agent-defs.test.ts +0 -1504
- package/src/agent-mcp-config.test.ts +0 -115
- package/src/agents.test.ts +0 -360
- package/src/auth.test.ts +0 -46
- package/src/backends/attached-queue.test.ts +0 -376
- package/src/backends/programmatic.test.ts +0 -1715
- package/src/backends/registry.test.ts +0 -1494
- package/src/backends/stream-json.test.ts +0 -570
- package/src/channel-backend-wiring.test.ts +0 -237
- package/src/credentials.test.ts +0 -274
- package/src/cron.test.ts +0 -342
- package/src/daemon-agent-def-api.test.ts +0 -166
- package/src/daemon-agent-defs-api.test.ts +0 -953
- package/src/daemon-agent-env-api.test.ts +0 -338
- package/src/daemon-attached-queue-store.test.ts +0 -65
- package/src/daemon-config-api.test.ts +0 -962
- package/src/daemon-jobs-api.test.ts +0 -271
- package/src/daemon-vault-chat.test.ts +0 -250
- package/src/daemon.test.ts +0 -746
- package/src/def-vaults.test.ts +0 -136
- package/src/delivery-state.test.ts +0 -110
- package/src/effective-env.test.ts +0 -114
- package/src/grants.test.ts +0 -638
- package/src/hub-jwt.test.ts +0 -161
- package/src/jobs.test.ts +0 -245
- package/src/mcp-http.test.ts +0 -265
- package/src/mint-token.test.ts +0 -152
- package/src/module-manifest.test.ts +0 -158
- package/src/programmatic-wiring.test.ts +0 -838
- package/src/registry.test.ts +0 -227
- package/src/resolve-port.test.ts +0 -64
- package/src/routing.test.ts +0 -184
- package/src/runner.test.ts +0 -506
- package/src/sandbox/config.test.ts +0 -150
- package/src/sandbox/egress.test.ts +0 -113
- package/src/sandbox/live-seatbelt.test.ts +0 -277
- package/src/sandbox/mounts.test.ts +0 -154
- package/src/sandbox/sandbox.test.ts +0 -168
- package/src/services-manifest.test.ts +0 -106
- package/src/spa-serve.test.ts +0 -116
- package/src/spawn-agent-cli.test.ts +0 -172
- package/src/spawn-agent.test.ts +0 -1218
- package/src/spawn-deps.test.ts +0 -54
- package/src/terminal-assets.test.ts +0 -50
- package/src/terminal.test.ts +0 -530
- package/src/transports/http-ui.test.ts +0 -455
- package/src/transports/telegram.test.ts +0 -174
- package/src/transports/vault.test.ts +0 -2011
- package/src/ui-kit.test.ts +0 -178
- package/web/ui/dist/assets/index-C-iWdFFV.css +0 -1
- package/web/ui/dist/assets/index-VFETBk0a.js +0 -60
- package/web/ui/tsconfig.json +0 -21
package/src/def-vaults.test.ts
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for the def-vault config (design 2026-06-17-vault-native-agents,
|
|
3
|
-
* Phase 4a "Commit 3"). Sandboxed to a throwaway state dir (NEVER the operator's
|
|
4
|
-
* ~/.parachute); the mint is driven by an injected fetch — deterministic, no real hub.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { describe, test, expect, afterEach } from "bun:test";
|
|
8
|
-
import { mkdtempSync, rmSync, existsSync, readFileSync, writeFileSync, statSync } from "fs";
|
|
9
|
-
import { join } from "path";
|
|
10
|
-
import { tmpdir } from "os";
|
|
11
|
-
import {
|
|
12
|
-
readDefVaultsFile,
|
|
13
|
-
writeDefVaultsFile,
|
|
14
|
-
defVaultsFilePath,
|
|
15
|
-
resolveDefVaults,
|
|
16
|
-
DEFAULT_DEF_VAULT_NAME,
|
|
17
|
-
} from "./def-vaults.ts";
|
|
18
|
-
|
|
19
|
-
const dirs: string[] = [];
|
|
20
|
-
function freshDir(): string {
|
|
21
|
-
const d = mkdtempSync(join(tmpdir(), "agent-defvaults-"));
|
|
22
|
-
dirs.push(d);
|
|
23
|
-
return d;
|
|
24
|
-
}
|
|
25
|
-
afterEach(() => {
|
|
26
|
-
for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true });
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
/** A fake hub mint endpoint returning a scripted token. */
|
|
30
|
-
function fakeMint(token: string): { fetchFn: typeof fetch; calls: Array<Record<string, unknown>> } {
|
|
31
|
-
const calls: Array<Record<string, unknown>> = [];
|
|
32
|
-
const fetchFn = (async (_url: string | URL | Request, init?: RequestInit) => {
|
|
33
|
-
calls.push(JSON.parse(String(init?.body ?? "{}")));
|
|
34
|
-
return new Response(JSON.stringify({ token, jti: "j", expires_at: "", scope: "vault:default:write" }), {
|
|
35
|
-
status: 200,
|
|
36
|
-
headers: { "content-type": "application/json" },
|
|
37
|
-
});
|
|
38
|
-
}) as typeof fetch;
|
|
39
|
-
return { fetchFn, calls };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
describe("agent-vaults.json read/write", () => {
|
|
43
|
-
test("readDefVaultsFile returns null when absent", () => {
|
|
44
|
-
expect(readDefVaultsFile(freshDir())).toBeNull();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("write then read round-trips; file is 0600", () => {
|
|
48
|
-
const dir = freshDir();
|
|
49
|
-
writeDefVaultsFile({ vaults: [{ vault: "default", vaultUrl: "http://x", token: "t" }] }, dir);
|
|
50
|
-
const back = readDefVaultsFile(dir);
|
|
51
|
-
expect(back?.vaults).toEqual([{ vault: "default", vaultUrl: "http://x", token: "t" }]);
|
|
52
|
-
const mode = statSync(defVaultsFilePath(dir)).mode & 0o777;
|
|
53
|
-
expect(mode).toBe(0o600);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test("a malformed file throws (operator error, not silently defaulted)", () => {
|
|
57
|
-
const dir = freshDir();
|
|
58
|
-
writeFileSync(defVaultsFilePath(dir), JSON.stringify({ nope: true }));
|
|
59
|
-
expect(() => readDefVaultsFile(dir)).toThrow(/"vaults" array/);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("an entry missing vault throws", () => {
|
|
63
|
-
const dir = freshDir();
|
|
64
|
-
writeFileSync(defVaultsFilePath(dir), JSON.stringify({ vaults: [{ token: "t" }] }));
|
|
65
|
-
expect(() => readDefVaultsFile(dir)).toThrow(/non-empty "vault"/);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
describe("resolveDefVaults", () => {
|
|
70
|
-
test("explicit agent-vaults.json wins (multi-vault, verbatim tokens, no mint)", async () => {
|
|
71
|
-
const dir = freshDir();
|
|
72
|
-
writeDefVaultsFile(
|
|
73
|
-
{
|
|
74
|
-
vaults: [
|
|
75
|
-
{ vault: "default", vaultUrl: "http://127.0.0.1:1940", token: "tok-default" },
|
|
76
|
-
{ vault: "research", vaultUrl: "http://127.0.0.1:1940", token: "tok-research" },
|
|
77
|
-
],
|
|
78
|
-
},
|
|
79
|
-
dir,
|
|
80
|
-
);
|
|
81
|
-
const { fetchFn, calls } = fakeMint("SHOULD-NOT-BE-USED");
|
|
82
|
-
const bindings = await resolveDefVaults({ stateDir: dir, managerBearer: "op", fetchFn });
|
|
83
|
-
expect(bindings.map((b) => b.vault)).toEqual(["default", "research"]);
|
|
84
|
-
expect(bindings[0]!.token).toBe("tok-default");
|
|
85
|
-
expect(bindings[1]!.token).toBe("tok-research");
|
|
86
|
-
// No mint when the file carries tokens.
|
|
87
|
-
expect(calls).toHaveLength(0);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test("no file + a manager bearer → mints a default vault:default:write token + persists", async () => {
|
|
91
|
-
const dir = freshDir();
|
|
92
|
-
const { fetchFn, calls } = fakeMint("minted-token");
|
|
93
|
-
const bindings = await resolveDefVaults({
|
|
94
|
-
stateDir: dir,
|
|
95
|
-
hubOrigin: "http://127.0.0.1:1939",
|
|
96
|
-
managerBearer: "operator-bearer",
|
|
97
|
-
fetchFn,
|
|
98
|
-
});
|
|
99
|
-
expect(bindings).toHaveLength(1);
|
|
100
|
-
expect(bindings[0]).toMatchObject({ vault: DEFAULT_DEF_VAULT_NAME, token: "minted-token" });
|
|
101
|
-
// Minted with the vault:default:write scope, attenuated to the operator bearer.
|
|
102
|
-
expect(calls[0]!.scope).toBe("vault:default:write");
|
|
103
|
-
// Persisted so a restart reuses it (no re-mint).
|
|
104
|
-
expect(existsSync(defVaultsFilePath(dir))).toBe(true);
|
|
105
|
-
const persisted = readDefVaultsFile(dir);
|
|
106
|
-
expect(persisted?.vaults[0]!.token).toBe("minted-token");
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test("persist:false → mints but does NOT write the file", async () => {
|
|
110
|
-
const dir = freshDir();
|
|
111
|
-
const { fetchFn } = fakeMint("minted");
|
|
112
|
-
const bindings = await resolveDefVaults({
|
|
113
|
-
stateDir: dir,
|
|
114
|
-
managerBearer: "op",
|
|
115
|
-
fetchFn,
|
|
116
|
-
persist: false,
|
|
117
|
-
});
|
|
118
|
-
expect(bindings).toHaveLength(1);
|
|
119
|
-
expect(existsSync(defVaultsFilePath(dir))).toBe(false);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test("no file + no manager bearer → NO bindings (vault-native path idle; channels.json unaffected)", async () => {
|
|
123
|
-
const bindings = await resolveDefVaults({ stateDir: freshDir(), managerBearer: null });
|
|
124
|
-
expect(bindings).toEqual([]);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
test("a mint failure → no bindings (best-effort; boot is not crashed)", async () => {
|
|
128
|
-
const dir = freshDir();
|
|
129
|
-
const fetchFn = (async () =>
|
|
130
|
-
new Response(JSON.stringify({ error: "invalid_scope" }), { status: 400 })) as unknown as typeof fetch;
|
|
131
|
-
const bindings = await resolveDefVaults({ stateDir: dir, managerBearer: "op", fetchFn });
|
|
132
|
-
expect(bindings).toEqual([]);
|
|
133
|
-
// Nothing persisted on a failed mint.
|
|
134
|
-
expect(existsSync(defVaultsFilePath(dir))).toBe(false);
|
|
135
|
-
});
|
|
136
|
-
});
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DeliveryState tests — the per-channel high-water-mark that gates backlog replay.
|
|
3
|
-
*
|
|
4
|
-
* Covered:
|
|
5
|
-
* - monotonic advance (never rewinds; reports whether it moved);
|
|
6
|
-
* - default-to-bootTime for an unknown channel (so a first connect never replays
|
|
7
|
-
* ancient history);
|
|
8
|
-
* - persist + reload across a simulated restart (a new instance reads the file);
|
|
9
|
-
* - blank-ts advance is a no-op (we never mark to an empty ts).
|
|
10
|
-
*
|
|
11
|
-
* Each test points the store at a throwaway temp dir, so there's no shared global
|
|
12
|
-
* state and no touch of the real `~/.parachute/agent/`.
|
|
13
|
-
*/
|
|
14
|
-
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
15
|
-
import { mkdtempSync, rmSync, existsSync, readFileSync, writeFileSync } from "fs";
|
|
16
|
-
import { join } from "path";
|
|
17
|
-
import { tmpdir } from "os";
|
|
18
|
-
import { DeliveryState } from "./delivery-state.ts";
|
|
19
|
-
|
|
20
|
-
let dir: string;
|
|
21
|
-
beforeEach(() => {
|
|
22
|
-
dir = mkdtempSync(join(tmpdir(), "channel-delivery-"));
|
|
23
|
-
});
|
|
24
|
-
afterEach(() => {
|
|
25
|
-
rmSync(dir, { recursive: true, force: true });
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
describe("getLastDelivered default", () => {
|
|
29
|
-
test("an unknown channel returns the boot-time default mark, not epoch", () => {
|
|
30
|
-
const boot = "2026-06-16T12:00:00.000Z";
|
|
31
|
-
const ds = new DeliveryState({ stateDir: dir, defaultMark: boot });
|
|
32
|
-
expect(ds.getLastDelivered("never-seen")).toBe(boot);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test("defaultMark defaults to ~now when omitted", () => {
|
|
36
|
-
const before = Date.now();
|
|
37
|
-
const ds = new DeliveryState({ stateDir: dir });
|
|
38
|
-
const mark = Date.parse(ds.getLastDelivered("x"));
|
|
39
|
-
const after = Date.now();
|
|
40
|
-
expect(mark).toBeGreaterThanOrEqual(before);
|
|
41
|
-
expect(mark).toBeLessThanOrEqual(after + 1000);
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
describe("advance is monotonic", () => {
|
|
46
|
-
test("moves forward + reports true; a stale/equal ts is a no-op reporting false", () => {
|
|
47
|
-
const ds = new DeliveryState({ stateDir: dir, defaultMark: "2026-01-01T00:00:00.000Z" });
|
|
48
|
-
expect(ds.advance("c", "2026-06-16T10:00:00.000Z")).toBe(true);
|
|
49
|
-
expect(ds.getLastDelivered("c")).toBe("2026-06-16T10:00:00.000Z");
|
|
50
|
-
|
|
51
|
-
// An OLDER ts must not rewind the mark.
|
|
52
|
-
expect(ds.advance("c", "2026-06-16T09:00:00.000Z")).toBe(false);
|
|
53
|
-
expect(ds.getLastDelivered("c")).toBe("2026-06-16T10:00:00.000Z");
|
|
54
|
-
|
|
55
|
-
// The SAME ts is also a no-op (strictly-greater advance).
|
|
56
|
-
expect(ds.advance("c", "2026-06-16T10:00:00.000Z")).toBe(false);
|
|
57
|
-
expect(ds.getLastDelivered("c")).toBe("2026-06-16T10:00:00.000Z");
|
|
58
|
-
|
|
59
|
-
// A NEWER ts advances again.
|
|
60
|
-
expect(ds.advance("c", "2026-06-16T11:00:00.000Z")).toBe(true);
|
|
61
|
-
expect(ds.getLastDelivered("c")).toBe("2026-06-16T11:00:00.000Z");
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test("a blank ts never advances the mark", () => {
|
|
65
|
-
const ds = new DeliveryState({ stateDir: dir, defaultMark: "2026-01-01T00:00:00.000Z" });
|
|
66
|
-
expect(ds.advance("c", "")).toBe(false);
|
|
67
|
-
expect(ds.getLastDelivered("c")).toBe("2026-01-01T00:00:00.000Z");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("per-channel marks are independent", () => {
|
|
71
|
-
const ds = new DeliveryState({ stateDir: dir, defaultMark: "2026-01-01T00:00:00.000Z" });
|
|
72
|
-
ds.advance("a", "2026-06-16T10:00:00.000Z");
|
|
73
|
-
expect(ds.getLastDelivered("a")).toBe("2026-06-16T10:00:00.000Z");
|
|
74
|
-
// b is untouched — still the default.
|
|
75
|
-
expect(ds.getLastDelivered("b")).toBe("2026-01-01T00:00:00.000Z");
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
describe("persist + reload (simulated restart)", () => {
|
|
80
|
-
test("a fresh instance reads the persisted marks (the restart case)", () => {
|
|
81
|
-
const ds1 = new DeliveryState({ stateDir: dir, defaultMark: "2026-01-01T00:00:00.000Z" });
|
|
82
|
-
ds1.advance("eng", "2026-06-16T10:00:00.000Z");
|
|
83
|
-
ds1.advance("ops", "2026-06-16T11:00:00.000Z");
|
|
84
|
-
expect(existsSync(join(dir, "delivery-state.json"))).toBe(true);
|
|
85
|
-
|
|
86
|
-
// Simulate a daemon restart: a brand-new instance with a LATER boot default.
|
|
87
|
-
// The persisted per-channel marks must win over the new default — that's what
|
|
88
|
-
// makes the replay-the-gap behavior work across a bounce.
|
|
89
|
-
const ds2 = new DeliveryState({ stateDir: dir, defaultMark: "2026-06-16T12:00:00.000Z" });
|
|
90
|
-
expect(ds2.getLastDelivered("eng")).toBe("2026-06-16T10:00:00.000Z");
|
|
91
|
-
expect(ds2.getLastDelivered("ops")).toBe("2026-06-16T11:00:00.000Z");
|
|
92
|
-
// A channel with no persisted mark still falls back to the new boot default.
|
|
93
|
-
expect(ds2.getLastDelivered("brand-new")).toBe("2026-06-16T12:00:00.000Z");
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test("the persisted file is valid JSON of channel→ts", () => {
|
|
97
|
-
const ds = new DeliveryState({ stateDir: dir, defaultMark: "2026-01-01T00:00:00.000Z" });
|
|
98
|
-
ds.advance("eng", "2026-06-16T10:00:00.000Z");
|
|
99
|
-
const onDisk = JSON.parse(readFileSync(join(dir, "delivery-state.json"), "utf8"));
|
|
100
|
-
expect(onDisk).toEqual({ eng: "2026-06-16T10:00:00.000Z" });
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test("a corrupt file is tolerated — starts empty, the default covers unknowns", () => {
|
|
104
|
-
writeFileSync(join(dir, "delivery-state.json"), "{ not json");
|
|
105
|
-
const ds = new DeliveryState({ stateDir: dir, defaultMark: "2026-06-16T12:00:00.000Z" });
|
|
106
|
-
expect(ds.getLastDelivered("eng")).toBe("2026-06-16T12:00:00.000Z");
|
|
107
|
-
// Still usable: advance + persist overwrites the corrupt file.
|
|
108
|
-
expect(ds.advance("eng", "2026-06-16T13:00:00.000Z")).toBe(true);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for the PURE effective-env composition (effective-env.ts) — the
|
|
3
|
-
* precedence/overridden logic + the no-material grant-name derivation, isolated from
|
|
4
|
-
* the daemon route + any I/O. (The route integration lives in daemon-agent-env-api.test.ts.)
|
|
5
|
-
*/
|
|
6
|
-
import { describe, test, expect } from "bun:test";
|
|
7
|
-
import {
|
|
8
|
-
approvedGrantEnvNames,
|
|
9
|
-
composeEffectiveEnv,
|
|
10
|
-
resolveEffectiveEnv,
|
|
11
|
-
} from "./effective-env.ts";
|
|
12
|
-
|
|
13
|
-
describe("approvedGrantEnvNames — names only, approved-service grants only, NO material", () => {
|
|
14
|
-
test("maps an approved service grant to its env-var name via serviceEnvVar", () => {
|
|
15
|
-
const out = approvedGrantEnvNames([{ kind: "service", target: "github", status: "approved" }]);
|
|
16
|
-
expect(out).toEqual([{ name: "GITHUB_TOKEN", source: "grant:github" }]);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test("a non-default service maps to <TARGET>_TOKEN", () => {
|
|
20
|
-
const out = approvedGrantEnvNames([{ kind: "service", target: "fireflies", status: "approved" }]);
|
|
21
|
-
expect(out).toEqual([{ name: "FIREFLIES_TOKEN", source: "grant:fireflies" }]);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test("ignores pending grants, vault grants, and mcp grants (none inject an env var)", () => {
|
|
25
|
-
const out = approvedGrantEnvNames([
|
|
26
|
-
{ kind: "service", target: "github", status: "pending" }, // not approved
|
|
27
|
-
{ kind: "vault", target: "research", status: "approved" }, // vault → MCP, not env
|
|
28
|
-
{ kind: "mcp", target: "https://x/mcp", status: "approved" }, // remote MCP, not env
|
|
29
|
-
{ kind: "service", target: "cloudflare", status: "approved" }, // the only env one
|
|
30
|
-
]);
|
|
31
|
-
expect(out).toEqual([{ name: "CLOUDFLARE_API_TOKEN", source: "grant:cloudflare" }]);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("undefined connections → empty", () => {
|
|
35
|
-
expect(approvedGrantEnvNames(undefined)).toEqual([]);
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
describe("composeEffectiveEnv — precedence channel > default > grant + overridden marking", () => {
|
|
40
|
-
const channelEnv = {
|
|
41
|
-
default: ["DEFAULT_VAR", "GITHUB_TOKEN"],
|
|
42
|
-
channels: { "uni-dev": ["CHANNEL_VAR", "GITHUB_TOKEN"] },
|
|
43
|
-
};
|
|
44
|
-
const connections = [{ kind: "service", target: "github", status: "approved" }];
|
|
45
|
-
|
|
46
|
-
test("a name set in all three layers → channel wins, default + grant marked overridden", () => {
|
|
47
|
-
const out = composeEffectiveEnv("uni-dev", channelEnv, connections);
|
|
48
|
-
const gh = out.filter((e) => e.name === "GITHUB_TOKEN");
|
|
49
|
-
expect(gh).toHaveLength(3);
|
|
50
|
-
const winner = gh.find((e) => !e.overridden)!;
|
|
51
|
-
expect(winner.source).toBe("channel");
|
|
52
|
-
expect(gh.filter((e) => e.overridden).map((e) => e.source).sort()).toEqual(["default", "grant:github"]);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("single-layer names carry no overridden flag", () => {
|
|
56
|
-
const out = composeEffectiveEnv("uni-dev", channelEnv, connections);
|
|
57
|
-
expect(out.find((e) => e.name === "DEFAULT_VAR")).toEqual({ name: "DEFAULT_VAR", source: "default" });
|
|
58
|
-
expect(out.find((e) => e.name === "CHANNEL_VAR")).toEqual({ name: "CHANNEL_VAR", source: "channel" });
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("default beats grant when no channel override exists", () => {
|
|
62
|
-
const out = composeEffectiveEnv(
|
|
63
|
-
"uni-dev",
|
|
64
|
-
{ default: ["GITHUB_TOKEN"], channels: {} },
|
|
65
|
-
connections,
|
|
66
|
-
);
|
|
67
|
-
const gh = out.filter((e) => e.name === "GITHUB_TOKEN");
|
|
68
|
-
expect(gh.find((e) => !e.overridden)!.source).toBe("default");
|
|
69
|
-
expect(gh.find((e) => e.overridden)!.source).toBe("grant:github");
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("an agent with no env-store entries + no connections → empty", () => {
|
|
73
|
-
expect(composeEffectiveEnv("nobody", { default: [], channels: {} }, undefined)).toEqual([]);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test("only the matching agent's channel layer is used (another agent's overrides are ignored)", () => {
|
|
77
|
-
const out = composeEffectiveEnv(
|
|
78
|
-
"uni-dev",
|
|
79
|
-
{ default: [], channels: { other: ["OTHER_VAR"], "uni-dev": ["MINE"] } },
|
|
80
|
-
undefined,
|
|
81
|
-
);
|
|
82
|
-
expect(out.map((e) => e.name)).toEqual(["MINE"]);
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
describe("resolveEffectiveEnv — degraded note when no def, never returns values", () => {
|
|
87
|
-
test("hasDef:false → attaches a note + still returns env layers", () => {
|
|
88
|
-
const res = resolveEffectiveEnv("uni-dev", {
|
|
89
|
-
describeEnv: () => ({ default: ["DEFAULT_VAR"], channels: { "uni-dev": ["CHANNEL_VAR"] } }),
|
|
90
|
-
hasDef: false,
|
|
91
|
-
});
|
|
92
|
-
expect(res.note).toBeDefined();
|
|
93
|
-
expect(res.env.map((e) => e.name).sort()).toEqual(["CHANNEL_VAR", "DEFAULT_VAR"]);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test("hasDef:true → no note", () => {
|
|
97
|
-
const res = resolveEffectiveEnv("uni-dev", {
|
|
98
|
-
describeEnv: () => ({ default: [], channels: {} }),
|
|
99
|
-
connections: [{ kind: "service", target: "github", status: "approved" }],
|
|
100
|
-
hasDef: true,
|
|
101
|
-
});
|
|
102
|
-
expect(res.note).toBeUndefined();
|
|
103
|
-
expect(res.env).toEqual([{ name: "GITHUB_TOKEN", source: "grant:github" }]);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test("the shape carries NO `value` field on any entry", () => {
|
|
107
|
-
const res = resolveEffectiveEnv("uni-dev", {
|
|
108
|
-
describeEnv: () => ({ default: ["A"], channels: { "uni-dev": ["B"] } }),
|
|
109
|
-
connections: [{ kind: "service", target: "github", status: "approved" }],
|
|
110
|
-
hasDef: true,
|
|
111
|
-
});
|
|
112
|
-
for (const e of res.env) expect(Object.keys(e).sort()).not.toContain("value");
|
|
113
|
-
});
|
|
114
|
-
});
|