test(coding-agent): expanded tests for model scope notification and collab link parsing

- Extended `parseCollabLink` test coverage to reject 16-byte and 40-byte fragments instead of only rejecting short keys.
- Added coverage for full-link fragments to ensure key and write token are parsed when present.
- Updated `GuestClient` test setup so `welcomeFrame` carries a `readOnly` flag into snapshot assertions.
This commit is contained in:
can1357
2026-06-12 13:46:58 +02:00
parent 2baea85a68
commit a57f5aa1ce
6 changed files with 40 additions and 12 deletions
+1 -3
View File
@@ -254,9 +254,7 @@ export function buildModelScopeNotification(
const modelList = scopedModelsForDisplay
.map(scopedModel => {
const thinkingStr =
scopedModel.explicitThinkingLevel && scopedModel.thinkingLevel
? `:${scopedModel.thinkingLevel}`
: "";
scopedModel.explicitThinkingLevel && scopedModel.thinkingLevel ? `:${scopedModel.thinkingLevel}` : "";
return `${scopedModel.model.id}${thinkingStr}`;
})
.join(", ");
@@ -1,5 +1,11 @@
import { describe, expect, it } from "bun:test";
import { generateRoomKey, generateWriteToken, importRoomKey, open, seal } from "@oh-my-pi/pi-coding-agent/collab/crypto";
import {
generateRoomKey,
generateWriteToken,
importRoomKey,
open,
seal,
} from "@oh-my-pi/pi-coding-agent/collab/crypto";
import {
type CollabFrame,
DEFAULT_RELAY_URL,
@@ -1,8 +1,9 @@
import { describe, expect, it } from "bun:test";
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import { buildModel } from "@oh-my-pi/pi-catalog/build";
import type { ScopedModel } from "@oh-my-pi/pi-coding-agent/config/model-resolver";
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import { buildModelScopeNotification } from "@oh-my-pi/pi-coding-agent/main";
function scopedModel(id: string): ScopedModel {
return {
model: buildModel({
@@ -33,7 +34,11 @@ describe("buildModelScopeNotification", () => {
});
});
it("includes thinking suffix only when explicitly scoped", () => {
const withExplicit = { ...scopedModel("claude-sonnet-4-5"), thinkingLevel: "high" as ThinkingLevel, explicitThinkingLevel: true };
const withExplicit = {
...scopedModel("claude-sonnet-4-5"),
thinkingLevel: "high" as ThinkingLevel,
explicitThinkingLevel: true,
};
expect(buildModelScopeNotification([withExplicit], false)).toEqual({
kind: "info",
message: "Model scope: claude-sonnet-4-5:high (Ctrl+P to cycle)",
@@ -44,7 +49,11 @@ describe("buildModelScopeNotification", () => {
// `applyRootSessionOptions` fills `sessionOptions.scopedModels[*].thinkingLevel`
// with the global default for Ctrl+P cycling — the banner must not surface that
// default as if the user had scoped `:high`.
const withDefault = { ...scopedModel("claude-sonnet-4-5"), thinkingLevel: "high" as ThinkingLevel, explicitThinkingLevel: false };
const withDefault = {
...scopedModel("claude-sonnet-4-5"),
thinkingLevel: "high" as ThinkingLevel,
explicitThinkingLevel: false,
};
expect(buildModelScopeNotification([withDefault], false)).toEqual({
kind: "info",
message: "Model scope: claude-sonnet-4-5 (Ctrl+P to cycle)",
+1 -1
View File
@@ -26,7 +26,7 @@
"dev": "bun ./index.html",
"mock-host": "bun scripts/mock-host.ts",
"relay": "bun scripts/local-relay.ts",
"build": "rm -rf dist && bun build ./index.html --outdir=dist --minify --asset-naming=[name].[ext] && cp -R public/. dist/",
"build": "rm -rf dist && bun build ./index.html --outdir=dist --minify --entry-naming=[hash].[ext] --chunk-naming=[hash].[ext] --asset-naming=[hash].[ext] && mv dist/*.html dist/index.html && cp -R public/. dist/",
"prepack": "bun run build",
"test": "bun test --parallel",
"check": "biome check . && bun run check:types",
+3 -3
View File
@@ -50,8 +50,8 @@ function messageEntry(id: string, message: WireMessage): SessionEntry {
return { type: "message", id, parentId: null, timestamp: "2026-06-12T00:00:01Z", message };
}
function welcomeFrame(entries: SessionEntry[] = []): HostFrame {
return { t: "welcome", proto: 1, header: HEADER, entries, state: STATE, agents: AGENTS };
function welcomeFrame(entries: SessionEntry[] = [], readOnly?: boolean): HostFrame {
return { t: "welcome", proto: 1, header: HEADER, entries, state: STATE, agents: AGENTS, readOnly };
}
function liveClient(entries: SessionEntry[] = []): GuestClient {
@@ -82,7 +82,7 @@ describe("GuestClient frame apply", () => {
it("welcome readOnly flag lands in the snapshot", () => {
const client = new GuestClient(LINK, "tester");
expect(client.getSnapshot().readOnly).toBe(false);
client.applyFrameForTest({ ...welcomeFrame(), readOnly: true } as HostFrame);
client.applyFrameForTest(welcomeFrame([], true));
expect(client.getSnapshot().readOnly).toBe(true);
});
+16 -1
View File
@@ -39,9 +39,24 @@ describe("collab link parsing", () => {
expect("error" in parseCollabLink(`ws://relay.example.com/r/${ROOM}#${KEY_TEXT}`)).toBe(true);
});
it("rejects keys that are not 32 base64url bytes", () => {
it("rejects keys that are not 32 or 48 base64url bytes", () => {
const shortKey = encodeBase64Url(new Uint8Array(16));
expect("error" in parseCollabLink(`${ROOM}#${shortKey}`)).toBe(true);
const midKey = encodeBase64Url(new Uint8Array(40));
expect("error" in parseCollabLink(`${ROOM}#${midKey}`)).toBe(true);
});
it("splits full-link fragments into key and write token", () => {
const token = Uint8Array.from({ length: 16 }, (_, i) => 0xf0 + i);
const full = parseCollabLink(formatCollabLink(DEFAULT_RELAY_URL, ROOM, KEY, token));
if ("error" in full) throw new Error(full.error);
expect(full.key).toEqual(KEY);
expect(full.writeToken).toEqual(token);
const view = parseCollabLink(formatCollabLink(DEFAULT_RELAY_URL, ROOM, KEY));
if ("error" in view) throw new Error(view.error);
expect(view.key).toEqual(KEY);
expect(view.writeToken).toBeUndefined();
});
it("parses web deep links (https://<relay>/#<link>)", () => {