@wenathlan/getry 2.0.154 → 2.0.158

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.
@@ -0,0 +1,112 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { bumprevision, fallbackroute, markprovider, resolveroute, routesfor, routevalid } from "../llm.js";
3
+ import type { modelroute, providerconfig } from "../types.js";
4
+
5
+ const now = 1_800_000_000_000;
6
+
7
+ /** Builds one provider config fixture with every value user configured. */
8
+ function provider(id: string, over: Partial<providerconfig> = {}): providerconfig {
9
+ return {
10
+ id,
11
+ name: `The ${id} gateway`,
12
+ endpoint: `https://${id}.example/v1`,
13
+ style: "chatcompletions",
14
+ models: ["model-a", "model-b"],
15
+ status: "available",
16
+ createdat: now,
17
+ ...over,
18
+ };
19
+ }
20
+
21
+ /** Builds one model route fixture. */
22
+ function route(over: Partial<modelroute> = {}): modelroute {
23
+ return {
24
+ id: "route1",
25
+ kind: "draftplan",
26
+ providerid: "prov1",
27
+ model: "model-a",
28
+ revision: 1,
29
+ updatedat: now,
30
+ ...over,
31
+ };
32
+ }
33
+
34
+ describe("modelroute", () => {
35
+ it("resolves the provider and model for a task kind", () => {
36
+ const resolved = resolveroute({ routes: [route()], providers: [provider("prov1")], kind: "draftplan" });
37
+ expect(resolved.provider?.id).toBe("prov1");
38
+ expect(resolved.model).toBe("model-a");
39
+ });
40
+
41
+ it("reports why nothing routed for an unrouted task kind", () => {
42
+ const resolved = resolveroute({ routes: [route()], providers: [provider("prov1")], kind: "parsecommand" });
43
+ expect(resolved.provider).toBeUndefined();
44
+ expect(resolved.reason).toMatch(/No model route/i);
45
+ });
46
+
47
+ it("keeps every route of one task kind in revision order", () => {
48
+ const routes = [
49
+ route({ revision: 1 }),
50
+ route({ id: "route2", revision: 5 }),
51
+ route({ id: "route3", kind: "reflect" }),
52
+ ];
53
+ expect(routesfor(routes, "draftplan").map((entry) => entry.id)).toEqual(["route2", "route1"]);
54
+ });
55
+
56
+ it("refuses a route whose model stays outside the provider model list", () => {
57
+ const resolved = resolveroute({
58
+ routes: [route({ model: "model-z" })],
59
+ providers: [provider("prov1")],
60
+ kind: "draftplan",
61
+ });
62
+ expect(resolved.reason).toMatch(/outside the model list/i);
63
+ });
64
+
65
+ it("marks providers unavailable and available again after their calls", () => {
66
+ const providers = markprovider({
67
+ providers: [provider("prov1"), provider("prov2")],
68
+ providerid: "prov1",
69
+ available: false,
70
+ now,
71
+ });
72
+ expect(providers.find((candidate) => candidate.id === "prov1")?.status).toBe("unavailable");
73
+ expect(providers.find((candidate) => candidate.id === "prov2")?.status).toBe("available");
74
+ const resolved = resolveroute({ routes: [route()], providers, kind: "draftplan" });
75
+ expect(resolved.reason).toMatch(/unavailable/i);
76
+ const back = markprovider({ providers, providerid: "prov1", available: true, now: now + 1000 });
77
+ expect(resolveroute({ routes: [route()], providers: back, kind: "draftplan" }).provider?.id).toBe("prov1");
78
+ expect(back.find((candidate) => candidate.id === "prov1")?.lastcheckedat).toBe(now + 1000);
79
+ });
80
+
81
+ it("falls back on the user configured fallback pair when the primary provider fails", () => {
82
+ const routes = [route({ fallbackproviderid: "prov2", fallbackmodel: "model-b" })];
83
+ const providers = markprovider({
84
+ providers: [provider("prov1"), provider("prov2")],
85
+ providerid: "prov1",
86
+ available: false,
87
+ now,
88
+ });
89
+ const fallen = fallbackroute({ routes, providers, kind: "draftplan" });
90
+ expect(fallen.provider?.id).toBe("prov2");
91
+ expect(fallen.model).toBe("model-b");
92
+ });
93
+
94
+ it("reports why nothing fell back without a fallback pair or with an unavailable fallback provider", () => {
95
+ expect(fallbackroute({ routes: [route()], providers: [provider("prov1")], kind: "draftplan" }).reason).toMatch(
96
+ /no user configured fallback/i,
97
+ );
98
+ const routes = [route({ fallbackproviderid: "prov2", fallbackmodel: "model-b" })];
99
+ const providers = [provider("prov1"), provider("prov2", { status: "unavailable" })];
100
+ expect(fallbackroute({ routes, providers, kind: "draftplan" }).reason).toMatch(/unavailable/i);
101
+ });
102
+
103
+ it("validates route entries and bumps revisions for the history", () => {
104
+ expect(routevalid(route()).allowed).toBe(true);
105
+ expect(routevalid(route({ kind: " " })).allowed).toBe(false);
106
+ expect(routevalid(route({ model: "" })).allowed).toBe(false);
107
+ expect(routevalid(route({ fallbackproviderid: "prov2" })).allowed).toBe(false);
108
+ const bumped = bumprevision(route(), now + 5000);
109
+ expect(bumped.revision).toBe(2);
110
+ expect(bumped.updatedat).toBe(now + 5000);
111
+ });
112
+ });
@@ -0,0 +1,116 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ latesttemplate,
4
+ removetemplate,
5
+ rendertemplate,
6
+ savetemplate,
7
+ searchtemplates,
8
+ templatevariables,
9
+ } from "../llm.js";
10
+ import type { prompttemplate } from "../types.js";
11
+
12
+ const now = 1_800_000_000_000;
13
+
14
+ describe("promptlibrary", () => {
15
+ it("extracts the template variables of the double braced placeholders", () => {
16
+ expect(templatevariables("Summarize {{url}} in {{tone}} tone about {{url}}.")).toEqual(["url", "tone"]);
17
+ expect(templatevariables("no placeholders here")).toEqual([]);
18
+ });
19
+
20
+ it("renders a template with its variables substituted", () => {
21
+ const template: prompttemplate = {
22
+ id: "t1",
23
+ name: "pagesummary",
24
+ body: "Summarize {{url}} in a {{tone}} tone.",
25
+ variables: ["url", "tone"],
26
+ version: 1,
27
+ createdat: now,
28
+ };
29
+ const rendered = rendertemplate({ template, variables: { url: "https://example.com", tone: "plain" } });
30
+ expect(rendered.text).toBe("Summarize https://example.com in a plain tone.");
31
+ });
32
+
33
+ it("refuses the render when a declared variable stays empty", () => {
34
+ const template: prompttemplate = {
35
+ id: "t1",
36
+ name: "pagesummary",
37
+ body: "Summarize {{url}}.",
38
+ variables: ["url"],
39
+ version: 1,
40
+ createdat: now,
41
+ };
42
+ expect(rendertemplate({ template, variables: {} }).reason).toMatch(/stay empty/i);
43
+ });
44
+
45
+ it("requires the consent notice of a sensitive flow and rides it in the render", () => {
46
+ const template: prompttemplate = {
47
+ id: "t1",
48
+ name: "extract",
49
+ body: "Extract {{fields}} from the page.",
50
+ variables: ["fields"],
51
+ version: 1,
52
+ createdat: now,
53
+ };
54
+ expect(rendertemplate({ template, variables: { fields: "names" }, sensitive: true }).reason).toMatch(
55
+ /consent notice/i,
56
+ );
57
+ const rendered = rendertemplate({
58
+ template,
59
+ variables: { fields: "names" },
60
+ sensitive: true,
61
+ consentnotice: "The extraction leaves the browser only with the granted fields.",
62
+ });
63
+ expect(rendered.text).toContain("Consent notice: The extraction leaves the browser only with the granted fields.");
64
+ });
65
+
66
+ it("stores template versions with change notes and keeps the history", () => {
67
+ const first = savetemplate({
68
+ templates: [],
69
+ name: "pagesummary",
70
+ body: "Summarize {{url}}.",
71
+ notes: "first version",
72
+ now,
73
+ });
74
+ expect(first[0]?.version).toBe(1);
75
+ const second = savetemplate({
76
+ templates: first,
77
+ name: "pagesummary",
78
+ body: "Summarize {{url}} in {{tone}}.",
79
+ notes: "adds the tone",
80
+ now: now + 1000,
81
+ });
82
+ expect(second[0]?.version).toBe(2);
83
+ expect(second[1]?.version).toBe(1);
84
+ expect(second[0]?.variables).toEqual(["url", "tone"]);
85
+ expect(latesttemplate(second, "pagesummary")?.notes).toBe("adds the tone");
86
+ expect(latesttemplate(second, "unknown")).toBeUndefined();
87
+ });
88
+
89
+ it("searches the library by name, body, notes and variables", () => {
90
+ const templates = [
91
+ { id: "t1", name: "pagesummary", body: "Summarize {{url}}.", variables: ["url"], version: 1, createdat: now },
92
+ {
93
+ id: "t2",
94
+ name: "extract",
95
+ body: "Extract {{fields}}.",
96
+ variables: ["fields"],
97
+ version: 3,
98
+ notes: "the field extractor",
99
+ createdat: now + 1,
100
+ },
101
+ ];
102
+ expect(searchtemplates(templates, "summar").map((template) => template.name)).toEqual(["pagesummary"]);
103
+ expect(searchtemplates(templates, "field").map((template) => template.name)).toEqual(["extract"]);
104
+ expect(searchtemplates(templates, "")).toHaveLength(2);
105
+ });
106
+
107
+ it("removes every version of one template name", () => {
108
+ const stored = savetemplate({
109
+ templates: savetemplate({ templates: [], name: "pagesummary", body: "v1", now }),
110
+ name: "pagesummary",
111
+ body: "v2",
112
+ now: now + 1,
113
+ });
114
+ expect(removetemplate(stored, "pagesummary")).toEqual([]);
115
+ });
116
+ });
@@ -0,0 +1,84 @@
1
+ /**
2
+ * providercli — the provider account console smoke (the ownership move of
3
+ * the gateway ownership wave, step 5): the console family answers from the
4
+ * getry owner now, and this lane pins the load-bearing seams of the move —
5
+ * the help text renders, the accounts path resolver agrees with the
6
+ * accounts owner's own resolution, the opencode model definitions answer
7
+ * from the models owner with the catalog shape, and the twin deletion
8
+ * (getDefaultAccountManager behind the historical getAccountManager name)
9
+ * answers the accounts owner's real manager. No network, no browser.
10
+ */
11
+
12
+ import { describe, expect, it, vi } from "vitest";
13
+ import {
14
+ buildOpenCodeModelDefinitions,
15
+ getAccountManager,
16
+ printHelp,
17
+ providercli,
18
+ resolveAccountsPath,
19
+ runproviderfamily,
20
+ } from "../providers.js";
21
+ import { getDefaultAccountManager, resolveAccountsPath as accountsResolveAccountsPath } from "../accounts.js";
22
+ import { MODELS_2026_CATALOG } from "../models.js";
23
+
24
+ describe("providercli smoke", () => {
25
+ it("renders the help text", () => {
26
+ const logged: string[] = [];
27
+ const spy = vi.spyOn(console, "log").mockImplementation((...parts: unknown[]) => {
28
+ logged.push(parts.join(" "));
29
+ });
30
+ try {
31
+ printHelp();
32
+ } finally {
33
+ spy.mockRestore();
34
+ }
35
+ const text = logged.join("\n");
36
+ expect(text).toContain("devthink provider CLI (unified merge)");
37
+ expect(text).toContain("Usage:");
38
+ expect(text).toContain("cli.ts <command> [subcommand] [flags]");
39
+ });
40
+
41
+ it("resolves the accounts path the accounts owner resolves", () => {
42
+ expect(resolveAccountsPath()).toBe(accountsResolveAccountsPath());
43
+ expect(resolveAccountsPath()).toContain("antigravity-accounts.json");
44
+ });
45
+
46
+ it("builds the opencode model definitions with the catalog shape", () => {
47
+ const defs = buildOpenCodeModelDefinitions();
48
+ expect(defs.pluginList).toEqual(["devthink"]);
49
+ for (const entry of MODELS_2026_CATALOG) {
50
+ expect(defs.providerModels[entry.id]).toBeDefined();
51
+ expect(defs.providerModels[entry.id].antigravity.endpoint).toContain("cloudcode-pa.googleapis.com");
52
+ expect(defs.topLevelModels[`antigravity/${entry.id}`]).toBeDefined();
53
+ }
54
+ expect(Object.keys(defs.topLevelModels).length).toBeGreaterThan(MODELS_2026_CATALOG.length);
55
+ });
56
+
57
+ it("answers the account manager from the accounts owner (the twin deletion)", () => {
58
+ expect(typeof getAccountManager).toBe("function");
59
+ const mgr = getAccountManager({ disableBackgroundTimer: true });
60
+ // the twin raced the real manager on the same file; the owner answers
61
+ // the real class now — the singleton and the direct call agree
62
+ const direct = getDefaultAccountManager({ disableBackgroundTimer: true });
63
+ expect(mgr).toBe(direct);
64
+ expect(typeof mgr.listAccounts).toBe("function");
65
+ expect(typeof mgr.getValidAccessToken).toBe("function");
66
+ });
67
+
68
+ it("exposes the family entry and the providercli compat const", async () => {
69
+ expect(typeof runproviderfamily).toBe("function");
70
+ expect(typeof providercli.runCLI).toBe("function");
71
+ expect(typeof providercli.interactiveMenu).toBe("function");
72
+ // the family entry answers --help without a tty, without network
73
+ const logged: string[] = [];
74
+ const spy = vi.spyOn(console, "log").mockImplementation((...parts: unknown[]) => {
75
+ logged.push(parts.join(" "));
76
+ });
77
+ try {
78
+ await runproviderfamily(["--help"]);
79
+ } finally {
80
+ spy.mockRestore();
81
+ }
82
+ expect(logged.join("\n")).toContain("devthink provider CLI (unified merge)");
83
+ });
84
+ });
@@ -0,0 +1,134 @@
1
+ /**
2
+ * @file constants.test.ts
3
+ * @description
4
+ * Tests for the constants module of maene (`../constants.js`). Confirms the
5
+ * shape and presence of the headline catalog aggregates (`ALL_MODELS_2026`,
6
+ * `MODEL_BY_ID`, `MODEL_ROUTING`), and verifies a few well-known model ids
7
+ * from the package.json `antigravity.latest_models` manifest are present in
8
+ * the flat string catalog used by per-family arrays.
9
+ *
10
+ * Runs on the vitest runner with `node:assert/strict` assertions per the architecture skill
11
+ * ("node:* first"). bun resolves `.js` specifiers to the sibling `.ts`
12
+ * source automatically. Imports only leaf modules; does NOT import
13
+ * `plugin.ts` or `index.ts` (they pull the optional @opencode-ai/plugin
14
+ * peer SDK which may not be installed in the test environment).
15
+ */
16
+
17
+ import assert from "node:assert/strict";
18
+ import { it as test } from "vitest";
19
+ /* the os leaf jitter ceiling stays devthink-owned behind the published
20
+ * specifier — never copied into the gateway. */
21
+ import { JITTER_MAX_MS } from "@wenathlan/devthink/constants";
22
+ import { OAUTH_CALLBACK_HOST, OAUTH_CALLBACK_PATH, PROJECT_FALLBACK } from "../constants.js";
23
+ import {
24
+ ALL_MODELS_2026,
25
+ ANTIGRAVITY_MODEL_IDS_2026,
26
+ MODEL_BY_ID,
27
+ MODEL_IDS_2026_08_25,
28
+ MODEL_ROUTING,
29
+ } from "../models.js";
30
+
31
+ test("ALL_MODELS_2026 is a non-empty array of ModelDefinition entries", () => {
32
+ try {
33
+ assert.ok(Array.isArray(ALL_MODELS_2026), "ALL_MODELS_2026 must be an array");
34
+ assert.ok(ALL_MODELS_2026.length > 0, "ALL_MODELS_2026 must be non-empty");
35
+ // Note: the const is declared with a `readonly ModelDefinition[]` type
36
+ // annotation, but it is NOT wrapped in `Object.freeze(...)` at runtime.
37
+ // So it is not strictly frozen — it is only type-checked as readonly.
38
+ } catch (err) {
39
+ assert.fail(`constants shape check failed: ${(err as Error).message}`);
40
+ }
41
+ });
42
+
43
+ test("ALL_MODELS_2026 entries have the expected ModelDefinition shape", () => {
44
+ try {
45
+ for (const m of ALL_MODELS_2026) {
46
+ assert.equal(typeof m.id, "string", "model.id must be a string");
47
+ assert.ok(m.id.length > 0, "model.id must be non-empty");
48
+ assert.equal(typeof m.name, "string", "model.name must be a string");
49
+ assert.equal(typeof m.context, "number", "model.context must be a number");
50
+ assert.equal(typeof m.output, "number", "model.output must be a number");
51
+ }
52
+ } catch (err) {
53
+ assert.fail(`ALL_MODELS_2026 entry shape check failed: ${(err as Error).message}`);
54
+ }
55
+ });
56
+
57
+ test("MODEL_BY_ID is an object indexed by id and resolves known entries", () => {
58
+ try {
59
+ assert.equal(typeof MODEL_BY_ID, "object", "MODEL_BY_ID must be an object");
60
+ assert.notEqual(MODEL_BY_ID, null, "MODEL_BY_ID must not be null");
61
+
62
+ // Every ALL_MODELS_2026 id must resolve in MODEL_BY_ID.
63
+ for (const m of ALL_MODELS_2026) {
64
+ const found = (MODEL_BY_ID as Record<string, unknown>)[m.id];
65
+ assert.ok(found, `MODEL_BY_ID must contain id=${m.id}`);
66
+ }
67
+
68
+ // Specific known id present in the canonical catalog (constants.ts:934).
69
+ const opus = (MODEL_BY_ID as Record<string, any>)["antigravity-claude-opus-4-6-thinking"];
70
+ assert.ok(opus, "antigravity-claude-opus-4-6-thinking must be in MODEL_BY_ID");
71
+ assert.equal(opus.id, "antigravity-claude-opus-4-6-thinking");
72
+ } catch (err) {
73
+ assert.fail(`MODEL_BY_ID shape check failed: ${(err as Error).message}`);
74
+ }
75
+ });
76
+
77
+ test("MODEL_ROUTING exists and exposes routing for the canonical antigravity- ids", () => {
78
+ try {
79
+ assert.equal(typeof MODEL_ROUTING, "object", "MODEL_ROUTING must be an object");
80
+ assert.notEqual(MODEL_ROUTING, null, "MODEL_ROUTING must not be null");
81
+ assert.ok(Object.keys(MODEL_ROUTING).length > 0, "MODEL_ROUTING must have entries");
82
+
83
+ const known = (MODEL_ROUTING as Record<string, any>)["antigravity-gemini-3.1-pro"];
84
+ assert.ok(known, "antigravity-gemini-3.1-pro must have a routing entry");
85
+ assert.equal(typeof known.endpoint, "string", "routing.endpoint must be string");
86
+ assert.equal(typeof known.stream, "boolean", "routing.stream must be boolean");
87
+ assert.ok(known.api === "antigravity" || known.api === "gemini-cli", "routing.api must be a known provider");
88
+ } catch (err) {
89
+ assert.fail(`MODEL_ROUTING shape check failed: ${(err as Error).message}`);
90
+ }
91
+ });
92
+
93
+ test("flat string catalog exposes the bare ids referenced by package.json antigravity.latest_models", () => {
94
+ // package.json `antigravity.latest_models` includes bare ids like
95
+ // `gemini-3.1-pro`, `claude-opus-4-6-thinking`, `gemini-3.7-flash`,
96
+ // etc. Those bare ids are NOT in ALL_MODELS_2026 (which is antigravity-*
97
+ // prefixed plus preview ids) — they live in the per-family MODELS_2026_08_25
98
+ // flat string catalog, exposed via MODEL_IDS_2026_08_25.
99
+ try {
100
+ const flat = (MODEL_IDS_2026_08_25 ?? []) as readonly string[];
101
+ assert.ok(Array.isArray(flat), "MODEL_IDS_2026_08_25 must be an array");
102
+ assert.ok(flat.length > 0, "MODEL_IDS_2026_08_25 must be non-empty");
103
+
104
+ // Validate that the flat catalog exposes bare ids (not antigravity- prefixed).
105
+ assert.ok(flat.includes("gemini-3.1-pro"), "flat catalog must include gemini-3.1-pro");
106
+ assert.ok(flat.includes("claude-opus-4-6-thinking"), "flat catalog must include claude-opus-4-6-thinking");
107
+ } catch (err) {
108
+ assert.fail(`flat catalog check failed: ${(err as Error).message}`);
109
+ }
110
+ });
111
+
112
+ test("ANTIGRAVITY_MODEL_IDS_2026 is a non-empty array of string ids", () => {
113
+ try {
114
+ const ids = (ANTIGRAVITY_MODEL_IDS_2026 ?? []) as readonly string[];
115
+ assert.ok(Array.isArray(ids), "ANTIGRAVITY_MODEL_IDS_2026 must be an array");
116
+ assert.ok(ids.length > 0, "ANTIGRAVITY_MODEL_IDS_2026 must be non-empty");
117
+ for (const id of ids) {
118
+ assert.equal(typeof id, "string", "id must be string");
119
+ }
120
+ } catch (err) {
121
+ assert.fail(`ANTIGRAVITY_MODEL_IDS_2026 check failed: ${(err as Error).message}`);
122
+ }
123
+ });
124
+
125
+ test("misc constants: PROJECT_FALLBACK, OAUTH_CALLBACK_HOST/PATH, JITTER_MAX_MS", () => {
126
+ try {
127
+ assert.equal(PROJECT_FALLBACK, "rising-fact-p41fc", "PROJECT_FALLBACK must be the shared public project id");
128
+ assert.equal(OAUTH_CALLBACK_HOST, "127.0.0.1", "OAUTH_CALLBACK_HOST must be loopback");
129
+ assert.equal(OAUTH_CALLBACK_PATH, "/callback", "OAUTH_CALLBACK_PATH must be /callback");
130
+ assert.equal(JITTER_MAX_MS, 80, "JITTER_MAX_MS must be 80ms (anti-rate-limit window)");
131
+ } catch (err) {
132
+ assert.fail(`misc constants check failed: ${(err as Error).message}`);
133
+ }
134
+ });
@@ -0,0 +1,149 @@
1
+ /**
2
+ * @file fingerprint.test.ts
3
+ * @description
4
+ * Tests for the pure hashing helpers exported from the fingerprint module
5
+ * (`../fingerprint.js`). Confirms FNV-1a-32 determinism, format, and
6
+ * distinctness; also smoke-tests `generateSessionId` and the 64-bit
7
+ * variant for shape.
8
+ *
9
+ * The fingerprint module uses `node:crypto` + `node:os` + `node:path`
10
+ * only — safe to import. Does NOT touch the network.
11
+ *
12
+ * Runs on the vitest runner with `node:assert/strict` assertions. Imports only leaf modules.
13
+ */
14
+
15
+ import assert from "node:assert/strict";
16
+ import { it as test } from "vitest";
17
+
18
+ import {
19
+ fnv1a32,
20
+ fnv1a32Hex,
21
+ fnv1a64,
22
+ generateSessionUuid,
23
+ getJitterMs,
24
+ pickRandom,
25
+ secureRandom,
26
+ } from "../fingerprint.js";
27
+
28
+ test("fnv1a32Hex returns an 8-char hex string (32-bit hash)", () => {
29
+ try {
30
+ // v2.1.15 Phase B: the hex-string form is fnv1a32Hex (core.js owner);
31
+ // fnv1a32 is the numeric hash of the same bytes.
32
+ const h = fnv1a32Hex("hello");
33
+ assert.equal(typeof h, "string", "fnv1a32Hex must return a string");
34
+ assert.equal(h.length, 8, "fnv1a32Hex must return 8 hex chars (32 bits, padded)");
35
+ assert.ok(/^[0-9a-f]{8}$/.test(h), "fnv1a32Hex must match /^[0-9a-f]{8}$/");
36
+ } catch (err) {
37
+ assert.fail(`fnv1a32Hex format check failed: ${(err as Error).message}`);
38
+ }
39
+ });
40
+
41
+ test("fnv1a32 is deterministic: same input -> same output", () => {
42
+ try {
43
+ const a = fnv1a32("maene");
44
+ const b = fnv1a32("maene");
45
+ assert.equal(a, b, "same input must produce identical hash");
46
+ } catch (err) {
47
+ assert.fail(`fnv1a32 determinism check failed: ${(err as Error).message}`);
48
+ }
49
+ });
50
+
51
+ test("fnv1a32 is distinct: different inputs -> different outputs", () => {
52
+ try {
53
+ const a = fnv1a32("maene");
54
+ const b = fnv1a32("Maene"); // different case, different bytes -> different hash
55
+ const c = fnv1a32("other-input");
56
+ assert.notEqual(a, b, "case-different inputs must hash differently");
57
+ assert.notEqual(a, c, "different inputs must hash differently");
58
+ } catch (err) {
59
+ assert.fail(`fnv1a32 distinctness check failed: ${(err as Error).message}`);
60
+ }
61
+ });
62
+
63
+ test("fnv1a32 of empty string matches the FNV offset basis (FNV_OFFSET_BASIS)", () => {
64
+ // FNV-1a of empty input is the offset basis: 0x811c9dc5 = "811c9dc5".
65
+ try {
66
+ const h = fnv1a32Hex("");
67
+ assert.equal(h, "811c9dc5", "fnv1a32Hex('') must equal the FNV offset basis");
68
+ } catch (err) {
69
+ assert.fail(`fnv1a32 empty check failed: ${(err as Error).message}`);
70
+ }
71
+ });
72
+
73
+ test("fnv1a64 returns a 16-char hex string (BigInt-based)", () => {
74
+ try {
75
+ const h = fnv1a64("hello");
76
+ assert.equal(typeof h, "string");
77
+ assert.equal(h.length, 16, "fnv1a64 must return 16 hex chars (64 bits, padded)");
78
+ assert.ok(/^[0-9a-f]{16}$/.test(h), "fnv1a64 must match /^[0-9a-f]{16}$/");
79
+ } catch (err) {
80
+ assert.fail(`fnv1a64 format check failed: ${(err as Error).message}`);
81
+ }
82
+ });
83
+
84
+ test("fnv1a64 is deterministic for the same input", () => {
85
+ try {
86
+ const a = fnv1a64("maene");
87
+ const b = fnv1a64("maene");
88
+ assert.equal(a, b, "same input must produce identical 64-bit hash");
89
+ assert.notEqual(a, fnv1a64("other"), "different inputs must produce different hashes");
90
+ } catch (err) {
91
+ assert.fail(`fnv1a64 determinism check failed: ${(err as Error).message}`);
92
+ }
93
+ });
94
+
95
+ test("generateSessionUuid returns a deterministic pseudo-UUID (same dir -> same id)", () => {
96
+ try {
97
+ const a = generateSessionUuid("/home/z/maene-work/maene");
98
+ const b = generateSessionUuid("/home/z/maene-work/maene");
99
+ assert.equal(typeof a, "string");
100
+ assert.ok(a.length > 0, "session id must be non-empty");
101
+ assert.equal(a, b, "same dir must produce the same session id");
102
+ // Pseudo-UUID shape: 8-4-4-4-12-ish (the implementation slices a bit
103
+ // differently, but should contain dashes).
104
+ assert.ok(a.includes("-"), "session id must contain dashes");
105
+ } catch (err) {
106
+ assert.fail(`generateSessionId check failed: ${(err as Error).message}`);
107
+ }
108
+ });
109
+
110
+ test("generateSessionId differs across distinct directories", () => {
111
+ try {
112
+ const a = generateSessionUuid("/home/z/maene-work/maene");
113
+ const b = generateSessionUuid("/tmp/another-folder");
114
+ assert.notEqual(a, b, "different dirs must produce different session ids");
115
+ } catch (err) {
116
+ assert.fail(`generateSessionId distinctness check failed: ${(err as Error).message}`);
117
+ }
118
+ });
119
+
120
+ test("secureRandom returns a float in [0,1)", () => {
121
+ try {
122
+ const r = secureRandom();
123
+ assert.equal(typeof r, "number");
124
+ assert.ok(r >= 0 && r < 1, "secureRandom must be in [0,1)");
125
+ } catch (err) {
126
+ assert.fail(`secureRandom check failed: ${(err as Error).message}`);
127
+ }
128
+ });
129
+
130
+ test("pickRandom returns an element from the provided pool", () => {
131
+ try {
132
+ const pool = [1, 2, 3, 4, 5];
133
+ const v = pickRandom(pool);
134
+ assert.ok(pool.includes(v), "pickRandom must return an element of the pool");
135
+ } catch (err) {
136
+ assert.fail(`pickRandom check failed: ${(err as Error).message}`);
137
+ }
138
+ });
139
+
140
+ test("getJitterMs returns an integer in [min, max]", () => {
141
+ try {
142
+ const v = getJitterMs(0, 80);
143
+ assert.equal(typeof v, "number");
144
+ assert.ok(Number.isInteger(v), "getJitterMs must return an integer (Math.floor)");
145
+ assert.ok(v >= 0 && v <= 80, "getJitterMs(0,80) must be in [0,80]");
146
+ } catch (err) {
147
+ assert.fail(`getJitterMs check failed: ${(err as Error).message}`);
148
+ }
149
+ });