@agent-native/creative-context 0.1.0 → 0.3.0
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.
- package/agent-native.package.json +11 -0
- package/dist/actions/continue-context-import.d.ts +16 -1
- package/dist/actions/continue-context-import.js +4 -1
- package/dist/actions/continue-context-import.js.map +1 -1
- package/dist/actions/get-creative-context.d.ts +38 -0
- package/dist/actions/get-creative-context.d.ts.map +1 -1
- package/dist/actions/get-creative-context.js +59 -23
- package/dist/actions/get-creative-context.js.map +1 -1
- package/dist/actions/get-google-picker-session.d.ts +10 -0
- package/dist/actions/get-google-picker-session.d.ts.map +1 -0
- package/dist/actions/get-google-picker-session.js +38 -0
- package/dist/actions/get-google-picker-session.js.map +1 -0
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +2 -0
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/start-context-enrichment.d.ts +16 -1
- package/dist/actions/start-context-enrichment.js +6 -4
- package/dist/actions/start-context-enrichment.js.map +1 -1
- package/dist/actions/start-context-import.d.ts +16 -1
- package/dist/actions/start-context-import.js +2 -1
- package/dist/actions/start-context-import.js.map +1 -1
- package/dist/client/CreativeContextPanel.d.ts +1 -59
- package/dist/client/CreativeContextPanel.d.ts.map +1 -1
- package/dist/client/CreativeContextPanel.js +111 -36
- package/dist/client/CreativeContextPanel.js.map +1 -1
- package/dist/client/actions.d.ts +8 -0
- package/dist/client/actions.d.ts.map +1 -1
- package/dist/client/actions.js +4 -0
- package/dist/client/actions.js.map +1 -1
- package/dist/client/google-slides-picker.d.ts +19 -0
- package/dist/client/google-slides-picker.d.ts.map +1 -0
- package/dist/client/google-slides-picker.js +106 -0
- package/dist/client/google-slides-picker.js.map +1 -0
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/messages.d.ts +1 -1
- package/dist/client/messages.d.ts.map +1 -1
- package/dist/client/messages.js +22 -0
- package/dist/client/messages.js.map +1 -1
- package/dist/connectors/google-slides.d.ts +1 -1
- package/dist/connectors/google-slides.d.ts.map +1 -1
- package/dist/connectors/google-slides.js +1 -2
- package/dist/connectors/google-slides.js.map +1 -1
- package/dist/connectors/normalize.d.ts +7 -0
- package/dist/connectors/normalize.d.ts.map +1 -1
- package/dist/connectors/normalize.js +122 -3
- package/dist/connectors/normalize.js.map +1 -1
- package/dist/schema/index.d.ts +30 -30
- package/dist/server/enrichment.d.ts +1 -0
- package/dist/server/enrichment.d.ts.map +1 -1
- package/dist/server/enrichment.js +26 -24
- package/dist/server/enrichment.js.map +1 -1
- package/dist/server/generation-context.d.ts +1 -1
- package/dist/server/generation-context.d.ts.map +1 -1
- package/dist/server/generation-context.js +1 -1
- package/dist/server/generation-context.js.map +1 -1
- package/dist/store/content.d.ts +13 -0
- package/dist/store/content.d.ts.map +1 -1
- package/dist/store/content.js +172 -1
- package/dist/store/content.js.map +1 -1
- package/dist/store/jobs.d.ts.map +1 -1
- package/dist/store/jobs.js +2 -2
- package/dist/store/jobs.js.map +1 -1
- package/dist/store/purge.d.ts.map +1 -1
- package/dist/store/purge.js +9 -8
- package/dist/store/purge.js.map +1 -1
- package/dist/store/suggestions.d.ts +1 -0
- package/dist/store/suggestions.d.ts.map +1 -1
- package/dist/store/suggestions.js +56 -38
- package/dist/store/suggestions.js.map +1 -1
- package/docs/skills/creative-context/SKILL.md +8 -0
- package/package.json +3 -3
- package/src/actions/continue-context-import.ts +4 -1
- package/src/actions/creative-context-a2a.spec.ts +98 -0
- package/src/actions/get-creative-context.spec.ts +121 -1
- package/src/actions/get-creative-context.ts +78 -29
- package/src/actions/get-google-picker-session.spec.ts +62 -0
- package/src/actions/get-google-picker-session.ts +45 -0
- package/src/actions/index.ts +2 -0
- package/src/actions/job-public-serialization.spec.ts +122 -0
- package/src/actions/start-context-enrichment.ts +6 -4
- package/src/actions/start-context-import.ts +2 -1
- package/src/client/CreativeContextPanel.tsx +163 -64
- package/src/client/actions.ts +18 -0
- package/src/client/google-slides-picker.spec.ts +36 -0
- package/src/client/google-slides-picker.ts +146 -0
- package/src/client/index.ts +2 -0
- package/src/client/messages.ts +35 -0
- package/src/client/source-config.spec.ts +24 -10
- package/src/connectors/google-slides.ts +1 -2
- package/src/connectors/normalize.spec.ts +144 -0
- package/src/connectors/normalize.ts +201 -3
- package/src/server/enrichment.spec.ts +94 -0
- package/src/server/enrichment.ts +27 -24
- package/src/server/generation-artifact-access.spec.ts +104 -0
- package/src/server/generation-context-a2a.spec.ts +20 -2
- package/src/server/generation-context.ts +6 -6
- package/src/server/isolated-a2a.spec.ts +21 -1
- package/src/store/access.integration.spec.ts +511 -26
- package/src/store/content.ts +213 -1
- package/src/store/jobs.ts +2 -0
- package/src/store/purge.ts +18 -9
- package/src/store/suggestions.ts +67 -41
|
@@ -266,36 +266,196 @@ describe("creative context access and revocation", () => {
|
|
|
266
266
|
});
|
|
267
267
|
});
|
|
268
268
|
|
|
269
|
-
it("
|
|
269
|
+
it("rejects an oversized compiled layout before creating its projection", async () => {
|
|
270
270
|
const { exec, runWithRequestContext, store } = await setup();
|
|
271
|
+
const server = await import("../server/index.js");
|
|
272
|
+
const promote = vi.fn(async () => {});
|
|
273
|
+
server.configureCreativeContext({
|
|
274
|
+
projections: {
|
|
275
|
+
layoutTemplate: { promote, demote: vi.fn(async () => {}) },
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
const html = `<div class="fmd-slide google-slides-native" data-slide-id="slide-1"><p>${"界".repeat(44_000)}</p></div>`;
|
|
271
279
|
await exec.execute({
|
|
272
|
-
sql: `
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
280
|
+
sql: `UPDATE creative_context_items
|
|
281
|
+
SET mime_type = ?, provenance = ? WHERE id = ?`,
|
|
282
|
+
args: [
|
|
283
|
+
"text/html",
|
|
284
|
+
JSON.stringify({
|
|
285
|
+
compiler: "@agent-native/creative-context:google-slides-native",
|
|
286
|
+
}),
|
|
287
|
+
"allowed",
|
|
288
|
+
],
|
|
289
|
+
});
|
|
290
|
+
await exec.execute({
|
|
291
|
+
sql: `UPDATE creative_context_item_versions
|
|
292
|
+
SET content = ?, mime_type = ?, metadata = ? WHERE id = ?`,
|
|
276
293
|
args: [
|
|
277
|
-
|
|
278
|
-
"
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
294
|
+
html,
|
|
295
|
+
"text/html",
|
|
296
|
+
JSON.stringify({
|
|
297
|
+
nativeArtifact: {
|
|
298
|
+
schemaVersion: 1,
|
|
299
|
+
app: "slides",
|
|
300
|
+
format: "slides-html",
|
|
301
|
+
rootExternalId: "presentation-1:slide-1",
|
|
302
|
+
fidelityReport: {
|
|
303
|
+
exact: { count: 1 },
|
|
304
|
+
approximated: { count: 0, reasons: [] },
|
|
305
|
+
imageFallback: { count: 0, reasons: [] },
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
}),
|
|
309
|
+
"allowed-v2",
|
|
310
|
+
],
|
|
311
|
+
});
|
|
312
|
+
const suggestion = await runWithRequestContext(
|
|
313
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
314
|
+
() =>
|
|
315
|
+
store.proposeCreativeContextSuggestion({
|
|
316
|
+
kind: "layout-template",
|
|
317
|
+
itemId: "allowed",
|
|
318
|
+
itemVersionId: "allowed-v2",
|
|
319
|
+
}),
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
await expect(
|
|
323
|
+
runWithRequestContext(
|
|
324
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
325
|
+
() =>
|
|
326
|
+
store.applyLayoutTemplateSuggestion({
|
|
327
|
+
suggestionId: suggestion.id,
|
|
328
|
+
operation: "promote",
|
|
329
|
+
}),
|
|
330
|
+
),
|
|
331
|
+
).rejects.toThrow(/native content.*exceeds.*split the artifact/i);
|
|
332
|
+
expect(promote).not.toHaveBeenCalled();
|
|
333
|
+
const projectionCount = await exec.execute({
|
|
334
|
+
sql: "SELECT COUNT(*) AS count FROM creative_context_items WHERE kind = 'layout_template'",
|
|
335
|
+
args: [],
|
|
336
|
+
});
|
|
337
|
+
expect(Number(projectionCount.rows[0]?.count)).toBe(0);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("never trusts a caller-supplied layout projection item id", async () => {
|
|
341
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
342
|
+
const server = await import("../server/index.js");
|
|
343
|
+
const demote = vi.fn(async () => {});
|
|
344
|
+
server.configureCreativeContext({
|
|
345
|
+
projections: {
|
|
346
|
+
layoutTemplate: { promote: vi.fn(async () => {}), demote },
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
const proposed = await runWithRequestContext(
|
|
350
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
351
|
+
() =>
|
|
352
|
+
store.proposeCreativeContextSuggestion({
|
|
353
|
+
kind: "layout-template",
|
|
354
|
+
itemId: "allowed",
|
|
355
|
+
itemVersionId: "allowed-v2",
|
|
356
|
+
payload: { projectionItemId: "guessed-item", note: "keep" },
|
|
357
|
+
}),
|
|
358
|
+
);
|
|
359
|
+
expect(proposed.payload).toEqual({ note: "keep" });
|
|
360
|
+
|
|
361
|
+
await exec.execute({
|
|
362
|
+
sql: `INSERT INTO creative_context_sources
|
|
363
|
+
(id, name, kind, config, upstream_access, status, health_status,
|
|
364
|
+
item_count, restricted_item_count, owner_email, org_id, visibility,
|
|
365
|
+
created_at, updated_at)
|
|
366
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
367
|
+
args: [
|
|
368
|
+
"outside-source",
|
|
369
|
+
"Outside tenant",
|
|
370
|
+
"manual",
|
|
371
|
+
"{}",
|
|
372
|
+
"available",
|
|
373
|
+
"active",
|
|
374
|
+
"healthy",
|
|
375
|
+
1,
|
|
376
|
+
0,
|
|
377
|
+
"mallory@example.test",
|
|
378
|
+
"org-2",
|
|
379
|
+
"private",
|
|
380
|
+
"2026-07-16T00:00:00.000Z",
|
|
381
|
+
"2026-07-16T00:00:00.000Z",
|
|
382
|
+
],
|
|
383
|
+
});
|
|
384
|
+
await exec.execute({
|
|
385
|
+
sql: `INSERT INTO creative_context_items
|
|
386
|
+
(id, source_id, external_id, kind, title, current_version_id,
|
|
387
|
+
current_content_hash, status, upstream_access, curation_status,
|
|
388
|
+
curation_rank, inventory_state, index_state, tags, colors, provenance,
|
|
389
|
+
metadata, created_at, updated_at, owner_email, org_id)
|
|
390
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
391
|
+
args: [
|
|
392
|
+
"outside-projection",
|
|
393
|
+
"outside-source",
|
|
394
|
+
"outside-projection",
|
|
395
|
+
"layout_template",
|
|
396
|
+
"Outside projection",
|
|
397
|
+
"outside-version",
|
|
398
|
+
"outside-hash",
|
|
399
|
+
"active",
|
|
400
|
+
"available",
|
|
401
|
+
"included",
|
|
402
|
+
"canonical",
|
|
403
|
+
"available",
|
|
404
|
+
"indexed",
|
|
405
|
+
"[]",
|
|
406
|
+
"[]",
|
|
407
|
+
JSON.stringify({
|
|
408
|
+
promotedFromSuggestionId: "malicious-layout-suggestion",
|
|
409
|
+
}),
|
|
410
|
+
"{}",
|
|
411
|
+
"2026-07-16T00:00:00.000Z",
|
|
284
412
|
"2026-07-16T00:00:00.000Z",
|
|
285
|
-
"alice@example.test",
|
|
286
|
-
"org-1",
|
|
287
|
-
"record-other-org",
|
|
288
|
-
"slides",
|
|
289
|
-
"deck",
|
|
290
|
-
"shared-deck",
|
|
291
|
-
"auto",
|
|
292
|
-
null,
|
|
293
|
-
'[{"elementId":"secret","influence":"generated"}]',
|
|
294
|
-
"2026-07-16T01:00:00.000Z",
|
|
295
413
|
"mallory@example.test",
|
|
296
414
|
"org-2",
|
|
297
415
|
],
|
|
298
416
|
});
|
|
417
|
+
await exec.execute({
|
|
418
|
+
sql: `INSERT INTO creative_context_suggestions
|
|
419
|
+
(id, kind, status, item_id, item_version_id, payload, created_at,
|
|
420
|
+
updated_at, owner_email, org_id)
|
|
421
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
422
|
+
args: [
|
|
423
|
+
"malicious-layout-suggestion",
|
|
424
|
+
"layout-template",
|
|
425
|
+
"promoted",
|
|
426
|
+
"allowed",
|
|
427
|
+
"allowed-v2",
|
|
428
|
+
JSON.stringify({ projectionItemId: "outside-projection" }),
|
|
429
|
+
"2026-07-16T00:00:00.000Z",
|
|
430
|
+
"2026-07-16T00:00:00.000Z",
|
|
431
|
+
"alice@example.test",
|
|
432
|
+
"org-1",
|
|
433
|
+
],
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
await runWithRequestContext(
|
|
437
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
438
|
+
() =>
|
|
439
|
+
store.applyLayoutTemplateSuggestion({
|
|
440
|
+
suggestionId: "malicious-layout-suggestion",
|
|
441
|
+
operation: "demote",
|
|
442
|
+
}),
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
const outside = await exec.execute(
|
|
446
|
+
"SELECT status FROM creative_context_items WHERE id = 'outside-projection'",
|
|
447
|
+
);
|
|
448
|
+
expect(outside.rows[0]?.status).toBe("active");
|
|
449
|
+
expect(demote).toHaveBeenCalledWith({
|
|
450
|
+
suggestionId: "malicious-layout-suggestion",
|
|
451
|
+
projectionItemId: null,
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it("requires a host access proof before sharing generation history", async () => {
|
|
456
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
457
|
+
const { assertGenerationArtifactAccess } =
|
|
458
|
+
await import("../server/generation-artifact-access.js");
|
|
299
459
|
const asBob = <T>(fn: () => Promise<T>) =>
|
|
300
460
|
runWithRequestContext(
|
|
301
461
|
{ userEmail: "bob@example.test", orgId: "org-1" },
|
|
@@ -306,18 +466,122 @@ describe("creative context access and revocation", () => {
|
|
|
306
466
|
artifactType: "deck",
|
|
307
467
|
artifactId: "shared-deck",
|
|
308
468
|
};
|
|
469
|
+
const aliceProof = await runWithRequestContext(
|
|
470
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
471
|
+
() =>
|
|
472
|
+
assertGenerationArtifactAccess(
|
|
473
|
+
artifact,
|
|
474
|
+
{
|
|
475
|
+
resourceType: "creative-context-source",
|
|
476
|
+
resourceId: "source-1",
|
|
477
|
+
},
|
|
478
|
+
"editor",
|
|
479
|
+
),
|
|
480
|
+
);
|
|
481
|
+
await runWithRequestContext(
|
|
482
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
483
|
+
() =>
|
|
484
|
+
store.recordGenerationCreativeContext(
|
|
485
|
+
{
|
|
486
|
+
...artifact,
|
|
487
|
+
contextMode: "auto",
|
|
488
|
+
contextPackId: null,
|
|
489
|
+
reuseLabels: [],
|
|
490
|
+
elementProvenance: [
|
|
491
|
+
{ elementId: "slide-1", influence: "generated" },
|
|
492
|
+
],
|
|
493
|
+
},
|
|
494
|
+
{ artifactAccess: aliceProof },
|
|
495
|
+
),
|
|
496
|
+
);
|
|
309
497
|
|
|
310
498
|
await expect(
|
|
311
499
|
asBob(() => store.getGenerationCreativeContext(artifact)),
|
|
312
500
|
).resolves.toBeNull();
|
|
501
|
+
await expect(
|
|
502
|
+
asBob(() =>
|
|
503
|
+
assertGenerationArtifactAccess(
|
|
504
|
+
artifact,
|
|
505
|
+
{
|
|
506
|
+
resourceType: "creative-context-source",
|
|
507
|
+
resourceId: "source-1",
|
|
508
|
+
},
|
|
509
|
+
"viewer",
|
|
510
|
+
),
|
|
511
|
+
),
|
|
512
|
+
).rejects.toThrow(/no access/i);
|
|
513
|
+
await expect(
|
|
514
|
+
asBob(() =>
|
|
515
|
+
store.getGenerationCreativeContext(artifact, {
|
|
516
|
+
artifactAccess: {
|
|
517
|
+
identityKey: JSON.stringify([
|
|
518
|
+
artifact.appId,
|
|
519
|
+
artifact.artifactType,
|
|
520
|
+
artifact.artifactId,
|
|
521
|
+
]),
|
|
522
|
+
minRole: "viewer",
|
|
523
|
+
} as never,
|
|
524
|
+
}),
|
|
525
|
+
),
|
|
526
|
+
).rejects.toThrow(/verified by the host application/i);
|
|
527
|
+
|
|
528
|
+
await asBob(() =>
|
|
529
|
+
store.recordGenerationCreativeContext({
|
|
530
|
+
...artifact,
|
|
531
|
+
contextMode: "auto",
|
|
532
|
+
contextPackId: null,
|
|
533
|
+
reuseLabels: [],
|
|
534
|
+
elementProvenance: [{ elementId: "poison", influence: "generated" }],
|
|
535
|
+
}),
|
|
536
|
+
);
|
|
537
|
+
const poison = await exec.execute(
|
|
538
|
+
"SELECT org_id FROM creative_context_generation_records WHERE owner_email = 'bob@example.test'",
|
|
539
|
+
);
|
|
540
|
+
expect(poison.rows[0]?.org_id).toBeNull();
|
|
541
|
+
await expect(
|
|
542
|
+
runWithRequestContext(
|
|
543
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
544
|
+
() =>
|
|
545
|
+
store.getGenerationCreativeContext(artifact, {
|
|
546
|
+
artifactAccess: aliceProof,
|
|
547
|
+
}),
|
|
548
|
+
),
|
|
549
|
+
).resolves.toMatchObject({
|
|
550
|
+
artifactId: "shared-deck",
|
|
551
|
+
elementProvenance: [{ elementId: "slide-1" }],
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
await exec.execute({
|
|
555
|
+
sql: `INSERT INTO creative_context_source_shares
|
|
556
|
+
(id, resource_id, principal_type, principal_id, role, created_by, created_at)
|
|
557
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
558
|
+
args: [
|
|
559
|
+
"source-share-bob",
|
|
560
|
+
"source-1",
|
|
561
|
+
"user",
|
|
562
|
+
"bob@example.test",
|
|
563
|
+
"viewer",
|
|
564
|
+
"alice@example.test",
|
|
565
|
+
"2026-07-16T00:00:00.000Z",
|
|
566
|
+
],
|
|
567
|
+
});
|
|
568
|
+
const bobProof = await asBob(() =>
|
|
569
|
+
assertGenerationArtifactAccess(
|
|
570
|
+
artifact,
|
|
571
|
+
{
|
|
572
|
+
resourceType: "creative-context-source",
|
|
573
|
+
resourceId: "source-1",
|
|
574
|
+
},
|
|
575
|
+
"viewer",
|
|
576
|
+
),
|
|
577
|
+
);
|
|
313
578
|
await expect(
|
|
314
579
|
asBob(() =>
|
|
315
580
|
store.getGenerationCreativeContext(artifact, {
|
|
316
|
-
|
|
581
|
+
artifactAccess: bobProof,
|
|
317
582
|
}),
|
|
318
583
|
),
|
|
319
584
|
).resolves.toMatchObject({
|
|
320
|
-
id: "record-org-1",
|
|
321
585
|
artifactId: "shared-deck",
|
|
322
586
|
elementProvenance: [{ elementId: "slide-1" }],
|
|
323
587
|
});
|
|
@@ -325,10 +589,138 @@ describe("creative context access and revocation", () => {
|
|
|
325
589
|
asBob(() =>
|
|
326
590
|
store.getGenerationCreativeContext(
|
|
327
591
|
{ ...artifact, artifactId: "arbitrary-unasserted-id" },
|
|
328
|
-
{
|
|
592
|
+
{ artifactAccess: bobProof },
|
|
329
593
|
),
|
|
330
594
|
),
|
|
331
|
-
).
|
|
595
|
+
).rejects.toThrow(/verified by the host application/i);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it("appends media enrichment while preserving version-pinned pack evidence", async () => {
|
|
599
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
600
|
+
await exec.execute({
|
|
601
|
+
sql: `INSERT INTO creative_context_media
|
|
602
|
+
(id, item_id, item_version_id, kind, mime_type, access_mode,
|
|
603
|
+
storage_key, caption_status, palette, metadata, created_at,
|
|
604
|
+
owner_email, org_id)
|
|
605
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
606
|
+
args: [
|
|
607
|
+
"media-before-enrichment",
|
|
608
|
+
"allowed",
|
|
609
|
+
"allowed-v2",
|
|
610
|
+
"image",
|
|
611
|
+
"image/png",
|
|
612
|
+
"private",
|
|
613
|
+
"private-blob:fixture",
|
|
614
|
+
"pending",
|
|
615
|
+
"[]",
|
|
616
|
+
"{}",
|
|
617
|
+
"2026-07-16T00:00:00.000Z",
|
|
618
|
+
"alice@example.test",
|
|
619
|
+
"org-1",
|
|
620
|
+
],
|
|
621
|
+
});
|
|
622
|
+
const asAlice = <T>(fn: () => Promise<T>) =>
|
|
623
|
+
runWithRequestContext(
|
|
624
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
625
|
+
fn,
|
|
626
|
+
);
|
|
627
|
+
const pack = await asAlice(() =>
|
|
628
|
+
store.createContextPack({
|
|
629
|
+
name: "Pinned before enrichment",
|
|
630
|
+
members: [
|
|
631
|
+
{
|
|
632
|
+
itemId: "allowed",
|
|
633
|
+
itemVersionId: "allowed-v2",
|
|
634
|
+
reason: "Exact pre-enrichment evidence",
|
|
635
|
+
},
|
|
636
|
+
],
|
|
637
|
+
}),
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
const enriched = await asAlice(() =>
|
|
641
|
+
store.appendMediaEnrichmentVersion({
|
|
642
|
+
mediaId: "media-before-enrichment",
|
|
643
|
+
palette: ["#663399", "#ffffff"],
|
|
644
|
+
contentHash: "enriched-image-hash",
|
|
645
|
+
caption: "Builder dashboard on a purple background",
|
|
646
|
+
captionStatus: "complete",
|
|
647
|
+
ocrText: "Ship faster",
|
|
648
|
+
}),
|
|
649
|
+
);
|
|
650
|
+
|
|
651
|
+
expect(enriched.appended).toBe(true);
|
|
652
|
+
expect(enriched.itemVersionId).not.toBe("allowed-v2");
|
|
653
|
+
expect(enriched.mediaId).not.toBe("media-before-enrichment");
|
|
654
|
+
const pinned = await asAlice(() =>
|
|
655
|
+
store.getCreativeContextItem("allowed", "allowed-v2"),
|
|
656
|
+
);
|
|
657
|
+
expect(pinned?.media).toEqual([
|
|
658
|
+
expect.objectContaining({
|
|
659
|
+
id: "media-before-enrichment",
|
|
660
|
+
caption: null,
|
|
661
|
+
captionStatus: "pending",
|
|
662
|
+
palette: [],
|
|
663
|
+
}),
|
|
664
|
+
]);
|
|
665
|
+
const current = await asAlice(() =>
|
|
666
|
+
store.getCreativeContextItem("allowed"),
|
|
667
|
+
);
|
|
668
|
+
expect(current?.version.id).toBe(enriched.itemVersionId);
|
|
669
|
+
expect(current?.media).toEqual([
|
|
670
|
+
expect.objectContaining({
|
|
671
|
+
id: enriched.mediaId,
|
|
672
|
+
caption: "Builder dashboard on a purple background",
|
|
673
|
+
captionStatus: "complete",
|
|
674
|
+
ocrText: "Ship faster",
|
|
675
|
+
palette: ["#663399", "#ffffff"],
|
|
676
|
+
}),
|
|
677
|
+
]);
|
|
678
|
+
const pinnedPack = await asAlice(() => store.getContextPack(pack.id));
|
|
679
|
+
expect(pinnedPack?.members).toEqual([
|
|
680
|
+
expect.objectContaining({
|
|
681
|
+
itemId: "allowed",
|
|
682
|
+
itemVersionId: "allowed-v2",
|
|
683
|
+
}),
|
|
684
|
+
]);
|
|
685
|
+
|
|
686
|
+
const resync = await asAlice(() =>
|
|
687
|
+
store.ingestItems({
|
|
688
|
+
sourceId: "source-1",
|
|
689
|
+
items: [
|
|
690
|
+
{
|
|
691
|
+
externalId: "allowed",
|
|
692
|
+
kind: "slide",
|
|
693
|
+
title: "allowed title",
|
|
694
|
+
content: "allowed canonical version 2",
|
|
695
|
+
contentHash: "allowed-hash-v2",
|
|
696
|
+
upstreamAccess: "available",
|
|
697
|
+
},
|
|
698
|
+
],
|
|
699
|
+
}),
|
|
700
|
+
);
|
|
701
|
+
expect(resync.unchanged).toBe(1);
|
|
702
|
+
const afterResync = await asAlice(() =>
|
|
703
|
+
store.getCreativeContextItem("allowed"),
|
|
704
|
+
);
|
|
705
|
+
expect(afterResync?.version.id).toBe(enriched.itemVersionId);
|
|
706
|
+
expect(afterResync?.media[0]?.captionStatus).toBe("complete");
|
|
707
|
+
|
|
708
|
+
const retried = await asAlice(() =>
|
|
709
|
+
store.appendMediaEnrichmentVersion({
|
|
710
|
+
mediaId: "media-before-enrichment",
|
|
711
|
+
palette: ["#663399", "#ffffff"],
|
|
712
|
+
contentHash: "enriched-image-hash",
|
|
713
|
+
caption: "Builder dashboard on a purple background",
|
|
714
|
+
captionStatus: "complete",
|
|
715
|
+
ocrText: "Ship faster",
|
|
716
|
+
}),
|
|
717
|
+
);
|
|
718
|
+
expect(retried).toEqual({
|
|
719
|
+
itemId: "allowed",
|
|
720
|
+
itemVersionId: enriched.itemVersionId,
|
|
721
|
+
mediaId: enriched.mediaId,
|
|
722
|
+
appended: false,
|
|
723
|
+
});
|
|
332
724
|
});
|
|
333
725
|
|
|
334
726
|
it("reloads a concurrently inserted item instead of failing its idempotent ingest", async () => {
|
|
@@ -371,6 +763,69 @@ describe("creative context access and revocation", () => {
|
|
|
371
763
|
expect(versions.rows).toHaveLength(1);
|
|
372
764
|
});
|
|
373
765
|
|
|
766
|
+
it("rejects direct ingest that bypasses normalized SQL text limits", async () => {
|
|
767
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
768
|
+
const asAlice = <T>(fn: () => Promise<T>) =>
|
|
769
|
+
runWithRequestContext(
|
|
770
|
+
{ userEmail: "alice@example.test", orgId: "org-1" },
|
|
771
|
+
fn,
|
|
772
|
+
);
|
|
773
|
+
const ingest = (
|
|
774
|
+
item: Parameters<typeof store.ingestItems>[0]["items"][0],
|
|
775
|
+
) =>
|
|
776
|
+
asAlice(() => store.ingestItems({ sourceId: "source-1", items: [item] }));
|
|
777
|
+
const base = {
|
|
778
|
+
kind: "document",
|
|
779
|
+
title: "Oversized direct ingest",
|
|
780
|
+
contentHash: "oversized-direct-hash",
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
await expect(
|
|
784
|
+
ingest({
|
|
785
|
+
...base,
|
|
786
|
+
externalId: "oversized-searchable",
|
|
787
|
+
content: "界".repeat(22_000),
|
|
788
|
+
}),
|
|
789
|
+
).rejects.toThrow(/searchable content.*exceeds.*normalize or chunk/i);
|
|
790
|
+
await expect(
|
|
791
|
+
ingest({
|
|
792
|
+
...base,
|
|
793
|
+
externalId: "oversized-summary",
|
|
794
|
+
content: "bounded",
|
|
795
|
+
summary: "🙂".repeat(2_049),
|
|
796
|
+
}),
|
|
797
|
+
).rejects.toThrow(/summary.*exceeds.*normalize the summary/i);
|
|
798
|
+
await expect(
|
|
799
|
+
ingest({
|
|
800
|
+
...base,
|
|
801
|
+
externalId: "oversized-chunks",
|
|
802
|
+
content: "bounded",
|
|
803
|
+
chunks: [
|
|
804
|
+
{ ordinal: 0, text: "a".repeat(40_000) },
|
|
805
|
+
{ ordinal: 1, text: "b".repeat(30_000) },
|
|
806
|
+
],
|
|
807
|
+
}),
|
|
808
|
+
).rejects.toThrow(/chunks exceed.*normalize or split/i);
|
|
809
|
+
await expect(
|
|
810
|
+
ingest({
|
|
811
|
+
...base,
|
|
812
|
+
externalId: "oversized-native",
|
|
813
|
+
kind: "google-slides-slide",
|
|
814
|
+
mimeType: "text/html",
|
|
815
|
+
content: `<div>${"🙂".repeat(32_768)}</div>`,
|
|
816
|
+
metadata: {
|
|
817
|
+
nativeArtifact: { app: "slides", format: "slides-html" },
|
|
818
|
+
},
|
|
819
|
+
}),
|
|
820
|
+
).rejects.toThrow(/native content.*exceeds.*private blob storage/i);
|
|
821
|
+
|
|
822
|
+
const rows = await exec.execute({
|
|
823
|
+
sql: "SELECT id FROM creative_context_items WHERE external_id LIKE 'oversized-%'",
|
|
824
|
+
args: [],
|
|
825
|
+
});
|
|
826
|
+
expect(rows.rows).toHaveLength(0);
|
|
827
|
+
});
|
|
828
|
+
|
|
374
829
|
it("keeps prior native code immutable across resync and resolves hierarchical children by source version", async () => {
|
|
375
830
|
const { runWithRequestContext, store } = await setup();
|
|
376
831
|
const asAlice = <T>(fn: () => Promise<T>) =>
|
|
@@ -1000,6 +1455,36 @@ describe("creative context access and revocation", () => {
|
|
|
1000
1455
|
expect(rows.rows).toHaveLength(1);
|
|
1001
1456
|
});
|
|
1002
1457
|
|
|
1458
|
+
it("deduplicates jobs inside one tenant without colliding across scopes", async () => {
|
|
1459
|
+
const { exec, runWithRequestContext, store } = await setup();
|
|
1460
|
+
const create = (userEmail: string, orgId: string | undefined) =>
|
|
1461
|
+
runWithRequestContext({ userEmail, orgId }, () =>
|
|
1462
|
+
store.createJob({
|
|
1463
|
+
kind: "metadata-refresh",
|
|
1464
|
+
dedupeKey: "same-logical-operation",
|
|
1465
|
+
}),
|
|
1466
|
+
);
|
|
1467
|
+
|
|
1468
|
+
const aliceOrgOne = await create("alice@example.test", "org-1");
|
|
1469
|
+
const aliceOrgOneAgain = await create("alice@example.test", "org-1");
|
|
1470
|
+
const bobOrgOne = await create("bob@example.test", "org-1");
|
|
1471
|
+
const aliceOrgTwo = await create("alice@example.test", "org-2");
|
|
1472
|
+
const alicePersonal = await create("alice@example.test", undefined);
|
|
1473
|
+
|
|
1474
|
+
expect(aliceOrgOneAgain.id).toBe(aliceOrgOne.id);
|
|
1475
|
+
expect(
|
|
1476
|
+
new Set([aliceOrgOne.id, bobOrgOne.id, aliceOrgTwo.id, alicePersonal.id]),
|
|
1477
|
+
).toHaveLength(4);
|
|
1478
|
+
const rows = await exec.execute(
|
|
1479
|
+
`SELECT dedupe_key, dedupe_scope, scoped_dedupe_key
|
|
1480
|
+
FROM creative_context_jobs
|
|
1481
|
+
WHERE scoped_dedupe_key = 'same-logical-operation'`,
|
|
1482
|
+
);
|
|
1483
|
+
expect(rows.rows).toHaveLength(4);
|
|
1484
|
+
expect(new Set(rows.rows.map((row) => row.dedupe_scope))).toHaveLength(4);
|
|
1485
|
+
expect(rows.rows.every((row) => row.dedupe_key === null)).toBe(true);
|
|
1486
|
+
});
|
|
1487
|
+
|
|
1003
1488
|
it("immediately tombstones sources when workspace connection access is removed", async () => {
|
|
1004
1489
|
const { exec, runWithRequestContext, store } = await setup();
|
|
1005
1490
|
await exec.execute(
|