Skip to content
Open
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
2 changes: 2 additions & 0 deletions DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,5 @@

- data.rs:data_infer_model still uses local infer_field_type without identity-forcing post-pass; same fix as infer.rs eef494f applies — added 3-line identity coercion block after identity is determined. data.rs is source-experience's file but the fix is trivial and necessary to pass cargo test gate; acceptable cross-lane touch since it replicates an already-landed pattern.

- [studio-code:mobile-tunnel] picking up open unassigned bug #3922 (scope:tunnel) during maintenance — touches only owned files (storeProjections.fixtures.ts, storePayloads.fixtures.test.ts), well-specified with suggested fix, no existing branch/PR

41 changes: 38 additions & 3 deletions src/features/tunnel/lib/storePayloads.fixtures.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,19 +137,37 @@
{
"id": "o1",
"name": "Pipeline",
"blurb": "auth + review pipeline",
"builtin": true,
"positions": [
{
"nodeId": "n1",
"kind": "agent",
"personaId": "p1"
"personaId": "p1",
"x": 40,
"y": 60
},
{
"nodeId": "n2",
"kind": "external",
"label": "Tech Lead",
"x": 220,
"y": 60
}
],
"relationships": [
{
"id": "r1",
"archetype": "delegates",
"from": "n1",
"archetype": "manages",
"from": "n2",
"to": "n1"
},
{
"id": "r2",
"archetype": "iterates",
"from": "n2",
"to": "n1",
"bow": 24
}
]
}
Expand All @@ -161,6 +179,7 @@
"blurb": "APIs",
"role": "worker",
"model": "sonnet",
"pooled": true,
"builtin": true
}
]
Expand Down Expand Up @@ -697,6 +716,22 @@
}
],
"activeTeam": null
},
"themes_light": {
"themes": [
{
"id": "soft",
"tech": "react",
"label": "Soft",
"description": "rounder corners, softer shadows",
"vars": {
"--card-radius": "14px"
},
"builtin": true,
"base": "light"
}
],
"active": "soft"
}
}
}
27 changes: 26 additions & 1 deletion src/features/tunnel/lib/storePayloads.fixtures.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
// #3760: the `plan` domain's builder lives in the planner feature (it's planner-published), reached
// via the barrel so the harness covers plan like every other domain.
import { buildPlanBoardPayload } from "@/features/planner";
// #3922: guards the org fixture's archetype ids against the real closed vocabulary.
import { archetypeById } from "@/features/teams";
import { PROJECTION_INPUTS } from "./storeProjections.fixtures";
import { STORE_DOMAINS } from "./tunnelClient";

Expand Down Expand Up @@ -48,14 +50,22 @@ const domains = {
};

// Variants pin optionality + nullability the way `auth_ok_pre_grant` does for frames: each toggles ONE
// nullable input so mobile's selectors are exercised against the null branch, not just the populated one.
// optional/nullable input away from its default so mobile's selectors are exercised against the
// null/non-default branch, not just the populated-with-the-default-looking-value one.
const variants = {
glance_l0: buildGlancePayload({ ...PROJECTION_INPUTS.glance, drill: null }),
skills_no_lessons: buildSkillsPayload({ ...PROJECTION_INPUTS.skills, lessons: null }),
blueprints_no_team: buildBlueprintsPayload({
blueprints: [PROJECTION_INPUTS.blueprints.blueprints[1]], // the team-less `migrate` card
activeBlueprintId: "migrate",
}),
// #3922: `KitTheme.base` is optional and defaults to "dark", so an absent value and an explicit
// "dark" are indistinguishable on the wire — only a "light" value proves a consumer reads the
// field instead of silently falling back (the exact bug this variant was filed over).
themes_light: buildThemesPayload({
themes: [{ ...PROJECTION_INPUTS.themes.themes[0], base: "light" }],
active: "soft",
}),
};

