Skip to content

Commit 544ca88

Browse files
author
Tehan
committed
chore(skill-memory): rebase onto upstream v0.33.0 (migrations v70/71/72)
Upstream v0.33.0 added migrations v54-v69 (authority identity, mirror cursors, live-memory resnapshots, mural, message-FTS convergence), colliding with the skill-memory slots. Renumbered skill P1/P2/P3a to v70/71/72; LATEST_SUPPORTED_VERSION 69 -> 72. Also adapts our tests to two upstream contract changes: - executeStatus is async on this branch; upstream's new #241 clamp tests needed await (they were added against the sync signature). - the historian output contract now requires the tiered paraphrase structure, so the skill_observations fixtures emit <p1> instead of a flat compartment body (same update upstream made to its e2e fixtures). - promotion.test.ts: upstream's two new embedding tests used the global mock.module('./embedding') this branch removed (CI mock bleed); they now use the non-global provider-factory seam.
1 parent ddfcf68 commit 544ca88

8 files changed

Lines changed: 237 additions & 63 deletions

File tree

packages/plugin/src/features/magic-context/memory/promotion.test.ts

Lines changed: 73 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:te
44
import { Database } from "../../../shared/sqlite";
55
import { closeQuietly } from "../../../shared/sqlite-helpers";
66
import { CATEGORY_DEFAULT_TTL } from "./constants";
7+
import type { EmbeddingProvider } from "./embedding-provider";
78
import { computeNormalizedHash } from "./normalize-hash";
89

910
const mockLog = mock(() => {});
@@ -23,6 +24,13 @@ const {
2324
insertMemory,
2425
} = await import("./storage-memory");
2526
const { embedPromotedFacts, promoteSessionFactsDurable } = await import("./promotion");
27+
const {
28+
_resetProjectEmbeddingRegistryForTests,
29+
_setTestProviderFactoryForProject,
30+
registerProjectEmbedding,
31+
} = await import("./embedding");
32+
const { runMigrations } = await import("../migrations");
33+
const { initializeDatabase } = await import("../storage-db");
2634

2735
let db: Database | null = null;
2836

@@ -99,6 +107,8 @@ afterEach(() => {
99107
db = null;
100108
}
101109
}
110+
_resetProjectEmbeddingRegistryForTests();
111+
_setTestProviderFactoryForProject(null);
102112
});
103113

