Skip to content

Commit d9059a1

Browse files
committed
test+fix(pi): v27 migration table guard, test fixtures, adoption unit tests
- migration v27: no-op when tags table absent so partial mid-ladder test fixtures don't throw on ensureColumn/CREATE INDEX (tags always exists in production by v2) - add entry_fingerprint column to hand-rolled tags fixtures in storage-tags/heuristic-cleanup/command-handler tests - 4 new adoption unit tests (fingerprint match->migrate keeping tag_number, duplicate surfaces both, guarded-UPDATE race returns false, real-id never adoptable) - pi context-handler: import sort after adoption storage imports
1 parent a3d0f82 commit d9059a1

5 files changed

Lines changed: 123 additions & 1 deletion

File tree

packages/pi-plugin/src/context-handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ import {
5555
} from "@magic-context/core/features/magic-context/message-index-async";
5656
import { createScheduler } from "@magic-context/core/features/magic-context/scheduler";
5757
import {
58-
type ContextDatabase,
5958
adoptFallbackTagMessageId,
59+
type ContextDatabase,
6060
clearPendingPiCompactionMarkerStateIf,
6161
findAdoptableFallbackTags,
6262
getActiveTagsBySession,

packages/plugin/src/features/magic-context/migrations.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,6 +1010,16 @@ const MIGRATIONS: Migration[] = [
10101010
// tag_number (hence §N§ and all per-tag state) stable. Nullable:
10111011
// OpenCode never writes it (real id on pass 1), so its rows stay
10121012
// NULL and adoption never fires.
1013+
//
1014+
// Guard on the tags table existing: in production tags is created
1015+
// (migration v1) long before this runs, but partial test fixtures
1016+
// that stamp a mid-ladder version without a tags table must not
1017+
// throw here (ensureColumn's ALTER + the CREATE INDEX both require
1018+
// the table). A DB with no tags table has nothing to index.
1019+
const hasTags = db
1020+
.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='tags' LIMIT 1")
1021+
.get();
1022+
if (!hasTags) return;
10131023
ensureColumn(db, "tags", "entry_fingerprint", "TEXT");
10141024
db.exec(
10151025
`CREATE INDEX IF NOT EXISTS idx_tags_pi_adopt

packages/plugin/src/features/magic-context/storage-tags.test.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { afterEach, describe, expect, it } from "bun:test";
44
import { Database } from "../../shared/sqlite";
55
import { closeQuietly } from "../../shared/sqlite-helpers";
66
import {
7+
adoptFallbackTagMessageId,
8+
findAdoptableFallbackTags,
79
getActiveTagsBySession,
810
getMaxDroppedTagNumber,
911
getTagById,
@@ -35,6 +37,7 @@ function makeMemoryDatabase(): Database {
3537
caveman_depth INTEGER NOT NULL DEFAULT 0,
3638
harness TEXT NOT NULL DEFAULT 'opencode',
3739
tool_owner_message_id TEXT DEFAULT NULL,
40+
entry_fingerprint TEXT,
3841
UNIQUE(session_id, id)
3942
);
4043
CREATE TABLE IF NOT EXISTS pending_ops (
@@ -516,4 +519,111 @@ describe("storage-tags", () => {
516519
expect(fromHelper).toBe(watermark);
517520
});
518521
});
522+
523+
describe("#given Pi fallback-tag adoption", () => {
524+
it("#when a fallback tag matches by fingerprint #then migrates message_id keeping tag_number", () => {
525+
db = makeMemoryDatabase();
526+
// Fallback-id tag (pass 1: in-flight message under pi-msg-*).
527+
insertTag(
528+
db,
529+
"ses-1",
530+
"pi-msg-0-123-user:p0",
531+
"message",
532+
100,
533+
1,
534+
0,
535+
null,
536+
0,
537+
null,
538+
"FP-A",
539+
);
540+
541+
const candidates = findAdoptableFallbackTags(db, "ses-1", "FP-A");
542+
expect(candidates).toHaveLength(1);
543+
expect(candidates[0]).toEqual({ tagNumber: 1, messageId: "pi-msg-0-123-user:p0" });
544+
545+
const migrated = adoptFallbackTagMessageId(
546+
db,
547+
"ses-1",
548+
1,
549+
"pi-msg-0-123-user:p0",
550+
"real-entry-abc:p0",
551+
);
552+
expect(migrated).toBe(true);
553+
// tag_number preserved; message_id migrated to the real id.
554+
const tag = getTagById(db, "ses-1", 1);
555+
expect(tag?.messageId).toBe("real-entry-abc:p0");
556+
// No longer adoptable (message_id is now a real id, not pi-msg-*).
557+
expect(findAdoptableFallbackTags(db, "ses-1", "FP-A")).toHaveLength(0);
558+
});
559+
560+
it("#when fingerprint is shared by two fallback messages #then both surface (caller skips on ambiguity)", () => {
561+
db = makeMemoryDatabase();
562+
insertTag(
563+
db,
564+
"ses-1",
565+
"pi-msg-0-1-user:p0",
566+
"message",
567+
100,
568+
1,
569+
0,
570+
null,
571+
0,
572+
null,
573+
"DUP",
574+
);
575+
insertTag(
576+
db,
577+
"ses-1",
578+
"pi-msg-1-2-user:p0",
579+
"message",
580+
100,
581+
2,
582+
0,
583+
null,
584+
0,
585+
null,
586+
"DUP",
587+
);
588+
// The lookup returns both; the adoption pre-pass applies its
589+
// unique-base-id guard and skips when >1 distinct base id matches.
590+
expect(findAdoptableFallbackTags(db, "ses-1", "DUP")).toHaveLength(2);
591+
});
592+
593+
it("#when the guarded UPDATE loses the race #then returns false and does not migrate", () => {
594+
db = makeMemoryDatabase();
595+
insertTag(
596+
db,
597+
"ses-1",
598+
"pi-msg-0-1-user:p0",
599+
"message",
600+
100,
601+
1,
602+
0,
603+
null,
604+
0,
605+
null,
606+
"FP-R",
607+
);
608+
// Old value in the WHERE clause doesn't match (sibling already
609+
// migrated it) → changes === 0 → false.
610+
const migrated = adoptFallbackTagMessageId(
611+
db,
612+
"ses-1",
613+
1,
614+
"pi-msg-STALE:p0",
615+
"real:p0",
616+
);
617+
expect(migrated).toBe(false);
618+
expect(getTagById(db, "ses-1", 1)?.messageId).toBe("pi-msg-0-1-user:p0");
619+
});
620+
621+
it("#when a real-id tag exists with a fingerprint #then it is never an adoption candidate", () => {
622+
db = makeMemoryDatabase();
623+
// A real-id tag that happens to carry a fingerprint must not match
624+
// (only pi-msg-* shaped rows are adoptable).
625+
insertTag(db, "ses-1", "real-entry-x:p0", "message", 100, 1, 0, null, 0, null, "FP-X");
626+
expect(findAdoptableFallbackTags(db, "ses-1", "FP-X")).toHaveLength(0);
627+
});
628+
});
519629
});

packages/plugin/src/hooks/magic-context/command-handler.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ function createTestDb(): Database {
2323
caveman_depth INTEGER NOT NULL DEFAULT 0,
2424
harness TEXT NOT NULL DEFAULT 'opencode',
2525
tool_owner_message_id TEXT DEFAULT NULL,
26+
entry_fingerprint TEXT,
2627
UNIQUE(session_id, tag_number)
2728
);
2829

packages/plugin/src/hooks/magic-context/heuristic-cleanup.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ function makeMemoryDatabase(): Database {
2424
caveman_depth INTEGER NOT NULL DEFAULT 0,
2525
harness TEXT NOT NULL DEFAULT 'opencode',
2626
tool_owner_message_id TEXT DEFAULT NULL,
27+
entry_fingerprint TEXT,
2728
UNIQUE(session_id, id)
2829
);
2930
CREATE TABLE IF NOT EXISTS pending_ops (

0 commit comments

Comments
 (0)