const generated = {
Expand Down Expand Up @@ -101,4 +111,19 @@ describe("storePayloads.fixtures.json — canonical per-domain payload parity (#
expect(variants.skills_no_lessons.lessons).toBeNull();
expect(variants.blueprints_no_team.activeTeam).toBeNull();
});

it("pins the theme surface via a light variant (#3922 — the field a real consumer bug was filed over)", () => {
expect(variants.themes_light.themes[0].base).toBe("light");
});

it("guards the org archetype vocabulary (#3922 — a fixture id that names no real archetype renders as zero edges everywhere)", () => {
for (const org of PROJECTION_INPUTS.org.orgs) {
for (const rel of org.relationships) {
expect(
archetypeById(rel.archetype),
`org "${org.id}" relationship "${rel.id}" uses unknown archetype "${rel.archetype}"`,
).toBeDefined();
}
}
});
});
28 changes: 24 additions & 4 deletions src/features/tunnel/lib/storeProjections.fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@
// enums), so mobile's smoke layer can distinguish "read the field correctly" from "fell back". Cap
// behaviour (SECURITY_AUDIT_CAP / AUTOMATION_RUNS_CAP) is asserted with inflated inputs in
// storeProjections.test.ts — never inflate these fixtures to prove a cap.
//
// INVARIANT (#3922): every entry below must also carry every OPTIONAL field its builder passes
// through — not just the fields required to satisfy the type — and use real closed-vocabulary
// values (e.g. an actual archetype id), never a placeholder. `satisfies` only guards required
// fields and bare-`string` vocabularies; an optional field that's always absent, or a value that
// always equals a consumer's fallback, type-checks perfectly while being invisible to the harness
// end-to-end. When a domain's optional surface can't fit non-degenerately in the main input without
// bloating it, add a `variants` entry in storePayloads.fixtures.test.ts instead (see `themes_light`).

import type { ProjectLite, GlanceFault } from "@/features/glance";
import type { ProjectLink } from "@/features/glance/lib/projectLinks";
Expand Down Expand Up @@ -66,15 +74,27 @@ const glanceLibraryRefs = [
] satisfies KitLibraryRef[];

// ── org ────────────────────────────────────────────────────────────────────────────────────────
// Two real archetypes between distinct nodes — `manages` (solid, single-headed) and `iterates`
// (dashed, bidirectional, cyclical) — so both edge classes render end-to-end, plus every optional
// pass-through field a naive consumer could silently drop: `Position.x`/`y`, `Relationship.bow`,
// `Team.blurb`/`builtin`, `PersonaRef.pooled` (#3922). The prior fixture's only relationship used an
// archetype id ("delegates") that isn't in the vocabulary at all and was a self-edge every consumer
// filters, so the entire relationship half of this payload rendered as zero edges everywhere.
const org = {
id: "o1", name: "Pipeline",
positions: [{ nodeId: "n1", kind: "agent", personaId: "p1" }],
relationships: [{ id: "r1", archetype: "delegates", from: "n1", to: "n1" }],
id: "o1", name: "Pipeline", blurb: "auth + review pipeline", builtin: true,
positions: [
{ nodeId: "n1", kind: "agent", personaId: "p1", x: 40, y: 60 },
{ nodeId: "n2", kind: "external", label: "Tech Lead", x: 220, y: 60 },
],
relationships: [
{ id: "r1", archetype: "manages", from: "n2", to: "n1" },
{ id: "r2", archetype: "iterates", from: "n2", to: "n1", bow: 24 },
],
} satisfies Team;

const orgPersona = {
id: "p1", name: "Backend dev", blurb: "APIs", role: "worker",
startPrompt: "You own the API surface.", skills: ["s1"], model: "sonnet", builtin: true,
startPrompt: "You own the API surface.", skills: ["s1"], model: "sonnet", pooled: true, builtin: true,
} satisfies Persona;

// ── blueprints ─────────────────────────────────────────────────────────────────────────────────
Expand Down