104114
describe("promotion", () => {
@@ -385,18 +395,35 @@ describe("promotion", () => {
385395

386396
describe("#given best-effort embedding of promoted facts", () => {
387397
it("stores the vector under the registered model when the memory is unchanged", async () => {
388-
db = makeMemoryDatabase();
398+
_resetProjectEmbeddingRegistryForTests();
399+
400+
db = new Database(":memory:");
401+
initializeDatabase(db);
402+
runMigrations(db);
403+
const snapshot = registerProjectEmbedding(
404+
db,
405+
"/repo/project",
406+
{ provider: "local", model: "mock-model" },
407+
{ memoryEnabled: true, gitCommitEnabled: false },
408+
"/repo/project",
409+
);
410+
_setTestProviderFactoryForProject(
411+
(): EmbeddingProvider => ({
412+
modelId: "mock:model",
413+
initialize: async () => true,
414+
embed: async (_text: string) => new Float32Array([1, 2]),
415+
embedBatch: async (texts: string[]) =>
416+
texts.map(() => new Float32Array([1, 2])),
417+
dispose: async () => {},
418+
isLoaded: () => true,
419+
}),
420+
);
421+
389422
const memory = insertMemory(db, {
390423
projectPath: "/repo/project",
391424
category: "ARCHITECTURE_DECISIONS",
392425
content: "Embed promoted facts eagerly",
393426
});
394-
mockEmbedText.mockImplementation(async () => ({
395-
vector: new Float32Array([1, 2]),
396-
modelId: "mock:model",
397-
chunkModelId: "mock:chunk",
398-
generation: 1,
399-
}));
400427

401428
await embedPromotedFacts(db, "ses-1", "/repo/project", [
402429
{ memoryId: memory.id, content: memory.content },
@@ -406,31 +433,52 @@ describe("promotion", () => {
406433
model_id: string;
407434
}>;
408435
expect(rows).toHaveLength(1);
409-
expect(rows[0].model_id).toBe("mock:model");
436+
expect(rows[0].model_id).toBe(snapshot.modelId);
410437
});
411438

412439
it("discards the stale vector when the memory is edited while embedding is in flight", async () => {
413-
db = makeMemoryDatabase();
440+
_resetProjectEmbeddingRegistryForTests();
441+
442+
db = new Database(":memory:");
443+
initializeDatabase(db);
444+
runMigrations(db);
445+
registerProjectEmbedding(
446+
db,
447+
"/repo/project",
448+
{ provider: "local", model: "mock-model" },
449+
{ memoryEnabled: true, gitCommitEnabled: false },
450+
"/repo/project",
451+
);
452+
453+
let resolveStarted: (() => void) | undefined;
454+
const started = new Promise<void>((resolve) => {
455+
resolveStarted = resolve;
456+
});
457+
let releaseBlocker: (() => void) | undefined;
458+
const blocker = new Promise<void>((resolve) => {
459+
releaseBlocker = resolve;
460+
});
461+
_setTestProviderFactoryForProject(
462+
(): EmbeddingProvider => ({
463+
modelId: "mock:model",
464+
initialize: async () => true,
465+
embed: async (_text: string): Promise<Float32Array> => {
466+
resolveStarted?.();
467+
await blocker;
468+
return new Float32Array([1, 2]);
469+
},
470+
embedBatch: async (texts: string[]) =>
471+
texts.map(() => new Float32Array([1, 2])),
472+
dispose: async () => {},
473+
isLoaded: () => true,
474+
}),
475+
);
476+
414477
const memory = insertMemory(db, {
415478
projectPath: "/repo/project",
416479
category: "ARCHITECTURE_DECISIONS",
417480
content: "Original promoted content",
418481
});
419-
let release: (() => void) | undefined;
420-
const started = new Promise<void>((resolve) => {
421-
mockEmbedText.mockImplementation(async () => {
422-
resolve();
423-
await new Promise<void>((done) => {
424-
release = done;
425-
});
426-
return {
427-
vector: new Float32Array([1, 2]),
428-
modelId: "mock:model",
429-
chunkModelId: "mock:chunk",
430-
generation: 1,
431-
};
432-
});
433-
});
434482

435483
const inFlight = embedPromotedFacts(db, "ses-1", "/repo/project", [
436484
{ memoryId: memory.id, content: memory.content },
@@ -441,7 +489,7 @@ describe("promotion", () => {
441489
db.prepare(
442490
"UPDATE memories SET content = ?, normalized_hash = ?, updated_at = ? WHERE id = ?",
443491
).run("Edited promoted content", "edited-hash", Date.now(), memory.id);
444-
release?.();
492+
releaseBlocker?.();
445493
await inFlight;
446494

447495
const count = (
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { Database } from "../../shared/sqlite";
3+
import { closeQuietly } from "../../shared/sqlite-helpers";
4+
import { LATEST_MIGRATION_VERSION, runMigrations } from "./migrations";
5+
import { initializeDatabase, LATEST_SUPPORTED_VERSION } from "./storage-db"; // ESM import (not require) — matches codebase pattern
6+
7+
function columnNames(db: Database, table: string): string[] {
8+
return (db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>).map(
9+
(c) => c.name,
10+
);
11+
}
12+
13+
function tableExists(db: Database, name: string): boolean {
14+
return Boolean(
15+
db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = ?").get(name),
16+
);
17+
}
18+
19+
describe("migration v70 — skill_memory table", () => {
20+
test("creates skill_memory table with correct columns on fresh DB, idempotently", () => {
21+
const db = new Database(":memory:");
22+
try {
23+
initializeDatabase(db);
24+
runMigrations(db);
25+
runMigrations(db); // idempotency check
26+
27+
expect(tableExists(db, "skill_memory")).toBe(true);
28+
29+
const cols = columnNames(db, "skill_memory");
30+
expect(cols).toContain("id");
31+
expect(cols).toContain("skill_id");
32+
expect(cols).toContain("resolved_path");
33+
expect(cols).toContain("tier");
34+
expect(cols).toContain("skill_source");
35+
expect(cols).toContain("project_identity");
36+
expect(cols).toContain("intent");
37+
expect(cols).toContain("intent_embedding");
38+
expect(cols).toContain("embedding_model_version");
39+
expect(cols).toContain("kind");
40+
expect(cols).toContain("delta");
41+
expect(cols).toContain("tags");
42+
expect(cols).toContain("hit_count");
43+
expect(cols).toContain("pinned");
44+
expect(cols).toContain("normalized_hash");
45+
expect(cols).toContain("created_at");
46+
expect(cols).toContain("last_used_at");
47+
48+
expect(
49+
db
50+
.prepare("SELECT version FROM schema_migrations ORDER BY version DESC LIMIT 1")
51+
.get(),
52+
).toEqual({ version: LATEST_MIGRATION_VERSION });
53+
} finally {
54+
closeQuietly(db);
55+
}
56+
});
57+
58+
test("skill_memory CHECK constraints reject invalid tier and kind values", () => {
59+
const db = new Database(":memory:");
60+
try {
61+
initializeDatabase(db);
62+
runMigrations(db);
63+
64+
const insert = db.prepare(`
65+
INSERT INTO skill_memory
66+
(skill_id, resolved_path, tier, project_identity, intent, kind, delta, normalized_hash, hit_count, pinned, created_at)
67+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, 0, ?)
68+
`);
69+
70+
// Valid row
71+
expect(() =>
72+
insert.run(
73+
"test-skill",
74+
"/path/SKILL.md",
75+
"project",
76+
"git:abc123",
77+
"test intent",
78+
"gotcha",
79+
"test delta",
80+
"hash1",
81+
Date.now(),
82+
),
83+
).not.toThrow();
84+
85+
// Invalid tier
86+
expect(() =>
87+
insert.run(
88+
"test-skill",
89+
"/path/SKILL.md",
90+
"invalid-tier",
91+
"git:abc123",
92+
"test intent",
93+
"gotcha",
94+
"test delta",
95+
"hash2",
96+
Date.now(),
97+
),
98+
).toThrow();
99+
100+
// Invalid kind
101+
expect(() =>
102+
insert.run(
103+
"test-skill",
104+
"/path/SKILL.md",
105+
"project",
106+
"git:abc123",
107+
"test intent",
108+
"general",
109+
"test delta",
110+
"hash3",
111+
Date.now(),
112+
),
113+
).toThrow();
114+
} finally {
115+
closeQuietly(db);
116+
}
117+
});
118+
119+
test("LATEST_SUPPORTED_VERSION equals LATEST_MIGRATION_VERSION after v70", () => {
120+
// This test will fail until storage-db.ts is bumped to 39.
121+
// Belt-and-braces: mirrors schema-version-fence.test.ts but is co-located with the migration.
122+
// If this feels redundant, keep it with this comment — co-location aids discoverability.
123+
// NOTE: use ESM import at the top of the file (not require()) to match codebase pattern.
124+
expect(LATEST_SUPPORTED_VERSION).toBe(LATEST_MIGRATION_VERSION);
125+
});
126+
});

packages/plugin/src/features/magic-context/migrations-v55.test.ts renamed to packages/plugin/src/features/magic-context/migrations-v71.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ function tableExists(db: Database, name: string): boolean {
1515
);
1616
}
1717

18-
describe("migration v55 — skill_memory embeddings + FTS", () => {
18+
describe("migration v71 — skill_memory embeddings + FTS", () => {
1919
test("fresh DB: delta_embedding column and skill_memory_fts exist, no throw", () => {
2020
const db = new Database(":memory:");
2121
try {
@@ -64,7 +64,7 @@ describe("migration v55 — skill_memory embeddings + FTS", () => {
6464
});
6565

6666
// requires `import { MIGRATIONS } from "./migrations";` (added above)
67-
test("v55 migration backfills FTS for rows that pre-existed v40", () => {
67+
test("v71 migration backfills FTS for rows that pre-existed v40", () => {
6868
const db = new Database(":memory:");
6969
try {
7070
initializeDatabase(db);
@@ -75,7 +75,7 @@ describe("migration v55 — skill_memory embeddings + FTS", () => {
7575
)) {
7676
m.up(db);
7777
}
78-
// Insert a row under the pre-v55 schema — no FTS table yet, so no AFTER-INSERT trigger indexes it.
78+
// Insert a row under the pre-v71 schema — no FTS table yet, so no AFTER-INSERT trigger indexes it.
7979
db.prepare(
8080
`INSERT INTO skill_memory
8181
(skill_id, resolved_path, tier, project_identity, intent, kind, delta, normalized_hash, hit_count, pinned, created_at)
@@ -92,9 +92,9 @@ describe("migration v55 — skill_memory embeddings + FTS", () => {
9292
Date.now(),
9393
);
9494
// Apply ONLY v40's up() — its body must ALTER + create the FTS table + BACKFILL the pre-existing row.
95-
const v55 = MIGRATIONS.find((m) => m.version === 55);
96-
if (!v55) throw new Error("v40 migration not found");
97-
v55.up(db);
95+
const v71 = MIGRATIONS.find((m) => m.version === 71);
96+
if (!v71) throw new Error("v40 migration not found");
97+
v71.up(db);
9898
const hit = db
9999
.prepare(
100100
`SELECT m.id FROM skill_memory_fts f JOIN skill_memory m ON m.id = f.rowid WHERE skill_memory_fts MATCH ?`,
@@ -106,8 +106,8 @@ describe("migration v55 — skill_memory embeddings + FTS", () => {
106106
}
107107
});
108108

109-
test("LATEST_SUPPORTED_VERSION equals LATEST_MIGRATION_VERSION after v55", () => {
109+
test("LATEST_SUPPORTED_VERSION equals LATEST_MIGRATION_VERSION after v71", () => {
110110
expect(LATEST_SUPPORTED_VERSION).toBe(LATEST_MIGRATION_VERSION);
111-
expect(LATEST_SUPPORTED_VERSION).toBe(56);
111+
expect(LATEST_SUPPORTED_VERSION).toBe(72);
112112
});
113113
});

0 commit comments

Comments
 (0)